diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ebe35d82..ac6982b1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -68,19 +68,6 @@ jobs: - name: Microbenchmarks run: ./gradlew jmh - - name: Cache Bazel stuff - if: ${{ matrix.java-version == '21' }} - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 - with: - path: | - ~/.cache/bazel - key: bazel-${{ hashFiles('**/.gitmodules') }} - restore-keys: bazel- - - - name: Conformance tests - if: ${{ matrix.java-version == '21' }} - run: conformance/run-conformance-tests.sh - - name: Capture test results uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: failure() diff --git a/README.md b/README.md index a90cf68f..5afd9958 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ The CEL specification can be found [here](https://github.com/google/cel-spec). - [Motivation](#motivation) - [Arbitrary Java classes](#arbitrary-java-classes) - [Unsigned 64-bit `uint`](#unsigned-64-bit-uint) + - [Protobuf enum semantics](#protobuf-enum-semantics) - [Native image and package verification](#native-image-and-package-verification) - [Not yet implemented](#not-yet-implemented) - [Unclear double-to-int rounding behavior](#unclear-double-to-int-rounding-behavior) @@ -394,6 +395,24 @@ but `123u == 123u` and `123 == 123` are. If you have a `uint32` or `uint64` in protobuf objects, or use `uint`s in CEL expressions, wrap those values with `org.projectnessie.cel.common.ULong`. +### Protobuf enum semantics + +CEL-Java follows the CEL-Spec v0.25.2 language definition for protobuf enum values: protobuf enum +constants and enum fields are represented as CEL `int` values. + +The upstream CEL-Spec conformance testdata also contains strong-enum cases where enum values +preserve their enum type. CEL-Java does not currently enable those strong-enum conformance cases. +They are not part of the v0.25.2 language-definition baseline and are mutually incompatible with the +legacy enum-as-int conformance sections in a single-mode runtime. + +Use numeric enum values directly, or use `int(...)` when writing expressions that should remain clear +if strong enum support is added in the future: + +```cel +TestAllTypes.NestedEnum.BAR == 1 +int(TestAllTypes.NestedEnum.BAR) == 1 +``` + ### Native image and package verification Native-image and package behavior must be verified in the consuming application's exact build. @@ -401,7 +420,7 @@ Native-image and package behavior must be verified in the consuming application' prove Quarkus native-image or package compatibility for every application. Before using CEL conditions in release-critical authorization paths, run JVM condition tests, the -consuming project's normal build, dependency tree review for protobuf/ANTLR/Jackson conflicts, and +consuming project's normal build, dependency tree review for protobuf/Jackson conflicts, and package/native-image verification if native execution is part of the release path. ### Not yet implemented @@ -433,17 +452,17 @@ The CEL-Go implementation does not pass these CEL-spec conformance tests: ```text --- FAIL: TestSimpleFile/conversions/int/double_truncate (0.01s) - simple_test.go:219: double_truncate: Eval got [int64_value:2], want [int64_value:1] + double_truncate: Eval got [int64_value:2], want [int64_value:1] --- FAIL: TestSimpleFile/conversions/int/double_truncate_neg (0.01s) - simple_test.go:219: double_truncate_neg: Eval got [int64_value:-8], want [int64_value:-7] + double_truncate_neg: Eval got [int64_value:-8], want [int64_value:-7] --- FAIL: TestSimpleFile/conversions/int/double_half_pos (0.01s) - simple_test.go:219: double_half_pos: Eval got [int64_value:12], want [int64_value:11] + double_half_pos: Eval got [int64_value:12], want [int64_value:11] --- FAIL: TestSimpleFile/conversions/int/double_half_neg (0.01s) - simple_test.go:219: double_half_neg: Eval got [int64_value:-4], want [int64_value:-3] + double_half_neg: Eval got [int64_value:-4], want [int64_value:-3] --- FAIL: TestSimpleFile/conversions/uint/double_truncate (0.01s) - simple_test.go:219: double_truncate: Eval got [uint64_value:2], want [uint64_value:1] + double_truncate: Eval got [uint64_value:2], want [uint64_value:1] --- FAIL: TestSimpleFile/conversions/uint/double_half (0.01s) - simple_test.go:219: double_half: Eval got [uint64_value:26], want [uint64_value:25] + double_half: Eval got [uint64_value:26], want [uint64_value:25] ``` ## Building and testing CEL-Java @@ -470,6 +489,11 @@ repository, in case you want to use CEL-Java snapshot artifacts from another pro The project uses Google Java style and the Spotless plugin. Run `./gradlew spotlessApply` to fix formatting issues. -To run the CEL-spec conformance tests, Go, Bazel, and their toolchains are required. From the -CEL-Java repo, run `conformance/run-conformance-tests.sh`. That script performs the necessary Gradle -and Bazel builds. +To run the CEL-Spec conformance tests, use the JUnit-based conformance suite: + +```shell +./gradlew :cel-conformance:test +``` + +The conformance suite reads upstream CEL-Spec textproto testdata from the `submodules/cel-spec` +submodule. It does not require Bazel, Go, or a separate conformance server. diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts index b50161b0..c0772de1 100644 --- a/bom/build.gradle.kts +++ b/bom/build.gradle.kts @@ -24,7 +24,6 @@ plugins { dependencies { constraints { api(project(":cel-core")) - api(project(":cel-generated-antlr")) api(project(":cel-generated-pb")) api(project(":cel-generated-pb3")) api(project(":cel-conformance")) diff --git a/build-logic/src/main/kotlin/cel-conventions.gradle.kts b/build-logic/src/main/kotlin/cel-conventions.gradle.kts index 49cdeef8..8a66f9c5 100644 --- a/build-logic/src/main/kotlin/cel-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/cel-conventions.gradle.kts @@ -20,8 +20,7 @@ nessieConfigureJava() val nonPublishedProjects = setOf( - "conformance", - "jacoco", + "cel-conformance", "cel-quarkus-smoke-standalone", "cel-quarkus-smoke-core-pb3-jackson3", ) diff --git a/build.gradle.kts b/build.gradle.kts index df3713e0..62a9c29a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -102,7 +102,6 @@ idea.project.settings { afterSync( ":cel-generated-pb:jar", ":cel-generated-pb:testJar", - ":cel-generated-antlr:shadowJar", ) } } diff --git a/conformance/README.md b/conformance/README.md index 8854a558..eb881226 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -1,57 +1,40 @@ -# Running CEL-spec conformance tests against CEL-Java +# Running CEL-Spec conformance tests against CEL-Java -If your environment is already setup, just run the shell script -```shell -./run-conformance-tests.sh -``` - -## Requirements & Setup +The CEL-Java conformance suite is a JUnit test suite that reads upstream CEL-Spec +simple testdata from the `submodules/cel-spec` Git submodule. -The CEL-spec conformance test suite is written in Go and uses the bazel build tool. +Run it with: -Required tools: -* [Bazel build tool](https://bazel.build/) (see [bazelisk](https://github.com/bazelbuild/bazelisk?tab=readme-ov-file#installation)) - - See [Bazel web site](https://bazel.build/install) for installation instructions. - Do **not** use the `bazel-bootstrap` package on Ubuntu, because it comes with - pre-compiled classes that are built with Java 17 or newer, preventing it from - running bazel using older Java versions. -* gcc - - On Ubuntu run `apt-get install gcc` +```shell +./gradlew :cel-conformance:test +``` -Other required dependencies like "Go" will be managed by bazel. +The suite does not require Bazel, Go, a separate conformance server, or the old +upstream `simple_test` binary. -## FAQ +## Test selection -### Bazel build hangs +The curated conformance file list and skip list live in: -If the bazel build does not start, i.e. it gets stuck with messages like -``` -Starting local Bazel server and connecting to it... -... still trying to connect to local Bazel server after 10 seconds ... -... still trying to connect to local Bazel server after 20 seconds ... -... still trying to connect to local Bazel server after 30 seconds ... +```text +conformance/src/test/java/org/projectnessie/cel/conformance/SimpleConformanceTest.java ``` -then kill all bazel processes make sure that Java 11 is the current one. It seems, that the -above *may* happen when with Java 8. -### Bazel build fails from `./run-conformance-tests.sh` +Each skip uses the upstream conformance path: -If the bazel build fails with an error like this (note the `Failed to create temporary file`), -run the bazel build once from the console: - -```shell -cd submodules/cel-spec - -bazel build ... +```text +file/section/test ``` -If the build still fails, try the following options: +or a whole-section path: -```shell -bazel build ... --sandbox_writable_path="${HOME}/.ccache" --strategy=CppCompile=standalone +```text +file/section ``` -After the build succeeds once from that directory, you can use the `./run-conformance-tests.sh` -script. +Unmatched skips fail the test suite, so stale skips are visible when upstream +testdata changes. + +Optional CEL-Spec files such as extension libraries, optionals, and type +deduction are intentionally not enabled by default. Add those separately with +explicit skip reasoning. diff --git a/conformance/build.gradle.kts b/conformance/build.gradle.kts index d1cae24b..8f4e2769 100644 --- a/conformance/build.gradle.kts +++ b/conformance/build.gradle.kts @@ -22,7 +22,6 @@ import org.gradle.api.tasks.compile.JavaCompile plugins { `java-library` - id("com.gradleup.shadow") id("cel-conventions") } @@ -33,22 +32,20 @@ val mainProtoResourcesDir = layout.buildDirectory.dir("generated/proto-resources val syncMainProtoSources = tasks.register("syncMainProtoSources") { into(syncedMainProtoDir) - from(layout.settingsDirectory.dir("submodules/googleapis/google/rpc")) { into("google/rpc") } - from(layout.settingsDirectory.dir("submodules/googleapis/google/api/expr/conformance")) { - into("google/api/expr/conformance") + from(layout.settingsDirectory.dir("submodules/cel-spec/proto/cel/expr")) { + include("checked.proto", "eval.proto", "syntax.proto", "value.proto") + into("cel/expr") + } + from(layout.settingsDirectory.dir("submodules/cel-spec/proto/cel/expr/conformance/test")) { + include("simple.proto") + into("cel/expr/conformance/test") } } val emptyTestProtoDir = layout.buildDirectory.dir("pb-src/test/proto") sourceSets.main { - java.setSrcDirs( - listOf( - layout.projectDirectory.dir("src/main/java"), - layout.buildDirectory.dir("generated/sources/proto/main/java"), - layout.buildDirectory.dir("generated/sources/proto/main/grpc"), - ) - ) + java.setSrcDirs(listOf(layout.buildDirectory.dir("generated/sources/proto/main/java"))) resources.setSrcDirs(listOf(mainProtoResourcesDir)) extensions.configure("proto") { setSrcDirs(listOf(syncedMainProtoDir)) @@ -68,15 +65,11 @@ configurations.all { exclude(group = "org.projectnessie.cel", module = "cel-gene dependencies { implementation(project(":cel-core")) implementation(project(":cel-generated-pb3")) - implementation(testFixtures(project(":cel-core"))) implementation(testFixtures(project(":cel-generated-pb3"))) implementation(libs.protobuf.java) { version { strictly(libs.versions.protobuf3.get()) } } - implementation(libs.grpc.protobuf) - implementation(libs.grpc.stub) - runtimeOnly(libs.grpc.netty.shaded) - compileOnly(libs.tomcat.annotations.api) + testImplementation(testFixtures(project(":cel-core"))) testImplementation(platform(libs.junit.bom)) testImplementation(libs.bundles.junit.testing) @@ -84,10 +77,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.named("shadowJar") { - manifest { attributes("Main-Class" to "org.projectnessie.cel.server.ConformanceServer") } -} - // *.proto files taken from https://github.com/google/cel-spec/ repo, available as a git submodule configure { // Configure the protoc executable @@ -95,10 +84,6 @@ configure { // Download from repositories artifact = "com.google.protobuf:protoc:${libs.versions.protobuf3.get()}" } - plugins { - this.create("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:${libs.versions.grpc.get()}" } - } - generateProtoTasks { all().configureEach { this.plugins.create("grpc") {} } } } tasks.named("generateProto") { dependsOn(syncMainProtoSources) } diff --git a/conformance/conformance-server.sh b/conformance/conformance-server.sh deleted file mode 100755 index 2425b553..00000000 --- a/conformance/conformance-server.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright (C) 2021 The Authors of CEL-Java -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -wd="$(dirname "$0")" - -pid_file="${wd}/conformance-server.pid" - -function kill_server() { - if [[ -f ${pid_file} ]] ; then - kill "$(cat "${pid_file}")" && rm "${pid_file}" - fi -} - -trap kill_server SIGINT SIGTERM - -java -Djava.net.preferIPv4Stack=true -jar "${wd}"/build/libs/cel-conformance-*-all.jar "${@}" & -java_pid=$! -echo "${java_pid}" > "${pid_file}" -wait diff --git a/conformance/run-conformance-tests.sh b/conformance/run-conformance-tests.sh deleted file mode 100755 index 1e614aa6..00000000 --- a/conformance/run-conformance-tests.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright (C) 2021 The Authors of CEL-Java -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -wd="$(dirname $0)" - -cd "${wd}/.." || exit 1 - -./gradlew :cel-conformance:shadowJar || exit 1 - -server_pid_file="$(realpath ./conformance/conformance-server.pid)" - -cd submodules/cel-spec || exit 1 - -# Bazel version 6.4.0 works, 7.0.2 does not work with the conformance tests -export USE_BAZEL_VERSION="6.5.0" - -if [[ -z "${CC:-}" ]] && command -v gcc >/dev/null 2>&1; then - export CC="$(command -v gcc)" -fi -if [[ -z "${CXX:-}" ]] && command -v g++ >/dev/null 2>&1; then - export CXX="$(command -v g++)" -fi - -bazel sync --configure || exit 1 -bazel build ... || exit 1 - -cel_java_skips=( - # proto2 enums are generated as Java enums, means: it is not possible to assign arbitrary - # ordinals, so these tests cannot work against the CEL-Java implementation (limitation of the - # protobuf/Java implementation for proto2). - "--skip_test=enums/legacy_proto2/select_big,select_neg,assign_standalone_int_big,assign_standalone_int_neg" - # Without the checker, it is quite difficult to verify whether an assignment is allowed (by - # the CEL spec), especially from a 'map' to a 'struct' as in these tests using the expression - # `TestAllTypes{single_struct: {1: 'uno'}}`. Note: the checker catches this case and the - # Java implementation currently converts the 'int(1)' to a 'string("1")', which is not strictly - # allowed, but OTOH overall not a serious issue. - "--skip_test=dynamic/struct/field_assign_proto2_bad" - "--skip_test=dynamic/struct/field_assign_proto3_bad" - # The test expects a -0.0d, but in Java `-0.0d==0.0d` is true, so -0.0d is evaluates as "not set", - # so it returns the field as empty. - "--skip_test=dynamic/float/field_assign_proto3_round_to_zero" - # "Malicious" protobuf message. The actual CEL-spec test produces a request with a too deeply - # nested protobuf-object-structure, which gets rejected during gRPC/protobuf request - # deserialization. Just skip those tests. - "--skip_test=parse/nest/message_literal" - # Proto equality specialties don't seem to be in effect for Java - "--skip_test=comparisons/eq_wrapper/eq_proto_nan_equal" - "--skip_test=comparisons/ne_literal/ne_proto_nan_not_equal" - - # TODO Actual known issue to fix, a protobuf Any returned via this test is wrapped twice (Any in Any). - "--skip_test=dynamic/any/var" -) - -cel_go_skips=( - "--skip_test=dynamic/int32/field_assign_proto2_range,field_assign_proto3_range" - "--skip_test=dynamic/uint32/field_assign_proto2_range,field_assign_proto3_range" - "--skip_test=dynamic/float/field_assign_proto2_range,field_assign_proto3_range" - "--skip_test=enums/legacy_proto2/assign_standalone_int_too_big,assign_standalone_int_too_neg" - "--skip_test=enums/legacy_proto3/assign_standalone_int_too_big,assign_standalone_int_too_neg" - "--skip_test=enums/strong_proto2" - "--skip_test=enums/strong_proto3" - # This conformance test is invalid nowadays - "--skip_test=fields/qualified_identifier_resolution/map_key_float" - # Unclear why the 'to_json_string' is expected to return a string, unlike the preceding to_json_number test. - "--skip_test=wrappers/uint64/to_json_string" - # TODO implement proper "toJson" at some point - "--skip_test=wrappers/field_mask/to_json" - "--skip_test=wrappers/timestamp/to_json" - "--skip_test=wrappers/empty/to_json" -) - -test_files=( - "tests/simple/testdata/basic.textproto" - "tests/simple/testdata/comparisons.textproto" - "tests/simple/testdata/conversions.textproto" - "tests/simple/testdata/dynamic.textproto" - "tests/simple/testdata/enums.textproto" - "tests/simple/testdata/fields.textproto" - "tests/simple/testdata/fp_math.textproto" - "tests/simple/testdata/integer_math.textproto" - "tests/simple/testdata/lists.textproto" - "tests/simple/testdata/logic.textproto" - "tests/simple/testdata/macros.textproto" - "tests/simple/testdata/namespace.textproto" - "tests/simple/testdata/parse.textproto" - "tests/simple/testdata/plumbing.textproto" - "tests/simple/testdata/proto2.textproto" - "tests/simple/testdata/proto3.textproto" - "tests/simple/testdata/string.textproto" - # TODO add when implemnting the string-extensions "tests/simple/testdata/string_ext.textproto" - "tests/simple/testdata/timestamps.textproto" - "tests/simple/testdata/unknowns.textproto" - "tests/simple/testdata/wrappers.textproto" -) - -bazel-bin/tests/simple/simple_test_/simple_test \ - --server ../../conformance/conformance-server.sh \ - "${cel_java_skips[@]}" \ - "${cel_go_skips[@]}" \ - "${test_files[@]}" -code=$? - -if [[ -f ${server_pid_file} ]] ; then - kill "$(cat "${server_pid_file}")" && rm "${server_pid_file}" || exit 1 -fi - -exit $code diff --git a/conformance/src/main/java/org/projectnessie/cel/server/ConformanceServer.java b/conformance/src/main/java/org/projectnessie/cel/server/ConformanceServer.java deleted file mode 100644 index 952e2ee6..00000000 --- a/conformance/src/main/java/org/projectnessie/cel/server/ConformanceServer.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2021 The Authors of CEL-Java - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.projectnessie.cel.server; - -import io.grpc.Server; -import io.grpc.ServerBuilder; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.util.List; - -public class ConformanceServer implements AutoCloseable { - - private final Server server; - - public ConformanceServer(Server server) { - this.server = server; - } - - public static String getListenHost(Server server) { - List addrs = server.getListenSockets(); - SocketAddress addr = addrs.get(0); - InetSocketAddress ia = (InetSocketAddress) addr; - InetAddress a = ia.getAddress(); - - String host; - if (a instanceof Inet6Address) { - if (a.isAnyLocalAddress()) { - host = "::1"; - } else { - host = a.getCanonicalHostName(); - } - } else { - if (a.isAnyLocalAddress()) { - host = "127.0.0.1"; - } else { - host = a.getCanonicalHostName(); - } - } - - return host; - } - - public void blockUntilShutdown() throws InterruptedException { - server.awaitTermination(); - } - - @Override - public void close() throws Exception { - server.shutdown().awaitTermination(); - } - - public static void main(String[] args) throws Exception { - ConformanceServiceImpl service = new ConformanceServiceImpl(); - - for (String arg : args) { - if ("--verbose".equals(arg) || "-v".equals(arg)) { - service.setVerboseEvalErrors(true); - } - } - - Server c = ServerBuilder.forPort(0).addService(service).build(); - - Thread hook = new Thread(c::shutdown); - - try (ConformanceServer cs = new ConformanceServer(c.start())) { - System.out.printf("Listening on %s:%d%n", getListenHost(cs.server), cs.server.getPort()); - - Runtime.getRuntime().addShutdownHook(hook); - cs.blockUntilShutdown(); - } finally { - try { - Runtime.getRuntime().removeShutdownHook(hook); - } catch (IllegalStateException e) { - // ignore (might happen, when a JVM shutdown is already in progress) - } - } - } -} diff --git a/conformance/src/main/java/org/projectnessie/cel/server/ConformanceServiceImpl.java b/conformance/src/main/java/org/projectnessie/cel/server/ConformanceServiceImpl.java deleted file mode 100644 index a3d86aa6..00000000 --- a/conformance/src/main/java/org/projectnessie/cel/server/ConformanceServiceImpl.java +++ /dev/null @@ -1,428 +0,0 @@ -/* - * Copyright (C) 2021 The Authors of CEL-Java - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.projectnessie.cel.server; - -import static org.projectnessie.cel.CEL.astToCheckedExpr; -import static org.projectnessie.cel.CEL.astToParsedExpr; -import static org.projectnessie.cel.CEL.checkedExprToAst; -import static org.projectnessie.cel.CEL.parsedExprToAst; -import static org.projectnessie.cel.Env.newCustomEnv; -import static org.projectnessie.cel.Env.newEnv; -import static org.projectnessie.cel.EnvOption.clearMacros; -import static org.projectnessie.cel.EnvOption.container; -import static org.projectnessie.cel.EnvOption.declarations; -import static org.projectnessie.cel.EnvOption.types; -import static org.projectnessie.cel.Library.StdLib; -import static org.projectnessie.cel.common.types.BoolT.True; -import static org.projectnessie.cel.common.types.BytesT.bytesOf; -import static org.projectnessie.cel.common.types.DoubleT.doubleOf; -import static org.projectnessie.cel.common.types.Err.isError; -import static org.projectnessie.cel.common.types.Err.newErr; -import static org.projectnessie.cel.common.types.IntT.intOf; -import static org.projectnessie.cel.common.types.StringT.stringOf; -import static org.projectnessie.cel.common.types.TypeT.newObjectTypeValue; -import static org.projectnessie.cel.common.types.Types.boolOf; -import static org.projectnessie.cel.common.types.UintT.uintOf; -import static org.projectnessie.cel.common.types.UnknownT.isUnknown; -import static org.projectnessie.cel.common.types.UnknownT.unknownOf; - -import com.google.api.expr.conformance.v1alpha1.CheckRequest; -import com.google.api.expr.conformance.v1alpha1.CheckResponse; -import com.google.api.expr.conformance.v1alpha1.ConformanceServiceGrpc.ConformanceServiceImplBase; -import com.google.api.expr.conformance.v1alpha1.EvalRequest; -import com.google.api.expr.conformance.v1alpha1.EvalResponse; -import com.google.api.expr.conformance.v1alpha1.IssueDetails; -import com.google.api.expr.conformance.v1alpha1.ParseRequest; -import com.google.api.expr.conformance.v1alpha1.ParseResponse; -import com.google.api.expr.conformance.v1alpha1.SourcePosition; -import com.google.api.expr.v1alpha1.ErrorSet; -import com.google.api.expr.v1alpha1.ExprValue; -import com.google.api.expr.v1alpha1.ListValue; -import com.google.api.expr.v1alpha1.MapValue; -import com.google.api.expr.v1alpha1.MapValue.Entry; -import com.google.api.expr.v1alpha1.UnknownSet; -import com.google.api.expr.v1alpha1.Value; -import com.google.protobuf.Any; -import com.google.protobuf.ByteString; -import com.google.protobuf.Duration; -import com.google.protobuf.Message; -import com.google.protobuf.Timestamp; -import com.google.rpc.Code; -import com.google.rpc.Status; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import org.projectnessie.cel.Ast; -import org.projectnessie.cel.Env; -import org.projectnessie.cel.Env.AstIssuesTuple; -import org.projectnessie.cel.EnvOption; -import org.projectnessie.cel.Program; -import org.projectnessie.cel.Program.EvalResult; -import org.projectnessie.cel.common.CELError; -import org.projectnessie.cel.common.types.Err; -import org.projectnessie.cel.common.types.IteratorT; -import org.projectnessie.cel.common.types.NullT; -import org.projectnessie.cel.common.types.TypeT; -import org.projectnessie.cel.common.types.Types; -import org.projectnessie.cel.common.types.ref.Type; -import org.projectnessie.cel.common.types.ref.TypeAdapter; -import org.projectnessie.cel.common.types.ref.Val; -import org.projectnessie.cel.common.types.traits.Lister; -import org.projectnessie.cel.common.types.traits.Mapper; - -public class ConformanceServiceImpl extends ConformanceServiceImplBase { - - private boolean verboseEvalErrors; - - public void setVerboseEvalErrors(boolean verboseEvalErrors) { - this.verboseEvalErrors = verboseEvalErrors; - } - - @Override - public void parse( - ParseRequest request, io.grpc.stub.StreamObserver responseObserver) { - try { - String sourceText = request.getCelSource(); - if (sourceText.trim().isEmpty()) { - throw new IllegalArgumentException("No source code."); - } - - // NOTE: syntax_version isn't currently used - List parseOptions = new ArrayList<>(); - if (request.getDisableMacros()) { - parseOptions.add(clearMacros()); - } - - Env env = newEnv(parseOptions.toArray(new EnvOption[0])); - AstIssuesTuple astIss = env.parse(sourceText); - - ParseResponse.Builder response = ParseResponse.newBuilder(); - if (!astIss.hasIssues()) { - // Success - response.setParsedExpr(astToParsedExpr(astIss.getAst())); - } else { - // Failure - appendErrors(astIss.getIssues().getErrors(), response::addIssuesBuilder); - } - - responseObserver.onNext(response.build()); - responseObserver.onCompleted(); - } catch (Exception e) { - responseObserver.onError( - io.grpc.Status.fromCode(io.grpc.Status.Code.UNKNOWN) - .withDescription(stacktrace(e)) - .asException()); - } - } - - @Override - public void check( - CheckRequest request, io.grpc.stub.StreamObserver responseObserver) { - try { - // Build the environment. - List checkOptions = new ArrayList<>(); - if (!request.getNoStdEnv()) { - checkOptions.add(StdLib()); - } - - checkOptions.add(container(request.getContainer())); - checkOptions.add(declarations(request.getTypeEnvList())); - checkOptions.add( - types( - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes - .getDefaultInstance(), - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance())); - Env env = newCustomEnv(checkOptions.toArray(new EnvOption[0])); - - // Check the expression. - AstIssuesTuple astIss = env.check(parsedExprToAst(request.getParsedExpr())); - CheckResponse.Builder resp = CheckResponse.newBuilder(); - - if (!astIss.hasIssues()) { - // Success - resp.setCheckedExpr(astToCheckedExpr(astIss.getAst())); - } else { - // Failure - appendErrors(astIss.getIssues().getErrors(), resp::addIssuesBuilder); - } - - responseObserver.onNext(resp.build()); - responseObserver.onCompleted(); - } catch (Exception e) { - responseObserver.onError( - io.grpc.Status.fromCode(io.grpc.Status.Code.UNKNOWN) - .withDescription(stacktrace(e)) - .asException()); - } - } - - @Override - public void eval( - EvalRequest request, io.grpc.stub.StreamObserver responseObserver) { - try { - Env env = - newEnv( - container(request.getContainer()), - types( - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes - .getDefaultInstance(), - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance())); - - Program prg; - Ast ast; - - switch (request.getExprKindCase()) { - case PARSED_EXPR: - ast = parsedExprToAst(request.getParsedExpr()); - break; - case CHECKED_EXPR: - ast = checkedExprToAst(request.getCheckedExpr()); - break; - default: - throw new IllegalArgumentException("No expression."); - } - - prg = env.program(ast); - - Map args = new HashMap<>(); - request - .getBindingsMap() - .forEach( - (name, exprValue) -> { - Val refVal = exprValueToRefValue(env.getTypeAdapter(), exprValue); - args.put(name, refVal); - }); - - // NOTE: the EvalState is currently discarded - EvalResult res = prg.eval(args); - ExprValue resultExprVal; - if (!isError(res.getVal())) { - resultExprVal = refValueToExprValue(res.getVal()); - } else { - Err err = (Err) res.getVal(); - - if (verboseEvalErrors) { - System.err.printf( - "%n" + "Eval error (not necessarily a bug!!!):%n" + " error: %s%n" + "%s", - err, err.hasCause() ? (stacktrace(err.toRuntimeException()) + "\n") : ""); - } - - resultExprVal = - ExprValue.newBuilder() - .setError( - ErrorSet.newBuilder().addErrors(Status.newBuilder().setMessage(err.toString()))) - .build(); - } - - EvalResponse.Builder resp = EvalResponse.newBuilder().setResult(resultExprVal); - - responseObserver.onNext(resp.build()); - responseObserver.onCompleted(); - } catch (Exception e) { - responseObserver.onError( - io.grpc.Status.fromCode(io.grpc.Status.Code.UNKNOWN) - .withDescription(stacktrace(e)) - .asException()); - } - } - - static String stacktrace(Throwable t) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - t.printStackTrace(pw); - pw.flush(); - return sw.toString(); - } - - /** - * appendErrors converts the errors from errs to Status messages and appends them to the list of - * issues. - */ - static void appendErrors(List errs, Supplier builderSupplier) { - errs.forEach(e -> errToStatus(e, IssueDetails.Severity.ERROR, builderSupplier.get())); - } - - /** ErrToStatus converts an Error to a Status message with the given severity. */ - static void errToStatus(CELError e, IssueDetails.Severity severity, Status.Builder status) { - IssueDetails.Builder detail = - IssueDetails.newBuilder() - .setSeverity(severity) - .setPosition( - SourcePosition.newBuilder() - .setLine(e.getLocation().line()) - .setColumn(e.getLocation().column()) - .build()); - - status - .setCode(Code.INVALID_ARGUMENT_VALUE) - .setMessage(e.getMessage()) - .addDetails(Any.pack(detail.build())); - } - - /** RefValueToExprValue converts between ref.Val and exprpb.ExprValue. */ - static ExprValue refValueToExprValue(Val res) { - if (isUnknown(res)) { - return ExprValue.newBuilder() - .setUnknown(UnknownSet.newBuilder().addExprs(res.intValue())) - .build(); - } - Value v = refValueToValue(res); - return ExprValue.newBuilder().setValue(v).build(); - } - - // TODO(jimlarson): The following conversion code should be moved to - // common/types/provider.go and consolidated/refactored as appropriate. - // In particular, make judicious use of types.NativeToValue(). - - /** - * RefValueToValue converts between ref.Val and Value. The ref.Val must not be error or unknown. - */ - static Value refValueToValue(Val res) { - switch (res.type().typeEnum()) { - case Bool: - return Value.newBuilder().setBoolValue(res.booleanValue()).build(); - case Bytes: - return Value.newBuilder().setBytesValue(res.convertToNative(ByteString.class)).build(); - case Double: - return Value.newBuilder().setDoubleValue(res.convertToNative(Double.class)).build(); - case Int: - return Value.newBuilder().setInt64Value(res.intValue()).build(); - case Null: - return Value.newBuilder().setNullValueValue(0).build(); - case String: - return Value.newBuilder().setStringValue(res.value().toString()).build(); - case Type: - return Value.newBuilder().setTypeValue(((TypeT) res).typeName()).build(); - case Uint: - return Value.newBuilder().setUint64Value(res.intValue()).build(); - case Duration: - Duration d = res.convertToNative(Duration.class); - return Value.newBuilder().setObjectValue(Any.pack(d)).build(); - case Timestamp: - Timestamp t = res.convertToNative(Timestamp.class); - return Value.newBuilder().setObjectValue(Any.pack(t)).build(); - case List: - Lister l = (Lister) res; - ListValue.Builder elts = ListValue.newBuilder(); - for (IteratorT i = l.iterator(); i.hasNext() == True; ) { - Val v = i.next(); - elts.addValues(refValueToValue(v)); - } - return Value.newBuilder().setListValue(elts).build(); - case Map: - Mapper m = (Mapper) res; - MapValue.Builder elems = MapValue.newBuilder(); - for (IteratorT i = m.iterator(); i.hasNext() == True; ) { - Val k = i.next(); - Val v = m.get(k); - Value kv = refValueToValue(k); - Value vv = refValueToValue(v); - elems.addEntriesBuilder().setKey(kv).setValue(vv); - } - return Value.newBuilder().setMapValue(elems).build(); - case Object: - // Object type - Message pb = (Message) res.value(); - Value.Builder v = Value.newBuilder(); - // Somehow the conformance tests - if (pb instanceof ListValue) { - v.setListValue((ListValue) pb); - } else if (pb instanceof MapValue) { - v.setMapValue((MapValue) pb); - } else { - v.setObjectValue(Any.pack(pb)); - } - return v.build(); - default: - throw new IllegalStateException(String.format("Unknown %s", res.type().typeEnum())); - } - } - - /** ExprValueToRefValue converts between exprpb.ExprValue and ref.Val. */ - static Val exprValueToRefValue(TypeAdapter adapter, ExprValue ev) { - switch (ev.getKindCase()) { - case VALUE: - return valueToRefValue(adapter, ev.getValue()); - case ERROR: - // An error ExprValue is a repeated set of rpcpb.Status - // messages, with no convention for the status details. - // To convert this to a types.Err, we need to convert - // these Status messages to a single string, and be - // able to decompose that string on output so we can - // round-trip arbitrary ExprValue messages. - // TODO(jimlarson) make a convention for this. - return newErr("XXX add details later"); - case UNKNOWN: - return unknownOf(ev.getUnknown().getExprs(0)); - } - throw new IllegalArgumentException("unknown ExprValue kind " + ev.getKindCase()); - } - - /** ValueToRefValue converts between exprpb.Value and ref.Val. */ - static Val valueToRefValue(TypeAdapter adapter, Value v) { - switch (v.getKindCase()) { - case NULL_VALUE: - return NullT.NullValue; - case BOOL_VALUE: - return boolOf(v.getBoolValue()); - case INT64_VALUE: - return intOf(v.getInt64Value()); - case UINT64_VALUE: - return uintOf(v.getUint64Value()); - case DOUBLE_VALUE: - return doubleOf(v.getDoubleValue()); - case STRING_VALUE: - return stringOf(v.getStringValue()); - case BYTES_VALUE: - return bytesOf(v.getBytesValue().toByteArray()); - case OBJECT_VALUE: - Any any = v.getObjectValue(); - return adapter.nativeToValue(any); - case MAP_VALUE: - MapValue m = v.getMapValue(); - Map entries = new HashMap<>(); - for (Entry entry : m.getEntriesList()) { - Val key = valueToRefValue(adapter, entry.getKey()); - Val pb = valueToRefValue(adapter, entry.getValue()); - entries.put(key, pb); - } - return adapter.nativeToValue(entries); - case LIST_VALUE: - ListValue l = v.getListValue(); - List elts = - l.getValuesList().stream() - .map(el -> valueToRefValue(adapter, el)) - .collect(Collectors.toList()); - return adapter.nativeToValue(elts); - case TYPE_VALUE: - String typeName = v.getTypeValue(); - Type tv = Types.getTypeByName(typeName); - if (tv != null) { - return tv; - } - return newObjectTypeValue(typeName); - default: - throw new IllegalArgumentException("unknown value " + v.getKindCase()); - } - } -} diff --git a/conformance/src/main/proto/google/api/expr/conformance b/conformance/src/main/proto/google/api/expr/conformance deleted file mode 120000 index 04adab4d..00000000 --- a/conformance/src/main/proto/google/api/expr/conformance +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../submodules/googleapis/google/api/expr/conformance \ No newline at end of file diff --git a/conformance/src/main/proto/google/rpc b/conformance/src/main/proto/google/rpc deleted file mode 120000 index 9b33cdfa..00000000 --- a/conformance/src/main/proto/google/rpc +++ /dev/null @@ -1 +0,0 @@ -../../../../../submodules/googleapis/google/rpc/ \ No newline at end of file diff --git a/conformance/src/test/java/org/projectnessie/cel/conformance/SimpleConformanceTest.java b/conformance/src/test/java/org/projectnessie/cel/conformance/SimpleConformanceTest.java new file mode 100644 index 00000000..22f137d0 --- /dev/null +++ b/conformance/src/test/java/org/projectnessie/cel/conformance/SimpleConformanceTest.java @@ -0,0 +1,825 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.conformance; + +import static java.util.stream.Collectors.toCollection; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.abort; +import static org.junit.jupiter.api.DynamicContainer.dynamicContainer; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.projectnessie.cel.CEL.astToCheckedExpr; +import static org.projectnessie.cel.CEL.astToParsedExpr; +import static org.projectnessie.cel.CEL.checkedExprToAst; +import static org.projectnessie.cel.CEL.parsedExprToAst; +import static org.projectnessie.cel.Env.newCustomEnv; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.clearMacros; +import static org.projectnessie.cel.EnvOption.container; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EnvOption.macros; +import static org.projectnessie.cel.EnvOption.types; +import static org.projectnessie.cel.EvalOption.OptDisableNativeEval; +import static org.projectnessie.cel.Library.StdLib; +import static org.projectnessie.cel.ProgramOption.evalOptions; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.BytesT.bytesOf; +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.Err.isError; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.TypeT.newObjectTypeValue; +import static org.projectnessie.cel.common.types.Types.boolOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; +import static org.projectnessie.cel.common.types.UnknownT.isUnknown; +import static org.projectnessie.cel.common.types.UnknownT.unknownOf; +import static org.projectnessie.cel.extension.EncodersLib.encoders; +import static org.projectnessie.cel.extension.MathLib.math; +import static org.projectnessie.cel.extension.NetworkLib.network; +import static org.projectnessie.cel.extension.OptionalLib.optionals; +import static org.projectnessie.cel.extension.ProtoLib.proto; +import static org.projectnessie.cel.extension.StringsLib.strings; + +import com.google.api.expr.v1alpha1.CheckedExpr; +import com.google.api.expr.v1alpha1.Decl; +import com.google.api.expr.v1alpha1.ErrorSet; +import com.google.api.expr.v1alpha1.ExprValue; +import com.google.api.expr.v1alpha1.ListValue; +import com.google.api.expr.v1alpha1.MapValue; +import com.google.api.expr.v1alpha1.MapValue.Entry; +import com.google.api.expr.v1alpha1.ParsedExpr; +import com.google.api.expr.v1alpha1.Type; +import com.google.api.expr.v1alpha1.UnknownSet; +import com.google.api.expr.v1alpha1.Value; +import com.google.protobuf.Any; +import com.google.protobuf.ByteString; +import com.google.protobuf.Duration; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import com.google.protobuf.TextFormat; +import com.google.protobuf.Timestamp; +import com.google.protobuf.TypeRegistry; +import com.google.rpc.Status; +import dev.cel.expr.conformance.test.SimpleTest; +import dev.cel.expr.conformance.test.SimpleTest.ResultMatcherCase; +import dev.cel.expr.conformance.test.SimpleTestFile; +import dev.cel.expr.conformance.test.SimpleTestSection; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.TestFactory; +import org.projectnessie.cel.Ast; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.EnvOption; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.Program.EvalResult; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.IteratorT; +import org.projectnessie.cel.common.types.NullT; +import org.projectnessie.cel.common.types.TypeT; +import org.projectnessie.cel.common.types.Types; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; +import org.projectnessie.cel.common.types.traits.Mapper; +import org.projectnessie.cel.parser.Macro; + +class SimpleConformanceTest { + + private static final Path TESTDATA_DIR = testdataDir(); + private static final TextFormat.Printer TEXT_PRINTER = + TextFormat.printer().emittingSingleLine(true); + + private static final List TEST_FILES = + List.of( + "basic.textproto", + "bindings_ext.textproto", + "block_ext.textproto", + "comparisons.textproto", + "conversions.textproto", + "dynamic.textproto", + "encoders_ext.textproto", + "enums.textproto", + "fields.textproto", + "fp_math.textproto", + "integer_math.textproto", + "lists.textproto", + "logic.textproto", + "macros.textproto", + "macros2.textproto", + "math_ext.textproto", + "namespace.textproto", + "network_ext.textproto", + "optionals.textproto", + "parse.textproto", + "plumbing.textproto", + "proto2.textproto", + "proto2_ext.textproto", + "proto3.textproto", + "string.textproto", + "string_ext.textproto", + "timestamps.textproto", + "type_deduction.textproto", + "unknowns.textproto", + "wrappers.textproto"); + + private static final Set SKIP_TESTS = + SkipList.parse( + // Strong enum semantics require typed enum values rather than treating enum literals as + // ints. + "enums/strong_proto2/literal_global", + "enums/strong_proto2/literal_nested", + "enums/strong_proto2/literal_zero", + "enums/strong_proto2/type_global", + "enums/strong_proto2/type_nested", + "enums/strong_proto2/select_default", + "enums/strong_proto2/field_type", + "enums/strong_proto2/assign_standalone_int", + "enums/strong_proto2/convert_int_inrange", + "enums/strong_proto2/convert_int_big", + "enums/strong_proto2/convert_int_neg", + "enums/strong_proto2/convert_int_too_big", + "enums/strong_proto2/convert_int_too_neg", + "enums/strong_proto2/convert_string", + "enums/strong_proto2/convert_string_bad", + "enums/strong_proto3/literal_global", + "enums/strong_proto3/literal_nested", + "enums/strong_proto3/literal_zero", + "enums/strong_proto3/type_global", + "enums/strong_proto3/type_nested", + "enums/strong_proto3/select_default", + "enums/strong_proto3/select", + "enums/strong_proto3/select_big", + "enums/strong_proto3/select_neg", + "enums/strong_proto3/field_type", + "enums/strong_proto3/assign_standalone_int", + "enums/strong_proto3/assign_standalone_int_big", + "enums/strong_proto3/assign_standalone_int_neg", + "enums/strong_proto3/convert_int_inrange", + "enums/strong_proto3/convert_int_big", + "enums/strong_proto3/convert_int_neg", + "enums/strong_proto3/convert_int_too_big", + "enums/strong_proto3/convert_int_too_neg", + "enums/strong_proto3/convert_string", + "enums/strong_proto3/convert_string_bad"); + + private static final Set matchedSkips = new LinkedHashSet<>(); + private static final AtomicInteger total = new AtomicInteger(); + private static final AtomicInteger passed = new AtomicInteger(); + private static final AtomicInteger skipped = new AtomicInteger(); + + private enum EvaluationMode { + NATIVE_ENABLED("native evaluation enabled"), + NATIVE_DISABLED("native evaluation disabled"); + + private final String displayName; + + EvaluationMode(String displayName) { + this.displayName = displayName; + } + } + + @TestFactory + Stream simpleConformance() { + List files = new ArrayList<>(); + for (EvaluationMode mode : EvaluationMode.values()) { + files.add( + dynamicContainer( + mode.displayName, + TEST_FILES.stream() + .map(fileName -> dynamicContainer(fileName, fileTests(fileName, mode))))); + } + files.add(dynamicTest("skip list matches testdata", this::assertAllSkipsMatched)); + return files.stream(); + } + + @AfterAll + static void printSummary() { + System.out.printf( + "Conformance test executions: %d total, %d passed, %d skipped%n", + total.get(), passed.get(), skipped.get()); + } + + private Stream fileTests(String fileName, EvaluationMode mode) { + SimpleTestFile file; + try { + file = parseSimpleFile(TESTDATA_DIR.resolve(fileName)); + } catch (IOException e) { + throw new IllegalStateException("Cannot parse conformance testdata " + fileName, e); + } + + return file.getSectionList().stream() + .map(section -> dynamicContainer(section.getName(), sectionTests(file, section, mode))); + } + + private Stream sectionTests( + SimpleTestFile file, SimpleTestSection section, EvaluationMode mode) { + return section.getTestList().stream() + .map(test -> dynamicTest(test.getName(), () -> run(file, section, test, mode))); + } + + private void run( + SimpleTestFile file, SimpleTestSection section, SimpleTest test, EvaluationMode mode) + throws InvalidProtocolBufferException { + total.incrementAndGet(); + String sectionPath = file.getName() + "/" + section.getName(); + String testPath = sectionPath + "/" + test.getName(); + if (SKIP_TESTS.contains(sectionPath)) { + matchedSkips.add(sectionPath); + skipped.incrementAndGet(); + abort("Skipped conformance section " + sectionPath); + } + if (SKIP_TESTS.contains(testPath)) { + matchedSkips.add(testPath); + skipped.incrementAndGet(); + abort("Skipped conformance test " + testPath); + } + + ConformanceCaseRunner.run(mode, testPath, test); + passed.incrementAndGet(); + } + + private void assertAllSkipsMatched() { + Set unmatched = + SKIP_TESTS.stream() + .filter(skip -> !matchedSkips.contains(skip)) + .collect(toCollection(LinkedHashSet::new)); + if (!unmatched.isEmpty()) { + fail("Skip did not match any test or section: " + unmatched); + } + } + + private static SimpleTestFile parseSimpleFile(Path testFile) throws IOException { + TypeRegistry typeRegistry = + TypeRegistry.newBuilder() + .add(dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor()) + .add(dev.cel.expr.conformance.proto2.NestedTestAllTypes.getDescriptor()) + .add(dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor()) + .add(dev.cel.expr.conformance.proto3.NestedTestAllTypes.getDescriptor()) + .build(); + + SimpleTestFile.Builder builder = SimpleTestFile.newBuilder(); + ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); + dev.cel.expr.conformance.proto2.TestAllTypesExtensions.registerAllExtensions(extensionRegistry); + TextFormat.Parser.newBuilder() + .setTypeRegistry(typeRegistry) + .build() + .merge(Files.readString(testFile, StandardCharsets.UTF_8), extensionRegistry, builder); + return builder.build(); + } + + private static Path testdataDir() { + Path rootRelative = Path.of("submodules/cel-spec/tests/simple/testdata"); + if (Files.isDirectory(rootRelative)) { + return rootRelative; + } + + Path moduleRelative = Path.of("../submodules/cel-spec/tests/simple/testdata"); + if (Files.isDirectory(moduleRelative)) { + return moduleRelative; + } + + throw new IllegalStateException("Cannot locate CEL-Spec simple conformance testdata"); + } + + private static final class ConformanceCaseRunner { + private ConformanceCaseRunner() {} + + private static void run(EvaluationMode mode, String testPath, SimpleTest test) + throws InvalidProtocolBufferException { + if (test.getName().isEmpty()) { + throw new IllegalArgumentException("simple test has no name"); + } + if (test.getExpr().isEmpty()) { + throw new IllegalArgumentException("test has no expression"); + } + + ParsedExpr parsedExpr = ConformanceEvaluator.parse(test); + CheckedExpr checkedExpr = null; + if (!test.getDisableCheck()) { + checkedExpr = ConformanceEvaluator.check(test, parsedExpr); + if (!checkedExpr.getTypeMapMap().containsKey(parsedExpr.getExpr().getId())) { + throw new AssertionError("no type for top-level expression"); + } + if (test.getResultMatcherCase() == ResultMatcherCase.TYPED_RESULT) { + Type expectedType = convert(test.getTypedResult().getDeducedType(), Type.class); + Type actualType = checkedExpr.getTypeMapOrThrow(parsedExpr.getExpr().getId()); + if (!actualType.equals(expectedType)) { + throw new AssertionError( + "deduced type mismatch, got " + + print(actualType) + + ", want " + + print(expectedType)); + } + } + } + + if (test.getCheckOnly()) { + return; + } + + String evaluationPath = mode.displayName + "/" + testPath; + match(evaluationPath, test, ConformanceEvaluator.evalParsed(mode, test, parsedExpr)); + if (checkedExpr != null) { + match(evaluationPath, test, ConformanceEvaluator.evalChecked(mode, test, checkedExpr)); + } + } + } + + private static final class ConformanceEvaluator { + private ConformanceEvaluator() {} + + private static ParsedExpr parse(SimpleTest test) { + String sourceText = test.getExpr(); + if (sourceText.trim().isEmpty()) { + throw new IllegalArgumentException("No source code."); + } + + List parseOptions = new ArrayList<>(); + if (test.getDisableMacros()) { + parseOptions.add(clearMacros()); + } + if (usesTestOnlyBlockMacros(test.getExpr())) { + parseOptions.add(macros(Macro.TestOnlyBlockMacros)); + } + if (usesOptionals(test.getExpr())) { + parseOptions.add(optionals()); + } + + Env env = newEnv(parseOptions.toArray(new EnvOption[0])); + AstIssuesTuple astIss = env.parse(sourceText); + if (astIss.hasIssues()) { + throw new AssertionError("fatal parse errors: " + astIss.getIssues().getErrors()); + } + return astToParsedExpr(astIss.getAst()); + } + + private static CheckedExpr check(SimpleTest test, ParsedExpr parsedExpr) + throws InvalidProtocolBufferException { + List typeEnv = new ArrayList<>(); + for (dev.cel.expr.Decl decl : test.getTypeEnvList()) { + typeEnv.add(convert(decl, Decl.class)); + } + + Env env = + newCustomEnv( + conformanceEnvOptions(test, StdLib(), declarations(typeEnv)) + .toArray(new EnvOption[0])); + + AstIssuesTuple astIss = env.check(parsedExprToAst(parsedExpr)); + if (astIss.hasIssues()) { + throw new AssertionError("fatal check errors: " + astIss.getIssues().getErrors()); + } + return astToCheckedExpr(astIss.getAst()); + } + + private static ExprValue evalParsed( + EvaluationMode mode, SimpleTest test, ParsedExpr parsedExpr) { + return eval(mode, test, parsedExprToAst(parsedExpr)); + } + + private static ExprValue evalChecked( + EvaluationMode mode, SimpleTest test, CheckedExpr checkedExpr) { + return eval(mode, test, checkedExprToAst(checkedExpr)); + } + + private static ExprValue eval(EvaluationMode mode, SimpleTest test, Ast ast) { + Env env = newEnv(conformanceEnvOptions(test).toArray(new EnvOption[0])); + + Program program = + mode == EvaluationMode.NATIVE_ENABLED + ? env.program(ast) + : env.program(ast, evalOptions(OptDisableNativeEval)); + Map args = new HashMap<>(); + test.getBindingsMap() + .forEach( + (name, exprValue) -> + args.put(name, exprValueToRefValue(env.getTypeAdapter(), exprValue))); + + EvalResult res = program.eval(args); + if (!isError(res.getVal())) { + return refValueToExprValue(env.getTypeAdapter(), res.getVal()); + } + + Err err = (Err) res.getVal(); + return ExprValue.newBuilder() + .setError(ErrorSet.newBuilder().addErrors(Status.newBuilder().setMessage(err.toString()))) + .build(); + } + + private static List conformanceEnvOptions(SimpleTest test, EnvOption... options) { + List envOptions = new ArrayList<>(); + envOptions.add(container(test.getContainer())); + envOptions.add( + types( + dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance(), + dev.cel.expr.conformance.proto2.Proto2ExtensionScopedMessage.getDefaultInstance(), + dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance())); + if (test.getExpr().startsWith("proto.hasExt(") + || test.getExpr().startsWith("proto.getExt(")) { + envOptions.add(proto()); + } + if (usesStringExtensions(test.getExpr())) { + envOptions.add(strings()); + } + if (test.getExpr().contains("base64.")) { + envOptions.add(encoders()); + } + if (test.getExpr().contains("math.")) { + envOptions.add(math()); + } + if (usesNetworkExtensions(test.getExpr())) { + envOptions.add(network()); + } + if (usesOptionals(test.getExpr())) { + envOptions.add(optionals()); + } + envOptions.addAll(List.of(options)); + return envOptions; + } + + private static boolean usesStringExtensions(String expression) { + return expression.contains(".charAt(") + || expression.contains(".indexOf(") + || expression.contains(".lastIndexOf(") + || expression.contains(".lowerAscii(") + || expression.contains(".upperAscii(") + || expression.contains(".replace(") + || expression.contains(".split(") + || expression.contains(".substring(") + || expression.contains(".trim(") + || expression.contains(".join(") + || expression.contains("strings.quote(") + || expression.contains(".format(") + || expression.contains(".reverse("); + } + + private static boolean usesOptionals(String expression) { + return expression.contains("optional.") + || expression.contains(".?") + || expression.contains("[?") + || expression.contains("{?"); + } + + private static boolean usesNetworkExtensions(String expression) { + return expression.contains("ip(") + || expression.contains("cidr(") + || expression.contains("isIP(") + || expression.contains("ip.isCanonical(") + || expression.contains("net.IP") + || expression.contains("net.CIDR"); + } + + private static boolean usesTestOnlyBlockMacros(String expression) { + return expression.contains("cel.block(") + || expression.contains("cel.index(") + || expression.contains("cel.iterVar(") + || expression.contains("cel.accuVar("); + } + } + + private static void match(String testPath, SimpleTest test, ExprValue actual) + throws InvalidProtocolBufferException { + switch (test.getResultMatcherCase()) { + case VALUE: + matchValue(testPath, convert(test.getValue(), Value.class), actual); + return; + case TYPED_RESULT: + matchValue(testPath, convert(test.getTypedResult().getResult(), Value.class), actual); + return; + case EVAL_ERROR: + matchError(testPath, convert(test.getEvalError(), ErrorSet.class), actual); + return; + case ANY_EVAL_ERRORS: + if (actual.getKindCase() == ExprValue.KindCase.ERROR) { + return; + } + throw new AssertionError("got " + print(actual) + ", want one of several eval errors"); + case UNKNOWN: + matchUnknown(testPath, convert(test.getUnknown(), UnknownSet.class), actual); + return; + case ANY_UNKNOWNS: + if (actual.getKindCase() == ExprValue.KindCase.UNKNOWN) { + return; + } + throw new AssertionError("got " + print(actual) + ", want one of several unknowns"); + case RESULTMATCHER_NOT_SET: + matchValue(testPath, Value.newBuilder().setBoolValue(true).build(), actual); + return; + } + throw new AssertionError("unsupported result matcher " + test.getResultMatcherCase()); + } + + private static void matchValue(String testPath, Value expected, ExprValue actual) { + if (actual.getKindCase() != ExprValue.KindCase.VALUE) { + throw new AssertionError("got " + print(actual) + ", want value " + print(expected)); + } + if (!valuesEqual(expected, actual.getValue())) { + throw new AssertionError( + testPath + + ": eval got [" + + print(actual.getValue()) + + "], want [" + + print(expected) + + "]"); + } + } + + private static void matchError(String testPath, ErrorSet expected, ExprValue actual) { + if (actual.getKindCase() != ExprValue.KindCase.ERROR) { + throw new AssertionError( + testPath + ": got " + print(actual) + ", want error " + print(expected)); + } + } + + private static void matchUnknown(String testPath, UnknownSet expected, ExprValue actual) { + if (actual.getKindCase() != ExprValue.KindCase.UNKNOWN) { + throw new AssertionError( + testPath + ": got " + print(actual) + ", want unknown " + print(expected)); + } + } + + private static boolean valuesEqual(Value expected, Value actual) { + if (expected.getKindCase() != actual.getKindCase()) { + return false; + } + + switch (expected.getKindCase()) { + case DOUBLE_VALUE: + double expectedValue = expected.getDoubleValue(); + double actualValue = actual.getDoubleValue(); + return expectedValue == actualValue + || (Double.isNaN(expectedValue) && Double.isNaN(actualValue)); + case MAP_VALUE: + return mapsEqual(expected.getMapValue(), actual.getMapValue()); + case LIST_VALUE: + if (expected.getListValue().getValuesCount() != actual.getListValue().getValuesCount()) { + return false; + } + for (int i = 0; i < expected.getListValue().getValuesCount(); i++) { + if (!valuesEqual( + expected.getListValue().getValues(i), actual.getListValue().getValues(i))) { + return false; + } + } + return true; + default: + return expected.equals(actual); + } + } + + private static boolean mapsEqual(MapValue expected, MapValue actual) { + if (expected.getEntriesCount() != actual.getEntriesCount()) { + return false; + } + boolean[] matched = new boolean[actual.getEntriesCount()]; + for (MapValue.Entry expectedEntry : expected.getEntriesList()) { + boolean found = false; + for (int i = 0; i < actual.getEntriesCount(); i++) { + if (!matched[i] + && valuesEqual(expectedEntry.getKey(), actual.getEntries(i).getKey()) + && valuesEqual(expectedEntry.getValue(), actual.getEntries(i).getValue())) { + matched[i] = true; + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + + private static Val exprValueToRefValue(TypeAdapter adapter, dev.cel.expr.ExprValue ev) { + try { + return exprValueToRefValue(adapter, convert(ev, ExprValue.class)); + } catch (InvalidProtocolBufferException e) { + throw new IllegalArgumentException("invalid expression value", e); + } + } + + private static Val exprValueToRefValue(TypeAdapter adapter, ExprValue ev) { + return switch (ev.getKindCase()) { + case VALUE -> valueToRefValue(adapter, ev.getValue()); + case ERROR -> newErr("XXX add details later"); + case UNKNOWN -> unknownOf(ev.getUnknown().getExprs(0)); + default -> throw new IllegalArgumentException("unknown ExprValue kind " + ev.getKindCase()); + }; + } + + private static Val valueToRefValue(TypeAdapter adapter, Value v) { + switch (v.getKindCase()) { + case NULL_VALUE: + return NullT.NullValue; + case BOOL_VALUE: + return boolOf(v.getBoolValue()); + case INT64_VALUE: + return intOf(v.getInt64Value()); + case UINT64_VALUE: + return uintOf(v.getUint64Value()); + case DOUBLE_VALUE: + return doubleOf(v.getDoubleValue()); + case STRING_VALUE: + return stringOf(v.getStringValue()); + case BYTES_VALUE: + return bytesOf(v.getBytesValue().toByteArray()); + case OBJECT_VALUE: + return adapter.nativeToValue(v.getObjectValue()); + case MAP_VALUE: + Map entries = new HashMap<>(); + for (Entry entry : v.getMapValue().getEntriesList()) { + entries.put( + valueToRefValue(adapter, entry.getKey()), valueToRefValue(adapter, entry.getValue())); + } + return adapter.nativeToValue(entries); + case LIST_VALUE: + List elements = new ArrayList<>(); + for (Value element : v.getListValue().getValuesList()) { + elements.add(valueToRefValue(adapter, element)); + } + return adapter.nativeToValue(elements); + case TYPE_VALUE: + String typeName = v.getTypeValue(); + org.projectnessie.cel.common.types.ref.Type type = Types.getTypeByName(typeName); + if (type != null) { + return type; + } + return newObjectTypeValue(typeName); + case ENUM_VALUE: + return intOf(v.getEnumValue().getValue()); + default: + throw new IllegalArgumentException("unknown value " + v.getKindCase()); + } + } + + private static ExprValue refValueToExprValue(TypeAdapter adapter, Val res) { + if (isUnknown(res)) { + return ExprValue.newBuilder() + .setUnknown(UnknownSet.newBuilder().addExprs(res.intValue())) + .build(); + } + return ExprValue.newBuilder().setValue(refValueToValue(adapter, res)).build(); + } + + private static Value refValueToValue(TypeAdapter adapter, Val res) { + switch (res.type().typeEnum()) { + case Bool: + return Value.newBuilder().setBoolValue(res.booleanValue()).build(); + case Bytes: + return Value.newBuilder() + .setBytesValue(adapter.valueToNative(res, ByteString.class)) + .build(); + case Double: + return Value.newBuilder().setDoubleValue(adapter.valueToDouble(res)).build(); + case Int: + return Value.newBuilder().setInt64Value(res.intValue()).build(); + case Null: + return Value.newBuilder().setNullValueValue(0).build(); + case String: + return Value.newBuilder().setStringValue(res.value().toString()).build(); + case Type: + return Value.newBuilder().setTypeValue(((TypeT) res).typeName()).build(); + case Uint: + return Value.newBuilder().setUint64Value(res.intValue()).build(); + case Duration: + return Value.newBuilder() + .setObjectValue(Any.pack(adapter.valueToNative(res, Duration.class))) + .build(); + case Timestamp: + return Value.newBuilder() + .setObjectValue(Any.pack(adapter.valueToNative(res, Timestamp.class))) + .build(); + case List: + Lister lister = (Lister) res; + ListValue.Builder elements = ListValue.newBuilder(); + for (IteratorT i = lister.iterator(); i.hasNext() == True; ) { + elements.addValues(refValueToValue(adapter, i.next())); + } + return Value.newBuilder().setListValue(elements).build(); + case Map: + Mapper mapper = (Mapper) res; + MapValue.Builder entries = MapValue.newBuilder(); + for (IteratorT i = mapper.iterator(); i.hasNext() == True; ) { + Val key = i.next(); + entries + .addEntriesBuilder() + .setKey(refValueToValue(adapter, key)) + .setValue(refValueToValue(adapter, mapper.get(key))); + } + return Value.newBuilder().setMapValue(entries).build(); + case Object: + Message pb = (Message) res.value(); + Value.Builder value = Value.newBuilder(); + if (pb instanceof Any) { + value.setObjectValue(unwrapNestedAny((Any) pb)); + } else if (pb instanceof ListValue) { + value.setListValue((ListValue) pb); + } else if (pb instanceof MapValue) { + value.setMapValue((MapValue) pb); + } else { + value.setObjectValue(Any.pack(pb)); + } + return value.build(); + default: + throw new IllegalStateException(String.format("Unknown %s", res.type().typeEnum())); + } + } + + private static Any unwrapNestedAny(Any any) { + Any current = any; + while (current.is(Any.class)) { + try { + Any next = current.unpack(Any.class); + if (next.equals(current)) { + return current; + } + current = next; + } catch (InvalidProtocolBufferException e) { + return current; + } + } + return current; + } + + private static T convert(Message message, Class targetType) + throws InvalidProtocolBufferException { + try { + @SuppressWarnings("unchecked") + T converted = + (T) targetType.getMethod("parseFrom", byte[].class).invoke(null, message.toByteArray()); + return converted; + } catch (ReflectiveOperationException e) { + Throwable cause = e.getCause(); + if (cause instanceof InvalidProtocolBufferException) { + throw (InvalidProtocolBufferException) cause; + } + throw new IllegalStateException("cannot convert to " + targetType.getName(), e); + } + } + + private static String print(Message message) { + return TEXT_PRINTER.printToString(message); + } + + private static final class SkipList { + private SkipList() {} + + private static Set parse(String... values) { + Set skipTests = new LinkedHashSet<>(); + for (String value : values) { + parse(value, skipTests); + } + return Set.copyOf(skipTests); + } + + private static void parse(String value, Set skipTests) { + int fileSeparator = value.indexOf('/'); + if (fileSeparator < 1 || fileSeparator == value.length() - 1) { + throw new IllegalArgumentException( + "skip argument must contain at least /
: " + value); + } + + String fileName = value.substring(0, fileSeparator); + String sectionString = value.substring(fileSeparator + 1); + for (String sectionValue : sectionString.split(";")) { + int sectionSeparator = sectionValue.indexOf('/'); + if (sectionSeparator < 0) { + skipTests.add(fileName + "/" + sectionValue); + } else { + String sectionName = sectionValue.substring(0, sectionSeparator); + String testString = sectionValue.substring(sectionSeparator + 1); + for (String test : testString.split(",")) { + skipTests.add(fileName + "/" + sectionName + "/" + test); + } + } + } + } + } +} diff --git a/conformance/src/test/java/org/projectnessie/cel/server/ConformanceServerTest.java b/conformance/src/test/java/org/projectnessie/cel/server/ConformanceServerTest.java deleted file mode 100644 index ce79883b..00000000 --- a/conformance/src/test/java/org/projectnessie/cel/server/ConformanceServerTest.java +++ /dev/null @@ -1,325 +0,0 @@ -/* - * Copyright (C) 2021 The Authors of CEL-Java - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.projectnessie.cel.server; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.projectnessie.cel.TestExpr.ExprCall; -import static org.projectnessie.cel.TestExpr.ExprLiteral; -import static org.projectnessie.cel.Util.mapOf; - -import com.google.api.expr.conformance.v1alpha1.CheckRequest; -import com.google.api.expr.conformance.v1alpha1.CheckResponse; -import com.google.api.expr.conformance.v1alpha1.ConformanceServiceGrpc; -import com.google.api.expr.conformance.v1alpha1.ConformanceServiceGrpc.ConformanceServiceBlockingStub; -import com.google.api.expr.conformance.v1alpha1.EvalRequest; -import com.google.api.expr.conformance.v1alpha1.EvalResponse; -import com.google.api.expr.conformance.v1alpha1.ParseRequest; -import com.google.api.expr.conformance.v1alpha1.ParseResponse; -import com.google.api.expr.v1alpha1.CheckedExpr; -import com.google.api.expr.v1alpha1.Constant; -import com.google.api.expr.v1alpha1.Constant.ConstantKindCase; -import com.google.api.expr.v1alpha1.Expr; -import com.google.api.expr.v1alpha1.Expr.Call; -import com.google.api.expr.v1alpha1.Expr.ExprKindCase; -import com.google.api.expr.v1alpha1.ExprValue; -import com.google.api.expr.v1alpha1.ExprValue.KindCase; -import com.google.api.expr.v1alpha1.ParsedExpr; -import com.google.api.expr.v1alpha1.SourceInfo; -import com.google.api.expr.v1alpha1.Type; -import com.google.api.expr.v1alpha1.Type.PrimitiveType; -import com.google.api.expr.v1alpha1.Type.TypeKindCase; -import com.google.api.expr.v1alpha1.Value; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import io.grpc.Server; -import io.grpc.ServerBuilder; -import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.projectnessie.cel.checker.Decls; -import org.projectnessie.cel.common.operators.Operator; - -class ConformanceServerTest { - - private static Server server; - private static ConformanceServiceBlockingStub stub; - private static ManagedChannel channel; - - private static ParsedExpr parsed; - - @BeforeAll - static void startServer() throws Exception { - server = ServerBuilder.forPort(0).addService(new ConformanceServiceImpl()).build(); - server.start(); - - String host = ConformanceServer.getListenHost(server); - - channel = ManagedChannelBuilder.forAddress(host, server.getPort()).usePlaintext().build(); - stub = ConformanceServiceGrpc.newBlockingStub(channel); - - parsed = - ParsedExpr.newBuilder() - .setExpr(ExprCall(1, Operator.Add.id, ExprLiteral(2, 1L), ExprLiteral(3, 1L))) - .setSourceInfo( - SourceInfo.newBuilder() - .setLocation("the location") - .putAllPositions( - mapOf( - 1L, 0, - 2L, 0, - 3L, 4)) - .build()) - .build(); - } - - @AfterAll - static void stopServer() throws Exception { - Exception x = null; - try { - channel.shutdown(); - } catch (Exception e) { - x = e; - } - - try { - server.shutdown(); - } catch (Exception e) { - if (x == null) x = e; - else x.addSuppressed(e); - } - - try { - channel.awaitTermination(30, TimeUnit.SECONDS); - } catch (Exception e) { - if (x == null) x = e; - else x.addSuppressed(e); - } - - try { - server.awaitTermination(30, TimeUnit.SECONDS); - } catch (Exception e) { - if (x == null) x = e; - else x.addSuppressed(e); - } - - if (x != null) throw x; - } - - /** TestParse tests the Parse method. */ - @Test - void Parse() { - ParseRequest req = ParseRequest.newBuilder().setCelSource("1 + 1").build(); - ParseResponse res = stub.parse(req); - assertThat(res.isInitialized()).isTrue(); - assertThat(res.getParsedExpr().isInitialized()).isTrue(); - // Could check against 'parsed' above, - // but the expression ids are arbitrary, - // and explicit comparison logic is about as - // much work as normalization would be. - assertThat(res.getParsedExpr().getExpr().isInitialized()).isTrue(); - assertThat(res.getParsedExpr().getExpr().getExprKindCase()).isSameAs(ExprKindCase.CALL_EXPR); - - Call c = res.getParsedExpr().getExpr().getCallExpr(); - assertThat(c.getTarget().isInitialized()).isTrue(); - assertThat(c.getFunction()).isEqualTo("_+_"); - assertThat(c.getArgsCount()).isEqualTo(2); - for (Expr a : c.getArgsList()) { - assertThat(a.getExprKindCase()).isSameAs(ExprKindCase.CONST_EXPR); - Constant l = a.getConstExpr(); - assertThat(l.getConstantKindCase()).isSameAs(ConstantKindCase.INT64_VALUE); - assertThat(l.getInt64Value()).isEqualTo(1); - } - } - - /** TestCheck tests the Check method. */ - @Test - void Check() { - // If TestParse() passes, it validates a good chunk - // of the server mechanisms for data conversion, so we - // won't be as fussy here.. - CheckRequest req = CheckRequest.newBuilder().setParsedExpr(parsed).build(); - CheckResponse res = stub.check(req); - assertThat(res.isInitialized()).isTrue(); - assertThat(res.getCheckedExpr().isInitialized()).isTrue(); - Type tp = res.getCheckedExpr().getTypeMapMap().get(1L); - assertThat(tp).isNotNull(); - assertThat(tp.getTypeKindCase()).isSameAs(TypeKindCase.PRIMITIVE); - assertThat(tp.getPrimitive()).isSameAs(PrimitiveType.INT64); - } - - /** TestEval tests the Eval method. */ - @Test - void Eval() { - EvalRequest req = EvalRequest.newBuilder().setParsedExpr(parsed).build(); - EvalResponse res = stub.eval(req); - assertThat(res.isInitialized()).isTrue(); - assertThat(res.getResult().isInitialized()).isTrue(); - - assertThat(res.getResult().getKindCase()).isSameAs(KindCase.VALUE); - assertThat(res.getResult().getValue().getKindCase()).isSameAs(Value.KindCase.INT64_VALUE); - assertThat(res.getResult().getValue().getInt64Value()).isEqualTo(2L); - } - - /** TestFullUp tests Parse, Check, and Eval back-to-back. */ - @Test - void FullUp() { - ParseRequest preq = ParseRequest.newBuilder().setCelSource("x + y").build(); - ParseResponse pres = stub.parse(preq); - assertThat(pres.isInitialized()).isTrue(); - ParsedExpr parsedExpr = pres.getParsedExpr(); - assertThat(parsedExpr.isInitialized()).isTrue(); - - CheckRequest creq = - CheckRequest.newBuilder() - .setParsedExpr(parsedExpr) - .addTypeEnv(Decls.newVar("x", Decls.Int)) - .addTypeEnv(Decls.newVar("y", Decls.Int)) - .build(); - CheckResponse cres = stub.check(creq); - assertThat(cres.isInitialized()).isTrue(); - CheckedExpr checkedExpr = cres.getCheckedExpr(); - assertThat(checkedExpr.isInitialized()).isTrue(); - - Type tp = checkedExpr.getTypeMapMap().get(1L); - assertThat(tp).isNotNull(); - assertThat(tp.getTypeKindCase()).isSameAs(TypeKindCase.PRIMITIVE); - assertThat(tp.getPrimitive()).isSameAs(PrimitiveType.INT64); - - EvalRequest ereq = - EvalRequest.newBuilder() - .setCheckedExpr(checkedExpr) - .putBindings("x", exprValueInt64(1)) - .putBindings("y", exprValueInt64(2)) - .build(); - EvalResponse eres = stub.eval(ereq); - assertThat(eres.isInitialized()).isTrue(); - assertThat(eres.getResult().isInitialized()).isTrue(); - - assertThat(eres.getResult().getKindCase()).isSameAs(KindCase.VALUE); - assertThat(eres.getResult().getValue().getKindCase()).isSameAs(Value.KindCase.INT64_VALUE); - assertThat(eres.getResult().getValue().getInt64Value()).isEqualTo(3L); - } - - static ExprValue exprValueInt64(long x) { - return ExprValue.newBuilder().setValue(Value.newBuilder().setInt64Value(x)).build(); - } - - static class FullPipelineResult { - final ParseResponse parseResponse; - final CheckResponse checkResponse; - final EvalResponse evalResponse; - - FullPipelineResult( - ParseResponse parseResponse, CheckResponse checkResponse, EvalResponse evalResponse) { - this.parseResponse = parseResponse; - this.checkResponse = checkResponse; - this.evalResponse = evalResponse; - } - } - - /** - * fullPipeline parses, checks, and evaluates the CEL expression in source and returns the result - * from the Eval call. - */ - FullPipelineResult fullPipeline(String source) { - - // Parse - ParseRequest preq = ParseRequest.newBuilder().setCelSource(source).build(); - ParseResponse pres = stub.parse(preq); - assertThat(pres.isInitialized()).isTrue(); - ParsedExpr parsedExpr = pres.getParsedExpr(); - assertThat(parsedExpr.isInitialized()).isTrue(); - assertThat(parsedExpr.getExpr().isInitialized()).isTrue(); - - // Check - CheckRequest creq = CheckRequest.newBuilder().setParsedExpr(parsedExpr).build(); - CheckResponse cres = stub.check(creq); - assertThat(cres.isInitialized()).isTrue(); - CheckedExpr checkedExpr = cres.getCheckedExpr(); - assertThat(checkedExpr.isInitialized()).isTrue(); - - // Eval - EvalRequest ereq = EvalRequest.newBuilder().setCheckedExpr(checkedExpr).build(); - EvalResponse eres = stub.eval(ereq); - assertThat(eres.isInitialized()).isTrue(); - assertThat(eres.getResult().isInitialized()).isTrue(); - - return new FullPipelineResult(pres, cres, eres); - } - - /** - * expectEvalTrue parses, checks, and evaluates the CEL expression in source and checks that the - * result is the boolean value 'true'. - */ - void expectEvalTrue(String source) { - FullPipelineResult fp = fullPipeline(source); - - long rootID = fp.parseResponse.getParsedExpr().getExpr().getId(); - Type topType = fp.checkResponse.getCheckedExpr().getTypeMapMap().get(rootID); - assertThat(topType).extracting(Type::getTypeKindCase).isEqualTo(Type.TypeKindCase.PRIMITIVE); - assertThat(topType).extracting(Type::getPrimitive).isEqualTo(Type.PrimitiveType.BOOL); - - ExprValue er = fp.evalResponse.getResult(); - assertThat(er).extracting(ExprValue::getKindCase).isEqualTo(ExprValue.KindCase.VALUE); - Value ev = er.getValue(); - assertThat(ev).extracting(Value::getKindCase).isEqualTo(Value.KindCase.BOOL_VALUE); - assertThat(ev).extracting(Value::getBoolValue).isEqualTo(true); - } - - /** TestCondTrue tests true conditional behavior. */ - @Test - void CondTrue() { - expectEvalTrue("(true ? 'a' : 'b') == 'a'"); - } - - /** TestCondFalse tests false conditional behavior. */ - @Test - void CondFalse() { - expectEvalTrue("(false ? 'a' : 'b') == 'b'"); - } - - /** TestMapOrderInsignificant tests that maps with different order are equal. */ - @Test - void MapOrderInsignificant() { - expectEvalTrue("{1: 'a', 2: 'b'} == {2: 'b', 1: 'a'}"); - } - - /** FailsTestOneMetaType tests that types of different types are equal. */ - @Test - void FailsTestOneMetaType() { - expectEvalTrue("type(type(1)) == type(type('foo'))"); - } - - /** FailsTestTypeType tests that the meta-type is its own type. */ - @Test - void FailsTestTypeType() { - expectEvalTrue("type(type) == type"); - } - - /** FailsTestNullTypeName checks that the type of null is "null_type". */ - @Test - void FailsTestNullTypeName() { - expectEvalTrue("type(null) == null_type"); - } - - /** TestError ensures that errors are properly transmitted. */ - @Test - void Error() { - FullPipelineResult fp = fullPipeline("1 / 0"); - assertThat(fp.evalResponse.getResult().getKindCase()).isEqualTo(ExprValue.KindCase.ERROR); - } -} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 67a2b529..52175403 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -24,14 +24,17 @@ plugins { `java-test-fixtures` } +val congocc = configurations.create("congocc") + configurations.named("jmhImplementation") { extendsFrom(configurations.testFixturesApi.get()) } dependencies { - implementation(project(":cel-generated-antlr")) compileOnly(project(":cel-generated-pb")) implementation(libs.agrona) + congocc(libs.congocc) + testImplementation(project(":cel-generated-pb")) testFixturesApi(platform(libs.junit.bom)) testFixturesApi(libs.bundles.junit.testing) @@ -46,13 +49,59 @@ dependencies { jmhAnnotationProcessor(libs.jmh.generator.annprocess) } +abstract class Generate : JavaExec() { + init { + outputs.cacheIf { true } + } + + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val sourceDir: DirectoryProperty + + @get:OutputDirectory abstract val outputDir: DirectoryProperty +} + +val generateCelGrammar = + tasks.register("generateCelGrammar") { + val generatedDir = layout.buildDirectory.dir("generated/sources/congocc/cel") + val projectDir = layout.projectDirectory + + sourceDir = projectDir.dir("src/main/congocc/cel") + outputDir = generatedDir + + classpath(congocc) + mainClass = "org.congocc.app.Main" + workingDir(projectDir) + + doFirst { generatedDir.get().asFile.deleteRecursively() } + + argumentProviders.add( + CommandLineArgumentProvider { + val sourceFile = sourceDir.file("cel-java.ccc").get().asFile.relativeTo(projectDir.asFile) + val base = + listOf( + "-d", + generatedDir.get().asFile.toString(), + "-jdk17", + "-n", + sourceFile.toString(), + ) + if (logger.isInfoEnabled) base else base + "-q" + } + ) + } + jmh { jmhVersion.set(libs.versions.jmh.get()) } +sourceSets.main { java.srcDir(generateCelGrammar) } + sourceSets.test { java.srcDir(layout.buildDirectory.dir("generated/source/proto/test/java")) java.destinationDirectory.set(layout.buildDirectory.dir("classes/java/generatedTest")) } +tasks.named("compileJava") { dependsOn(generateCelGrammar) } + tasks.named("check") { dependsOn(tasks.named("jmh")) } tasks.named("assemble") { dependsOn(tasks.named("jmhJar")) } diff --git a/core/src/jmh/java/org/projectnessie/cel/CompileBuildBench.java b/core/src/jmh/java/org/projectnessie/cel/CompileBuildBench.java index 76bb13ab..8fe49da5 100644 --- a/core/src/jmh/java/org/projectnessie/cel/CompileBuildBench.java +++ b/core/src/jmh/java/org/projectnessie/cel/CompileBuildBench.java @@ -51,17 +51,17 @@ public static class CompileState { public String expression; String source() { - switch (expression) { - case "simplePredicate": - return "resource == 'projects/p1' && user == 'alice'"; - case "deepSelectors": - return "request.auth.claims.email.endsWith('@example.com')" - + " && request.resource.labels['env'] == 'prod'"; - case "macroPipeline": - return "items.filter(i, i.score > 10).map(i, i.name).exists(n, n.startsWith('a'))"; - default: - throw new IllegalArgumentException("Unknown compile benchmark expression: " + expression); - } + return switch (expression) { + case "simplePredicate" -> "resource == 'projects/p1' && user == 'alice'"; + case "deepSelectors" -> + "request.auth.claims.email.endsWith('@example.com')" + + " && request.resource.labels['env'] == 'prod'"; + case "macroPipeline" -> + "items.filter(i, i.score > 10).map(i, i.name).exists(n, n.startsWith('a'))"; + default -> + throw new IllegalArgumentException( + "Unknown compile benchmark expression: " + expression); + }; } } diff --git a/core/src/jmh/java/org/projectnessie/cel/EvaluatorBaselineBench.java b/core/src/jmh/java/org/projectnessie/cel/EvaluatorBaselineBench.java new file mode 100644 index 00000000..35025ca1 --- /dev/null +++ b/core/src/jmh/java/org/projectnessie/cel/EvaluatorBaselineBench.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel; + +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EnvOption.types; +import static org.projectnessie.cel.EvalOption.OptOptimize; +import static org.projectnessie.cel.ProgramOption.evalOptions; +import static org.projectnessie.cel.interpreter.Activation.newActivation; + +import dev.cel.expr.conformance.proto3.TestAllTypes; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.interpreter.Activation; + +/** Attributes evaluator cost at the raw, public-program, and native-result boundaries. */ +@Warmup(iterations = 3, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class EvaluatorBaselineBench { + + @State(Scope.Benchmark) + public static class EvaluationState { + @Param({ + "identityInt", + "addInt", + "chainInt", + "chainDouble", + "chainString", + "shortCircuit", + "mapSelection", + "protoSelection" + }) + public String expression; + + Program program; + Prog internalProgram; + Map variables; + Activation activation; + Class nativeResultType; + Supplier nativeJava; + + @Setup + public void init() { + Env env; + String source; + switch (expression) { + case "identityInt": + env = newEnv(declarations(Decls.newVar("x", Decls.Int))); + source = "x"; + variables = Map.of("x", 41L); + nativeResultType = Long.class; + nativeJava = () -> variables.get("x"); + break; + case "addInt": + env = newEnv(declarations(Decls.newVar("x", Decls.Int))); + source = "x + 1"; + variables = Map.of("x", 41L); + nativeResultType = Long.class; + nativeJava = () -> (long) variables.get("x") + 1L; + break; + case "chainInt": + env = newEnv(declarations(Decls.newVar("x", Decls.Int))); + source = "((x + 1) * 3 - 2) / 2"; + variables = Map.of("x", 41L); + nativeResultType = Long.class; + nativeJava = () -> (((long) variables.get("x") + 1L) * 3L - 2L) / 2L; + break; + case "chainDouble": + env = newEnv(declarations(Decls.newVar("x", Decls.Double))); + source = "((x + 1.25) * 3.0 - 2.0) / 2.0"; + variables = Map.of("x", 41.5d); + nativeResultType = Double.class; + nativeJava = () -> (((double) variables.get("x") + 1.25d) * 3.0d - 2.0d) / 2.0d; + break; + case "chainString": + env = + newEnv( + declarations( + Decls.newVar("prefix", Decls.String), + Decls.newVar("x", Decls.String), + Decls.newVar("suffix", Decls.String))); + source = "prefix + x + suffix"; + variables = Map.of("prefix", "pre-", "x", "value", "suffix", "-post"); + nativeResultType = String.class; + nativeJava = + () -> (String) variables.get("prefix") + variables.get("x") + variables.get("suffix"); + break; + case "shortCircuit": + env = + newEnv( + declarations( + Decls.newVar("enabled", Decls.Bool), Decls.newVar("expensive", Decls.Bool))); + source = "enabled && expensive"; + variables = Map.of("enabled", false, "expensive", true); + nativeResultType = Boolean.class; + nativeJava = + () -> (boolean) variables.get("enabled") && (boolean) variables.get("expensive"); + break; + case "mapSelection": + env = + newEnv( + declarations( + Decls.newVar("attrs", Decls.newMapType(Decls.String, Decls.Int)), + Decls.newVar("key", Decls.String), + Decls.newVar("target", Decls.Int))); + source = "attrs[key] == target"; + variables = Map.of("attrs", Map.of("answer", 42L), "key", "answer", "target", 42L); + nativeResultType = Boolean.class; + nativeJava = + () -> + ((Map) variables.get("attrs")) + .get(variables.get("key")) + .equals(variables.get("target")); + break; + case "protoSelection": + env = + newEnv( + types(TestAllTypes.getDefaultInstance()), + declarations( + Decls.newVar( + "msg", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")), + Decls.newVar("target", Decls.Int))); + source = "msg.single_int64 == target"; + TestAllTypes message = TestAllTypes.newBuilder().setSingleInt64(42L).build(); + variables = Map.of("msg", message, "target", 42L); + nativeResultType = Boolean.class; + nativeJava = () -> message.getSingleInt64() == (long) variables.get("target"); + break; + default: + throw new IllegalArgumentException( + "Unknown evaluator benchmark expression: " + expression); + } + + AstIssuesTuple ast = env.compile(source); + if (ast.hasIssues()) { + throw ast.getIssues().err(); + } + program = env.program(ast.getAst(), evalOptions(OptOptimize)); + internalProgram = (Prog) program; + activation = newActivation(variables); + } + } + + @Benchmark + public Object rawEvaluator(EvaluationState state) { + return state.internalProgram.interpretable.eval(state.activation); + } + + @Benchmark + public Program.EvalResult programEval(EvaluationState state) { + return state.program.eval(state.variables); + } + + @Benchmark + public Object programEvalNative(EvaluationState state) { + return state.internalProgram.e.adapter.valueToNative( + state.program.eval(state.variables).getVal(), state.nativeResultType); + } + + @Benchmark + public Object nativeJava(EvaluationState state) { + return state.nativeJava.get(); + } +} diff --git a/core/src/jmh/java/org/projectnessie/cel/NativePlanBench.java b/core/src/jmh/java/org/projectnessie/cel/NativePlanBench.java new file mode 100644 index 00000000..eeb83884 --- /dev/null +++ b/core/src/jmh/java/org/projectnessie/cel/NativePlanBench.java @@ -0,0 +1,2439 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel; + +import static java.util.Objects.requireNonNull; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EnvOption.types; +import static org.projectnessie.cel.EvalOption.OptDisableNativeEval; +import static org.projectnessie.cel.ProgramOption.evalOptions; +import static org.projectnessie.cel.interpreter.Activation.newActivation; + +import com.google.protobuf.DynamicMessage; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; +import org.projectnessie.cel.interpreter.Activation; +import org.projectnessie.cel.interpreter.Interpretable; +import org.projectnessie.cel.interpreter.NativeCapabilityBenchmark; + +/** Measures production native scalar plans against the existing evaluator and program boundary. */ +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class NativePlanBench { + @State(Scope.Benchmark) + public abstract static class ScalarState { + @Param({"1", "4", "16"}) + int depth; + + Env env; + Ast ast; + Prog nativeProgram; + NativeCapabilityBenchmark nativePlan; + Program currentProgram; + Interpretable currentPlan; + Activation activation; + Map variables; + + final void setup(TypeFixture fixture) { + env = newEnv(declarations(Decls.newVar("x", fixture.type))); + ast = compile(env, expression(fixture)); + nativeProgram = (Prog) env.program(ast); + nativePlan = requireNative(nativeProgram); + currentProgram = env.program(ast, evalOptions(OptDisableNativeEval)); + currentPlan = ((Prog) currentProgram).interpretable; + variables = Map.of("x", fixture.value); + activation = newActivation(variables); + assertEquivalent(nativePlan.eval(activation), currentPlan.eval(activation)); + } + + private String expression(TypeFixture fixture) { + String source = "x"; + for (int i = 0; i < depth; i++) { + source = fixture.next(source, i); + } + return source; + } + } + + @State(Scope.Benchmark) + public static class IntState extends ScalarState { + @Setup + public void setup() { + setup( + new TypeFixture(Decls.Int, 50_021L) { + @Override + String next(String source, int level) { + return switch (level & 3) { + case 0 -> "(" + source + " + 17)"; + case 1 -> "(" + source + " * 3)"; + case 2 -> "(" + source + " - 11)"; + default -> "(" + source + " / 2)"; + }; + } + }); + } + } + + @State(Scope.Benchmark) + public static class DoubleState extends ScalarState { + @Setup + public void setup() { + setup( + new TypeFixture(Decls.Double, 50_021.25d) { + @Override + String next(String source, int level) { + return switch (level & 3) { + case 0 -> "(" + source + " + 17.25)"; + case 1 -> "(" + source + " * 1.5)"; + case 2 -> "(" + source + " - 11.5)"; + default -> "(" + source + " / 2.0)"; + }; + } + }); + } + } + + @State(Scope.Benchmark) + public static class ConstructionState { + @Param({ + "smallNative", + "deepNative", + "unsupportedRoot", + "unsupportedLate", + "literal16", + "literal1024" + }) + String shape; + + Env env; + Ast ast; + Map variables; + + @Setup + public void setup() { + env = newEnv(declarations(Decls.newVar("x", Decls.Int))); + String source = + switch (shape) { + case "smallNative" -> "x + 17"; + case "deepNative" -> deepExpression(); + case "unsupportedRoot" -> "[x][0]"; + case "unsupportedLate" -> deepExpression() + " + [x][0]"; + case "literal16" -> listLiteralIndexExpression(16); + case "literal1024" -> listLiteralIndexExpression(1024); + default -> throw new IllegalArgumentException("unknown construction shape " + shape); + }; + ast = compile(env, source); + variables = Map.of("x", 50_021L); + } + } + + @State(Scope.Benchmark) + public static class MapSelectorState { + @Param({"dot", "constantIndex"}) + String shape; + + NativeCapabilityBenchmark comparisonPlan; + NativeCapabilityBenchmark terminalPlan; + Interpretable currentComparison; + Interpretable currentTerminal; + Program nativeProgram; + Program currentProgram; + Activation activation; + Map variables; + Map values; + + @SuppressWarnings("DuplicatedCode") + @Setup + public void setup() { + Env env = + newEnv( + declarations( + Decls.newVar("attrs", Decls.newMapType(Decls.String, Decls.Int)), + Decls.newVar("key", Decls.String), + Decls.newVar("target", Decls.Int))); + String selection = + switch (shape) { + case "dot" -> "attrs.answer"; + case "constantIndex" -> "attrs['answer']"; + default -> throw new IllegalArgumentException("unknown map selector shape " + shape); + }; + Ast comparisonAst = compile(env, selection + " == target"); + Ast terminalAst = compile(env, selection); + Prog comparisonProgram = (Prog) env.program(comparisonAst); + Prog terminalProgram = (Prog) env.program(terminalAst); + comparisonPlan = requireNative(comparisonProgram); + terminalPlan = requireNative(terminalProgram); + currentComparison = currentPlan(comparisonProgram, comparisonAst); + currentTerminal = currentPlan(terminalProgram, terminalAst); + nativeProgram = comparisonProgram; + currentProgram = env.program(comparisonAst, evalOptions(OptDisableNativeEval)); + values = Map.of("answer", 50_021L); + variables = Map.of("attrs", values, "key", "answer", "target", 50_021L); + activation = newActivation(variables); + assertEquivalent(comparisonPlan.eval(activation), currentComparison.eval(activation)); + assertEquivalent(terminalPlan.eval(activation), currentTerminal.eval(activation)); + } + } + + @State(Scope.Benchmark) + public static class ProtoSelectorState { + @Param({"generated", "dynamic"}) + String representation; + + NativeCapabilityBenchmark comparisonPlan; + NativeCapabilityBenchmark terminalPlan; + Interpretable currentComparison; + Interpretable currentTerminal; + Program nativeProgram; + Program currentProgram; + Activation activation; + Map variables; + TestAllTypes generated; + + @SuppressWarnings("DuplicatedCode") + @Setup + public void setup() throws Exception { + generated = TestAllTypes.newBuilder().setSingleInt64(50_021L).build(); + Object message = + representation.equals("generated") + ? generated + : DynamicMessage.parseFrom(TestAllTypes.getDescriptor(), generated.toByteString()); + Env env = + newEnv( + types(TestAllTypes.getDefaultInstance()), + declarations( + Decls.newVar( + "msg", Decls.newObjectType(TestAllTypes.getDescriptor().getFullName())), + Decls.newVar("target", Decls.Int))); + Ast comparisonAst = compile(env, "msg.single_int64 == target"); + Ast terminalAst = compile(env, "msg.single_int64"); + Prog comparisonProgram = (Prog) env.program(comparisonAst); + Prog terminalProgram = (Prog) env.program(terminalAst); + comparisonPlan = requireNative(comparisonProgram); + terminalPlan = requireNative(terminalProgram); + currentComparison = currentPlan(comparisonProgram, comparisonAst); + currentTerminal = currentPlan(terminalProgram, terminalAst); + nativeProgram = comparisonProgram; + currentProgram = env.program(comparisonAst, evalOptions(OptDisableNativeEval)); + variables = Map.of("msg", message, "target", 50_021L); + activation = newActivation(variables); + assertEquivalent(comparisonPlan.eval(activation), currentComparison.eval(activation)); + assertEquivalent(terminalPlan.eval(activation), currentTerminal.eval(activation)); + } + } + + @State(Scope.Benchmark) + public static class ProtoStringSelectorState { + @Param({"generated", "dynamic"}) + String representation; + + NativeCapabilityBenchmark comparisonPlan; + Interpretable currentComparison; + Program nativeProgram; + Program currentProgram; + Activation activation; + Map variables; + + @Setup + public void setup() throws Exception { + TestAllTypes generated = + TestAllTypes.newBuilder().setSingleString("native-selector-value").build(); + Object message = + representation.equals("generated") + ? generated + : DynamicMessage.parseFrom(TestAllTypes.getDescriptor(), generated.toByteString()); + Env env = + newEnv( + types(TestAllTypes.getDefaultInstance()), + declarations( + Decls.newVar( + "msg", Decls.newObjectType(TestAllTypes.getDescriptor().getFullName())), + Decls.newVar("target", Decls.String))); + Ast ast = compile(env, "msg.single_string == target"); + Prog program = (Prog) env.program(ast); + comparisonPlan = requireNative(program); + currentComparison = currentPlan(program, ast); + nativeProgram = program; + currentProgram = env.program(ast, evalOptions(OptDisableNativeEval)); + variables = Map.of("msg", message, "target", "native-selector-value"); + activation = newActivation(variables); + assertEquivalent(comparisonPlan.eval(activation), currentComparison.eval(activation)); + } + } + + @State(Scope.Benchmark) + public static class ListIndexState { + @Param({"longArray", "javaList", "adaptedList"}) + String representation; + + @Param({"16", "1024"}) + int size; + + int index; + long target; + Object values; + NativeCapabilityBenchmark comparisonPlan; + NativeCapabilityBenchmark terminalPlan; + Interpretable currentComparison; + Interpretable currentTerminal; + Program nativeProgram; + Program currentProgram; + Activation activation; + Map variables; + + @SuppressWarnings("DuplicatedCode") + @Setup + public void setup() { + index = size / 2; + target = 50_021L; + long[] longValues = new long[size]; + List javaValues = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + long value = i == index ? target : i; + longValues[i] = value; + javaValues.add(value); + } + values = + switch (representation) { + case "longArray" -> longValues; + case "javaList" -> List.copyOf(javaValues); + case "adaptedList" -> DefaultTypeAdapter.Instance.nativeToValue(javaValues); + default -> + throw new IllegalArgumentException("unknown list representation " + representation); + }; + + Env env = + newEnv( + declarations( + Decls.newVar("numbers", Decls.newListType(Decls.Int)), + Decls.newVar("target", Decls.Int))); + String selection = "numbers[" + index + "]"; + Ast comparisonAst = compile(env, selection + " == target"); + Ast terminalAst = compile(env, selection); + Prog comparisonProgram = (Prog) env.program(comparisonAst); + Prog terminalProgram = (Prog) env.program(terminalAst); + comparisonPlan = requireNative(comparisonProgram); + terminalPlan = requireNative(terminalProgram); + currentComparison = currentPlan(comparisonProgram, comparisonAst); + currentTerminal = currentPlan(terminalProgram, terminalAst); + nativeProgram = comparisonProgram; + currentProgram = env.program(comparisonAst, evalOptions(OptDisableNativeEval)); + variables = Map.of("numbers", values, "target", target); + activation = newActivation(variables); + assertEquivalent(comparisonPlan.eval(activation), currentComparison.eval(activation)); + assertEquivalent(terminalPlan.eval(activation), currentTerminal.eval(activation)); + } + } + + @State(Scope.Benchmark) + public static class StringListIndexState { + @Param({"stringArray", "javaList", "adaptedList"}) + String representation; + + @Param({"16", "1024"}) + int size; + + int index; + String target; + Object values; + NativeCapabilityBenchmark comparisonPlan; + NativeCapabilityBenchmark terminalPlan; + Interpretable currentComparison; + Interpretable currentTerminal; + Program nativeProgram; + Program currentProgram; + Activation activation; + Map variables; + + @SuppressWarnings("DuplicatedCode") + @Setup + public void setup() { + index = size / 2; + String[] stringValues = new String[size]; + List javaValues = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + String value = "value-" + i; + stringValues[i] = value; + javaValues.add(value); + } + target = stringValues[index]; + values = + switch (representation) { + case "stringArray" -> stringValues; + case "javaList" -> List.copyOf(javaValues); + case "adaptedList" -> DefaultTypeAdapter.Instance.nativeToValue(javaValues); + default -> + throw new IllegalArgumentException("unknown list representation " + representation); + }; + + Env env = + newEnv( + declarations( + Decls.newVar("words", Decls.newListType(Decls.String)), + Decls.newVar("target", Decls.String))); + String selection = "words[" + index + "]"; + Ast comparisonAst = compile(env, selection + " == target"); + Ast terminalAst = compile(env, selection); + Prog comparisonProgram = (Prog) env.program(comparisonAst); + Prog terminalProgram = (Prog) env.program(terminalAst); + comparisonPlan = requireNative(comparisonProgram); + terminalPlan = requireNative(terminalProgram); + currentComparison = currentPlan(comparisonProgram, comparisonAst); + currentTerminal = currentPlan(terminalProgram, terminalAst); + nativeProgram = comparisonProgram; + currentProgram = env.program(comparisonAst, evalOptions(OptDisableNativeEval)); + variables = Map.of("words", values, "target", target); + activation = newActivation(variables); + assertEquivalent(comparisonPlan.eval(activation), currentComparison.eval(activation)); + assertEquivalent(terminalPlan.eval(activation), currentTerminal.eval(activation)); + } + } + + @State(Scope.Benchmark) + public static class StringListMembershipState { + @Param({"stringArray", "javaList", "adaptedList"}) + String representation; + + @Param({"16", "1024"}) + int size; + + @Param({"first", "middle", "absent"}) + String position; + + String needle; + Object values; + NativeCapabilityBenchmark nativePlan; + Interpretable currentPlan; + Program nativeProgram; + Program currentProgram; + Activation activation; + Map variables; + + @SuppressWarnings("DuplicatedCode") + @Setup + public void setup() { + String[] stringValues = new String[size]; + List javaValues = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + String value = "value-" + i; + stringValues[i] = value; + javaValues.add(value); + } + needle = + switch (position) { + case "first" -> stringValues[0]; + case "middle" -> stringValues[size / 2]; + case "absent" -> "absent"; + default -> throw new IllegalArgumentException("unknown match position " + position); + }; + values = + switch (representation) { + case "stringArray" -> stringValues; + case "javaList" -> List.copyOf(javaValues); + case "adaptedList" -> DefaultTypeAdapter.Instance.nativeToValue(javaValues); + default -> + throw new IllegalArgumentException("unknown list representation " + representation); + }; + + Env env = + newEnv( + declarations( + Decls.newVar("needle", Decls.String), + Decls.newVar("values", Decls.newListType(Decls.String)))); + Ast ast = compile(env, "needle in values"); + Prog program = (Prog) env.program(ast); + nativePlan = requireNative(program); + currentPlan = currentPlan(program, ast); + nativeProgram = program; + currentProgram = env.program(ast, evalOptions(OptDisableNativeEval)); + variables = Map.of("needle", needle, "values", values); + activation = newActivation(variables); + assertEquivalent(nativePlan.eval(activation), currentPlan.eval(activation)); + } + } + + @State(Scope.Benchmark) + public static class MappedStringMembershipState { + @Param({"unfilteredIdentity", "unfilteredComputed", "filteredComputed", "filter"}) + String shape; + + @Param({"16", "1024"}) + int size; + + @Param({"first", "middle", "absent"}) + String position; + + String needle; + String[] values; + String suffix; + String excluded; + NativeCapabilityBenchmark nativePlan; + Interpretable currentPlan; + Program nativeProgram; + Program currentProgram; + Activation activation; + Map variables; + + @Setup + public void setup() { + values = new String[size]; + for (int i = 0; i < size; i++) { + values[i] = "value-" + i; + } + suffix = "-mapped"; + excluded = values[size - 1]; + boolean filtered = shape.equals("filteredComputed") || shape.equals("filter"); + int acceptedSize = filtered ? size - 1 : size; + int matchIndex = + switch (position) { + case "first" -> 0; + case "middle" -> acceptedSize / 2; + case "absent" -> -1; + default -> throw new IllegalArgumentException("unknown match position " + position); + }; + boolean computed = shape.equals("unfilteredComputed") || shape.equals("filteredComputed"); + needle = matchIndex == -1 ? "absent" : values[matchIndex] + (computed ? suffix : ""); + + Env env = + newEnv( + declarations( + Decls.newVar("needle", Decls.String), + Decls.newVar("values", Decls.newListType(Decls.String)), + Decls.newVar("suffix", Decls.String), + Decls.newVar("excluded", Decls.String))); + String expression = + switch (shape) { + case "unfilteredIdentity" -> "needle in values.map(value, value)"; + case "unfilteredComputed" -> "needle in values.map(value, value + suffix)"; + case "filteredComputed" -> + "needle in values.map(value, value != excluded, value + suffix)"; + case "filter" -> "needle in values.filter(value, value != excluded)"; + default -> + throw new IllegalArgumentException("unknown mapped membership shape " + shape); + }; + Ast ast = compile(env, expression); + Prog program = (Prog) env.program(ast); + nativePlan = requireNative(program); + currentPlan = currentPlan(program, ast); + nativeProgram = program; + currentProgram = env.program(ast, evalOptions(OptDisableNativeEval)); + variables = + Map.of("needle", needle, "values", values, "suffix", suffix, "excluded", excluded); + activation = newActivation(variables); + assertEquivalent(nativePlan.eval(activation), currentPlan.eval(activation)); + } + } + + @State(Scope.Benchmark) + public static class QuantifierState { + @Param({"exists", "all", "existsOne"}) + String quantifier; + + @Param({"longArray", "stringArray"}) + String representation; + + @Param({"16", "1024"}) + int size; + + @Param({"first", "last", "absent", "twice"}) + String position; + + Object values; + Object target; + NativeCapabilityBenchmark nativePlan; + Interpretable currentPlan; + Program nativeProgram; + Program currentProgram; + Activation activation; + Map variables; + + @Setup + public void setup() { + int targetIndex = + switch (position) { + case "first", "twice" -> 0; + case "last" -> size - 1; + case "absent" -> -1; + default -> throw new IllegalArgumentException("unknown match position " + position); + }; + Env env; + String expression; + if (representation.equals("longArray")) { + long[] longValues = new long[size]; + for (int i = 0; i < size; i++) { + longValues[i] = i; + } + if (position.equals("twice")) { + longValues[size - 1] = 0L; + } + values = longValues; + target = targetIndex >= 0 ? (long) targetIndex : -1L; + env = + newEnv( + declarations( + Decls.newVar("values", Decls.newListType(Decls.Int)), + Decls.newVar("target", Decls.Int))); + expression = "values"; + } else if (representation.equals("stringArray")) { + String[] stringValues = new String[size]; + for (int i = 0; i < size; i++) { + stringValues[i] = "value-" + i; + } + if (position.equals("twice")) { + stringValues[size - 1] = stringValues[0]; + } + values = stringValues; + target = targetIndex >= 0 ? stringValues[targetIndex] : "absent"; + env = + newEnv( + declarations( + Decls.newVar("values", Decls.newListType(Decls.String)), + Decls.newVar("target", Decls.String))); + expression = "values"; + } else { + throw new IllegalArgumentException("unknown list representation " + representation); + } + expression += + switch (quantifier) { + case "exists" -> ".exists(value, value == target)"; + case "all" -> ".all(value, value != target)"; + case "existsOne" -> ".exists_one(value, value == target)"; + default -> throw new IllegalArgumentException("unknown quantifier " + quantifier); + }; + + Ast ast = compile(env, expression); + Prog program = (Prog) env.program(ast); + nativePlan = requireNative(program); + currentPlan = currentPlan(program, ast); + nativeProgram = program; + currentProgram = env.program(ast, evalOptions(OptDisableNativeEval)); + variables = Map.of("values", values, "target", target); + activation = newActivation(variables); + assertEquivalent(nativePlan.eval(activation), currentPlan.eval(activation)); + } + } + + @State(Scope.Benchmark) + public static class MapAggregateState { + @Param({"exists", "all", "existsOne"}) + String quantifier; + + @Param({"unfiltered", "filteredNone", "filteredHalf", "filteredAlmostAll"}) + String shape; + + @Param({"identity", "computed"}) + String transformation; + + @Param({"16", "1024"}) + int size; + + @Param({"first", "last", "absent", "twice"}) + String position; + + long[] values; + long argument; + long excluded; + long target; + NativeCapabilityBenchmark nativePlan; + Interpretable currentPlan; + Program nativeProgram; + Program currentProgram; + Activation activation; + Map variables; + + @Setup + public void setup() { + values = new long[size]; + for (int i = 0; i < size; i++) { + values[i] = i; + } + argument = 17L; + excluded = values[size / 2]; + int targetIndex = + switch (position) { + case "first" -> 0; + case "last" -> shape.equals("filteredHalf") ? size - 2 : size - 1; + case "absent" -> -1; + case "twice" -> { + values[size - 1] = values[0]; + yield 0; + } + default -> throw new IllegalArgumentException("unknown match position " + position); + }; + target = + targetIndex >= 0 + ? values[targetIndex] + (transformation.equals("computed") ? argument : 0L) + : -1L; + + Env env = + newEnv( + declarations( + Decls.newVar("values", Decls.newListType(Decls.Int)), + Decls.newVar("argument", Decls.Int), + Decls.newVar("excluded", Decls.Int), + Decls.newVar("target", Decls.Int))); + String transform = transformation.equals("identity") ? "value" : "value + argument"; + String aggregate = + switch (quantifier) { + case "exists" -> ".exists(mapped, mapped == target)"; + case "all" -> ".all(mapped, mapped != target)"; + case "existsOne" -> ".exists_one(mapped, mapped == target)"; + default -> throw new IllegalArgumentException("unknown quantifier " + quantifier); + }; + String mapping = + switch (shape) { + case "unfiltered" -> "values.map(value, " + transform + ")"; + case "filteredNone" -> + transformation.equals("identity") + ? "values.filter(value, false)" + : "values.map(value, false, " + transform + ")"; + case "filteredHalf" -> + transformation.equals("identity") + ? "values.filter(value, value % 2 == 0)" + : "values.map(value, value % 2 == 0, " + transform + ")"; + case "filteredAlmostAll" -> + transformation.equals("identity") + ? "values.filter(value, value != excluded)" + : "values.map(value, value != excluded, " + transform + ")"; + default -> + throw new IllegalArgumentException("unknown mapped aggregate shape " + shape); + }; + Ast ast = compile(env, mapping + aggregate); + Prog program = (Prog) env.program(ast); + nativePlan = requireNative(program); + currentPlan = currentPlan(program, ast); + nativeProgram = program; + currentProgram = env.program(ast, evalOptions(OptDisableNativeEval)); + variables = + Map.of("values", values, "argument", argument, "excluded", excluded, "target", target); + activation = newActivation(variables); + assertEquivalent(nativePlan.eval(activation), currentPlan.eval(activation)); + } + + boolean accepts(long value) { + return switch (shape) { + case "unfiltered" -> true; + case "filteredNone" -> false; + case "filteredHalf" -> value % 2 == 0; + case "filteredAlmostAll" -> value != excluded; + default -> throw new IllegalArgumentException("unknown mapped aggregate shape " + shape); + }; + } + } + + @State(Scope.Benchmark) + public static class FilterSizeState { + @Param({"longArray", "stringArray"}) + String representation; + + @Param({"16", "1024"}) + int size; + + @Param({"first", "last", "absent"}) + String position; + + Object values; + Object target; + NativeCapabilityBenchmark nativePlan; + Interpretable currentPlan; + Program nativeProgram; + Program currentProgram; + Activation activation; + Map variables; + + @SuppressWarnings("DuplicatedCode") + @Setup + public void setup() { + int targetIndex = + switch (position) { + case "first" -> 0; + case "last" -> size - 1; + case "absent" -> -1; + default -> throw new IllegalArgumentException("unknown match position " + position); + }; + Env env; + if (representation.equals("longArray")) { + long[] longValues = new long[size]; + for (int i = 0; i < size; i++) { + longValues[i] = i; + } + values = longValues; + target = targetIndex >= 0 ? (long) targetIndex : -1L; + env = + newEnv( + declarations( + Decls.newVar("values", Decls.newListType(Decls.Int)), + Decls.newVar("target", Decls.Int))); + } else if (representation.equals("stringArray")) { + String[] stringValues = new String[size]; + for (int i = 0; i < size; i++) { + stringValues[i] = "value-" + i; + } + values = stringValues; + target = targetIndex >= 0 ? stringValues[targetIndex] : "absent"; + env = + newEnv( + declarations( + Decls.newVar("values", Decls.newListType(Decls.String)), + Decls.newVar("target", Decls.String))); + } else { + throw new IllegalArgumentException("unknown list representation " + representation); + } + + Ast ast = compile(env, "size(values.filter(value, value == target))"); + Prog program = (Prog) env.program(ast); + nativePlan = requireNative(program); + currentPlan = currentPlan(program, ast); + nativeProgram = program; + currentProgram = env.program(ast, evalOptions(OptDisableNativeEval)); + variables = Map.of("values", values, "target", target); + activation = newActivation(variables); + assertEquivalent(nativePlan.eval(activation), currentPlan.eval(activation)); + } + } + + @State(Scope.Benchmark) + public static class MapSizeState { + @Param({"longArray", "stringArray"}) + String representation; + + @Param({"16", "1024"}) + int size; + + @Param({"identity", "computed"}) + String transformation; + + @Param({"unfiltered", "filtered"}) + String mapping; + + Object values; + Object argument; + Object excluded; + NativeCapabilityBenchmark nativePlan; + Interpretable currentPlan; + Program nativeProgram; + Program currentProgram; + Activation activation; + Map variables; + + @SuppressWarnings("DuplicatedCode") + @Setup + public void setup() { + Env env; + if (representation.equals("longArray")) { + long[] longValues = new long[size]; + for (int i = 0; i < size; i++) { + longValues[i] = i; + } + values = longValues; + argument = 17L; + excluded = (long) size - 1L; + env = + newEnv( + declarations( + Decls.newVar("values", Decls.newListType(Decls.Int)), + Decls.newVar("argument", Decls.Int), + Decls.newVar("excluded", Decls.Int))); + } else if (representation.equals("stringArray")) { + String[] stringValues = new String[size]; + for (int i = 0; i < size; i++) { + stringValues[i] = "value-" + i; + } + values = stringValues; + argument = "-mapped"; + excluded = stringValues[size - 1]; + env = + newEnv( + declarations( + Decls.newVar("values", Decls.newListType(Decls.String)), + Decls.newVar("argument", Decls.String), + Decls.newVar("excluded", Decls.String))); + } else { + throw new IllegalArgumentException("unknown list representation " + representation); + } + + String transform = transformation.equals("identity") ? "value" : "value + argument"; + String expression = + switch (mapping) { + case "unfiltered" -> "size(values.map(value, " + transform + "))"; + case "filtered" -> "size(values.map(value, value != excluded, " + transform + "))"; + default -> throw new IllegalArgumentException("unknown mapping " + mapping); + }; + Ast ast = compile(env, expression); + Prog program = (Prog) env.program(ast); + nativePlan = requireNative(program); + currentPlan = currentPlan(program, ast); + nativeProgram = program; + currentProgram = env.program(ast, evalOptions(OptDisableNativeEval)); + variables = + mapping.equals("filtered") + ? Map.of("values", values, "argument", argument, "excluded", excluded) + : Map.of("values", values, "argument", argument); + activation = newActivation(variables); + assertEquivalent(nativePlan.eval(activation), currentPlan.eval(activation)); + } + } + + @State(Scope.Benchmark) + public static class MapIndexState { + @Param({"longArray", "stringArray"}) + String representation; + + @Param({"16", "1024"}) + int size; + + @Param({"identity", "computed"}) + String transformation; + + @Param({"unfiltered", "filtered"}) + String mapping; + + @Param({"first", "middle", "last"}) + String position; + + int index; + Object values; + Object argument; + Object excluded; + Object target; + NativeCapabilityBenchmark nativePlan; + Interpretable currentPlan; + Program nativeProgram; + Program currentProgram; + Activation activation; + Map variables; + + @SuppressWarnings("DuplicatedCode") + @Setup + public void setup() { + int acceptedSize = + switch (mapping) { + case "unfiltered" -> size; + case "filtered" -> size - 1; + default -> throw new IllegalArgumentException("unknown mapping " + mapping); + }; + index = + switch (position) { + case "first" -> 0; + case "middle" -> acceptedSize / 2; + case "last" -> acceptedSize - 1; + default -> throw new IllegalArgumentException("unknown index position " + position); + }; + + Env env; + if (representation.equals("longArray")) { + long[] longValues = new long[size]; + for (int i = 0; i < size; i++) { + longValues[i] = i; + } + values = longValues; + argument = 17L; + excluded = (long) size - 1L; + target = + transformation.equals("identity") + ? longValues[index] + : Math.addExact(longValues[index], (Long) argument); + env = + newEnv( + declarations( + Decls.newVar("values", Decls.newListType(Decls.Int)), + Decls.newVar("argument", Decls.Int), + Decls.newVar("excluded", Decls.Int), + Decls.newVar("target", Decls.Int))); + } else if (representation.equals("stringArray")) { + String[] stringValues = new String[size]; + for (int i = 0; i < size; i++) { + stringValues[i] = "value-" + i; + } + values = stringValues; + argument = "-mapped"; + excluded = stringValues[size - 1]; + target = + transformation.equals("identity") + ? stringValues[index] + : stringValues[index] + argument; + env = + newEnv( + declarations( + Decls.newVar("values", Decls.newListType(Decls.String)), + Decls.newVar("argument", Decls.String), + Decls.newVar("excluded", Decls.String), + Decls.newVar("target", Decls.String))); + } else { + throw new IllegalArgumentException("unknown list representation " + representation); + } + + String transform = transformation.equals("identity") ? "value" : "value + argument"; + String expression = + switch (mapping) { + case "unfiltered" -> "values.map(value, " + transform + ")[" + index + "] == target"; + case "filtered" -> + "values.map(value, value != excluded, " + transform + ")[" + index + "] == target"; + default -> throw new IllegalArgumentException("unknown mapping " + mapping); + }; + Ast ast = compile(env, expression); + Prog program = (Prog) env.program(ast); + nativePlan = requireNative(program); + currentPlan = currentPlan(program, ast); + nativeProgram = program; + currentProgram = env.program(ast, evalOptions(OptDisableNativeEval)); + variables = + mapping.equals("filtered") + ? Map.of( + "values", values, "argument", argument, "excluded", excluded, "target", target) + : Map.of("values", values, "argument", argument, "target", target); + activation = newActivation(variables); + assertEquivalent(nativePlan.eval(activation), currentPlan.eval(activation)); + } + } + + @State(Scope.Benchmark) + public static class StringListLiteralMembershipState { + @Param({"constant", "computed"}) + String shape; + + @Param({"16", "1024"}) + int size; + + @Param({"first", "middle", "absent"}) + String position; + + String needle; + String fallback; + String[] values; + boolean condition; + NativeCapabilityBenchmark nativePlan; + Interpretable currentPlan; + Program nativeProgram; + Program currentProgram; + Activation activation; + Map variables; + + @SuppressWarnings("DuplicatedCode") + @Setup + public void setup() { + values = new String[size]; + fallback = "fallback"; + condition = true; + StringBuilder literal = new StringBuilder("["); + for (int i = 0; i < size; i++) { + if (i > 0) { + literal.append(','); + } + String value = "value-" + i; + values[i] = value; + if (shape.equals("constant")) { + literal.append('\'').append(value).append('\''); + } else if (shape.equals("computed")) { + literal.append("(condition?'").append(value).append("':fallback)"); + } else { + throw new IllegalArgumentException("unknown list literal shape " + shape); + } + } + literal.append(']'); + needle = + switch (position) { + case "first" -> values[0]; + case "middle" -> values[size / 2]; + case "absent" -> "absent"; + default -> throw new IllegalArgumentException("unknown match position " + position); + }; + + Env env = + newEnv( + declarations( + Decls.newVar("needle", Decls.String), + Decls.newVar("condition", Decls.Bool), + Decls.newVar("fallback", Decls.String))); + Ast ast = compile(env, "needle in " + literal); + Prog program = (Prog) env.program(ast); + nativePlan = requireNative(program); + currentPlan = currentPlan(program, ast); + nativeProgram = program; + currentProgram = env.program(ast, evalOptions(OptDisableNativeEval)); + variables = Map.of("needle", needle, "condition", condition, "fallback", fallback); + activation = newActivation(variables); + assertEquivalent(nativePlan.eval(activation), currentPlan.eval(activation)); + } + } + + @State(Scope.Benchmark) + public static class BoolListLiteralState { + @Param({"constant", "computed"}) + String shape; + + @Param({"16", "1024"}) + int size; + + int index; + boolean fallback; + boolean[] values; + boolean condition; + NativeCapabilityBenchmark nativeIndexPlan; + NativeCapabilityBenchmark nativeSizePlan; + Interpretable currentIndexPlan; + Interpretable currentSizePlan; + Program nativeIndexProgram; + Program nativeSizeProgram; + Program currentIndexProgram; + Program currentSizeProgram; + Activation activation; + Map variables; + + @SuppressWarnings("DuplicatedCode") + @Setup + public void setup() { + index = size / 2; + values = new boolean[size]; + fallback = false; + condition = true; + StringBuilder literal = new StringBuilder("["); + for (int i = 0; i < size; i++) { + if (i > 0) { + literal.append(','); + } + boolean value = (i & 1) == 0; + values[i] = value; + if (shape.equals("constant")) { + literal.append(value); + } else if (shape.equals("computed")) { + literal.append("(condition?").append(value).append(":fallback)"); + } else { + throw new IllegalArgumentException("unknown list literal shape " + shape); + } + } + literal.append(']'); + + Env env = + newEnv( + declarations( + Decls.newVar("condition", Decls.Bool), Decls.newVar("fallback", Decls.Bool))); + Ast indexAst = compile(env, literal + "[" + index + "]"); + Ast sizeAst = compile(env, "size(" + literal + ")"); + Prog indexProgram = (Prog) env.program(indexAst); + Prog sizeProgram = (Prog) env.program(sizeAst); + nativeIndexPlan = requireNative(indexProgram); + nativeSizePlan = requireNative(sizeProgram); + currentIndexPlan = currentPlan(indexProgram, indexAst); + currentSizePlan = currentPlan(sizeProgram, sizeAst); + nativeIndexProgram = indexProgram; + nativeSizeProgram = sizeProgram; + currentIndexProgram = env.program(indexAst, evalOptions(OptDisableNativeEval)); + currentSizeProgram = env.program(sizeAst, evalOptions(OptDisableNativeEval)); + variables = Map.of("condition", condition, "fallback", fallback); + activation = newActivation(variables); + assertEquivalent(nativeIndexPlan.eval(activation), currentIndexPlan.eval(activation)); + assertEquivalent(nativeSizePlan.eval(activation), currentSizePlan.eval(activation)); + } + } + + @State(Scope.Benchmark) + public static class StringListLiteralState { + @Param({"constant", "computed"}) + String shape; + + @Param({"16", "1024"}) + int size; + + int index; + String fallback; + String[] values; + boolean condition; + NativeCapabilityBenchmark nativeIndexPlan; + NativeCapabilityBenchmark nativeSizePlan; + Interpretable currentIndexPlan; + Interpretable currentSizePlan; + Program nativeIndexProgram; + Program nativeSizeProgram; + Program currentIndexProgram; + Program currentSizeProgram; + Activation activation; + Map variables; + + @SuppressWarnings("DuplicatedCode") + @Setup + public void setup() { + index = size / 2; + values = new String[size]; + fallback = "fallback"; + condition = true; + StringBuilder literal = new StringBuilder("["); + for (int i = 0; i < size; i++) { + if (i > 0) { + literal.append(','); + } + String value = "value-" + i; + values[i] = value; + if (shape.equals("constant")) { + literal.append('\'').append(value).append('\''); + } else if (shape.equals("computed")) { + literal.append("(condition?'").append(value).append("':fallback)"); + } else { + throw new IllegalArgumentException("unknown list literal shape " + shape); + } + } + literal.append(']'); + + Env env = + newEnv( + declarations( + Decls.newVar("condition", Decls.Bool), Decls.newVar("fallback", Decls.String))); + Ast indexAst = compile(env, literal + "[" + index + "]"); + Ast sizeAst = compile(env, "size(" + literal + ")"); + Prog indexProgram = (Prog) env.program(indexAst); + Prog sizeProgram = (Prog) env.program(sizeAst); + nativeIndexPlan = requireNative(indexProgram); + nativeSizePlan = requireNative(sizeProgram); + currentIndexPlan = currentPlan(indexProgram, indexAst); + currentSizePlan = currentPlan(sizeProgram, sizeAst); + nativeIndexProgram = indexProgram; + nativeSizeProgram = sizeProgram; + currentIndexProgram = env.program(indexAst, evalOptions(OptDisableNativeEval)); + currentSizeProgram = env.program(sizeAst, evalOptions(OptDisableNativeEval)); + variables = Map.of("condition", condition, "fallback", fallback); + activation = newActivation(variables); + assertEquivalent(nativeIndexPlan.eval(activation), currentIndexPlan.eval(activation)); + assertEquivalent(nativeSizePlan.eval(activation), currentSizePlan.eval(activation)); + } + } + + @State(Scope.Benchmark) + public static class IntListLiteralState { + @Param({"constant", "computed"}) + String shape; + + @Param({"16", "1024"}) + int size; + + int index; + long x; + long target; + NativeCapabilityBenchmark nativePlan; + NativeCapabilityBenchmark nativeSizePlan; + Interpretable currentPlan; + Interpretable currentSizePlan; + Interpretable currentTerminal; + Program nativeProgram; + Program nativeSizeProgram; + Program currentProgram; + Program currentSizeProgram; + Program currentTerminalProgram; + Activation activation; + Map variables; + + @Setup + public void setup() { + index = size / 2; + x = 50_021L; + StringBuilder literal = new StringBuilder("["); + for (int i = 0; i < size; i++) { + if (i > 0) { + literal.append(','); + } + if (shape.equals("constant")) { + literal.append(50_021L + i); + } else if (shape.equals("computed")) { + literal.append("x+").append(i); + } else { + throw new IllegalArgumentException("unknown list literal shape " + shape); + } + } + literal.append(']'); + target = 50_021L + index; + + Env env = + newEnv(declarations(Decls.newVar("x", Decls.Int), Decls.newVar("target", Decls.Int))); + Ast ast = compile(env, literal + "[" + index + "] == target"); + Ast sizeAst = compile(env, "size(" + literal + ")"); + Ast terminalAst = compile(env, literal.toString()); + Prog program = (Prog) env.program(ast); + Prog sizeProgram = (Prog) env.program(sizeAst); + nativePlan = requireNative(program); + nativeSizePlan = requireNative(sizeProgram); + currentPlan = currentPlan(program, ast); + currentSizePlan = currentPlan(sizeProgram, sizeAst); + currentTerminal = currentPlan(program, terminalAst); + nativeProgram = program; + nativeSizeProgram = sizeProgram; + currentProgram = env.program(ast, evalOptions(OptDisableNativeEval)); + currentSizeProgram = env.program(sizeAst, evalOptions(OptDisableNativeEval)); + currentTerminalProgram = env.program(terminalAst, evalOptions(OptDisableNativeEval)); + variables = Map.of("x", x, "target", target); + activation = newActivation(variables); + assertEquivalent(nativePlan.eval(activation), currentPlan.eval(activation)); + assertEquivalent(nativeSizePlan.eval(activation), currentSizePlan.eval(activation)); + } + } + + @State(Scope.Benchmark) + public static class DoubleListLiteralState { + @Param({"constant", "computed"}) + String shape; + + @Param({"16", "1024"}) + int size; + + int index; + double d; + NativeCapabilityBenchmark nativeIndexPlan; + NativeCapabilityBenchmark nativeSizePlan; + Interpretable currentIndexPlan; + Interpretable currentSizePlan; + Program nativeIndexProgram; + Program nativeSizeProgram; + Program currentIndexProgram; + Program currentSizeProgram; + Activation activation; + Map variables; + + @SuppressWarnings("DuplicatedCode") + @Setup + public void setup() { + index = size / 2; + d = 50_021.25d; + StringBuilder literal = new StringBuilder("["); + for (int i = 0; i < size; i++) { + if (i > 0) { + literal.append(','); + } + if (shape.equals("constant")) { + literal.append(d + i); + } else if (shape.equals("computed")) { + literal.append("d+").append(i).append(".0"); + } else { + throw new IllegalArgumentException("unknown list literal shape " + shape); + } + } + literal.append(']'); + + Env env = newEnv(declarations(Decls.newVar("d", Decls.Double))); + Ast indexAst = compile(env, literal + "[" + index + "]"); + Ast sizeAst = compile(env, "size(" + literal + ")"); + Prog indexProgram = (Prog) env.program(indexAst); + Prog sizeProgram = (Prog) env.program(sizeAst); + nativeIndexPlan = requireNative(indexProgram); + nativeSizePlan = requireNative(sizeProgram); + currentIndexPlan = currentPlan(indexProgram, indexAst); + currentSizePlan = currentPlan(sizeProgram, sizeAst); + nativeIndexProgram = indexProgram; + nativeSizeProgram = sizeProgram; + currentIndexProgram = env.program(indexAst, evalOptions(OptDisableNativeEval)); + currentSizeProgram = env.program(sizeAst, evalOptions(OptDisableNativeEval)); + variables = Map.of("d", d); + activation = newActivation(variables); + assertEquivalent(nativeIndexPlan.eval(activation), currentIndexPlan.eval(activation)); + assertEquivalent(nativeSizePlan.eval(activation), currentSizePlan.eval(activation)); + } + } + + @Benchmark + public long intNativePrimitive(IntState state) { + return state.nativePlan.evalInt(state.activation); + } + + @Benchmark + public Val intNativeFinalVal(IntState state) { + return state.nativePlan.eval(state.activation); + } + + @Benchmark + public Val intCurrentRaw(IntState state) { + return state.currentPlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult intNativeProgram(IntState state) { + return state.nativeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult intCurrentProgram(IntState state) { + return state.currentProgram.eval(state.variables); + } + + @Benchmark + public double doubleNativePrimitive(DoubleState state) { + return state.nativePlan.evalDouble(state.activation); + } + + @Benchmark + public Val doubleNativeFinalVal(DoubleState state) { + return state.nativePlan.eval(state.activation); + } + + @Benchmark + public Val doubleCurrentRaw(DoubleState state) { + return state.currentPlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult doubleNativeProgram(DoubleState state) { + return state.nativeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult doubleCurrentProgram(DoubleState state) { + return state.currentProgram.eval(state.variables); + } + + @Benchmark + public boolean mapSelectorNativePrimitive(MapSelectorState state) { + return state.comparisonPlan.evalBoolean(state.activation); + } + + @Benchmark + public long mapSelectorTerminalPrimitive(MapSelectorState state) { + return state.terminalPlan.evalInt(state.activation); + } + + @Benchmark + public Val mapSelectorNativeFinalVal(MapSelectorState state) { + return state.comparisonPlan.eval(state.activation); + } + + @Benchmark + public Val mapSelectorCurrentRaw(MapSelectorState state) { + return state.currentComparison.eval(state.activation); + } + + @Benchmark + public Val mapSelectorTerminalCurrentRaw(MapSelectorState state) { + return state.currentTerminal.eval(state.activation); + } + + @Benchmark + public Program.EvalResult mapSelectorNativeProgram(MapSelectorState state) { + return state.nativeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult mapSelectorCurrentProgram(MapSelectorState state) { + return state.currentProgram.eval(state.variables); + } + + @Benchmark + public boolean mapSelectorJava(MapSelectorState state) { + return state.values.get("answer") == 50_021L; + } + + @Benchmark + public boolean protoSelectorNativePrimitive(ProtoSelectorState state) { + return state.comparisonPlan.evalBoolean(state.activation); + } + + @Benchmark + public long protoSelectorTerminalPrimitive(ProtoSelectorState state) { + return state.terminalPlan.evalInt(state.activation); + } + + @Benchmark + public Val protoSelectorNativeFinalVal(ProtoSelectorState state) { + return state.comparisonPlan.eval(state.activation); + } + + @Benchmark + public Val protoSelectorCurrentRaw(ProtoSelectorState state) { + return state.currentComparison.eval(state.activation); + } + + @Benchmark + public Val protoSelectorTerminalCurrentRaw(ProtoSelectorState state) { + return state.currentTerminal.eval(state.activation); + } + + @Benchmark + public Program.EvalResult protoSelectorNativeProgram(ProtoSelectorState state) { + return state.nativeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult protoSelectorCurrentProgram(ProtoSelectorState state) { + return state.currentProgram.eval(state.variables); + } + + @Benchmark + public boolean protoSelectorJava(ProtoSelectorState state) { + return state.generated.getSingleInt64() == 50_021L; + } + + @Benchmark + public boolean protoStringSelectorNativePrimitive(ProtoStringSelectorState state) { + return state.comparisonPlan.evalBoolean(state.activation); + } + + @Benchmark + public Val protoStringSelectorCurrentRaw(ProtoStringSelectorState state) { + return state.currentComparison.eval(state.activation); + } + + @Benchmark + public Program.EvalResult protoStringSelectorNativeProgram(ProtoStringSelectorState state) { + return state.nativeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult protoStringSelectorCurrentProgram(ProtoStringSelectorState state) { + return state.currentProgram.eval(state.variables); + } + + @Benchmark + public boolean listIndexNativePrimitive(ListIndexState state) { + return state.comparisonPlan.evalBoolean(state.activation); + } + + @Benchmark + public long listIndexTerminalPrimitive(ListIndexState state) { + return state.terminalPlan.evalInt(state.activation); + } + + @Benchmark + public Val listIndexNativeFinalVal(ListIndexState state) { + return state.comparisonPlan.eval(state.activation); + } + + @Benchmark + public Val listIndexCurrentRaw(ListIndexState state) { + return state.currentComparison.eval(state.activation); + } + + @Benchmark + public Val listIndexTerminalCurrentRaw(ListIndexState state) { + return state.currentTerminal.eval(state.activation); + } + + @Benchmark + public Program.EvalResult listIndexNativeProgram(ListIndexState state) { + return state.nativeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult listIndexCurrentProgram(ListIndexState state) { + return state.currentProgram.eval(state.variables); + } + + @Benchmark + public boolean listIndexJava(ListIndexState state) { + long value = + switch (state.representation) { + case "longArray" -> ((long[]) state.values)[state.index]; + case "javaList" -> + ((List) state.values).get(state.index) instanceof Long element + ? element + : Long.MIN_VALUE; + case "adaptedList" -> ((Lister) state.values).nativeGetAt(state.index).intValue(); + default -> throw new IllegalArgumentException(state.representation); + }; + return value == state.target; + } + + @Benchmark + public boolean stringListIndexNativePrimitive(StringListIndexState state) { + return state.comparisonPlan.evalBoolean(state.activation); + } + + @Benchmark + public String stringListIndexTerminalPrimitive(StringListIndexState state) { + return state.terminalPlan.evalString(state.activation); + } + + @Benchmark + public Val stringListIndexNativeFinalVal(StringListIndexState state) { + return state.comparisonPlan.eval(state.activation); + } + + @Benchmark + public Val stringListIndexCurrentRaw(StringListIndexState state) { + return state.currentComparison.eval(state.activation); + } + + @Benchmark + public Val stringListIndexTerminalCurrentRaw(StringListIndexState state) { + return state.currentTerminal.eval(state.activation); + } + + @Benchmark + public Program.EvalResult stringListIndexNativeProgram(StringListIndexState state) { + return state.nativeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult stringListIndexCurrentProgram(StringListIndexState state) { + return state.currentProgram.eval(state.variables); + } + + @Benchmark + public boolean stringListIndexJava(StringListIndexState state) { + String value = + switch (state.representation) { + case "stringArray" -> ((String[]) state.values)[state.index]; + case "javaList" -> (String) ((List) state.values).get(state.index); + case "adaptedList" -> (String) ((Lister) state.values).nativeGetAt(state.index).value(); + default -> throw new IllegalArgumentException(state.representation); + }; + return value.equals(state.target); + } + + @Benchmark + public boolean stringListMembershipNativePrimitive(StringListMembershipState state) { + return state.nativePlan.evalBoolean(state.activation); + } + + @Benchmark + public Val stringListMembershipNativeFinalVal(StringListMembershipState state) { + return state.nativePlan.eval(state.activation); + } + + @Benchmark + public Val stringListMembershipCurrentRaw(StringListMembershipState state) { + return state.currentPlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult stringListMembershipNativeProgram(StringListMembershipState state) { + return state.nativeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult stringListMembershipCurrentProgram(StringListMembershipState state) { + return state.currentProgram.eval(state.variables); + } + + @Benchmark + public boolean stringListMembershipJava(StringListMembershipState state) { + for (int i = 0; i < state.size; i++) { + String value = + switch (state.representation) { + case "stringArray" -> ((String[]) state.values)[i]; + case "javaList" -> (String) ((List) state.values).get(i); + case "adaptedList" -> (String) ((Lister) state.values).nativeGetAt(i).value(); + default -> throw new IllegalArgumentException(state.representation); + }; + if (state.needle.equals(value)) { + return true; + } + } + return false; + } + + @Benchmark + public boolean mappedStringMembershipNativePrimitive(MappedStringMembershipState state) { + return state.nativePlan.evalBoolean(state.activation); + } + + @Benchmark + public Val mappedStringMembershipNativeFinalVal(MappedStringMembershipState state) { + return state.nativePlan.eval(state.activation); + } + + @Benchmark + public Val mappedStringMembershipCurrentRaw(MappedStringMembershipState state) { + return state.currentPlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult mappedStringMembershipNativeProgram(MappedStringMembershipState state) { + return state.nativeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult mappedStringMembershipCurrentProgram( + MappedStringMembershipState state) { + return state.currentProgram.eval(state.variables); + } + + @Benchmark + public boolean mappedStringMembershipJava(MappedStringMembershipState state) { + boolean filtered = state.shape.equals("filteredComputed") || state.shape.equals("filter"); + boolean computed = + state.shape.equals("unfilteredComputed") || state.shape.equals("filteredComputed"); + boolean match = false; + long checksum = 0L; + for (String source : state.values) { + if (filtered && source.equals(state.excluded)) { + continue; + } + String value = computed ? source + state.suffix : source; + checksum ^= value.hashCode(); + if (state.needle.equals(value)) { + match = true; + } + } + return match != (checksum == Long.MIN_VALUE); + } + + @Benchmark + public boolean quantifierNativePrimitive(QuantifierState state) { + return state.nativePlan.evalBoolean(state.activation); + } + + @Benchmark + public Val quantifierNativeFinalVal(QuantifierState state) { + return state.nativePlan.eval(state.activation); + } + + @Benchmark + public Val quantifierCurrentRaw(QuantifierState state) { + return state.currentPlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult quantifierNativeProgram(QuantifierState state) { + return state.nativeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult quantifierCurrentProgram(QuantifierState state) { + return state.currentProgram.eval(state.variables); + } + + @Benchmark + public boolean quantifierJava(QuantifierState state) { + int matches = 0; + if (state.values instanceof long[] longValues) { + long target = (Long) state.target; + for (long value : longValues) { + if (value == target) { + if (state.quantifier.equals("exists")) { + return true; + } + if (state.quantifier.equals("all")) { + return false; + } + matches++; + } + } + } else { + String target = (String) state.target; + for (String value : (String[]) state.values) { + if (value.equals(target)) { + if (state.quantifier.equals("exists")) { + return true; + } + if (state.quantifier.equals("all")) { + return false; + } + matches++; + } + } + } + return switch (state.quantifier) { + case "exists" -> false; + case "all" -> true; + case "existsOne" -> matches == 1; + default -> throw new IllegalArgumentException("unknown quantifier " + state.quantifier); + }; + } + + @Benchmark + public boolean mapAggregateNativePrimitive(MapAggregateState state) { + return state.nativePlan.evalBoolean(state.activation); + } + + @Benchmark + public Val mapAggregateNativeFinalVal(MapAggregateState state) { + return state.nativePlan.eval(state.activation); + } + + @Benchmark + public Val mapAggregateCurrentRaw(MapAggregateState state) { + return state.currentPlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult mapAggregateNativeProgram(MapAggregateState state) { + return state.nativeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult mapAggregateCurrentProgram(MapAggregateState state) { + return state.currentProgram.eval(state.variables); + } + + @Benchmark + public boolean mapAggregateJava(MapAggregateState state) { + int accepted = 0; + for (long value : state.values) { + if (state.accepts(value)) { + accepted++; + } + } + long[] mapped = new long[accepted]; + int mappedSize = 0; + for (long value : state.values) { + if (!state.accepts(value)) { + continue; + } + mapped[mappedSize++] = + state.transformation.equals("identity") ? value : Math.addExact(value, state.argument); + } + int matches = 0; + for (long value : mapped) { + if (value == state.target) { + if (state.quantifier.equals("exists")) { + return true; + } + if (state.quantifier.equals("all")) { + return false; + } + matches++; + } + } + return switch (state.quantifier) { + case "exists" -> false; + case "all" -> true; + case "existsOne" -> matches == 1; + default -> throw new IllegalArgumentException("unknown quantifier " + state.quantifier); + }; + } + + @Benchmark + public long filterSizeNativePrimitive(FilterSizeState state) { + return state.nativePlan.evalInt(state.activation); + } + + @Benchmark + public Val filterSizeNativeFinalVal(FilterSizeState state) { + return state.nativePlan.eval(state.activation); + } + + @Benchmark + public Val filterSizeCurrentRaw(FilterSizeState state) { + return state.currentPlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult filterSizeNativeProgram(FilterSizeState state) { + return state.nativeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult filterSizeCurrentProgram(FilterSizeState state) { + return state.currentProgram.eval(state.variables); + } + + @Benchmark + public long filterSizeJava(FilterSizeState state) { + long matches = 0L; + if (state.values instanceof long[] longValues) { + long target = (Long) state.target; + for (long value : longValues) { + if (value == target) { + matches++; + } + } + } else { + String target = (String) state.target; + for (String value : (String[]) state.values) { + if (value.equals(target)) { + matches++; + } + } + } + return matches; + } + + @Benchmark + public long mapSizeNativePrimitive(MapSizeState state) { + return state.nativePlan.evalInt(state.activation); + } + + @Benchmark + public Val mapSizeNativeFinalVal(MapSizeState state) { + return state.nativePlan.eval(state.activation); + } + + @Benchmark + public Val mapSizeCurrentRaw(MapSizeState state) { + return state.currentPlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult mapSizeNativeProgram(MapSizeState state) { + return state.nativeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult mapSizeCurrentProgram(MapSizeState state) { + return state.currentProgram.eval(state.variables); + } + + @Benchmark + public long mapSizeJava(MapSizeState state) { + long transformed = 0L; + long accepted = 0L; + if (state.values instanceof long[] longValues) { + long excluded = (Long) state.excluded; + if (state.transformation.equals("identity")) { + for (long value : longValues) { + if (state.mapping.equals("unfiltered") || value != excluded) { + transformed ^= value; + accepted++; + } + } + } else { + long argument = (Long) state.argument; + for (long value : longValues) { + if (state.mapping.equals("unfiltered") || value != excluded) { + transformed ^= Math.addExact(value, argument); + accepted++; + } + } + } + } else { + String excluded = (String) state.excluded; + if (state.transformation.equals("identity")) { + for (String value : (String[]) state.values) { + if (state.mapping.equals("unfiltered") || !value.equals(excluded)) { + transformed ^= value.hashCode(); + accepted++; + } + } + } else { + String argument = (String) state.argument; + for (String value : (String[]) state.values) { + if (state.mapping.equals("unfiltered") || !value.equals(excluded)) { + transformed ^= (value + argument).hashCode(); + accepted++; + } + } + } + } + return accepted + (transformed == Long.MIN_VALUE ? 1 : 0); + } + + @Benchmark + public boolean mapIndexNativePrimitive(MapIndexState state) { + return state.nativePlan.evalBoolean(state.activation); + } + + @Benchmark + public Val mapIndexNativeFinalVal(MapIndexState state) { + return state.nativePlan.eval(state.activation); + } + + @Benchmark + public Val mapIndexCurrentRaw(MapIndexState state) { + return state.currentPlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult mapIndexNativeProgram(MapIndexState state) { + return state.nativeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult mapIndexCurrentProgram(MapIndexState state) { + return state.currentProgram.eval(state.variables); + } + + @Benchmark + public boolean mapIndexJava(MapIndexState state) { + long checksum = 0L; + boolean equal; + if (state.values instanceof long[] longValues) { + long selected = 0L; + long argument = (Long) state.argument; + long excluded = (Long) state.excluded; + int accepted = 0; + for (long longValue : longValues) { + if (state.mapping.equals("filtered") && longValue == excluded) { + continue; + } + long value = + state.transformation.equals("identity") + ? longValue + : Math.addExact(longValue, argument); + checksum ^= value; + if (accepted == state.index) { + selected = value; + } + accepted++; + } + equal = selected == (Long) state.target; + } else { + String selected = null; + String argument = (String) state.argument; + String excluded = (String) state.excluded; + String[] stringValues = (String[]) state.values; + int accepted = 0; + for (String stringValue : stringValues) { + if (state.mapping.equals("filtered") && stringValue.equals(excluded)) { + continue; + } + String value = + state.transformation.equals("identity") ? stringValue : stringValue + argument; + checksum ^= value.hashCode(); + if (accepted == state.index) { + selected = value; + } + accepted++; + } + equal = requireNonNull(selected).equals(state.target); + } + return equal != (checksum == Long.MIN_VALUE); + } + + @Benchmark + public boolean stringListLiteralMembershipNativePrimitive( + StringListLiteralMembershipState state) { + return state.nativePlan.evalBoolean(state.activation); + } + + @Benchmark + public Val stringListLiteralMembershipNativeFinalVal(StringListLiteralMembershipState state) { + return state.nativePlan.eval(state.activation); + } + + @Benchmark + public Val stringListLiteralMembershipCurrentRaw(StringListLiteralMembershipState state) { + return state.currentPlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult stringListLiteralMembershipNativeProgram( + StringListLiteralMembershipState state) { + return state.nativeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult stringListLiteralMembershipCurrentProgram( + StringListLiteralMembershipState state) { + return state.currentProgram.eval(state.variables); + } + + @Benchmark + public boolean stringListLiteralMembershipJava(StringListLiteralMembershipState state) { + for (String value : state.values) { + String evaluated = state.shape.equals("constant") || state.condition ? value : state.fallback; + if (state.needle.equals(evaluated)) { + return true; + } + } + return false; + } + + @Benchmark + public boolean boolListLiteralIndexNativePrimitive(BoolListLiteralState state) { + return state.nativeIndexPlan.evalBoolean(state.activation); + } + + @Benchmark + public Val boolListLiteralIndexNativeFinalVal(BoolListLiteralState state) { + return state.nativeIndexPlan.eval(state.activation); + } + + @Benchmark + public Val boolListLiteralIndexCurrentRaw(BoolListLiteralState state) { + return state.currentIndexPlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult boolListLiteralIndexNativeProgram(BoolListLiteralState state) { + return state.nativeIndexProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult boolListLiteralIndexCurrentProgram(BoolListLiteralState state) { + return state.currentIndexProgram.eval(state.variables); + } + + @Benchmark + public boolean boolListLiteralIndexJava(BoolListLiteralState state) { + boolean selected = false; + for (int i = 0; i < state.size; i++) { + boolean value = + state.shape.equals("constant") || state.condition ? state.values[i] : state.fallback; + if (i == state.index) { + selected = value; + } + } + return selected; + } + + @Benchmark + public long boolListLiteralSizeNativePrimitive(BoolListLiteralState state) { + return state.nativeSizePlan.evalInt(state.activation); + } + + @Benchmark + public Val boolListLiteralSizeNativeFinalVal(BoolListLiteralState state) { + return state.nativeSizePlan.eval(state.activation); + } + + @Benchmark + public Val boolListLiteralSizeCurrentRaw(BoolListLiteralState state) { + return state.currentSizePlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult boolListLiteralSizeNativeProgram(BoolListLiteralState state) { + return state.nativeSizeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult boolListLiteralSizeCurrentProgram(BoolListLiteralState state) { + return state.currentSizeProgram.eval(state.variables); + } + + @Benchmark + public int boolListLiteralSizeJava(BoolListLiteralState state) { + boolean evaluated = false; + if (state.shape.equals("computed")) { + for (boolean value : state.values) { + evaluated ^= state.condition ? value : state.fallback; + } + } + return state.size; + } + + @Benchmark + public String stringListLiteralIndexNativePrimitive(StringListLiteralState state) { + return state.nativeIndexPlan.evalString(state.activation); + } + + @Benchmark + public Val stringListLiteralIndexNativeFinalVal(StringListLiteralState state) { + return state.nativeIndexPlan.eval(state.activation); + } + + @Benchmark + public Val stringListLiteralIndexCurrentRaw(StringListLiteralState state) { + return state.currentIndexPlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult stringListLiteralIndexNativeProgram(StringListLiteralState state) { + return state.nativeIndexProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult stringListLiteralIndexCurrentProgram(StringListLiteralState state) { + return state.currentIndexProgram.eval(state.variables); + } + + @Benchmark + public String stringListLiteralIndexJava(StringListLiteralState state) { + String selected = null; + for (int i = 0; i < state.size; i++) { + String value = + state.shape.equals("constant") || state.condition ? state.values[i] : state.fallback; + if (i == state.index) { + selected = value; + } + } + return selected; + } + + @Benchmark + public long stringListLiteralSizeNativePrimitive(StringListLiteralState state) { + return state.nativeSizePlan.evalInt(state.activation); + } + + @Benchmark + public Val stringListLiteralSizeNativeFinalVal(StringListLiteralState state) { + return state.nativeSizePlan.eval(state.activation); + } + + @Benchmark + public Val stringListLiteralSizeCurrentRaw(StringListLiteralState state) { + return state.currentSizePlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult stringListLiteralSizeNativeProgram(StringListLiteralState state) { + return state.nativeSizeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult stringListLiteralSizeCurrentProgram(StringListLiteralState state) { + return state.currentSizeProgram.eval(state.variables); + } + + @Benchmark + public int stringListLiteralSizeJava(StringListLiteralState state) { + String evaluated = null; + if (state.shape.equals("computed")) { + for (String value : state.values) { + evaluated = state.condition ? value : state.fallback; + } + } + return evaluated == null && state.shape.equals("computed") ? -1 : state.size; + } + + @Benchmark + public boolean intListLiteralIndexNativePrimitive(IntListLiteralState state) { + return state.nativePlan.evalBoolean(state.activation); + } + + @Benchmark + public Val intListLiteralIndexNativeFinalVal(IntListLiteralState state) { + return state.nativePlan.eval(state.activation); + } + + @Benchmark + public Val intListLiteralIndexCurrentRaw(IntListLiteralState state) { + return state.currentPlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult intListLiteralIndexNativeProgram(IntListLiteralState state) { + return state.nativeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult intListLiteralIndexCurrentProgram(IntListLiteralState state) { + return state.currentProgram.eval(state.variables); + } + + @Benchmark + public boolean intListLiteralIndexJava(IntListLiteralState state) { + long selected = 0L; + for (int i = 0; i < state.size; i++) { + long value = state.shape.equals("constant") ? 50_021L + i : state.x + i; + if (i == state.index) { + selected = value; + } + } + return selected == state.target; + } + + @Benchmark + public long intListLiteralSizeNativePrimitive(IntListLiteralState state) { + return state.nativeSizePlan.evalInt(state.activation); + } + + @Benchmark + public Val intListLiteralSizeNativeFinalVal(IntListLiteralState state) { + return state.nativeSizePlan.eval(state.activation); + } + + @Benchmark + public Val intListLiteralSizeCurrentRaw(IntListLiteralState state) { + return state.currentSizePlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult intListLiteralSizeNativeProgram(IntListLiteralState state) { + return state.nativeSizeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult intListLiteralSizeCurrentProgram(IntListLiteralState state) { + return state.currentSizeProgram.eval(state.variables); + } + + @Benchmark + public int intListLiteralSizeJava(IntListLiteralState state) { + long evaluated = 0L; + if (state.shape.equals("computed")) { + for (int i = 0; i < state.size; i++) { + evaluated ^= state.x + i; + } + } + return evaluated == Long.MIN_VALUE ? -1 : state.size; + } + + @Benchmark + public double doubleListLiteralIndexNativePrimitive(DoubleListLiteralState state) { + return state.nativeIndexPlan.evalDouble(state.activation); + } + + @Benchmark + public Val doubleListLiteralIndexNativeFinalVal(DoubleListLiteralState state) { + return state.nativeIndexPlan.eval(state.activation); + } + + @Benchmark + public Val doubleListLiteralIndexCurrentRaw(DoubleListLiteralState state) { + return state.currentIndexPlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult doubleListLiteralIndexNativeProgram(DoubleListLiteralState state) { + return state.nativeIndexProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult doubleListLiteralIndexCurrentProgram(DoubleListLiteralState state) { + return state.currentIndexProgram.eval(state.variables); + } + + @Benchmark + public double doubleListLiteralIndexJava(DoubleListLiteralState state) { + double selected = 0.0d; + for (int i = 0; i < state.size; i++) { + double value = state.d + i; + if (i == state.index) { + selected = value; + } + } + return selected; + } + + @Benchmark + public long doubleListLiteralSizeNativePrimitive(DoubleListLiteralState state) { + return state.nativeSizePlan.evalInt(state.activation); + } + + @Benchmark + public Val doubleListLiteralSizeNativeFinalVal(DoubleListLiteralState state) { + return state.nativeSizePlan.eval(state.activation); + } + + @Benchmark + public Val doubleListLiteralSizeCurrentRaw(DoubleListLiteralState state) { + return state.currentSizePlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult doubleListLiteralSizeNativeProgram(DoubleListLiteralState state) { + return state.nativeSizeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult doubleListLiteralSizeCurrentProgram(DoubleListLiteralState state) { + return state.currentSizeProgram.eval(state.variables); + } + + @Benchmark + public int doubleListLiteralSizeJava(DoubleListLiteralState state) { + double evaluated = 0.0d; + if (state.shape.equals("computed")) { + for (int i = 0; i < state.size; i++) { + evaluated += state.d + i; + } + } + return evaluated == Double.NEGATIVE_INFINITY ? -1 : state.size; + } + + @Benchmark + public Val intListLiteralTerminalCurrentRaw(IntListLiteralState state) { + return state.currentTerminal.eval(state.activation); + } + + @Benchmark + public Program.EvalResult intListLiteralTerminalCurrentProgram(IntListLiteralState state) { + return state.currentTerminalProgram.eval(state.variables); + } + + @Benchmark + public long[] intListLiteralTerminalJava(IntListLiteralState state) { + long[] values = new long[state.size]; + for (int i = 0; i < values.length; i++) { + values[i] = state.shape.equals("constant") ? 50_021L + i : state.x + i; + } + return values; + } + + @Benchmark + public Program constructNativeProgram(ConstructionState state) { + return state.env.program(state.ast); + } + + @Benchmark + public Program constructCurrentProgram(ConstructionState state) { + return state.env.program(state.ast, evalOptions(OptDisableNativeEval)); + } + + @Benchmark + public Program.EvalResult constructAndEvaluate(ConstructionState state) { + return state.env.program(state.ast).eval(state.variables); + } + + private static Ast compile(Env env, String source) { + AstIssuesTuple result = env.compile(source); + if (result.hasIssues()) { + throw requireNonNull(result.getIssues().err()); + } + return result.getAst(); + } + + private static NativeCapabilityBenchmark requireNative(Prog program) { + return NativeCapabilityBenchmark.require(program.interpretable); + } + + private static Interpretable currentPlan(Prog program, Ast ast) { + return ((Prog) program.e.program(ast, evalOptions(OptDisableNativeEval))).interpretable; + } + + private static String deepExpression() { + String source = "x"; + for (int level = 0; level < 16; level++) { + source = + switch (level & 3) { + case 0 -> "(" + source + " + 17)"; + case 1 -> "(" + source + " * 3)"; + case 2 -> "(" + source + " - 11)"; + default -> "(" + source + " / 2)"; + }; + } + return source; + } + + private static String listLiteralIndexExpression(int size) { + StringBuilder source = new StringBuilder("["); + for (int i = 0; i < size; i++) { + if (i > 0) { + source.append(','); + } + source.append("x+").append(i); + } + return source.append("][").append(size / 2).append(']').toString(); + } + + private static void assertEquivalent(Val actual, Val expected) { + if (actual.getClass() != expected.getClass() || !actual.value().equals(expected.value())) { + throw new IllegalStateException("native and current results differ"); + } + } + + private abstract static class TypeFixture { + final com.google.api.expr.v1alpha1.Type type; + final Object value; + + TypeFixture(com.google.api.expr.v1alpha1.Type type, Object value) { + this.type = type; + this.value = value; + } + + abstract String next(String source, int level); + } +} diff --git a/core/src/jmh/java/org/projectnessie/cel/common/types/AdapterAllocationBench.java b/core/src/jmh/java/org/projectnessie/cel/common/types/AdapterAllocationBench.java index 6c44576e..6d398054 100644 --- a/core/src/jmh/java/org/projectnessie/cel/common/types/AdapterAllocationBench.java +++ b/core/src/jmh/java/org/projectnessie/cel/common/types/AdapterAllocationBench.java @@ -87,30 +87,19 @@ public void nativeToValue(NativeValueState state, Blackhole blackhole) { } private static Object value(String kind, int size) { - switch (kind) { - case "arrayList": - return list(size); - case "linkedHashSet": - return set(size); - case "objectArray": - return list(size).toArray(); - case "stringArray": - return stringArray(size); - case "intArray": - return intArray(size); - case "longArray": - return longArray(size); - case "doubleArray": - return doubleArray(size); - case "mapStringInt": - return mapStringInt(size); - case "mapValVal": - return mapValVal(size); - case "listValue": - return listValue(size); - default: - throw new IllegalArgumentException("Unknown native value kind: " + kind); - } + return switch (kind) { + case "arrayList" -> list(size); + case "linkedHashSet" -> set(size); + case "objectArray" -> list(size).toArray(); + case "stringArray" -> stringArray(size); + case "intArray" -> intArray(size); + case "longArray" -> longArray(size); + case "doubleArray" -> doubleArray(size); + case "mapStringInt" -> mapStringInt(size); + case "mapValVal" -> mapValVal(size); + case "listValue" -> listValue(size); + default -> throw new IllegalArgumentException("Unknown native value kind: " + kind); + }; } private static List list(int size) { diff --git a/core/src/jmh/java/org/projectnessie/cel/common/types/TypeAdapterConversionBench.java b/core/src/jmh/java/org/projectnessie/cel/common/types/TypeAdapterConversionBench.java new file mode 100644 index 00000000..465fb879 --- /dev/null +++ b/core/src/jmh/java/org/projectnessie/cel/common/types/TypeAdapterConversionBench.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.common.types; + +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; +import static org.projectnessie.cel.common.types.pb.ProtoTypeRegistry.newRegistry; + +import com.google.api.expr.v1alpha1.ParsedExpr; +import com.google.protobuf.Any; +import com.google.protobuf.Value; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.common.types.ref.TypeRegistry; +import org.projectnessie.cel.common.types.ref.Val; + +@Warmup(iterations = 1, time = 1500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 300, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(2) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@SuppressWarnings({"deprecation", "removal"}) +public class TypeAdapterConversionBench { + + @State(Scope.Thread) + public static class ConversionState { + TypeRegistry registry; + + boolean primitiveBoolean; + long primitiveLong; + double primitiveDouble; + + Object nativeBoolean; + Object nativeLong; + Object nativeDouble; + Object nativeString; + Object nativeList; + Object nativeMap; + Object nativeMessage; + + BoolT boolValue; + IntT intValue; + UintT uintValue; + DoubleT doubleValue; + Val stringValue; + Val listValue; + Val mapValue; + Val messageValue; + + @Setup + public void init() { + ParsedExpr parsedExpr = ParsedExpr.getDefaultInstance(); + registry = newRegistry(parsedExpr); + + primitiveBoolean = true; + primitiveLong = 42L; + primitiveDouble = 42.5d; + + nativeBoolean = Boolean.TRUE; + nativeLong = Long.valueOf(42L); + nativeDouble = Double.valueOf(42.5d); + nativeString = "forty-two"; + nativeList = List.of(1L, 2L, 3L); + nativeMap = Map.of("one", 1L, "two", 2L); + nativeMessage = parsedExpr; + + boolValue = BoolT.True; + intValue = intOf(42L); + uintValue = uintOf(42L); + doubleValue = doubleOf(42.5d); + stringValue = stringOf("forty-two"); + listValue = registry.nativeToValue(nativeList); + mapValue = registry.nativeToValue(nativeMap); + messageValue = registry.nativeToValue(nativeMessage); + } + } + + @Benchmark + public Val boxedNativeToValueBoolean(ConversionState state) { + return state.registry.nativeToValue(state.nativeBoolean); + } + + @Benchmark + public Val boxedNativeToValueLong(ConversionState state) { + return state.registry.nativeToValue(state.nativeLong); + } + + @Benchmark + public Val boxedNativeToValueDouble(ConversionState state) { + return state.registry.nativeToValue(state.nativeDouble); + } + + @Benchmark + public Val boxedNativeToValueString(ConversionState state) { + return state.registry.nativeToValue(state.nativeString); + } + + @Benchmark + public Val boxedNativeToValueList(ConversionState state) { + return state.registry.nativeToValue(state.nativeList); + } + + @Benchmark + public Val boxedNativeToValueMap(ConversionState state) { + return state.registry.nativeToValue(state.nativeMap); + } + + @Benchmark + public Val boxedNativeToValueMessage(ConversionState state) { + return state.registry.nativeToValue(state.nativeMessage); + } + + @Benchmark + public Val primitiveNativeToValueBoolean(ConversionState state) { + return state.registry.nativeToValue(state.primitiveBoolean); + } + + @Benchmark + public Val primitiveNativeToValueLong(ConversionState state) { + return state.registry.nativeToValue(state.primitiveLong); + } + + @Benchmark + public Val primitiveNativeToValueDouble(ConversionState state) { + return state.registry.nativeToValue(state.primitiveDouble); + } + + @Benchmark + public Boolean directConvertBoolToBoolean(ConversionState state) { + return state.boolValue.convertToNative(Boolean.class); + } + + @Benchmark + public Long directConvertIntToLong(ConversionState state) { + return state.intValue.convertToNative(Long.class); + } + + @Benchmark + public Long directConvertUintToLong(ConversionState state) { + return state.uintValue.convertToNative(Long.class); + } + + @Benchmark + public Double directConvertDoubleToDouble(ConversionState state) { + return state.doubleValue.convertToNative(Double.class); + } + + @Benchmark + public String directConvertStringToString(ConversionState state) { + return state.stringValue.convertToNative(String.class); + } + + @Benchmark + public List directConvertListToList(ConversionState state) { + return state.listValue.convertToNative(List.class); + } + + @Benchmark + public Map directConvertMapToMap(ConversionState state) { + return state.mapValue.convertToNative(Map.class); + } + + @Benchmark + public Value directConvertBoolToProtoValue(ConversionState state) { + return state.boolValue.convertToNative(Value.class); + } + + @Benchmark + public Any directConvertIntToAny(ConversionState state) { + return state.intValue.convertToNative(Any.class); + } + + @Benchmark + public ParsedExpr directConvertMessageToGenerated(ConversionState state) { + return state.messageValue.convertToNative(ParsedExpr.class); + } + + @Benchmark + public Boolean adapterConvertBoolToBoolean(ConversionState state) { + return state.registry.valueToNative(state.boolValue, Boolean.class); + } + + @Benchmark + public Long adapterConvertIntToLong(ConversionState state) { + return state.registry.valueToNative(state.intValue, Long.class); + } + + @Benchmark + public Double adapterConvertDoubleToDouble(ConversionState state) { + return state.registry.valueToNative(state.doubleValue, Double.class); + } + + @Benchmark + public String adapterConvertStringToString(ConversionState state) { + return state.registry.valueToNative(state.stringValue, String.class); + } + + @Benchmark + public List adapterConvertListToList(ConversionState state) { + return state.registry.valueToNative(state.listValue, List.class); + } + + @Benchmark + public Map adapterConvertMapToMap(ConversionState state) { + return state.registry.valueToNative(state.mapValue, Map.class); + } + + @Benchmark + public ParsedExpr adapterConvertMessageToGenerated(ConversionState state) { + return state.registry.valueToNative(state.messageValue, ParsedExpr.class); + } + + @Benchmark + public boolean adapterBooleanValue(ConversionState state) { + return state.registry.valueToBoolean(state.boolValue); + } + + @Benchmark + public long adapterIntValue(ConversionState state) { + return state.registry.valueToLong(state.intValue); + } + + @Benchmark + public long adapterUintValue(ConversionState state) { + return state.registry.valueToLong(state.uintValue); + } + + @Benchmark + public double adapterDoubleValue(ConversionState state) { + return state.registry.valueToDouble(state.doubleValue); + } + + @Benchmark + public boolean directBooleanValue(ConversionState state) { + return state.boolValue.booleanValue(); + } + + @Benchmark + public long directIntValue(ConversionState state) { + return state.intValue.intValue(); + } + + @Benchmark + public long directUintValue(ConversionState state) { + return state.uintValue.intValue(); + } + + @Benchmark + public double directDoubleValue(ConversionState state) { + return state.doubleValue.doubleValue(); + } +} diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/AggregateSourceBench.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/AggregateSourceBench.java new file mode 100644 index 00000000..712ccb94 --- /dev/null +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/AggregateSourceBench.java @@ -0,0 +1,506 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.pb.ProtoTypeRegistry.newRegistry; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.common.types.IteratorT; +import org.projectnessie.cel.common.types.ref.TypeRegistry; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Mapper; + +/** + * Characterizes host aggregate representations before introducing an exact aggregate-source SPI. + * + *

The zero-sized cases expose fixed source and adaptation overhead. Comparing raw traversal with + * the two adapted traversals exposes element-proportional adaptation and callback costs. The shared + * kernel deliberately uses a reusable typed consumer in a real summing operation rather than timing + * an otherwise unused callback. + */ +@Warmup(iterations = 3, time = 250, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 300, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class AggregateSourceBench { + + @State(Scope.Thread) + public static class ListLikeState { + @Param({"longArray", "objectArray", "list", "collection", "set"}) + public String representation; + + @Param({"0", "1", "16", "1024"}) + public int size; + + TypeRegistry adapter; + Object raw; + LongAggregateSource source; + AdaptingSumConsumer consumer; + long needle; + + @Setup + public void setup() { + adapter = newRegistry(); + raw = listLike(representation, size); + source = source(representation, raw); + consumer = new AdaptingSumConsumer(adapter); + needle = size == 0 ? -1L : size - 1L; + } + } + + @Benchmark + public Val establishedListLikeAdaptation(ListLikeState state) { + return state.adapter.nativeToValue(state.raw); + } + + @Benchmark + public int structuralSize(ListLikeState state) { + return state.source.size(); + } + + @Benchmark + public long structuralIndexLast(ListLikeState state) { + return state.size == 0 ? -1L : state.source.valueAt(state.size - 1); + } + + @Benchmark + public long structuralTraversal(ListLikeState state) { + return state.source.rawSum(); + } + + @Benchmark + public boolean structuralMembershipLast(ListLikeState state) { + return state.source.contains(state.needle); + } + + @Benchmark + public long adaptedTraversalDirectSpecialized(ListLikeState state) { + TypeRegistry adapter = state.adapter; + return switch (state.representation) { + case "longArray" -> { + long sum = 0L; + for (long value : (long[]) state.raw) { + sum += adapter.nativeToValue(value).intValue(); + } + yield sum; + } + case "objectArray" -> { + long sum = 0L; + for (Object value : (Object[]) state.raw) { + sum += adapter.nativeToValue(value).intValue(); + } + yield sum; + } + case "list" -> { + long sum = 0L; + List values = (List) state.raw; + for (Object value : values) { + sum += adapter.nativeToValue(value).intValue(); + } + yield sum; + } + case "collection", "set" -> { + long sum = 0L; + for (Object value : (Collection) state.raw) { + sum += adapter.nativeToValue(value).intValue(); + } + yield sum; + } + default -> throw new IllegalArgumentException(state.representation); + }; + } + + @Benchmark + public long adaptedTraversalSharedKernel(ListLikeState state) { + AdaptingSumConsumer consumer = state.consumer; + consumer.reset(); + state.source.forEach(consumer); + return consumer.sum(); + } + + @State(Scope.Thread) + public static class MapState { + @Param({"0", "1", "16", "1024"}) + public int size; + + TypeRegistry adapter; + Map raw; + Mapper adapted; + String lookupKey; + Val adaptedLookupKey; + + @Setup + public void setup() { + adapter = newRegistry(); + raw = map(size); + lookupKey = size == 0 ? "missing" : "key-" + (size - 1); + adaptedLookupKey = adapter.nativeToValue(lookupKey); + adapted = (Mapper) adapter.nativeToValue(raw); + } + } + + @Benchmark + public Val establishedMapAdaptation(MapState state) { + return state.adapter.nativeToValue(state.raw); + } + + /** + * Prototypes the current adapted-key snapshot without adapting values. Values remain raw until a + * lookup or traversal consumes them. + */ + @Benchmark + public Map mapAdaptedKeySnapshot(MapState state) { + Map snapshot = new HashMap<>(hashCapacity(state.raw.size())); + for (Map.Entry entry : state.raw.entrySet()) { + snapshot.put(state.adapter.nativeToValue(entry.getKey()), entry.getValue()); + } + return snapshot; + } + + @Benchmark + public int mapStructuralSize(MapState state) { + return state.raw.size(); + } + + @Benchmark + public Object mapRawLookup(MapState state) { + return state.raw.get(state.lookupKey); + } + + @Benchmark + public boolean mapRawMembership(MapState state) { + return state.raw.containsKey(state.lookupKey); + } + + /** + * The map was key-snapshotted during setup, so this isolates lookup plus one lazy value + * adaptation. + */ + @Benchmark + public Val mapLookupWithLazyValueAdaptation(MapState state) { + return state.adapted.find(state.adaptedLookupKey); + } + + @Benchmark + public Val mapAdaptedMembership(MapState state) { + return state.adapted.contains(state.adaptedLookupKey); + } + + @Benchmark + public long mapRawTraversal(MapState state) { + long sum = 0L; + for (Long value : state.raw.values()) { + sum += value; + } + return sum; + } + + /** Traverses the adapted-key snapshot and forces lazy adaptation of every corresponding value. */ + @Benchmark + public long mapTraversalWithLazyValueAdaptation(MapState state) { + long sum = 0L; + IteratorT keys = state.adapted.iterator(); + while (keys.hasNext().booleanValue()) { + Val value = state.adapted.find(keys.next()); + sum += value.intValue(); + } + return sum; + } + + private interface LongAggregateSource { + int size(); + + long valueAt(int index); + + long rawSum(); + + boolean contains(long needle); + + void forEach(LongElementConsumer consumer); + } + + private interface LongElementConsumer { + void acceptLong(long value); + + void acceptObject(Object value); + } + + private static final class AdaptingSumConsumer implements LongElementConsumer { + private final TypeRegistry adapter; + private long sum; + + private AdaptingSumConsumer(TypeRegistry adapter) { + this.adapter = adapter; + } + + void reset() { + sum = 0L; + } + + long sum() { + return sum; + } + + @Override + public void acceptLong(long value) { + sum += adapter.nativeToValue(value).intValue(); + } + + @Override + public void acceptObject(Object value) { + sum += adapter.nativeToValue(value).intValue(); + } + } + + private record LongArraySource(long[] values) implements LongAggregateSource { + + @Override + public int size() { + return values.length; + } + + @Override + public long valueAt(int index) { + return values[index]; + } + + @Override + public long rawSum() { + long sum = 0L; + for (long value : values) { + sum += value; + } + return sum; + } + + @Override + public boolean contains(long needle) { + for (long value : values) { + if (value == needle) { + return true; + } + } + return false; + } + + @Override + public void forEach(LongElementConsumer consumer) { + for (long value : values) { + consumer.acceptLong(value); + } + } + } + + private record ObjectArraySource(Long[] values) implements LongAggregateSource { + + @Override + public int size() { + return values.length; + } + + @Override + public long valueAt(int index) { + return values[index]; + } + + @Override + public long rawSum() { + long sum = 0L; + for (Long value : values) { + sum += value; + } + return sum; + } + + @Override + public boolean contains(long needle) { + for (Long value : values) { + if (value == needle) { + return true; + } + } + return false; + } + + @Override + public void forEach(LongElementConsumer consumer) { + for (Long value : values) { + consumer.acceptObject(value); + } + } + } + + private record ListSource(List values) implements LongAggregateSource { + + @Override + public int size() { + return values.size(); + } + + @Override + public long valueAt(int index) { + return values.get(index); + } + + @Override + public long rawSum() { + long sum = 0L; + for (Long value : values) { + sum += value; + } + return sum; + } + + @Override + public boolean contains(long needle) { + return values.contains(needle); + } + + @Override + public void forEach(LongElementConsumer consumer) { + for (Long value : values) { + consumer.acceptObject(value); + } + } + } + + private record CollectionSource(Collection values) implements LongAggregateSource { + + @Override + public int size() { + return values.size(); + } + + @Override + public long valueAt(int index) { + int current = 0; + for (Long value : values) { + if (current++ == index) { + return value; + } + } + throw new IndexOutOfBoundsException(index); + } + + @Override + public long rawSum() { + long sum = 0L; + for (Long value : values) { + sum += value; + } + return sum; + } + + @Override + public boolean contains(long needle) { + return values.contains(needle); + } + + @Override + public void forEach(LongElementConsumer consumer) { + for (Long value : values) { + consumer.acceptObject(value); + } + } + } + + private static Object listLike(String representation, int size) { + return switch (representation) { + case "longArray" -> { + long[] values = new long[size]; + for (int i = 0; i < size; i++) { + values[i] = i; + } + yield values; + } + case "objectArray" -> { + Long[] values = new Long[size]; + for (int i = 0; i < size; i++) { + values[i] = (long) i; + } + yield values; + } + case "list" -> { + List values = new ArrayList<>(size); + addValues(values, size); + yield values; + } + case "collection" -> { + Collection values = new ArrayDeque<>(Math.max(1, size)); + addValues(values, size); + yield values; + } + case "set" -> { + Set values = new LinkedHashSet<>(hashCapacity(size)); + addValues(values, size); + yield values; + } + default -> throw new IllegalArgumentException(representation); + }; + } + + @SuppressWarnings("unchecked") + private static LongAggregateSource source(String representation, Object raw) { + return switch (representation) { + case "longArray" -> new LongArraySource((long[]) raw); + case "objectArray" -> new ObjectArraySource((Long[]) raw); + case "list" -> new ListSource((List) raw); + case "collection", "set" -> new CollectionSource((Collection) raw); + default -> throw new IllegalArgumentException(representation); + }; + } + + private static void addValues(Collection values, int size) { + for (long i = 0; i < size; i++) { + values.add(i); + } + } + + private static Map map(int size) { + Map values = new LinkedHashMap<>(hashCapacity(size)); + for (long i = 0; i < size; i++) { + values.put("key-" + i, i); + } + return values; + } + + private static int hashCapacity(int size) { + return size * 4 / 3 + 1; + } +} diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/AttributesBench.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/AttributesBench.java index fba39a39..a4c965e3 100644 --- a/core/src/jmh/java/org/projectnessie/cel/interpreter/AttributesBench.java +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/AttributesBench.java @@ -25,9 +25,9 @@ import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; import static org.projectnessie.cel.interpreter.Interpretable.newConstValue; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedMessage; import com.google.api.expr.v1alpha1.Type; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -42,6 +42,7 @@ import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; import org.projectnessie.cel.common.containers.Container; +import org.projectnessie.cel.common.types.ref.FieldType; import org.projectnessie.cel.common.types.ref.TypeRegistry; import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; import org.projectnessie.cel.interpreter.AttributeFactory.NamespacedAttribute; @@ -56,6 +57,24 @@ @OutputTimeUnit(TimeUnit.MICROSECONDS) public class AttributesBench { + @State(Scope.Benchmark) + public static class GeneratedScalarFieldGetterState { + + final NestedMessage message; + final FieldType fieldType; + + public GeneratedScalarFieldGetterState() { + message = NestedMessage.newBuilder().setBb(123).build(); + TypeRegistry reg = newRegistry(message); + fieldType = reg.findFieldType("cel.expr.conformance.proto3.TestAllTypes.NestedMessage", "bb"); + } + } + + @Benchmark + public Object generatedScalarFieldGetter(GeneratedScalarFieldGetterState state) { + return state.fieldType.getFrom.getFrom(state.message); + } + @Benchmark public void attributesConditionalAttr_TrueBranch() { TypeRegistry reg = newRegistry(); @@ -120,9 +139,9 @@ public BenchmarkResolverFieldQualifierState() { TypeRegistry reg = newRegistry(msg); Activation vars = newActivation(mapOf("msg", msg)); - Type opType = reg.findType("google.api.expr.test.v1.proto3.TestAllTypes"); + Type opType = reg.findType("cel.expr.conformance.proto3.TestAllTypes"); assertThat(opType).isNotNull(); - Type fieldType = reg.findType("google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage"); + Type fieldType = reg.findType("cel.expr.conformance.proto3.TestAllTypes.NestedMessage"); assertThat(fieldType).isNotNull(); AttributeFactory attrs = newAttributeFactory(Container.defaultContainer, reg, reg); @@ -157,7 +176,7 @@ public BenchmarkResolverCustomQualifierState() { Qualifier qualBB = attrs.newQualifier( Type.newBuilder() - .setMessageType("google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage") + .setMessageType("cel.expr.conformance.proto3.TestAllTypes.NestedMessage") .build(), 2, "bb"); diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/CollectionOperationBench.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/CollectionOperationBench.java new file mode 100644 index 00000000..aebf5dd4 --- /dev/null +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/CollectionOperationBench.java @@ -0,0 +1,376 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EnvOption.types; +import static org.projectnessie.cel.ProgramOption.evalOptions; +import static org.projectnessie.cel.Util.mapOf; +import static org.projectnessie.cel.common.types.IntT.intOf; + +import dev.cel.expr.conformance.proto3.TestAllTypes; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.EvalOption; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.IteratorT; +import org.projectnessie.cel.common.types.MapT; +import org.projectnessie.cel.common.types.pb.ProtoTypeRegistry; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; +import org.projectnessie.cel.common.types.traits.Mapper; + +@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 300, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class CollectionOperationBench { + private static final String TEST_ALL_TYPES = "cel.expr.conformance.proto3.TestAllTypes"; + + @State(Scope.Benchmark) + public static class DirectListState { + @Param({"containsLast", "containsMiss", "equal", "iterate", "indexScan"}) + public String operation; + + @Param({"intArray", "longArray", "doubleArray", "javaList"}) + public String representation; + + @Param({"16", "1024"}) + public int size; + + Lister left; + Val right; + Val needle; + + @Setup + public void init() { + ProtoTypeRegistry adapter = ProtoTypeRegistry.newRegistry(); + Object leftNative = listValue(representation, size, 50_000); + Object rightNative = listValue(representation, size, 50_000); + left = (Lister) adapter.nativeToValue(leftNative); + right = adapter.nativeToValue(rightNative); + needle = intOf(operation.equals("containsMiss") ? -50_000 : 50_000L + size - 1); + if (representation.equals("doubleArray")) { + needle = adapter.nativeToValue(needle.intValue() * 1.0d); + } + } + } + + @Benchmark + public Val directList(DirectListState state) { + return switch (state.operation) { + case "containsLast", "containsMiss" -> state.left.contains(state.needle); + case "equal" -> state.left.equal(state.right); + case "iterate" -> consumeIterator(state.left.iterator()); + case "indexScan" -> consumeIndexed(state.left); + default -> throw new IllegalArgumentException(state.operation); + }; + } + + @State(Scope.Benchmark) + public static class ListProgramState { + @Param({"containsLast", "containsMiss", "equal", "existsLate"}) + public String operation; + + @Param({"intArray", "longArray", "javaList"}) + public String representation; + + @Param({"16", "1024"}) + public int size; + + Program program; + Map vars; + + @Setup + public void init() { + String expression; + switch (operation) { + case "containsLast", "containsMiss" -> expression = "target in items"; + case "equal" -> expression = "items == other"; + case "existsLate" -> expression = "items.exists(i, i == target)"; + default -> throw new IllegalArgumentException(operation); + } + program = + program( + expression, + Decls.newVar("items", Decls.newListType(Decls.Int)), + Decls.newVar("other", Decls.newListType(Decls.Int)), + Decls.newVar("target", Decls.Int)); + long target = operation.equals("containsMiss") ? -50_000 : 50_000L + size - 1; + vars = + mapOf( + "items", + listValue(representation, size, 50_000), + "other", + listValue(representation, size, 50_000), + "target", + target); + } + } + + @Benchmark + public Object listProgram(ListProgramState state) { + return state.program.eval(state.vars); + } + + @State(Scope.Benchmark) + public static class DirectMapState { + @Param({"adapt", "contains", "find", "size", "equal", "equalWrapped", "equalCross", "iterate"}) + public String operation; + + @Param({"16", "1024"}) + public int size; + + ProtoTypeRegistry adapter; + Map nativeMap; + Mapper map; + Val other; + Mapper wrappedMap; + Val wrappedOther; + Val key; + + @Setup + public void init() { + adapter = ProtoTypeRegistry.newRegistry(); + nativeMap = stringMap(size); + map = (Mapper) adapter.nativeToValue(nativeMap); + other = adapter.nativeToValue(stringMap(size)); + wrappedMap = wrappedStringMap(adapter, size); + wrappedOther = wrappedStringMap(adapter, size); + key = adapter.nativeToValue("key-" + (size - 1)); + } + } + + @Benchmark + public Val directMap(DirectMapState state) { + return switch (state.operation) { + case "adapt" -> state.adapter.nativeToValue(state.nativeMap); + case "contains" -> state.map.contains(state.key); + case "find" -> state.map.find(state.key); + case "size" -> state.map.size(); + case "equal" -> state.map.equal(state.other); + case "equalWrapped" -> state.wrappedMap.equal(state.wrappedOther); + case "equalCross" -> state.map.equal(state.wrappedOther); + case "iterate" -> consumeIterator(state.map.iterator()); + default -> throw new IllegalArgumentException(state.operation); + }; + } + + @State(Scope.Benchmark) + public static class MapProgramState { + @Param({"contains", "size", "index", "equal"}) + public String operation; + + @Param({"16", "1024"}) + public int size; + + Program program; + Map vars; + + @Setup + public void init() { + String expression = + switch (operation) { + case "contains" -> "key in attrs"; + case "size" -> "size(attrs) == expectedSize"; + case "index" -> "attrs[key] == target"; + case "equal" -> "attrs == other"; + default -> throw new IllegalArgumentException(operation); + }; + program = + program( + expression, + Decls.newVar("attrs", Decls.newMapType(Decls.String, Decls.Int)), + Decls.newVar("other", Decls.newMapType(Decls.String, Decls.Int)), + Decls.newVar("key", Decls.String), + Decls.newVar("target", Decls.Int), + Decls.newVar("expectedSize", Decls.Int)); + vars = + mapOf( + "attrs", + stringMap(size), + "other", + stringMap(size), + "key", + "key-" + (size - 1), + "target", + (long) size - 1, + "expectedSize", + (long) size); + } + } + + @Benchmark + public Object mapProgram(MapProgramState state) { + return state.program.eval(state.vars); + } + + @State(Scope.Benchmark) + public static class ProtoMapProgramState { + @Param({"contains", "lookup", "lookupRepeated", "equal"}) + public String operation; + + @Param({"16", "1024"}) + public int size; + + Program program; + Map vars; + + @Setup + public void init() { + String expression = + switch (operation) { + case "contains" -> "key in msg.map_string_uint64"; + case "lookup" -> "msg.map_string_uint64[key] == target"; + case "lookupRepeated" -> + "msg.map_string_uint64[key] == target && msg.map_string_uint64[key] == target"; + case "equal" -> "msg.map_string_uint64 == other.map_string_uint64"; + default -> throw new IllegalArgumentException(operation); + }; + Env env = + newEnv( + types(TestAllTypes.getDefaultInstance()), + declarations( + Decls.newVar("msg", Decls.newObjectType(TEST_ALL_TYPES)), + Decls.newVar("other", Decls.newObjectType(TEST_ALL_TYPES)), + Decls.newVar("key", Decls.String), + Decls.newVar("target", Decls.Uint))); + program = program(env, expression); + TestAllTypes message = protoMessage(size); + vars = + mapOf( + "msg", + message, + "other", + protoMessage(size), + "key", + "key-" + (size - 1), + "target", + (long) size - 1); + } + } + + @Benchmark + public Object protoMapProgram(ProtoMapProgramState state) { + return state.program.eval(state.vars); + } + + private static Val consumeIterator(IteratorT iterator) { + Val value = intOf(0); + while (iterator.hasNext().booleanValue()) { + value = iterator.next(); + } + return value; + } + + private static Val consumeIndexed(Lister list) { + Val value = intOf(0); + int size = list.nativeSize(); + for (int i = 0; i < size; i++) { + value = list.nativeGetAt(i); + } + return value; + } + + private static Object listValue(String representation, int size, int offset) { + return switch (representation) { + case "intArray" -> { + int[] values = new int[size]; + for (int i = 0; i < size; i++) { + values[i] = offset + i; + } + yield values; + } + case "longArray" -> { + long[] values = new long[size]; + for (int i = 0; i < size; i++) { + values[i] = offset + i; + } + yield values; + } + case "doubleArray" -> { + double[] values = new double[size]; + for (int i = 0; i < size; i++) { + values[i] = offset + i; + } + yield values; + } + case "javaList" -> { + Long[] values = new Long[size]; + for (int i = 0; i < size; i++) { + values[i] = (long) offset + i; + } + yield java.util.Arrays.asList(values); + } + default -> throw new IllegalArgumentException(representation); + }; + } + + private static Map stringMap(int size) { + Map values = new HashMap<>(size * 4 / 3 + 1); + for (long i = 0; i < size; i++) { + values.put("key-" + i, i); + } + return values; + } + + private static Mapper wrappedStringMap(ProtoTypeRegistry adapter, int size) { + Map values = new HashMap<>(size * 4 / 3 + 1); + for (long i = 0; i < size; i++) { + values.put(adapter.nativeToValue("key-" + i), adapter.nativeToValue(i)); + } + return (Mapper) MapT.newWrappedMap(adapter, values); + } + + private static TestAllTypes protoMessage(int size) { + TestAllTypes.Builder builder = TestAllTypes.newBuilder(); + for (int i = 0; i < size; i++) { + builder.putMapStringUint64("key-" + i, i); + } + return builder.build(); + } + + private static Program program(String expression, com.google.api.expr.v1alpha1.Decl... decls) { + return program(newEnv(declarations(decls)), expression); + } + + private static Program program(Env env, String expression) { + AstIssuesTuple ast = env.compile(expression); + if (ast.hasIssues()) { + throw ast.getIssues().err(); + } + return env.program(ast.getAst(), evalOptions(EvalOption.OptOptimize)); + } +} diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/FieldAccessBench.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/FieldAccessBench.java new file mode 100644 index 00000000..44068327 --- /dev/null +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/FieldAccessBench.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.pb.ProtoTypeRegistry.newRegistry; +import static org.projectnessie.cel.interpreter.Activation.newActivation; +import static org.projectnessie.cel.interpreter.AttributeFactory.newAttributeFactory; + +import com.google.api.expr.v1alpha1.Type; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.Int32Value; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.common.containers.Container; +import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.pb.PbObjectT; +import org.projectnessie.cel.common.types.ref.FieldType; +import org.projectnessie.cel.common.types.ref.TypeRegistry; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.AttributeFactory.NamespacedAttribute; + +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Fork(3) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class FieldAccessBench { + + @State(Scope.Benchmark) + public static class FieldAccessState { + final TestAllTypes message; + final DynamicMessage dynamicMessage; + final PbObjectT object; + final PbObjectT dynamicObject; + final PbObjectT absentObject; + final StringT int32Name = stringOf("single_int32"); + final StringT optionalBoolName = stringOf("optional_bool"); + final StringT nestedMessageName = stringOf("single_nested_message"); + final StringT uint32Name = stringOf("single_uint32"); + final StringT enumName = stringOf("standalone_enum"); + final StringT wrapperName = stringOf("single_int32_wrapper"); + final StringT repeatedName = stringOf("repeated_int32"); + final StringT mapName = stringOf("map_string_string"); + final Activation activation; + final NamespacedAttribute int32Attribute; + final NamespacedAttribute nestedInt32Attribute; + final Interpretable presence; + final FieldType int32; + final FieldType optionalBool; + final FieldType nestedMessage; + final FieldType uint32; + final FieldType enumField; + final FieldType wrapper; + final FieldType repeated; + final FieldType map; + + public FieldAccessState() { + Map mapValues = new HashMap<>(); + for (int i = 0; i < 16; i++) { + mapValues.put("key-" + i, "value-" + i); + } + + TestAllTypes.Builder builder = + TestAllTypes.newBuilder() + .setSingleInt32(50_000) + .setSingleInt64(0x51a7_19b3_42c5L) + .setSingleFloat(12_345.25f) + .setSingleDouble(12_345.25d) + .setOptionalBool(false) + .setSingleNestedMessage(NestedMessage.newBuilder().setBb(50_000)) + .setSingleUint32(-1) + .setSingleUint64(-1L) + .setStandaloneEnumValue(12_345) + .setSingleInt32Wrapper(Int32Value.of(50_000)) + .putAllMapStringString(mapValues); + for (int i = 0; i < 16; i++) { + builder.addRepeatedInt32(50_000 + i); + } + message = builder.build(); + dynamicMessage = + DynamicMessage.newBuilder(message.getDescriptorForType()).mergeFrom(message).build(); + + TypeRegistry registry = newRegistry(TestAllTypes.getDefaultInstance()); + object = (PbObjectT) registry.nativeToValue(message); + dynamicObject = (PbObjectT) registry.nativeToValue(dynamicMessage); + absentObject = (PbObjectT) registry.nativeToValue(TestAllTypes.getDefaultInstance()); + + String messageType = message.getDescriptorForType().getFullName(); + int32 = registry.findFieldType(messageType, "single_int32"); + optionalBool = registry.findFieldType(messageType, "optional_bool"); + nestedMessage = registry.findFieldType(messageType, "single_nested_message"); + uint32 = registry.findFieldType(messageType, "single_uint32"); + enumField = registry.findFieldType(messageType, "standalone_enum"); + wrapper = registry.findFieldType(messageType, "single_int32_wrapper"); + repeated = registry.findFieldType(messageType, "repeated_int32"); + map = registry.findFieldType(messageType, "map_string_string"); + + Type outerType = registry.findType(messageType); + Type nestedType = registry.findType("cel.expr.conformance.proto3.TestAllTypes.NestedMessage"); + AttributeFactory attributes = + newAttributeFactory(Container.defaultContainer, registry, registry); + + int32Attribute = attributes.absoluteAttribute(1, "message"); + int32Attribute.addQualifier(attributes.newQualifier(outerType, 2, "single_int32")); + + nestedInt32Attribute = attributes.absoluteAttribute(3, "message"); + nestedInt32Attribute.addQualifier( + attributes.newQualifier(outerType, 4, "single_nested_message")); + nestedInt32Attribute.addQualifier(attributes.newQualifier(nestedType, 5, "bb")); + + EvalAttr presenceOperand = new EvalAttr(registry, attributes.absoluteAttribute(6, "message")); + presence = new EvalTestOnly(7, presenceOperand, optionalBoolName, optionalBool); + activation = newActivation(Map.of("message", message)); + } + } + + @Benchmark + public int directGeneratedInt32(FieldAccessState state) { + return state.message.getSingleInt32(); + } + + @Benchmark + public Object rawGeneratedInt32(FieldAccessState state) { + return state.int32.getFrom.getFrom(state.message); + } + + @Benchmark + public Object rawDynamicFallbackInt32(FieldAccessState state) { + return state.int32.getFrom.getFrom(state.dynamicMessage); + } + + @Benchmark + public Val objectGeneratedInt32(FieldAccessState state) { + return state.object.get(state.int32Name); + } + + @Benchmark + public Val objectDynamicInt32(FieldAccessState state) { + return state.dynamicObject.get(state.int32Name); + } + + @Benchmark + public Object checkedTerminalInt32(FieldAccessState state) { + return state.int32Attribute.resolve(state.activation); + } + + @Benchmark + public Object checkedNestedInt32(FieldAccessState state) { + return state.nestedInt32Attribute.resolve(state.activation); + } + + @Benchmark + public boolean rawPresencePresent(FieldAccessState state) { + return state.optionalBool.isSet.isSet(state.message); + } + + @Benchmark + public boolean rawPresenceAbsent(FieldAccessState state) { + return state.optionalBool.isSet.isSet(TestAllTypes.getDefaultInstance()); + } + + @Benchmark + public Val checkedPresence(FieldAccessState state) { + return state.presence.eval(state.activation); + } + + @Benchmark + public Val objectPresencePresent(FieldAccessState state) { + return state.object.isSet(state.optionalBoolName); + } + + @Benchmark + public Val objectPresenceAbsent(FieldAccessState state) { + return state.absentObject.isSet(state.optionalBoolName); + } + + @Benchmark + public Object rawNestedMessage(FieldAccessState state) { + return state.nestedMessage.getFrom.getFrom(state.message); + } + + @Benchmark + public Val objectNestedMessage(FieldAccessState state) { + return state.object.get(state.nestedMessageName); + } + + @Benchmark + public Object rawUnsigned(FieldAccessState state) { + return state.uint32.getFrom.getFrom(state.message); + } + + @Benchmark + public Val objectUnsigned(FieldAccessState state) { + return state.object.get(state.uint32Name); + } + + @Benchmark + public Object rawEnum(FieldAccessState state) { + return state.enumField.getFrom.getFrom(state.message); + } + + @Benchmark + public Val objectEnum(FieldAccessState state) { + return state.object.get(state.enumName); + } + + @Benchmark + public Object rawWrapper(FieldAccessState state) { + return state.wrapper.getFrom.getFrom(state.message); + } + + @Benchmark + public Val objectWrapper(FieldAccessState state) { + return state.object.get(state.wrapperName); + } + + @Benchmark + public Object rawRepeated(FieldAccessState state) { + return state.repeated.getFrom.getFrom(state.message); + } + + @Benchmark + public Val objectRepeated(FieldAccessState state) { + return state.object.get(state.repeatedName); + } + + @Benchmark + public Object rawMap(FieldAccessState state) { + return state.map.getFrom.getFrom(state.message); + } + + @Benchmark + public Val objectMap(FieldAccessState state) { + return state.object.get(state.mapName); + } +} diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/InterpreterAllocationBench.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/InterpreterAllocationBench.java index 781f863a..1e9fff5f 100644 --- a/core/src/jmh/java/org/projectnessie/cel/interpreter/InterpreterAllocationBench.java +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/InterpreterAllocationBench.java @@ -21,7 +21,7 @@ import static org.projectnessie.cel.ProgramOption.evalOptions; import static org.projectnessie.cel.Util.mapOf; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -54,7 +54,7 @@ @OutputTimeUnit(TimeUnit.MICROSECONDS) public class InterpreterAllocationBench { - private static final String TEST_ALL_TYPES = "google.api.expr.test.v1.proto3.TestAllTypes"; + private static final String TEST_ALL_TYPES = "cel.expr.conformance.proto3.TestAllTypes"; @State(Scope.Benchmark) public static class JavaInputState { diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/InterpreterBench.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/InterpreterBench.java index 79b167d3..e270a6a7 100644 --- a/core/src/jmh/java/org/projectnessie/cel/interpreter/InterpreterBench.java +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/InterpreterBench.java @@ -80,11 +80,11 @@ public void init() { @Benchmark @Threads(1) public void interpreterSingle(Case state) { - state.program.interpretable.eval(state.program.activation); + state.program.interpretable().eval(state.program.activation()); } @Benchmark public void interpreterParallel(Case state) { - state.program.interpretable.eval(state.program.activation); + state.program.interpretable().eval(state.program.activation()); } } diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/NativeCapabilityBenchmark.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/NativeCapabilityBenchmark.java new file mode 100644 index 00000000..de038897 --- /dev/null +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/NativeCapabilityBenchmark.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import org.projectnessie.cel.common.types.ref.Val; + +/** JMH-only typed access to an integrated native-capability root. */ +public final class NativeCapabilityBenchmark { + private final NativeIsland plan; + + private NativeCapabilityBenchmark(NativeIsland plan) { + this.plan = plan; + } + + public static NativeCapabilityBenchmark require(Interpretable root) { + if (!(root instanceof NativeIsland plan)) { + throw new IllegalStateException("expected integrated native-capability root but got " + root); + } + return new NativeCapabilityBenchmark(plan); + } + + public Val eval(Activation activation) { + return plan.eval(activation); + } + + public boolean evalBoolean(Activation activation) { + return ((NativeBooleanCapability) plan.root()).evalBoolean(activation); + } + + public long evalInt(Activation activation) { + return ((NativeIntCapability) plan.root()).evalInt(activation); + } + + public double evalDouble(Activation activation) { + return ((NativeDoubleCapability) plan.root()).evalDouble(activation); + } + + public String evalString(Activation activation) { + return ((NativeStringCapability) plan.root()).evalString(activation); + } +} diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/NativeExactMapSourceBench.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/NativeExactMapSourceBench.java new file mode 100644 index 00000000..9ce6d1f6 --- /dev/null +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/NativeExactMapSourceBench.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EvalOption.OptDisableNativeEval; +import static org.projectnessie.cel.ProgramOption.evalOptions; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.Ast; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.ExactAggregateTypeAdapter; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; + +/** End-to-end benchmark for exact constant-key map-source consumers. */ +@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 300, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class NativeExactMapSourceBench { + @State(Scope.Benchmark) + public static class MapState { + @Param({"size", "hit", "membershipMiss", "presentNull"}) + public String operation; + + @Param({"0", "1", "16", "1024"}) + public int size; + + Program exactNative; + Program exactDisabled; + Program general; + Map variables; + Map values; + Map nullable; + + @Setup + public void setup() { + String expression = + switch (operation) { + case "size" -> "size(values)"; + case "hit" -> "cardinality == 0 ? -1 : values['last']"; + case "membershipMiss" -> "'missing' in values"; + case "presentNull" -> "nullable['present']"; + default -> throw new IllegalArgumentException(operation); + }; + values = new LinkedHashMap<>(); + for (long value = 0; value < size; value++) { + values.put(value == size - 1L ? "last" : value + "-key", value); + } + nullable = new LinkedHashMap<>(); + nullable.put("present", null); + variables = + Map.of( + "values", values, + "nullable", nullable, + "cardinality", (long) size); + + Env exactEnv = environment(new ExactAdapter()); + Env generalEnv = environment(DefaultTypeAdapter.Instance); + Ast exactAst = compile(exactEnv, expression); + exactNative = exactEnv.program(exactAst); + exactDisabled = exactEnv.program(exactAst, evalOptions(OptDisableNativeEval)); + general = generalEnv.program(compile(generalEnv, expression)); + } + } + + @Benchmark + public Object exactNative(MapState state) { + return state.exactNative.eval(state.variables); + } + + @Benchmark + public Object exactDisabled(MapState state) { + return state.exactDisabled.eval(state.variables); + } + + @Benchmark + public Object generalAdapter(MapState state) { + return state.general.eval(state.variables); + } + + @Benchmark + public Object javaCeiling(MapState state) { + return switch (state.operation) { + case "size" -> state.values.size(); + case "hit" -> state.size == 0 ? -1L : state.values.get("last"); + case "membershipMiss" -> state.values.containsKey("missing"); + case "presentNull" -> state.nullable.get("present"); + default -> throw new IllegalArgumentException(state.operation); + }; + } + + private static Env environment(org.projectnessie.cel.common.types.ref.TypeAdapter adapter) { + return newEnv( + customTypeAdapter(adapter), + declarations( + Decls.newVar("values", Decls.newMapType(Decls.String, Decls.Int)), + Decls.newVar("nullable", Decls.newMapType(Decls.String, Decls.Null)), + Decls.newVar("cardinality", Decls.Int))); + } + + private static Ast compile(Env env, String expression) { + var result = env.compile(expression); + if (result.hasIssues()) { + throw new IllegalStateException(result.getIssues().toString()); + } + return result.getAst(); + } + + private static final class ExactAdapter + implements ExactAggregateTypeAdapter, StandardScalarTypeAdapter { + @Override + public Val nativeToValue(Object value) { + return DefaultTypeAdapter.Instance.nativeToValue(value); + } + } +} diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/NativeExactSetOperationBench.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/NativeExactSetOperationBench.java new file mode 100644 index 00000000..495df28e --- /dev/null +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/NativeExactSetOperationBench.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EvalOption.OptDisableNativeEval; +import static org.projectnessie.cel.ProgramOption.evalOptions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.Ast; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.ExactAggregateTypeAdapter; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; + +/** + * End-to-end comparison of exact Set membership and encounter-order list equality. + * + *

The size-zero {@code membershipHit} case necessarily behaves like a miss. The one-element + * {@code unequalOrder} case uses a different value because reordering one element is impossible. + */ +@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 300, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class NativeExactSetOperationBench { + @State(Scope.Benchmark) + public static class OperationState { + @Param({ + "membershipHit", + "membershipMiss", + "equalSetSet", + "equalSetList", + "equalListSet", + "unequalOrder" + }) + public String operation; + + @Param({"0", "1", "16", "1024"}) + public int size; + + Program exactNativeProgram; + Program exactNativeDisabledProgram; + Program generalProgram; + Map variables; + Set leftSet; + Object right; + long needle; + + @Setup + public void init() { + String expression; + boolean membership = operation.startsWith("membership"); + if (membership) { + expression = "needle in left"; + } else { + expression = + switch (operation) { + case "equalSetSet", "equalSetList", "unequalOrder" -> "left == right"; + case "equalListSet" -> "right == left"; + default -> throw new IllegalArgumentException(operation); + }; + } + + Env exactEnv = + newEnv( + customTypeAdapter(new ExactAdapter()), + declarations( + Decls.newVar("needle", Decls.Int), + Decls.newVar("left", Decls.newListType(Decls.Int)), + Decls.newVar("right", Decls.newListType(Decls.Int)))); + Env generalEnv = + newEnv( + declarations( + Decls.newVar("needle", Decls.Int), + Decls.newVar("left", Decls.newListType(Decls.Int)), + Decls.newVar("right", Decls.newListType(Decls.Int)))); + var exactAst = compile(exactEnv, expression); + exactNativeProgram = exactEnv.program(exactAst); + exactNativeDisabledProgram = exactEnv.program(exactAst, evalOptions(OptDisableNativeEval)); + generalProgram = generalEnv.program(compile(generalEnv, expression)); + + leftSet = values(size); + needle = + operation.equals("membershipMiss") || size == 0 ? Long.MIN_VALUE : 50_000L + size - 1; + right = + switch (operation) { + case "equalSetList", "equalListSet" -> new ArrayList<>(leftSet); + case "unequalOrder" -> unequalEncounterOrder(leftSet); + default -> values(size); + }; + variables = Map.of("needle", needle, "left", leftSet, "right", right); + } + } + + @Benchmark + public Object exactNative(OperationState state) { + return state.exactNativeProgram.eval(state.variables); + } + + @Benchmark + public Object exactNativeDisabled(OperationState state) { + return state.exactNativeDisabledProgram.eval(state.variables); + } + + @Benchmark + public Object generalAdapter(OperationState state) { + return state.generalProgram.eval(state.variables); + } + + @Benchmark + public boolean javaCeiling(OperationState state) { + if (state.operation.startsWith("membership")) { + return state.leftSet.contains(state.needle); + } + Object left = state.operation.equals("equalListSet") ? state.right : state.leftSet; + Object right = state.operation.equals("equalListSet") ? state.leftSet : state.right; + return encounterOrderEquals(left, right); + } + + private static Ast compile(Env env, String expression) { + var compiled = env.compile(expression); + if (compiled.hasIssues()) { + throw new IllegalStateException(compiled.getIssues().toString()); + } + return compiled.getAst(); + } + + private static LinkedHashSet values(int size) { + LinkedHashSet values = new LinkedHashSet<>(hashCapacity(size)); + for (int i = 0; i < size; i++) { + values.add(50_000L + i); + } + return values; + } + + private static Object unequalEncounterOrder(Set left) { + if (left.isEmpty()) { + return List.of(50_000L); + } + if (left.size() == 1) { + return List.of(left.iterator().next() + 1L); + } + List reversed = new ArrayList<>(left); + Collections.reverse(reversed); + return new LinkedHashSet<>(reversed); + } + + private static boolean encounterOrderEquals(Object left, Object right) { + Iterator leftIterator = ((Iterable) left).iterator(); + Iterator rightIterator = ((Iterable) right).iterator(); + while (leftIterator.hasNext() && rightIterator.hasNext()) { + if (!leftIterator.next().equals(rightIterator.next())) { + return false; + } + } + return !leftIterator.hasNext() && !rightIterator.hasNext(); + } + + private static int hashCapacity(int size) { + return Math.max(16, size * 4 / 3 + 1); + } + + private static final class ExactAdapter + implements ExactAggregateTypeAdapter, StandardScalarTypeAdapter { + @Override + public Val nativeToValue(Object value) { + return DefaultTypeAdapter.Instance.nativeToValue(value); + } + } +} diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/NativeListConcatBench.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/NativeListConcatBench.java new file mode 100644 index 00000000..ca7898d3 --- /dev/null +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/NativeListConcatBench.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EvalOption.OptDisableNativeEval; +import static org.projectnessie.cel.ProgramOption.evalOptions; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.Ast; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.ExactAggregateTypeAdapter; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; + +/** End-to-end benchmark for immediate checked list-concatenation consumers. */ +@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 300, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class NativeListConcatBench { + @State(Scope.Benchmark) + public static class ConcatState { + @Param({"size", "boundaryIndex"}) + public String operation; + + @Param({"0", "1", "16", "1024"}) + public int size; + + Program exactNative; + Program exactDisabled; + Program general; + Map variables; + List left; + List right; + + @Setup + public void setup() { + String expression = + operation.equals("size") + ? "size(left + right)" + : size == 0 ? "-1" : "(left + right)[" + size + "]"; + left = values(size, 0); + right = values(size, size); + variables = Map.of("left", left, "right", right); + + Env exactEnv = + newEnv( + customTypeAdapter(new ExactAdapter()), + declarations( + Decls.newVar("left", Decls.newListType(Decls.Int)), + Decls.newVar("right", Decls.newListType(Decls.Int)))); + Env generalEnv = + newEnv( + declarations( + Decls.newVar("left", Decls.newListType(Decls.Int)), + Decls.newVar("right", Decls.newListType(Decls.Int)))); + Ast exactAst = compile(exactEnv, expression); + exactNative = exactEnv.program(exactAst); + exactDisabled = exactEnv.program(exactAst, evalOptions(OptDisableNativeEval)); + general = generalEnv.program(compile(generalEnv, expression)); + } + } + + @Benchmark + public Object exactNative(ConcatState state) { + return state.exactNative.eval(state.variables); + } + + @Benchmark + public Object exactDisabled(ConcatState state) { + return state.exactDisabled.eval(state.variables); + } + + @Benchmark + public Object generalAdapter(ConcatState state) { + return state.general.eval(state.variables); + } + + @Benchmark + public long javaCeiling(ConcatState state) { + return state.operation.equals("size") + ? (long) state.left.size() + state.right.size() + : state.size == 0 ? -1L : state.right.get(0); + } + + private static List values(int size, int offset) { + List values = new ArrayList<>(size); + for (long value = 0; value < size; value++) { + values.add(offset + value); + } + return values; + } + + private static Ast compile(Env env, String expression) { + var result = env.compile(expression); + if (result.hasIssues()) { + throw new IllegalStateException(result.getIssues().toString()); + } + return result.getAst(); + } + + private static final class ExactAdapter + implements ExactAggregateTypeAdapter, StandardScalarTypeAdapter { + @Override + public Val nativeToValue(Object value) { + return DefaultTypeAdapter.Instance.nativeToValue(value); + } + } +} diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/NativeListSourceConsumerBench.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/NativeListSourceConsumerBench.java new file mode 100644 index 00000000..3319c0f8 --- /dev/null +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/NativeListSourceConsumerBench.java @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EvalOption.OptDisableNativeEval; +import static org.projectnessie.cel.ProgramOption.evalOptions; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.Ast; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.ExactAggregateTypeAdapter; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; + +/** + * End-to-end benchmark for exact list-source consumers. + * + *

The cases deliberately pair each consumer with two representative host shapes instead of + * taking the full consumer-by-representation Cartesian product. Index cases use a conditional at + * size zero so all configured sizes remain semantically comparable without benchmarking an + * exception path. + */ +@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 300, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class NativeListSourceConsumerBench { + @State(Scope.Benchmark) + public static class ConsumerState { + @Param({ + "arraySize", + "setSize", + "arrayConstantIndex", + "listConstantIndex", + "collectionDynamicIndex", + "setDynamicIndex", + "arrayExists", + "setExists" + }) + public String consumer; + + @Param({"0", "1", "16", "1024"}) + public int size; + + Program exactNative; + Program exactNativeDisabled; + Program general; + Map variables; + Object values; + int index; + long needle; + + @Setup + public void setup() { + String expression = + switch (consumer) { + case "arraySize", "setSize" -> "size(values)"; + case "arrayConstantIndex", "listConstantIndex" -> "size(values) == 0 ? -1 : values[0]"; + case "collectionDynamicIndex", "setDynamicIndex" -> + "size(values) == 0 ? -1 : values[index]"; + case "arrayExists", "setExists" -> "values.exists(value, value == needle)"; + default -> throw new IllegalArgumentException("unknown consumer " + consumer); + }; + values = values(representation(consumer), size); + index = size == 0 ? 0 : size - 1; + needle = size == 0 ? -1L : size - 1L; + variables = Map.of("values", values, "index", (long) index, "needle", needle); + + Env exactEnv = + newEnv( + customTypeAdapter(new ExactAdapter()), + declarations( + Decls.newVar("values", Decls.newListType(Decls.Int)), + Decls.newVar("index", Decls.Int), + Decls.newVar("needle", Decls.Int))); + Env generalEnv = + newEnv( + declarations( + Decls.newVar("values", Decls.newListType(Decls.Int)), + Decls.newVar("index", Decls.Int), + Decls.newVar("needle", Decls.Int))); + Ast exactAst = compile(exactEnv, expression); + exactNative = exactEnv.program(exactAst); + exactNativeDisabled = exactEnv.program(exactAst, evalOptions(OptDisableNativeEval)); + general = generalEnv.program(compile(generalEnv, expression)); + } + } + + @Benchmark + public Object exactNative(ConsumerState state) { + return state.exactNative.eval(state.variables); + } + + @Benchmark + public Object exactNativeDisabled(ConsumerState state) { + return state.exactNativeDisabled.eval(state.variables); + } + + @Benchmark + public Object generalAdapter(ConsumerState state) { + return state.general.eval(state.variables); + } + + @Benchmark + public Object javaCeiling(ConsumerState state) { + return switch (state.consumer) { + case "arraySize", "setSize" -> state.size; + case "arrayConstantIndex", "listConstantIndex" -> + state.size == 0 ? -1L : valueAt(state.values, 0); + case "collectionDynamicIndex", "setDynamicIndex" -> + state.size == 0 ? -1L : valueAt(state.values, state.index); + case "arrayExists", "setExists" -> anyMatches(state.values, state.needle); + default -> throw new IllegalArgumentException("unknown consumer " + state.consumer); + }; + } + + private static Ast compile(Env env, String expression) { + var compiled = env.compile(expression); + if (compiled.hasIssues()) { + throw new IllegalStateException(compiled.getIssues().toString()); + } + return compiled.getAst(); + } + + private static String representation(String consumer) { + if (consumer.startsWith("array")) { + return "array"; + } + if (consumer.startsWith("list")) { + return "list"; + } + if (consumer.startsWith("collection")) { + return "collection"; + } + return "set"; + } + + private static Object values(String representation, int size) { + return switch (representation) { + case "array" -> { + long[] values = new long[size]; + for (int i = 0; i < size; i++) { + values[i] = i; + } + yield values; + } + case "list" -> { + List values = new ArrayList<>(size); + addValues(values, size); + yield values; + } + case "collection" -> { + Collection values = new ArrayDeque<>(size); + addValues(values, size); + yield values; + } + case "set" -> { + Collection values = new LinkedHashSet<>(hashCapacity(size)); + addValues(values, size); + yield values; + } + default -> throw new IllegalArgumentException("unknown representation " + representation); + }; + } + + private static void addValues(Collection values, int size) { + for (long i = 0; i < size; i++) { + values.add(i); + } + } + + private static long valueAt(Object values, int index) { + if (values instanceof long[] array) { + return array[index]; + } + if (values instanceof List list) { + return (Long) list.get(index); + } + Iterator iterator = ((Collection) values).iterator(); + for (int current = 0; iterator.hasNext(); current++) { + Object value = iterator.next(); + if (current == index) { + return (Long) value; + } + } + throw new AssertionError("index out of range"); + } + + private static boolean anyMatches(Object values, long needle) { + if (values instanceof long[] array) { + for (long value : array) { + if (value == needle) { + return true; + } + } + return false; + } + for (Object value : (Collection) values) { + if (((Long) value) == needle) { + return true; + } + } + return false; + } + + private static int hashCapacity(int size) { + return Math.max(16, size * 4 / 3 + 1); + } + + private static final class ExactAdapter + implements ExactAggregateTypeAdapter, StandardScalarTypeAdapter { + @Override + public Val nativeToValue(Object value) { + return DefaultTypeAdapter.Instance.nativeToValue(value); + } + } +} diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/NativePlumbingBench.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/NativePlumbingBench.java new file mode 100644 index 00000000..9cec1538 --- /dev/null +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/NativePlumbingBench.java @@ -0,0 +1,643 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.CEL.astToCheckedExpr; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EvalOption.OptDisableNativeEval; +import static org.projectnessie.cel.ProgramOption.evalOptions; +import static org.projectnessie.cel.checker.Decls.Int; +import static org.projectnessie.cel.checker.Decls.String; +import static org.projectnessie.cel.checker.Decls.newFunction; +import static org.projectnessie.cel.checker.Decls.newListType; +import static org.projectnessie.cel.checker.Decls.newMapType; +import static org.projectnessie.cel.checker.Decls.newOverload; +import static org.projectnessie.cel.checker.Decls.newVar; +import static org.projectnessie.cel.common.containers.Container.defaultContainer; +import static org.projectnessie.cel.interpreter.Activation.newActivation; +import static org.projectnessie.cel.interpreter.AttributeFactory.newAttributeFactory; +import static org.projectnessie.cel.interpreter.Dispatcher.newDispatcher; +import static org.projectnessie.cel.interpreter.Interpreter.newInterpreter; +import static org.projectnessie.cel.interpreter.functions.Overload.standardOverloads; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.Ast; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.functions.Overload; + +/** Measures the integrated typed-capability plumbing independently of production selection. */ +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class NativePlumbingBench { + @State(Scope.Benchmark) + public static class ScalarState { + @Param({"1", "4", "16"}) + int depth; + + Interpretable established; + NativeIsland island; + NativeIntCapability semantic; + Program integrated; + Program establishedProgram; + Activation activation; + Map variables; + + @SuppressWarnings("StringConcatenationInLoop") + @Setup + public void setup() { + Env env = newEnv(declarations(newVar("x", Int))); + String expression = "x"; + for (int i = 0; i < depth; i++) { + expression = "(" + expression + " + " + (i + 1) + ")"; + } + Ast ast = compile(env, expression); + established = plan(env, ast, false); + island = (NativeIsland) plan(env, ast, true); + semantic = (NativeIntCapability) island.root(); + integrated = env.program(ast); + establishedProgram = env.program(ast, evalOptions(OptDisableNativeEval)); + variables = Map.of("x", 50_021L); + activation = newActivation(variables); + } + } + + @State(Scope.Benchmark) + public static class MixedState { + Interpretable established; + NativeIsland island; + NativeBooleanCapability semantic; + Activation activation; + + @Setup + public void setup() { + Env env = newEnv(declarations(newVar("x", Int), newVar("y", Int))); + Ast ast = compile(env, "(x + 1) == y"); + established = plan(env, ast, false); + island = (NativeIsland) plan(env, ast, true); + semantic = (NativeBooleanCapability) island.root(); + activation = newActivation(Map.of("x", 50_021L, "y", 50_022L)); + } + } + + @State(Scope.Benchmark) + public static class EstablishedOuterState { + @Param({"1", "2"}) + int islands; + + Interpretable established; + Interpretable integrated; + Activation activation; + + @Setup + public void setup() { + Overload implementation; + Env env; + Ast ast; + if (islands == 1) { + env = + newEnv( + declarations( + newVar("x", Int), + newFunction("opaque", newOverload("opaque_int", java.util.List.of(Int), Int)))); + ast = compile(env, "opaque(x + 1)"); + implementation = Overload.unary("opaque_int", value -> value); + } else { + env = + newEnv( + declarations( + newVar("x", Int), + newVar("y", Int), + newFunction( + "opaque2", newOverload("opaque2_int", java.util.List.of(Int, Int), Int)))); + ast = compile(env, "opaque2(x + 1, y + 1)"); + implementation = Overload.binary("opaque2_int", (left, right) -> left); + } + established = plan(env, ast, false, implementation); + integrated = plan(env, ast, true, implementation); + activation = newActivation(Map.of("x", 50_021L, "y", 50_022L)); + } + } + + @State(Scope.Benchmark) + public static class SelectorState { + Interpretable established; + NativeIsland island; + NativeIntCapability semantic; + Activation activation; + + @Setup + public void setup() { + Env env = newEnv(declarations(newVar("attrs", newMapType(String, Int)))); + Ast ast = compile(env, "attrs.answer + 1"); + established = plan(env, ast, false); + island = (NativeIsland) plan(env, ast, true); + semantic = (NativeIntCapability) island.root(); + activation = newActivation(Map.of("attrs", Map.of("answer", 50_021L))); + } + } + + @State(Scope.Benchmark) + public static class ListIndexState { + Interpretable established; + NativeIsland island; + NativeIntCapability semantic; + Activation activation; + + @Setup + public void setup() { + Env env = newEnv(declarations(newVar("numbers", newListType(Int)))); + Ast ast = compile(env, "numbers[1] + 1"); + established = plan(env, ast, false); + island = (NativeIsland) plan(env, ast, true); + semantic = (NativeIntCapability) island.root(); + activation = newActivation(Map.of("numbers", new long[] {50_020L, 50_021L})); + } + } + + @State(Scope.Benchmark) + public static class AggregateScalarRootState { + @Param({"mapSelectorInt", "mapSelectorString", "mapIndexInt", "mapIndexString"}) + String shape; + + Interpretable established; + Interpretable integrated; + Program establishedProgram; + Program integratedProgram; + Activation activation; + Map variables; + + @SuppressWarnings("DuplicatedCode") + @Setup + public void setup() { + Env env; + String expression; + Object input; + switch (shape) { + case "mapSelectorInt": + env = newEnv(declarations(newVar("attrs", newMapType(String, Int)))); + expression = "attrs.answer"; + input = Map.of("answer", 50_021L); + variables = Map.of("attrs", input); + break; + case "mapSelectorString": + env = newEnv(declarations(newVar("attrs", newMapType(String, String)))); + expression = "attrs.answer"; + input = Map.of("answer", "native-plumbing"); + variables = Map.of("attrs", input); + break; + case "mapIndexInt": + env = newEnv(declarations(newVar("attrs", newMapType(String, Int)))); + expression = "attrs['answer']"; + input = Map.of("answer", 50_021L); + variables = Map.of("attrs", input); + break; + case "mapIndexString": + env = newEnv(declarations(newVar("attrs", newMapType(String, String)))); + expression = "attrs['answer']"; + input = Map.of("answer", "native-plumbing"); + variables = Map.of("attrs", input); + break; + default: + throw new IllegalArgumentException("unknown aggregate scalar root shape " + shape); + } + Ast ast = compile(env, expression); + established = plan(env, ast, false); + integrated = plan(env, ast, true); + establishedProgram = env.program(ast, evalOptions(OptDisableNativeEval)); + integratedProgram = env.program(ast); + activation = newActivation(variables); + } + } + + @State(Scope.Benchmark) + public static class ConstructionState { + @Param({"1", "4", "16"}) + int depth; + + Env env; + Ast ast; + + @SuppressWarnings("StringConcatenationInLoop") + @Setup + public void setup() { + env = newEnv(declarations(newVar("x", Int))); + String expression = "x"; + for (int i = 0; i < depth; i++) { + expression = "(" + expression + " + " + (i + 1) + ")"; + } + ast = compile(env, expression); + } + } + + @State(Scope.Benchmark) + public static class ScalarRootState { + @Param({"int", "double", "string", "null"}) + String kind; + + Interpretable established; + Interpretable integrated; + Program establishedProgram; + Program integratedProgram; + Activation activation; + Map variables; + Env env; + Ast ast; + + @Setup + public void setup() { + var declaration = + switch (kind) { + case "int" -> newVar("value", Int); + case "double" -> newVar("value", org.projectnessie.cel.checker.Decls.Double); + case "string" -> newVar("value", String); + case "null" -> newVar("value", org.projectnessie.cel.checker.Decls.Null); + default -> throw new IllegalArgumentException("unknown scalar root kind " + kind); + }; + Object value = + switch (kind) { + case "int" -> 50_021L; + case "double" -> 50_021.5d; + case "string" -> "value"; + case "null" -> null; + default -> throw new IllegalArgumentException("unknown scalar root kind " + kind); + }; + variables = new HashMap<>(); + variables.put("value", value); + env = newEnv(declarations(declaration)); + ast = compile(env, "value"); + established = plan(env, ast, false); + integrated = plan(env, ast, true); + establishedProgram = env.program(ast, evalOptions(OptDisableNativeEval)); + integratedProgram = env.program(ast); + activation = newActivation(variables); + } + } + + @State(Scope.Benchmark) + public static class StrictScalarState { + @Param({"intArithmetic", "doubleArithmetic", "stringConcat", "scalarComparison"}) + String shape; + + Interpretable established; + Interpretable integrated; + Program establishedProgram; + Program integratedProgram; + Activation activation; + Map variables; + + @SuppressWarnings("DuplicatedCode") + @Setup + public void setup() { + Env env; + String expression; + switch (shape) { + case "intArithmetic": + env = newEnv(declarations(newVar("left", Int), newVar("right", Int))); + expression = "(left + 1) * (right - 2)"; + variables = Map.of("left", 50_021L, "right", 42L); + break; + case "doubleArithmetic": + env = + newEnv( + declarations( + newVar("left", org.projectnessie.cel.checker.Decls.Double), + newVar("right", org.projectnessie.cel.checker.Decls.Double))); + expression = "(left + 1.5) / (right - 2.0)"; + variables = Map.of("left", 50_021.5d, "right", 42.0d); + break; + case "stringConcat": + env = newEnv(declarations(newVar("left", String), newVar("right", String))); + expression = "left + right"; + variables = Map.of("left", "native", "right", "-plumbing"); + break; + case "scalarComparison": + env = newEnv(declarations(newVar("left", String), newVar("right", String))); + expression = "left < right"; + variables = Map.of("left", "native", "right", "wrapped"); + break; + default: + throw new IllegalArgumentException("unknown strict scalar shape " + shape); + } + Ast ast = compile(env, expression); + established = plan(env, ast, false); + integrated = plan(env, ast, true); + establishedProgram = env.program(ast, evalOptions(OptDisableNativeEval)); + integratedProgram = env.program(ast); + activation = newActivation(variables); + } + } + + @State(Scope.Benchmark) + public static class ScalarControlState { + @Param({"equality", "logical", "conditional"}) + String shape; + + Interpretable established; + Interpretable integrated; + Program establishedProgram; + Program integratedProgram; + Activation activation; + Map variables; + + @SuppressWarnings("DuplicatedCode") + @Setup + public void setup() { + Env env; + String expression; + switch (shape) { + case "equality": + env = newEnv(declarations(newVar("x", Int), newVar("y", Int))); + expression = "(x + 1) == y"; + variables = Map.of("x", 50_021L, "y", 50_022L); + break; + case "logical": + env = newEnv(declarations(newVar("x", Int), newVar("y", Int), newVar("z", Int))); + expression = "(x < y) && (y < z)"; + variables = Map.of("x", 50_021L, "y", 50_022L, "z", 50_023L); + break; + case "conditional": + env = + newEnv( + declarations( + newVar("condition", org.projectnessie.cel.checker.Decls.Bool), + newVar("x", Int), + newVar("y", Int))); + expression = "condition ? x + 1 : y - 1"; + variables = Map.of("condition", true, "x", 50_021L, "y", 50_023L); + break; + default: + throw new IllegalArgumentException("unknown scalar control shape " + shape); + } + Ast ast = compile(env, expression); + established = plan(env, ast, false); + integrated = plan(env, ast, true); + establishedProgram = env.program(ast, evalOptions(OptDisableNativeEval)); + integratedProgram = env.program(ast); + activation = newActivation(variables); + } + } + + @Benchmark + public Val establishedEval(ScalarState state) { + return state.established.eval(state.activation); + } + + @Benchmark + public Val dualNodeEstablishedEval(ScalarState state) { + return state.island.root().eval(state.activation); + } + + @Benchmark + public long typedInt(ScalarState state) { + return state.semantic.evalInt(state.activation); + } + + @Benchmark + public Val islandEval(ScalarState state) { + return state.island.eval(state.activation); + } + + @Benchmark + public Program.EvalResult integratedProgramBoundary(ScalarState state) { + return state.integrated.eval(state.variables); + } + + @Benchmark + public Program.EvalResult establishedProgramBoundary(ScalarState state) { + return state.establishedProgram.eval(state.variables); + } + + @Benchmark + public Val selectorEstablished(SelectorState state) { + return state.established.eval(state.activation); + } + + @Benchmark + public long selectorTyped(SelectorState state) { + return state.semantic.evalInt(state.activation); + } + + @Benchmark + public Val selectorDualNodeEstablished(SelectorState state) { + return state.island.root().eval(state.activation); + } + + @Benchmark + public Val selectorIsland(SelectorState state) { + return state.island.eval(state.activation); + } + + @Benchmark + public Val listIndexEstablished(ListIndexState state) { + return state.established.eval(state.activation); + } + + @Benchmark + public long listIndexTyped(ListIndexState state) { + return state.semantic.evalInt(state.activation); + } + + @Benchmark + public Val listIndexDualNodeEstablished(ListIndexState state) { + return state.island.root().eval(state.activation); + } + + @Benchmark + public Val listIndexIsland(ListIndexState state) { + return state.island.eval(state.activation); + } + + @Benchmark + public Val aggregateScalarRootEstablished(AggregateScalarRootState state) { + return state.established.eval(state.activation); + } + + @Benchmark + public Val aggregateScalarRootIntegrated(AggregateScalarRootState state) { + return state.integrated.eval(state.activation); + } + + @Benchmark + public Program.EvalResult aggregateScalarRootEstablishedProgram(AggregateScalarRootState state) { + return state.establishedProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult aggregateScalarRootIntegratedProgram(AggregateScalarRootState state) { + return state.integratedProgram.eval(state.variables); + } + + @Benchmark + public Val mixedEstablished(MixedState state) { + return state.established.eval(state.activation); + } + + @Benchmark + public boolean mixedTyped(MixedState state) { + return state.semantic.evalBoolean(state.activation); + } + + @Benchmark + public Val mixedIsland(MixedState state) { + return state.island.eval(state.activation); + } + + @Benchmark + public Val establishedOuter(EstablishedOuterState state) { + return state.established.eval(state.activation); + } + + @Benchmark + public Val establishedOuterWithIslands(EstablishedOuterState state) { + return state.integrated.eval(state.activation); + } + + @Benchmark + public Interpretable constructEstablished(ConstructionState state) { + return plan(state.env, state.ast, false); + } + + @Benchmark + public Interpretable constructIntegrated(ConstructionState state) { + return plan(state.env, state.ast, true); + } + + @Benchmark + public Val scalarRootEstablished(ScalarRootState state) { + return state.established.eval(state.activation); + } + + @Benchmark + public Val scalarRootIntegrated(ScalarRootState state) { + return state.integrated.eval(state.activation); + } + + @Benchmark + public Program.EvalResult scalarRootEstablishedProgram(ScalarRootState state) { + return state.establishedProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult scalarRootIntegratedProgram(ScalarRootState state) { + return state.integratedProgram.eval(state.variables); + } + + @Benchmark + public Program constructScalarRootEstablishedProgram(ScalarRootState state) { + return state.env.program(state.ast, evalOptions(OptDisableNativeEval)); + } + + @Benchmark + public Program constructScalarRootIntegratedProgram(ScalarRootState state) { + return state.env.program(state.ast); + } + + @Benchmark + public Val strictScalarEstablished(StrictScalarState state) { + return state.established.eval(state.activation); + } + + @Benchmark + public Val strictScalarIntegrated(StrictScalarState state) { + return state.integrated.eval(state.activation); + } + + @Benchmark + public Program.EvalResult strictScalarEstablishedProgram(StrictScalarState state) { + return state.establishedProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult strictScalarIntegratedProgram(StrictScalarState state) { + return state.integratedProgram.eval(state.variables); + } + + @Benchmark + public Val scalarControlEstablished(ScalarControlState state) { + return state.established.eval(state.activation); + } + + @Benchmark + public Val scalarControlIntegrated(ScalarControlState state) { + return state.integrated.eval(state.activation); + } + + @Benchmark + public Program.EvalResult scalarControlEstablishedProgram(ScalarControlState state) { + return state.establishedProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult scalarControlIntegratedProgram(ScalarControlState state) { + return state.integratedProgram.eval(state.variables); + } + + private static Interpretable plan(Env env, Ast ast, boolean nativePlanning) { + return plan(env, ast, nativePlanning, new Overload[0]); + } + + private static Interpretable plan( + Env env, Ast ast, boolean nativePlanning, Overload... customOverloads) { + Dispatcher dispatcher = newDispatcher(); + dispatcher.add(standardOverloads()); + dispatcher.add(customOverloads); + AttributeFactory attributes = + newAttributeFactory(defaultContainer, env.getTypeAdapter(), env.getTypeProvider()); + Interpreter interpreter = + newInterpreter( + dispatcher, + defaultContainer, + env.getTypeProvider(), + env.getTypeAdapter(), + attributes, + nativePlanning); + var checked = astToCheckedExpr(ast); + return nativePlanning + ? ((ExprInterpreter) interpreter).checkedPlanner(checked).plan(checked.getExpr()) + : interpreter.newInterpretable(checked); + } + + private static Ast compile(Env env, String expression) { + AstIssuesTuple result = env.compile(expression); + if (result.hasIssues()) { + throw Objects.requireNonNull(result.getIssues().err()); + } + return result.getAst(); + } +} diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/OverloadDispatchBench.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/OverloadDispatchBench.java new file mode 100644 index 00000000..6eb082a6 --- /dev/null +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/OverloadDispatchBench.java @@ -0,0 +1,473 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.TypeT.newObjectTypeValue; +import static org.projectnessie.cel.interpreter.Activation.emptyActivation; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Library; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.common.types.pb.ProtoTypeRegistry; +import org.projectnessie.cel.common.types.ref.BaseVal; +import org.projectnessie.cel.common.types.ref.Type; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Receiver; +import org.projectnessie.cel.common.types.traits.Trait; +import org.projectnessie.cel.extension.MathLib; +import org.projectnessie.cel.extension.StringsLib; +import org.projectnessie.cel.interpreter.functions.BinaryOp; +import org.projectnessie.cel.interpreter.functions.FunctionOp; +import org.projectnessie.cel.interpreter.functions.QuaternaryOp; +import org.projectnessie.cel.interpreter.functions.QuinaryOp; +import org.projectnessie.cel.interpreter.functions.TernaryOp; +import org.projectnessie.cel.interpreter.functions.UnaryOp; + +@Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Fork(3) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class OverloadDispatchBench { + + private static final String FUNCTION = "benchmark"; + private static final String OVERLOAD = "benchmark_overload"; + private static final Type RECEIVER_TYPE = + newObjectTypeValue("benchmark_receiver", Trait.ReceiverType); + + @State(Scope.Thread) + public static class DispatchState { + Activation activation; + + FunctionOp zeroOp; + UnaryOp unaryOp; + BinaryOp binaryOp; + TernaryOp ternaryOp; + QuaternaryOp quaternaryOp; + QuinaryOp quinaryOp; + FunctionOp functionOp; + + Val unaryArg; + Val binaryLhs; + Val binaryRhs; + Val[] emptyArgs; + Val[] arity3Args; + Val[] arity4Args; + Val[] arity5Args; + + BenchmarkReceiver receiver; + Val[] receiverTail0; + Val[] receiverTail1; + Val[] receiverTail2; + Val[] receiverTail3; + Val[] receiverTail4; + + Interpretable evalZero; + Interpretable evalUnary; + Interpretable evalBinary; + Interpretable evalVarArgs3; + Interpretable evalTernary3; + Interpretable evalVarArgs4; + Interpretable evalQuaternary4; + Interpretable evalVarArgs5; + Interpretable evalQuinary5; + Interpretable evalReceiverTail0; + Interpretable evalReceiverTail1; + Interpretable evalReceiverTail2; + Interpretable evalReceiverTail3; + Interpretable evalReceiverTail4; + Interpretable evalBoundGenericTraitSuccess3; + Interpretable evalBoundGenericTraitFallback3; + Program stringsIndexOfOffset; + Program stringsReplaceN; + Program mathGreatest3; + Program mathGreatest4; + Program mathGreatest5; + + @Setup + public void init() { + activation = emptyActivation(); + + zeroOp = values -> True; + unaryOp = value -> value; + binaryOp = (lhs, rhs) -> rhs; + ternaryOp = (first, second, third) -> third; + quaternaryOp = (first, second, third, fourth) -> fourth; + quinaryOp = (first, second, third, fourth, fifth) -> fifth; + functionOp = values -> values[values.length - 1]; + + unaryArg = intOf(1); + binaryLhs = intOf(2); + binaryRhs = intOf(3); + emptyArgs = new Val[0]; + arity3Args = new Val[] {intOf(4), intOf(5), intOf(6)}; + arity4Args = new Val[] {intOf(7), intOf(8), intOf(9), intOf(10)}; + arity5Args = new Val[] {intOf(11), intOf(12), intOf(13), intOf(14), intOf(15)}; + + receiver = new BenchmarkReceiver(); + receiverTail0 = new Val[0]; + receiverTail1 = new Val[] {intOf(11)}; + receiverTail2 = new Val[] {intOf(12), intOf(13)}; + receiverTail3 = new Val[] {intOf(14), intOf(15), intOf(16)}; + receiverTail4 = new Val[] {intOf(17), intOf(18), intOf(19), intOf(20)}; + + evalZero = new EvalZeroArity(1, FUNCTION, OVERLOAD, zeroOp); + evalUnary = new EvalUnary(2, FUNCTION, OVERLOAD, constant(2, unaryArg), null, unaryOp); + evalBinary = + new EvalBinary( + 3, + FUNCTION, + OVERLOAD, + constant(3, binaryLhs), + constant(4, binaryRhs), + null, + binaryOp); + evalVarArgs3 = + new EvalVarArgs(5, FUNCTION, OVERLOAD, constants(5, arity3Args), null, functionOp); + evalTernary3 = + new EvalTernary( + 6, + FUNCTION, + OVERLOAD, + constant(8, arity3Args[0]), + constant(9, arity3Args[1]), + constant(10, arity3Args[2]), + null, + ternaryOp); + evalVarArgs4 = + new EvalVarArgs(7, FUNCTION, OVERLOAD, constants(11, arity4Args), null, functionOp); + evalQuaternary4 = + new EvalQuaternary( + 8, + FUNCTION, + OVERLOAD, + constant(15, arity4Args[0]), + constant(16, arity4Args[1]), + constant(17, arity4Args[2]), + constant(18, arity4Args[3]), + null, + quaternaryOp); + evalVarArgs5 = + new EvalVarArgs(9, FUNCTION, OVERLOAD, constants(19, arity5Args), null, functionOp); + evalQuinary5 = + new EvalQuinary( + 10, + FUNCTION, + OVERLOAD, + constant(24, arity5Args[0]), + constant(25, arity5Args[1]), + constant(26, arity5Args[2]), + constant(27, arity5Args[3]), + constant(28, arity5Args[4]), + null, + quinaryOp); + + evalReceiverTail0 = new EvalUnary(7, FUNCTION, OVERLOAD, constant(12, receiver), null, null); + evalReceiverTail1 = + new EvalBinary( + 8, + FUNCTION, + OVERLOAD, + constant(13, receiver), + constant(14, receiverTail1[0]), + null, + null); + evalReceiverTail2 = + new EvalReceiverVarArgs(9, FUNCTION, OVERLOAD, receiverArgs(15, receiver, receiverTail2)); + evalReceiverTail3 = + new EvalReceiverVarArgs( + 10, FUNCTION, OVERLOAD, receiverArgs(18, receiver, receiverTail3)); + evalReceiverTail4 = + new EvalReceiverVarArgs( + 11, FUNCTION, OVERLOAD, receiverArgs(22, receiver, receiverTail4)); + + var receiverArgs3 = receiverArgs(27, receiver, receiverTail2); + evalBoundGenericTraitSuccess3 = + new EvalVarArgs(12, FUNCTION, OVERLOAD, receiverArgs3, Trait.ReceiverType, functionOp); + evalBoundGenericTraitFallback3 = + new EvalVarArgs(13, FUNCTION, OVERLOAD, receiverArgs3, Trait.AdderType, functionOp); + + var env = + Env.newCustomEnv( + ProtoTypeRegistry.newRegistry(), + List.of(Library.StdLib(), StringsLib.strings(), MathLib.math())); + stringsIndexOfOffset = compile(env, "'tacocat'.indexOf('a', 3)"); + stringsReplaceN = compile(env, "'hello hello'.replace('he', 'we', 1)"); + mathGreatest3 = compile(env, "math.greatest(5, 10, 3)"); + mathGreatest4 = compile(env, "math.greatest(5, 10, 3, 8)"); + mathGreatest5 = compile(env, "math.greatest(5, 10, 3, 8, 7)"); + } + + private static EvalConst constant(long id, Val value) { + return new EvalConst(id, value); + } + + private static Interpretable[] constants(long firstId, Val[] values) { + var constants = new Interpretable[values.length]; + for (int i = 0; i < values.length; i++) { + constants[i] = constant(firstId + i, values[i]); + } + return constants; + } + + private static Interpretable[] receiverArgs( + long firstId, BenchmarkReceiver receiver, Val[] tail) { + var values = new Val[tail.length + 1]; + values[0] = receiver; + System.arraycopy(tail, 0, values, 1, tail.length); + return constants(firstId, values); + } + + private static Program compile(Env env, String expression) { + var ast = env.compile(expression); + if (ast.hasIssues()) { + throw new IllegalStateException(ast.getIssues().toString()); + } + return env.program(ast.getAst()); + } + } + + @Benchmark + public Val directFunctionZero(DispatchState state) { + return state.zeroOp.invoke(state.emptyArgs); + } + + @Benchmark + public Val evalZero(DispatchState state) { + return state.evalZero.eval(state.activation); + } + + @Benchmark + public Val directUnary(DispatchState state) { + return state.unaryOp.invoke(state.unaryArg); + } + + @Benchmark + public Val evalUnary(DispatchState state) { + return state.evalUnary.eval(state.activation); + } + + @Benchmark + public Val directBinary(DispatchState state) { + return state.binaryOp.invoke(state.binaryLhs, state.binaryRhs); + } + + @Benchmark + public Val evalBinary(DispatchState state) { + return state.evalBinary.eval(state.activation); + } + + @Benchmark + public Val directFunctionArity3(DispatchState state) { + return state.functionOp.invoke(state.arity3Args); + } + + @Benchmark + public Val directTernary(DispatchState state) { + return state.ternaryOp.invoke(state.arity3Args[0], state.arity3Args[1], state.arity3Args[2]); + } + + @Benchmark + public Val evalVarArgsArity3(DispatchState state) { + return state.evalVarArgs3.eval(state.activation); + } + + @Benchmark + public Val evalTernaryArity3(DispatchState state) { + return state.evalTernary3.eval(state.activation); + } + + @Benchmark + public Val directFunctionArity4(DispatchState state) { + return state.functionOp.invoke(state.arity4Args); + } + + @Benchmark + public Val directQuaternary(DispatchState state) { + return state.quaternaryOp.invoke( + state.arity4Args[0], state.arity4Args[1], state.arity4Args[2], state.arity4Args[3]); + } + + @Benchmark + public Val evalVarArgsArity4(DispatchState state) { + return state.evalVarArgs4.eval(state.activation); + } + + @Benchmark + public Val evalQuaternaryArity4(DispatchState state) { + return state.evalQuaternary4.eval(state.activation); + } + + @Benchmark + public Val directFunctionArity5(DispatchState state) { + return state.functionOp.invoke(state.arity5Args); + } + + @Benchmark + public Val evalVarArgsArity5(DispatchState state) { + return state.evalVarArgs5.eval(state.activation); + } + + @Benchmark + public Val directQuinary(DispatchState state) { + return state.quinaryOp.invoke( + state.arity5Args[0], + state.arity5Args[1], + state.arity5Args[2], + state.arity5Args[3], + state.arity5Args[4]); + } + + @Benchmark + public Val evalQuinaryArity5(DispatchState state) { + return state.evalQuinary5.eval(state.activation); + } + + @Benchmark + public Val directReceiveTail0(DispatchState state) { + return state.receiver.receive(FUNCTION, OVERLOAD, state.receiverTail0); + } + + @Benchmark + public Val evalReceiverTail0(DispatchState state) { + return state.evalReceiverTail0.eval(state.activation); + } + + @Benchmark + public Val directReceiveTail1(DispatchState state) { + return state.receiver.receive(FUNCTION, OVERLOAD, state.receiverTail1); + } + + @Benchmark + public Val evalReceiverTail1(DispatchState state) { + return state.evalReceiverTail1.eval(state.activation); + } + + @Benchmark + public Val directReceiveTail2(DispatchState state) { + return state.receiver.receive(FUNCTION, OVERLOAD, state.receiverTail2); + } + + @Benchmark + public Val evalReceiverTail2(DispatchState state) { + return state.evalReceiverTail2.eval(state.activation); + } + + @Benchmark + public Val directReceiveTail3(DispatchState state) { + return state.receiver.receive(FUNCTION, OVERLOAD, state.receiverTail3); + } + + @Benchmark + public Val evalReceiverTail3(DispatchState state) { + return state.evalReceiverTail3.eval(state.activation); + } + + @Benchmark + public Val directReceiveTail4(DispatchState state) { + return state.receiver.receive(FUNCTION, OVERLOAD, state.receiverTail4); + } + + @Benchmark + public Val evalReceiverTail4(DispatchState state) { + return state.evalReceiverTail4.eval(state.activation); + } + + @Benchmark + public Val evalBoundGenericTraitSuccessArity3(DispatchState state) { + return state.evalBoundGenericTraitSuccess3.eval(state.activation); + } + + @Benchmark + public Val evalBoundGenericTraitFallbackArity3(DispatchState state) { + return state.evalBoundGenericTraitFallback3.eval(state.activation); + } + + @Benchmark + public Val evalStringsIndexOfOffset(DispatchState state) { + return state.stringsIndexOfOffset.eval(state.activation).getVal(); + } + + @Benchmark + public Val evalStringsReplaceN(DispatchState state) { + return state.stringsReplaceN.eval(state.activation).getVal(); + } + + @Benchmark + public Val evalMathGreatest3(DispatchState state) { + return state.mathGreatest3.eval(state.activation).getVal(); + } + + @Benchmark + public Val evalMathGreatest4(DispatchState state) { + return state.mathGreatest4.eval(state.activation).getVal(); + } + + @Benchmark + public Val evalMathGreatest5(DispatchState state) { + return state.mathGreatest5.eval(state.activation).getVal(); + } + + private static final class BenchmarkReceiver extends BaseVal implements Receiver { + @SuppressWarnings("removal") + @Override + public T convertToNative(Class typeDesc) { + throw new UnsupportedOperationException(); + } + + @Override + public Val convertToType(Type typeValue) { + return this; + } + + @Override + public Val equal(Val other) { + return other == this ? True : False; + } + + @Override + public Type type() { + return RECEIVER_TYPE; + } + + @Override + public Object value() { + return "benchmark_receiver"; + } + + @Override + public Val receive(String function, String overload, Val... args) { + return args.length == 0 ? this : args[args.length - 1]; + } + } +} diff --git a/core/src/main/congocc/cel/cel-java.ccc b/core/src/main/congocc/cel/cel-java.ccc new file mode 100644 index 00000000..07ac96ee --- /dev/null +++ b/core/src/main/congocc/cel/cel-java.ccc @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +PARSER_PACKAGE="org.projectnessie.cel.parser"; +PARSER_CLASS=CelGrammarParser; +LEXER_CLASS=CelGrammarLexer; +NODE_PACKAGE="org.projectnessie.cel.parser.ast"; + +INCLUDE "cel.ccc" + +INJECT PARSER_CLASS : +{ + private boolean nextTokenStartsExpression() { + TokenType type = getToken(1).getType(); + return switch (type) { + case LBRACKET, LBRACE, LPAREN, DOT, MINUS, EXCLAM, TRUE, FALSE, NULL, NUM_UINT, NUM_FLOAT, + NUM_INT, STRING, BYTES, IDENTIFIER -> true; + default -> false; + }; + } +} + +INJECT ParseException : +{ + @SuppressWarnings("unchecked") + public java.util.Set getExpectedTokenTypes() { + java.util.Set x = expectedTypes; + return x; + } +} + +INJECT Expr : implements CelExprNode; +INJECT Expr : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitExpr(this); + } +} + +INJECT ConditionalOr : implements CelExprNode; +INJECT ConditionalOr : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitBalanced(this, org.projectnessie.cel.common.operators.Operator.LogicalOr); + } +} + +INJECT ConditionalAnd : implements CelExprNode; +INJECT ConditionalAnd : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitBalanced(this, org.projectnessie.cel.common.operators.Operator.LogicalAnd); + } +} + +INJECT Relation : implements CelExprNode; +INJECT Relation : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitBinary(this); + } +} + +INJECT Calc : implements CelExprNode; +INJECT Calc : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitBinary(this); + } +} + +INJECT Multiplicative : implements CelExprNode; +INJECT Multiplicative : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitBinary(this); + } +} + +INJECT Unary : implements CelExprNode; +INJECT Unary : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitUnary(this); + } +} + +INJECT Member : implements CelExprNode; +INJECT Member : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitMember(this); + } +} + +INJECT Primary : implements CelExprNode; +INJECT Primary : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitPrimary(this); + } +} + +INJECT ConstantLiteral : implements CelExprNode; +INJECT ConstantLiteral : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitLiteral(this); + } +} + +INJECT Literal : implements CelExprNode; +INJECT Literal : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitLiteral(this); + } +} + +INJECT Identifier : implements CelExprNode; +INJECT Identifier : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitIdentifier(this); + } +} diff --git a/core/src/main/congocc/cel/cel-lexer.ccc b/core/src/main/congocc/cel/cel-lexer.ccc new file mode 100644 index 00000000..62c4c32f --- /dev/null +++ b/core/src/main/congocc/cel/cel-lexer.ccc @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +SKIP : #Whitespace; + +UNPARSED : #Comment; + +TOKEN #Operator : + + | ="> + | + | + | + | + | "> + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + ; + +TOKEN #Literal : + + | + | + | <#HEXDIGIT : ["0"-"9", "a"-"f", "A"-"F"]> + | <#DIGIT : ["0"-"9"]> + | <#EXPONENT : ["e", "E"] (["+", "-"])? ()+> + | )+ | "0x" ()+) ["u", "U"]> + | )+ "." ()+ ()? | ()+ | "." ()+ ()?)> + | )+ | "0x" ()+> + | <#ESC_CHAR_SEQ : "\\" ["a", "b", "f", "n", "r", "t", "v", "\"", "'", "\\", "?", "`"]> + | <#ESC_OCT_SEQ : "\\" ["0"-"3"] ["0"-"7"] ["0"-"7"]> + | <#ESC_BYTE_SEQ : "\\" ["x", "X"] > + | <#ESC_UNI_SEQ : "\\" "u" | "\\" "U" > + | <#ESC_SEQ : | | | > + | <#DQ_CHAR : | ~["\\", "\"", "\n", "\r"]> + | <#SQ_CHAR : | ~["\\", "'", "\n", "\r"]> + | <#TDQ_CHAR : | ~["\\"]> + | <#TSQ_CHAR : | ~["\\"]> + | )* "\"" + | "'" ()* "'" + | "\"\"\"" ()* "\"\"\"" + | "'''" ()* "'''" + | ["r", "R"] "\"" (~["\"", "\n", "\r"])* "\"" + | ["r", "R"] "'" (~["'", "\n", "\r"])* "'" + | ["r", "R"] "\"\"\"" (~[])* "\"\"\"" + | ["r", "R"] "'''" (~[])* "'''" + > + | > + ; + +TOKEN #Identifier : + + | | ~["\\", "`", "\n", "\r"])* "`"> + ; diff --git a/core/src/main/congocc/cel/cel.ccc b/core/src/main/congocc/cel/cel.ccc new file mode 100644 index 00000000..fbd02f89 --- /dev/null +++ b/core/src/main/congocc/cel/cel.ccc @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +INCLUDE "cel-lexer.ccc" + +Start : Expr! ! ; + +Expr : + ConditionalOr + [ ConditionalOr Expr] + ; + +ConditionalOr : + ConditionalAnd ( ConditionalAnd)*! + ; + +ConditionalAnd : + Relation ( Relation)*! + ; + +Relation : + Calc (( | | | | | | ) Calc)*! + ; + +Calc : + Multiplicative (( | ) Multiplicative)*! + ; + +Multiplicative : + Unary (( | | ) Unary)*! + ; + +Unary : + ( | )* Member + ; + +Member : + Primary + ( + [] Field [ ( | ExprList )] + | [] Expr + | [FieldInitializerList] [] + )*! + ; + +Primary : + [] [ ( | ExprList )] + | Expr + | ( | ListInitializerList [] ) + | ( | MapInitializerList [] ) + | ConstantLiteral + ; + +ExprList : + Expr ( Expr =>||)*! + ; + +ListInitializerList : + [] Expr ( [] Expr =>||)*! + ; + +FieldInitializerList : + [] Field Expr ( [] Field Expr =>||)*! + ; + +Field : + + | + ; + +MapInitializerList : + [] Expr Expr ( [] Expr Expr =>||)*! + ; + +ConstantLiteral : + + | + | + | + | + | + | + | + ; diff --git a/core/src/main/java/org/projectnessie/cel/CEL.java b/core/src/main/java/org/projectnessie/cel/CEL.java index 25d55a9c..9410bcad 100644 --- a/core/src/main/java/org/projectnessie/cel/CEL.java +++ b/core/src/main/java/org/projectnessie/cel/CEL.java @@ -38,6 +38,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; import org.projectnessie.cel.interpreter.Activation; import org.projectnessie.cel.interpreter.Activation.PartialActivation; import org.projectnessie.cel.interpreter.AttributePattern; @@ -86,7 +87,12 @@ public static Program newProgram(Env e, Ast ast, ProgramOption... opts) { Interpreter interp = newInterpreter( - disp, e.getContainer(), e.getTypeProvider(), e.getTypeAdapter(), p.attrFactory); + disp, + e.getContainer(), + e.getTypeProvider(), + e.getTypeAdapter(), + p.attrFactory, + nativePlanningPermitted(p.evalOpts, p.decorators)); p.interpreter = interp; // Translate the EvalOption flags into InterpretableDecorator instances. @@ -158,6 +164,11 @@ private static Program initInterpretable( return p; } + static boolean nativePlanningPermitted( + Set evalOptions, List decorators) { + return evalOptions.isEmpty() && decorators.isEmpty(); + } + /** CheckedExprToAst converts a checked expression proto message to an Ast. */ public static Ast checkedExprToAst(CheckedExpr checkedExpr) { Map refMap = checkedExpr.getReferenceMapMap(); diff --git a/core/src/main/java/org/projectnessie/cel/Env.java b/core/src/main/java/org/projectnessie/cel/Env.java index 02b27346..0ddbb0f6 100644 --- a/core/src/main/java/org/projectnessie/cel/Env.java +++ b/core/src/main/java/org/projectnessie/cel/Env.java @@ -281,9 +281,8 @@ public Env extend(List opts) { // be immutable. Since it is possible to set the TypeProvider separately // from the TypeAdapter, the possible configurations which could use a // TypeRegistry as the base implementation are captured below. - if (this.adapter instanceof TypeRegistry && this.provider instanceof TypeRegistry) { - TypeRegistry adapterReg = (TypeRegistry) this.adapter; - TypeRegistry providerReg = (TypeRegistry) this.provider; + if (this.adapter instanceof TypeRegistry adapterReg + && this.provider instanceof TypeRegistry providerReg) { TypeRegistry reg = providerReg.copy(); provider = reg; // If the adapter and provider are the same object, set the adapter diff --git a/core/src/main/java/org/projectnessie/cel/EnvOption.java b/core/src/main/java/org/projectnessie/cel/EnvOption.java index 71f824af..9ddc2ba5 100644 --- a/core/src/main/java/org/projectnessie/cel/EnvOption.java +++ b/core/src/main/java/org/projectnessie/cel/EnvOption.java @@ -229,12 +229,11 @@ static EnvOption abbrevs(String... qualifiedNames) { */ static EnvOption types(List addTypes) { return e -> { - if (!(e.provider instanceof TypeRegistry)) { + if (!(e.provider instanceof TypeRegistry reg)) { throw new RuntimeException( String.format( "custom types not supported by provider: %s", e.provider.getClass().getName())); } - TypeRegistry reg = (TypeRegistry) e.provider; for (Object t : addTypes) { reg.register(t); } diff --git a/core/src/main/java/org/projectnessie/cel/EvalOption.java b/core/src/main/java/org/projectnessie/cel/EvalOption.java index 7b24ffb9..db08e2e1 100644 --- a/core/src/main/java/org/projectnessie/cel/EvalOption.java +++ b/core/src/main/java/org/projectnessie/cel/EvalOption.java @@ -42,7 +42,13 @@ public enum EvalOption { *

By itself, OptPartialEval does not change evaluation behavior unless the input to the * Program Eval is an PartialVars. */ - OptPartialEval(8); + OptPartialEval(8), + + /** + * OptDisableNativeEval forces checked expressions to use the standard interpreter instead of + * native-value planned evaluation. + */ + OptDisableNativeEval(16); private final int mask; diff --git a/core/src/main/java/org/projectnessie/cel/Library.java b/core/src/main/java/org/projectnessie/cel/Library.java index a3dedb5b..45e0a533 100644 --- a/core/src/main/java/org/projectnessie/cel/Library.java +++ b/core/src/main/java/org/projectnessie/cel/Library.java @@ -17,12 +17,12 @@ import static org.projectnessie.cel.EnvOption.declarations; import static org.projectnessie.cel.EnvOption.macros; -import static org.projectnessie.cel.ProgramOption.functions; import static org.projectnessie.cel.checker.Checker.StandardDeclarations; import static org.projectnessie.cel.interpreter.functions.Overload.standardOverloads; import static org.projectnessie.cel.parser.Macro.AllMacros; import java.util.List; +import org.projectnessie.cel.interpreter.functions.Overload; /** * Library provides a collection of EnvOption and ProgramOption values used to confiugre a CEL @@ -83,7 +83,12 @@ public List getCompileOptions() { /** ProgramOptions returns function implementations for the standard CEL functions. */ @Override public List getProgramOptions() { - return List.of(functions(standardOverloads())); + Overload[] overloads = standardOverloads(); + return List.of( + p -> { + p.dispatcher.add(overloads); + return p; + }); } } } diff --git a/core/src/main/java/org/projectnessie/cel/checker/Checker.java b/core/src/main/java/org/projectnessie/cel/checker/Checker.java index 25737a5a..5fdc2cc2 100644 --- a/core/src/main/java/org/projectnessie/cel/checker/Checker.java +++ b/core/src/main/java/org/projectnessie/cel/checker/Checker.java @@ -48,8 +48,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.projectnessie.cel.checker.Types.Kind; import org.projectnessie.cel.common.Location; import org.projectnessie.cel.common.Source; @@ -228,9 +230,11 @@ void checkIdent(Expr.Builder e) { Decl ident = env.lookupIdent(identExpr.getName()); if (ident != null) { setType(e, ident.getIdent().getType()); - setReference(e, newIdentReference(ident.getName(), ident.getIdent().getValue())); + String identName = + identExpr.getName().startsWith(".") ? "." + ident.getName() : ident.getName(); + setReference(e, newIdentReference(identName, ident.getIdent().getValue())); // Overwrite the identifier with its fully qualified name. - identExpr.setName(ident.getName()); + identExpr.setName(identName); return; } @@ -242,7 +246,7 @@ void checkSelect(Expr.Builder e) { Select.Builder sel = e.getSelectExprBuilder(); // Before traversing down the tree, try to interpret as qualified name. String qname = Container.toQualifiedName(e.build()); - if (qname != null) { + if (qname != null && !isQualifiedLocalVariableSelection(sel.getOperandBuilder())) { Decl ident = env.lookupIdent(qname); if (ident != null) { if (sel.getTestOnly()) { @@ -281,6 +285,13 @@ void checkSelect(Expr.Builder e) { resultType = fieldType.type; } break; + case kindAbstract: + if (isOptionalType(targetType)) { + resultType = Decls.newAbstractType("optional_type", Collections.singletonList(Decls.Dyn)); + } else { + errors.typeDoesNotSupportFieldSelection(location(e), targetType); + } + break; case kindTypeParam: // Set the operand type to DYN to prevent assignment to a potentionally incorrect type // at a later point in type-checking. The isAssignable call will update the type @@ -305,6 +316,20 @@ void checkSelect(Expr.Builder e) { setType(e, resultType); } + private static boolean isOptionalType(Type type) { + return type.hasAbstractType() && "optional_type".equals(type.getAbstractType().getName()); + } + + private boolean isQualifiedLocalVariableSelection(Expr.Builder e) { + if (e.getExprKindCase() == Expr.ExprKindCase.IDENT_EXPR) { + return env.hasLocalIdent(e.getIdentExpr().getName()); + } + if (e.getExprKindCase() == Expr.ExprKindCase.SELECT_EXPR) { + return isQualifiedLocalVariableSelection(e.getSelectExprBuilder().getOperandBuilder()); + } + return false; + } + void checkCall(Expr.Builder e) { // Note: similar logic exists within the `interpreter/planner.go`. If making changes here // please consider the impact on planner.go and consolidate implementations or mirror code @@ -415,10 +440,11 @@ OverloadResolution resolveOverload( } Type overloadType = Decls.newFunctionType(overload.getResultType(), overload.getParamsList()); - if (overload.getTypeParamsCount() > 0) { + Set typeParams = collectOverloadTypeParams(overload); + if (!typeParams.isEmpty()) { // Instantiate overload's type with fresh type variables. Mapping substitutions = newMapping(); - for (String typePar : overload.getTypeParamsList()) { + for (String typePar : typeParams) { substitutions.add(Decls.newTypeParamType(typePar), newTypeVar()); } overloadType = substitute(substitutions, overloadType, false); @@ -454,10 +480,21 @@ OverloadResolution resolveOverload( void checkCreateList(Expr.Builder e) { CreateList.Builder create = e.getListExprBuilder(); Type elemType = null; + boolean[] optionalIndices = new boolean[create.getElementsCount()]; + for (int index : create.getOptionalIndicesList()) { + optionalIndices[index] = true; + } for (int i = 0; i < create.getElementsBuilderList().size(); i++) { Expr.Builder el = create.getElementsBuilderList().get(i); check(el); - elemType = joinTypes(location(el), elemType, getType(el)); + Type type = getType(el); + if (optionalIndices[i]) { + Type unwrapped = optionalValueType(type); + if (unwrapped != null) { + type = unwrapped; + } + } + elemType = joinTypes(location(el), elemType, type); } if (elemType == null) { // If the list is empty, assign free type var to elem type. @@ -486,7 +523,14 @@ void checkCreateMap(Expr.Builder e) { Expr.Builder val = ent.getValueBuilder(); check(val); - valueType = joinTypes(location(val), valueType, getType(val)); + Type type = getType(val); + if (ent.getOptionalEntry()) { + Type unwrapped = optionalValueType(type); + if (unwrapped != null) { + type = unwrapped; + } + } + valueType = joinTypes(location(val), valueType, type); } if (keyType == null) { // If the map is empty, assign free type variables to typeKey and value type. @@ -538,12 +582,26 @@ void checkCreateMessage(Expr.Builder e) { if (t != null) { fieldType = t.type; } - if (!isAssignable(fieldType, getType(value))) { + Type valueType = getType(value); + if (ent.getOptionalEntry()) { + Type unwrapped = optionalValueType(valueType); + if (unwrapped != null) { + valueType = unwrapped; + } + } + if (!isAssignable(fieldType, valueType)) { errors.fieldTypeMismatch(locationByID(ent.getId()), field, fieldType, getType(value)); } } } + private static Type optionalValueType(Type type) { + if (!isOptionalType(type) || type.getAbstractType().getParameterTypesCount() == 0) { + return null; + } + return type.getAbstractType().getParameterTypes(0); + } + void checkComprehension(Expr.Builder e) { Comprehension.Builder comp = e.getComprehensionExprBuilder(); check(comp.getIterRangeBuilder()); @@ -551,14 +609,23 @@ void checkComprehension(Expr.Builder e) { Type accuType = getType(comp.getAccuInitBuilder()); Type rangeType = getType(comp.getIterRangeBuilder()); Type varType; + Type var2Type = null; switch (kindOf(rangeType)) { case kindList: - varType = rangeType.getListType().getElemType(); + if (comp.getIterVar2().isEmpty()) { + varType = rangeType.getListType().getElemType(); + } else { + varType = Decls.Int; + var2Type = rangeType.getListType().getElemType(); + } break; case kindMap: // Ranges over the keys. varType = rangeType.getMapType().getKeyType(); + if (!comp.getIterVar2().isEmpty()) { + var2Type = rangeType.getMapType().getValueType(); + } break; case kindDyn: case kindError: @@ -569,10 +636,16 @@ void checkComprehension(Expr.Builder e) { isAssignable(Decls.Dyn, rangeType); // Set the range iteration variable to type DYN as well. varType = Decls.Dyn; + if (!comp.getIterVar2().isEmpty()) { + var2Type = Decls.Dyn; + } break; default: errors.notAComprehensionRange(location(comp.getIterRangeBuilder()), rangeType); varType = Decls.Error; + if (!comp.getIterVar2().isEmpty()) { + var2Type = Decls.Error; + } break; } @@ -583,6 +656,9 @@ void checkComprehension(Expr.Builder e) { // Create a block scope for the loop. env = env.enterScope(); env.add(Decls.newVar(comp.getIterVar(), varType)); + if (!comp.getIterVar2().isEmpty()) { + env.add(Decls.newVar(comp.getIterVar2(), var2Type)); + } // Check the variable references in the condition and step. check(comp.getLoopConditionBuilder()); assertType(comp.getLoopConditionBuilder(), Decls.Bool); @@ -682,7 +758,7 @@ void setType(Expr.Builder e, Type t) { } Type getType(Expr.Builder e) { - return types.get(e.getId()); + return substitute(mappings, types.get(e.getId()), false); } void setReference(Expr.Builder e, Reference r) { @@ -716,6 +792,44 @@ static OverloadResolution newResolution(Reference checkedRef, Type t) { return new OverloadResolution(checkedRef, t); } + private static Set collectOverloadTypeParams(Overload overload) { + Set typeParams = new LinkedHashSet<>(overload.getTypeParamsList()); + overload.getParamsList().forEach(type -> collectTypeParams(type, typeParams)); + collectTypeParams(overload.getResultType(), typeParams); + return typeParams; + } + + private static void collectTypeParams(Type type, Set typeParams) { + switch (kindOf(type)) { + case kindTypeParam: + typeParams.add(type.getTypeParam()); + return; + case kindAbstract: + type.getAbstractType() + .getParameterTypesList() + .forEach(t -> collectTypeParams(t, typeParams)); + return; + case kindFunction: + type.getFunction().getArgTypesList().forEach(t -> collectTypeParams(t, typeParams)); + collectTypeParams(type.getFunction().getResultType(), typeParams); + return; + case kindList: + collectTypeParams(type.getListType().getElemType(), typeParams); + return; + case kindMap: + MapType mapType = type.getMapType(); + collectTypeParams(mapType.getKeyType(), typeParams); + collectTypeParams(mapType.getValueType(), typeParams); + return; + case kindType: + if (type.getType() != Type.getDefaultInstance()) { + collectTypeParams(type.getType(), typeParams); + } + return; + default: + } + } + Location location(Expr.Builder e) { return locationByID(e.getId()); } diff --git a/core/src/main/java/org/projectnessie/cel/checker/CheckerEnv.java b/core/src/main/java/org/projectnessie/cel/checker/CheckerEnv.java index 6ed4258e..cc68d810 100644 --- a/core/src/main/java/org/projectnessie/cel/checker/CheckerEnv.java +++ b/core/src/main/java/org/projectnessie/cel/checker/CheckerEnv.java @@ -36,6 +36,7 @@ import java.util.Collections; import java.util.List; import org.projectnessie.cel.common.containers.Container; +import org.projectnessie.cel.common.types.ref.TypeEnum; import org.projectnessie.cel.common.types.ref.TypeProvider; import org.projectnessie.cel.common.types.ref.Val; import org.projectnessie.cel.parser.Macro; @@ -118,6 +119,12 @@ public void add(List decls) { * such identifier is found in the Env. */ public Decl lookupIdent(String name) { + if (!name.startsWith(".") && hasLocalIdent(name)) { + Decl ident = declarations.findIdentInScope(name); + if (ident != null) { + return ident; + } + } for (String candidate : container.resolveCandidateNames(name)) { Decl ident = declarations.findIdent(candidate); if (ident != null) { @@ -146,10 +153,25 @@ public Decl lookupIdent(String name) { declarations.addIdent(decl); return decl; } + + Val identValue = provider.findIdent(candidate); + if (identValue != null && identValue.type().typeEnum() == TypeEnum.String) { + Decl decl = + Decls.newIdent( + candidate, + Decls.String, + Constant.newBuilder().setStringValue(identValue.value().toString()).build()); + declarations.addIdent(decl); + return decl; + } } return null; } + boolean hasLocalIdent(String name) { + return declarations.hasParent() && declarations.findIdentInScope(name) != null; + } + /** * LookupFunction returns a Decl proto for typeName as a function in env. Returns nil if no such * function is found in env. diff --git a/core/src/main/java/org/projectnessie/cel/checker/Printer.java b/core/src/main/java/org/projectnessie/cel/checker/Printer.java index 6a62054b..c8152d80 100644 --- a/core/src/main/java/org/projectnessie/cel/checker/Printer.java +++ b/core/src/main/java/org/projectnessie/cel/checker/Printer.java @@ -35,11 +35,10 @@ static final class SemanticAdorner implements Adorner { @Override public String getMetadata(Object elem) { - if (!(elem instanceof Expr)) { + if (!(elem instanceof Expr e)) { return ""; } StringBuilder result = new StringBuilder(); - Expr e = (Expr) elem; Type t = checks.getTypeMapMap().get(e.getId()); if (t != null) { result.append("~"); diff --git a/core/src/main/java/org/projectnessie/cel/checker/Scopes.java b/core/src/main/java/org/projectnessie/cel/checker/Scopes.java index 7e21e558..3a966fd9 100644 --- a/core/src/main/java/org/projectnessie/cel/checker/Scopes.java +++ b/core/src/main/java/org/projectnessie/cel/checker/Scopes.java @@ -59,6 +59,10 @@ public Scopes pop() { return this; } + boolean hasParent() { + return parent != null; + } + /** * AddIdent adds the ident Decl in the current scope. Note: If the name collides with an existing * identifier in the scope, the Decl is overwritten. diff --git a/core/src/main/java/org/projectnessie/cel/checker/TypeErrors.java b/core/src/main/java/org/projectnessie/cel/checker/TypeErrors.java index 303e7b6c..10f1efda 100644 --- a/core/src/main/java/org/projectnessie/cel/checker/TypeErrors.java +++ b/core/src/main/java/org/projectnessie/cel/checker/TypeErrors.java @@ -117,10 +117,6 @@ void typeMismatch(Location l, Type expected, Type actual) { formatCheckedType(actual)); } - public void unknownType(Location l, String info) { - // reportError(l, "unknown type%s", info != null ? " for: " + info : ""); - } - static String formatFunction(Type resultType, List argTypes, boolean isInstance) { StringBuilder result = new StringBuilder(); formatFunction(result, resultType, argTypes, isInstance); diff --git a/core/src/main/java/org/projectnessie/cel/checker/Types.java b/core/src/main/java/org/projectnessie/cel/checker/Types.java index 1032310e..4209bd9a 100644 --- a/core/src/main/java/org/projectnessie/cel/checker/Types.java +++ b/core/src/main/java/org/projectnessie/cel/checker/Types.java @@ -62,22 +62,17 @@ public static String formatCheckedType(Type t) { case kindNull: return "null"; case kindPrimitive: - switch (t.getPrimitive()) { - case UINT64: - return "uint"; - case INT64: - return "int"; - case BOOL: - return "bool"; - case BYTES: - return "bytes"; - case DOUBLE: - return "double"; - case STRING: - return "string"; - } - // unrecognizes & not-specified - ignore above - return t.getPrimitive().toString().toLowerCase(Locale.ROOT).trim(); + return switch (t.getPrimitive()) { + case UINT64 -> "uint"; + case INT64 -> "int"; + case BOOL -> "bool"; + case BYTES -> "bytes"; + case DOUBLE -> "double"; + case STRING -> "string"; + default -> + // unrecognizes & not-specified - ignore above + t.getPrimitive().toString().toLowerCase(Locale.ROOT).trim(); + }; case kindWellKnown: switch (t.getWellKnown()) { case ANY: @@ -196,14 +191,11 @@ private static void formatCheckedTypePrimitive(StringBuilder sb, Type.PrimitiveT static boolean isDyn(Type t) { // Note: object type values that are well-known and map to a DYN value in practice // are sanitized prior to being added to the environment. - switch (kindOf(t)) { - case kindDyn: - return true; - case kindWellKnown: - return t.getWellKnown() == WellKnownType.ANY; - default: - return false; - } + return switch (kindOf(t)) { + case kindDyn -> true; + case kindWellKnown -> t.getWellKnown() == WellKnownType.ANY; + default -> false; + }; } /** isDynOrError returns true if the input is either an Error, DYN, or well-known ANY message. */ @@ -337,6 +329,9 @@ static boolean internalIsAssignable(Mapping m, Type t1, Type t2) { if (isDynOrError(t1) || isDynOrError(t2)) { return true; } + if (kind2 == Kind.kindNull) { + return internalIsAssignableNull(t1); + } // Test for when the types do not need to agree, but are more specific than dyn. switch (kind1) { @@ -353,28 +348,22 @@ static boolean internalIsAssignable(Mapping m, Type t1, Type t2) { } // Test for when the types must agree. - switch (kind1) { + return switch (kind1) { // ERROR, TYPE_PARAM, and DYN handled above. - case kindAbstract: - return internalIsAssignableAbstractType(m, t1.getAbstractType(), t2.getAbstractType()); - case kindFunction: - return internalIsAssignableFunction(m, t1.getFunction(), t2.getFunction()); - case kindList: - return internalIsAssignable( - m, t1.getListType().getElemType(), t2.getListType().getElemType()); - case kindMap: - return internalIsAssignableMap(m, t1.getMapType(), t2.getMapType()); - case kindObject: - return t1.getMessageType().equals(t2.getMessageType()); - case kindType: - // A type is a type is a type, any additional parameterization of the - // type cannot affect method resolution or assignability. - return true; - case kindWellKnown: - return t1.getWellKnown() == t2.getWellKnown(); - default: - return false; - } + case kindAbstract -> + internalIsAssignableAbstractType(m, t1.getAbstractType(), t2.getAbstractType()); + case kindFunction -> internalIsAssignableFunction(m, t1.getFunction(), t2.getFunction()); + case kindList -> + internalIsAssignable(m, t1.getListType().getElemType(), t2.getListType().getElemType()); + case kindMap -> internalIsAssignableMap(m, t1.getMapType(), t2.getMapType()); + case kindObject -> t1.getMessageType().equals(t2.getMessageType()); + case kindType -> + // A type is a type is a type, any additional parameterization of the + // type cannot affect method resolution or assignability. + true; + case kindWellKnown -> t1.getWellKnown() == t2.getWellKnown(); + default -> false; + }; } /** @@ -426,16 +415,10 @@ static boolean internalIsAssignableMap(Mapping m, MapType m1, MapType m2) { /** internalIsAssignableNull returns true if the type is nullable. */ static boolean internalIsAssignableNull(Type t) { - switch (kindOf(t)) { - case kindAbstract: - case kindObject: - case kindNull: - case kindWellKnown: - case kindWrapper: - return true; - default: - return false; - } + return switch (kindOf(t)) { + case kindAbstract, kindObject, kindNull, kindWellKnown, kindWrapper -> true; + default -> false; + }; } /** @@ -443,14 +426,11 @@ static boolean internalIsAssignableNull(Type t) { * for the primitive type. */ static boolean internalIsAssignablePrimitive(PrimitiveType p, Type target) { - switch (kindOf(target)) { - case kindPrimitive: - return p == target.getPrimitive(); - case kindWrapper: - return p == target.getWrapper(); - default: - return false; - } + return switch (kindOf(target)) { + case kindPrimitive -> p == target.getPrimitive(); + case kindWrapper -> p == target.getWrapper(); + default -> false; + }; } /** isAssignable returns an updated type substitution mapping if t1 is assignable to t2. */ @@ -476,37 +456,44 @@ static Kind kindOf(Type t) { if (t == null || t.getTypeKindCase() == TypeKindCase.TYPEKIND_NOT_SET) { return Kind.kindUnknown; } - switch (t.getTypeKindCase()) { - case ERROR: - return Kind.kindError; - case FUNCTION: - return Kind.kindFunction; - case DYN: - return Kind.kindDyn; - case PRIMITIVE: - return Kind.kindPrimitive; - case WELL_KNOWN: - return Kind.kindWellKnown; - case WRAPPER: - return Kind.kindWrapper; - case NULL: - return Kind.kindNull; - case TYPE: - return Kind.kindType; - case LIST_TYPE: - return Kind.kindList; - case MAP_TYPE: - return Kind.kindMap; - case MESSAGE_TYPE: - return Kind.kindObject; - case TYPE_PARAM: - return Kind.kindTypeParam; - } - return Kind.kindUnknown; + return switch (t.getTypeKindCase()) { + case ERROR -> Kind.kindError; + case FUNCTION -> Kind.kindFunction; + case DYN -> Kind.kindDyn; + case PRIMITIVE -> Kind.kindPrimitive; + case WELL_KNOWN -> Kind.kindWellKnown; + case WRAPPER -> Kind.kindWrapper; + case NULL -> Kind.kindNull; + case ABSTRACT_TYPE -> Kind.kindAbstract; + case TYPE -> Kind.kindType; + case LIST_TYPE -> Kind.kindList; + case MAP_TYPE -> Kind.kindMap; + case MESSAGE_TYPE -> Kind.kindObject; + case TYPE_PARAM -> Kind.kindTypeParam; + default -> Kind.kindUnknown; + }; } /** mostGeneral returns the more general of two types which are known to unify. */ static Type mostGeneral(Type t1, Type t2) { + Kind kind1 = kindOf(t1); + Kind kind2 = kindOf(t2); + if (kind1 == Kind.kindNull && internalIsAssignableNull(t2)) { + return t2; + } + if (kind2 == Kind.kindNull && internalIsAssignableNull(t1)) { + return t1; + } + if (kind1 == Kind.kindPrimitive && kind2 == Kind.kindWrapper) { + if (t1.getPrimitive() == t2.getWrapper()) { + return t2; + } + } + if (kind1 == Kind.kindWrapper && kind2 == Kind.kindPrimitive) { + if (t1.getWrapper() == t2.getPrimitive()) { + return t1; + } + } if (isEqualOrLessSpecific(t1, t2)) { return t1; } diff --git a/core/src/main/java/org/projectnessie/cel/common/CELError.java b/core/src/main/java/org/projectnessie/cel/common/CELError.java index 7bb93511..88d039c4 100644 --- a/core/src/main/java/org/projectnessie/cel/common/CELError.java +++ b/core/src/main/java/org/projectnessie/cel/common/CELError.java @@ -102,9 +102,7 @@ public String toDisplayString(Source source) { // sophisticated way, maybe use jline's WCWidth, but that one is also quite rudimentary wrt // code-blocks (e.g. doesn't know about emojis). result.append("\n | "); - for (int i = 0; i < location.column(); i++) { - result.append(dot); - } + result.append(String.valueOf(dot).repeat(Math.max(0, location.column()))); result.append(ind); } return result.toString(); diff --git a/core/src/main/java/org/projectnessie/cel/common/debug/Debug.java b/core/src/main/java/org/projectnessie/cel/common/debug/Debug.java index 8ea2e168..d3cbde99 100644 --- a/core/src/main/java/org/projectnessie/cel/common/debug/Debug.java +++ b/core/src/main/java/org/projectnessie/cel/common/debug/Debug.java @@ -363,9 +363,7 @@ void appendFormat(String f, Object... args) { void doIndent() { if (lineStart) { lineStart = false; - for (int i = 0; i < indent; i++) { - buffer.append(" "); - } + buffer.append(" ".repeat(Math.max(0, indent))); } } diff --git a/core/src/main/java/org/projectnessie/cel/common/operators/Operator.java b/core/src/main/java/org/projectnessie/cel/common/operators/Operator.java index bd601a42..73f7744b 100644 --- a/core/src/main/java/org/projectnessie/cel/common/operators/Operator.java +++ b/core/src/main/java/org/projectnessie/cel/common/operators/Operator.java @@ -42,6 +42,8 @@ public enum Operator { Modulo("_%_", 3, "%"), Negate("-_", 2, "-"), Index("_[_]", 1, null), + OptionalSelect("@optional_select"), + OptionalIndex("@optional_index"), // Macros, must have a valid identifier. Has("has"), All("all"), diff --git a/core/src/main/java/org/projectnessie/cel/common/types/BoolT.java b/core/src/main/java/org/projectnessie/cel/common/types/BoolT.java index 3f0549e8..fd80c5bf 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/BoolT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/BoolT.java @@ -18,7 +18,6 @@ import static org.projectnessie.cel.common.types.Err.newTypeConversionError; import static org.projectnessie.cel.common.types.Err.noSuchOverload; import static org.projectnessie.cel.common.types.IntT.intOfCompare; -import static org.projectnessie.cel.common.types.StringT.stringOf; import com.google.protobuf.Any; import com.google.protobuf.BoolValue; @@ -65,7 +64,7 @@ public Val compare(Val other) { } /** ConvertToNative implements the ref.Val interface method. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc == Boolean.class || typeDesc == boolean.class || typeDesc == Object.class) { @@ -93,37 +92,22 @@ public T convertToNative(Class typeDesc) { /** ConvertToType implements the ref.Val interface method. */ @Override public Val convertToType(Type typeVal) { - switch (typeVal.typeEnum()) { - case String: - return stringOf(Boolean.toString(b)); - case Bool: - return this; - case Type: - return BoolType; - } - return newTypeConversionError(BoolType, typeVal); + return switch (typeVal.typeEnum()) { + case String -> b ? StringT.TRUE : StringT.FALSE; + case Bool -> this; + case Type -> BoolType; + default -> newTypeConversionError(BoolType, typeVal); + }; } /** Equal implements the ref.Val interface method. */ @Override public Val equal(Val other) { - switch (other.type().typeEnum()) { - case Bool: - return Types.boolOf(b == ((BoolT) other).b); - case Null: - case Bytes: - case Double: - case Int: - case List: - case Map: - case Object: - case String: - case Type: - case Uint: - return False; - default: - return noSuchOverload(this, "equal", other); - } + return switch (other.type().typeEnum()) { + case Bool -> Types.boolOf(b == ((BoolT) other).b); + case Null, Bytes, Double, Int, List, Map, Object, String, Type, Uint -> False; + default -> noSuchOverload(this, "equal", other); + }; } /** Negate implements the traits.Negater interface method. */ diff --git a/core/src/main/java/org/projectnessie/cel/common/types/BytesT.java b/core/src/main/java/org/projectnessie/cel/common/types/BytesT.java index dfc5a15f..0a2690c8 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/BytesT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/BytesT.java @@ -102,7 +102,7 @@ public Val compare(Val other) { } /** ConvertToNative implements the ref.Val interface method. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc == ByteString.class || typeDesc == Object.class) { @@ -163,23 +163,11 @@ public Val convertToType(Type typeValue) { /** Equal implements the ref.Val interface method. */ @Override public Val equal(Val other) { - switch (other.type().typeEnum()) { - case Bytes: - return boolOf(Arrays.equals(b, ((BytesT) other).b)); - case Null: - case Bool: - case Double: - case Int: - case List: - case Map: - case Object: - case String: - case Type: - case Uint: - return False; - default: - return noSuchOverload(this, "equal", other); - } + return switch (other.type().typeEnum()) { + case Bytes -> boolOf(Arrays.equals(b, ((BytesT) other).b)); + case Null, Bool, Double, Int, List, Map, Object, String, Type, Uint -> False; + default -> noSuchOverload(this, "equal", other); + }; } /** Size implements the traits.Sizer interface method. */ @@ -188,6 +176,11 @@ public Val size() { return IntT.intOf(b.length); } + @Override + public int nativeSize() { + return b.length; + } + /** Type implements the ref.Val interface method. */ @Override public Type type() { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/DoubleT.java b/core/src/main/java/org/projectnessie/cel/common/types/DoubleT.java index fed76630..87863b84 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/DoubleT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/DoubleT.java @@ -19,7 +19,6 @@ import static org.projectnessie.cel.common.types.Err.newTypeConversionError; import static org.projectnessie.cel.common.types.Err.noSuchOverload; import static org.projectnessie.cel.common.types.Err.rangeError; -import static org.projectnessie.cel.common.types.IntT.IntZero; import static org.projectnessie.cel.common.types.IntT.intOf; import static org.projectnessie.cel.common.types.IntT.intOfCompare; import static org.projectnessie.cel.common.types.StringT.stringOf; @@ -58,7 +57,25 @@ public final class DoubleT extends BaseVal Trait.NegatorType, Trait.SubtractorType); + public static final DoubleT DoubleNegOne = new DoubleT(-1d); + public static final DoubleT DoubleNegZero = new DoubleT(-0d); + public static final DoubleT DoubleZero = new DoubleT(0d); + public static final DoubleT DoubleOne = new DoubleT(1d); + public static DoubleT doubleOf(double d) { + var raw = Double.doubleToRawLongBits(d); + if (raw == 0L) { + return DoubleZero; + } + if (raw == 0x8000000000000000L) { + return DoubleNegZero; + } + if (raw == 0x3ff0000000000000L) { + return DoubleOne; + } + if (raw == 0xbff0000000000000L) { + return DoubleNegOne; + } return new DoubleT(d); } @@ -83,7 +100,7 @@ public Val add(Val other) { } /** ConvertToNative implements ref.Val.ConvertToNative. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc == Double.class || typeDesc == double.class || typeDesc == Object.class) { @@ -175,18 +192,11 @@ public Val divide(Val other) { public Val compare(Val other) { switch (other.type().typeEnum()) { case Uint: + return intOfCompare(NumericComparison.compareDoubleUint(d, other.intValue())); case Int: + return intOfCompare(NumericComparison.compareDoubleInt(d, other.intValue())); case Double: - Val converted = other.convertToType(type()); - if (converted.type().typeEnum() == TypeEnum.Err) { - return converted; - } - double od = ((DoubleT) converted).d; - if (d == od) { - // work around for special case of -0.0d == 0.0d (IEEE 754) - return IntZero; - } - return intOfCompare(Double.compare(d, od)); + return intOfCompare(NumericComparison.compareDouble(d, other.doubleValue())); default: return noSuchOverload(this, "compare", other); } @@ -197,15 +207,11 @@ public Val compare(Val other) { public Val equal(Val other) { switch (other.type().typeEnum()) { case Uint: + return boolOf(NumericComparison.equalDoubleUint(d, other.intValue())); case Int: + return boolOf(NumericComparison.equalDoubleInt(d, other.intValue())); case Double: - Val converted = other.convertToType(type()); - if (converted.type().typeEnum() == TypeEnum.Err) { - return converted; - } - double o = ((DoubleT) converted).d; - // TODO: Handle NaNs properly. - return boolOf(d == o); + return boolOf(NumericComparison.equalDouble(d, other.doubleValue())); case Null: case Bool: case Bytes: @@ -270,7 +276,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - // Used to allow heterogeneous numeric map keys - return (int) d; + return Types.numericHashCode(d); } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/DurationT.java b/core/src/main/java/org/projectnessie/cel/common/types/DurationT.java index 2d629db6..618ce9f9 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/DurationT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/DurationT.java @@ -158,7 +158,7 @@ public Val compare(Val other) { } /** ConvertToNative implements ref.Val.ConvertToNative. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (Duration.class.isAssignableFrom(typeDesc)) { @@ -212,30 +212,23 @@ private String toPbString() { /** ConvertToType implements ref.Val.ConvertToType. */ @Override public Val convertToType(Type typeValue) { - switch (typeValue.typeEnum()) { - case String: - return stringOf(toPbString()); - case Int: - return IntT.intOf(toJavaLong()); - case Duration: - return this; - case Type: - return DurationType; - } - return newTypeConversionError(DurationType, typeValue); + return switch (typeValue.typeEnum()) { + case String -> stringOf(toPbString()); + case Int -> IntT.intOf(toJavaLong()); + case Duration -> this; + case Type -> DurationType; + default -> newTypeConversionError(DurationType, typeValue); + }; } /** Equal implements ref.Val.Equal. */ @Override public Val equal(Val other) { - switch (other.type().typeEnum()) { - case Duration: - return boolOf(d.equals(((DurationT) other).d)); - case Null: - return False; - default: - return noSuchOverload(this, "equal", other); - } + return switch (other.type().typeEnum()) { + case Duration -> boolOf(d.equals(((DurationT) other).d)); + case Null -> False; + default -> noSuchOverload(this, "equal", other); + }; } /** Negate implements traits.Negater.Negate. */ diff --git a/core/src/main/java/org/projectnessie/cel/common/types/Err.java b/core/src/main/java/org/projectnessie/cel/common/types/Err.java index 216324bd..1000d012 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/Err.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/Err.java @@ -200,6 +200,7 @@ public Val getErr() { /** ConvertToNative implements ref.Val.ConvertToNative. */ @Override + @SuppressWarnings("removal") public T convertToNative(Class typeDesc) { throw new UnsupportedOperationException(error); } @@ -268,8 +269,7 @@ public RuntimeException toRuntimeException() { } public static void throwErrorAsIllegalStateException(Val val) { - if (val instanceof Err) { - Err e = (Err) val; + if (val instanceof Err e) { if (e.cause != null) { throw new IllegalStateException(e.error, e.cause); } else { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/IntT.java b/core/src/main/java/org/projectnessie/cel/common/types/IntT.java index 17c2baf9..36dd3491 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/IntT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/IntT.java @@ -16,7 +16,6 @@ package org.projectnessie.cel.common.types; import static org.projectnessie.cel.common.types.BoolT.False; -import static org.projectnessie.cel.common.types.DoubleT.DoubleType; import static org.projectnessie.cel.common.types.DoubleT.doubleOf; import static org.projectnessie.cel.common.types.Err.divideByZero; import static org.projectnessie.cel.common.types.Err.errIntOverflow; @@ -38,6 +37,7 @@ import com.google.protobuf.Int64Value; import com.google.protobuf.Value; import java.time.Instant; +import java.util.stream.IntStream; import org.projectnessie.cel.common.types.Overflow.OverflowException; import org.projectnessie.cel.common.types.ref.BaseVal; import org.projectnessie.cel.common.types.ref.Type; @@ -68,11 +68,14 @@ public final class IntT extends BaseVal Trait.NegatorType, Trait.SubtractorType); + private static final IntT[] CONST = + IntStream.rangeClosed(-10, 10).mapToObj(IntT::new).toArray(IntT[]::new); + /** Int constants used for comparison results. IntZero is the zero-value for Int */ - public static final IntT IntZero = new IntT(0); + public static final IntT IntZero = CONST[10]; - public static final IntT IntOne = new IntT(1); - public static final IntT IntNegOne = new IntT(-1); + public static final IntT IntOne = CONST[11]; + public static final IntT IntNegOne = CONST[9]; /** maxIntJSON is defined as the Number.MAX_SAFE_INTEGER value per EcmaScript 6. */ public static final long maxIntJSON = (1L << 53) - 1; @@ -97,14 +100,8 @@ public static IntT intOfCompare(int compareToResult) { } public static IntT intOf(long i) { - if (i == 0L) { - return IntZero; - } - if (i == 1L) { - return IntOne; - } - if (i == -1L) { - return IntNegOne; + if (i >= -10 && i <= 10) { + return CONST[(int) i + 10]; } return new IntT(i); } @@ -120,7 +117,7 @@ public double doubleValue() { } /** ConvertToNative implements ref.Val.ConvertToNative. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc == Long.class || typeDesc == long.class || typeDesc == Object.class) { @@ -139,6 +136,9 @@ public T convertToNative(Class typeDesc) { return (T) Int64Value.of(i); } if (typeDesc == Int32Value.class) { + if (i < Integer.MIN_VALUE || i > Integer.MAX_VALUE) { + Err.throwErrorAsIllegalStateException(rangeError(i, "Java int")); + } return (T) Int32Value.of((int) i); } if (typeDesc == Val.class || typeDesc == IntT.class) { @@ -175,88 +175,50 @@ public T convertToNative(Class typeDesc) { /** ConvertToType implements ref.Val.ConvertToType. */ @Override public Val convertToType(Type typeValue) { - switch (typeValue.typeEnum()) { - case Int: - return this; - case Uint: + return switch (typeValue.typeEnum()) { + case Int -> this; + case Uint -> { if (i < 0) { - return rangeError(i, "uint"); + yield rangeError(i, "uint"); } - return uintOf(i); - case Double: - return doubleOf(i); - case String: - return stringOf(Long.toString(i)); - case Timestamp: + yield uintOf(i); + } + case Double -> doubleOf(i); + case String -> stringOf(Long.toString(i)); + case Timestamp -> { // The maximum positive value that can be passed to time.Unix is math.MaxInt64 minus the // number of seconds between year 1 and year 1970. See comments on unixToInternal. if (i < minUnixTime || i > maxUnixTime) { - return errTimestampOverflow; + yield errTimestampOverflow; } - return timestampOf(Instant.ofEpochSecond(i).atZone(ZoneIdZ)); - case Type: - return IntType; - } - return newTypeConversionError(IntType, typeValue); + yield timestampOf(Instant.ofEpochSecond(i).atZone(ZoneIdZ)); + } + case Type -> IntType; + default -> newTypeConversionError(IntType, typeValue); + }; } /** Compare implements traits.Comparer.Compare. */ @Override public Val compare(Val other) { - switch (other.type().typeEnum()) { - case Double: - DoubleT cmp = (DoubleT) convertToType(DoubleType); - return cmp.compare(other); - case Uint: - if (i < 0L) { - // this int is negative, so it MUST be smaller than any uint - return IntNegOne; - } - if (other.intValue() < 0L) { - // the OTHER uint is > Integer.MAX_VALUE, so THIS int MUST be smaller - return IntNegOne; - } - return intOfCompare(Long.compareUnsigned(i, other.intValue())); - case Int: - Val converted = other.convertToType(type()); - if (converted.type().typeEnum() == TypeEnum.Err) { - return converted; - } - return intOfCompare(Long.compare(i, converted.intValue())); - default: - return noSuchOverload(this, "compare", other); - } + return switch (other.type().typeEnum()) { + case Double -> intOfCompare(NumericComparison.compareIntDouble(i, other.doubleValue())); + case Uint -> intOfCompare(NumericComparison.compareIntUint(i, other.intValue())); + case Int -> intOfCompare(NumericComparison.compareInt(i, other.intValue())); + default -> noSuchOverload(this, "compare", other); + }; } /** Equal implements ref.Val.Equal. */ @Override public Val equal(Val other) { - switch (other.type().typeEnum()) { - case Double: - DoubleT cmp = (DoubleT) convertToType(DoubleType); - return cmp.equal(other); - case Uint: - if (other.intValue() < 0L) { - return False; - } - case Int: - Val converted = other.convertToType(type()); - if (converted.type().typeEnum() == TypeEnum.Err) { - return converted; - } - return boolOf(i == converted.intValue()); - case Null: - case Bool: - case Bytes: - case List: - case Map: - case Object: - case String: - case Type: - return False; - default: - return noSuchOverload(this, "equal", other); - } + return switch (other.type().typeEnum()) { + case Double -> boolOf(NumericComparison.equalIntDouble(i, other.doubleValue())); + case Uint -> boolOf(NumericComparison.equalIntUint(i, other.intValue())); + case Int -> boolOf(NumericComparison.equalInt(i, other.intValue())); + case Null, Bool, Bytes, List, Map, Object, String, Type -> False; + default -> noSuchOverload(this, "equal", other); + }; } /** Divide implements traits.Divider.Divide. */ @@ -368,7 +330,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - // Used to allow heterogeneous numeric map keys - return (int) i; + return Types.numericHashCode(i); } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/IteratorT.java b/core/src/main/java/org/projectnessie/cel/common/types/IteratorT.java index 911bf857..bfa6ebb5 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/IteratorT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/IteratorT.java @@ -67,6 +67,7 @@ public Val next() { } @Override + @SuppressWarnings("removal") public T convertToNative(Class typeDesc) { throw new UnsupportedOperationException(); } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ListT.java b/core/src/main/java/org/projectnessie/cel/common/types/ListT.java index f40c6cd6..a838efe4 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/ListT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/ListT.java @@ -35,7 +35,7 @@ import java.lang.reflect.Array; import java.util.Arrays; import java.util.List; -import java.util.function.Function; +import org.projectnessie.cel.common.ULong; import org.projectnessie.cel.common.operators.Operator; import org.projectnessie.cel.common.types.ref.BaseVal; import org.projectnessie.cel.common.types.ref.Type; @@ -98,7 +98,7 @@ abstract static class BaseListT extends ListT { this.size = size; } - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc.isArray()) { @@ -146,15 +146,15 @@ private ListValue toPbListValue() { ListValue.Builder list = ListValue.newBuilder(); int s = (int) size; for (int i = 0; i < s; i++) { - Val v = get(intOf(i)); - Value e = v.convertToNative(Value.class); + Val v = getUnchecked(i); + Value e = adapter.valueToNative(v, Value.class); list.addValues(e); } return list.build(); } private List toJavaList() { - return asList(convertToNative(Object[].class)); + return asList((Object[]) toJavaArray(Object[].class)); } @SuppressWarnings({"rawtypes", "unchecked"}) @@ -167,12 +167,9 @@ private Object toJavaArray(Class typeDesc) { } Object array = Array.newInstance(compType, s); - Function fixForTarget = Function.identity(); - for (int i = 0; i < s; i++) { - Val v = get(intOf(i)); - Object e = v.convertToNative(compType); - e = fixForTarget.apply(e); + Val v = getUnchecked(i); + Object e = adapter.valueToNative(v, compType); Array.set(array, i, e); } return array; @@ -180,13 +177,11 @@ private Object toJavaArray(Class typeDesc) { @Override public Val convertToType(Type typeValue) { - switch (typeValue.typeEnum()) { - case List: - return this; - case Type: - return ListType; - } - return newTypeConversionError(ListType, typeValue); + return switch (typeValue.typeEnum()) { + case List -> this; + case Type -> ListType; + default -> newTypeConversionError(ListType, typeValue); + }; } @Override @@ -194,22 +189,45 @@ public IteratorT iterator() { return new ArrayListIteratorT(); } + abstract Val getUnchecked(int index); + + static Val elementAt(Lister list, int index) { + return list instanceof BaseListT baseList + ? baseList.getUnchecked(index) + : list.nativeGetAt(index); + } + + final Object nativeListElement(Val value, Class componentType) { + // A Java Long is a CEL int when adapted again. Preserve CEL uint's type while still avoiding + // embedded Val instances in raw Object arrays produced by concatenation. + return componentType == Object.class && value instanceof UintT + ? adapter.valueToNative(value, ULong.class) + : adapter.valueToNative(value, componentType); + } + + @Override + public Val nativeGetAt(int index) { + if (index < 0 || index >= size) { + return newErr( + "invalid_argument: index '%d' out of range in list of size '%d'", index, size); + } + return getUnchecked(index); + } + @Override public Val equal(Val other) { - if (other.type() != ListType) { + if (!(other instanceof ListT o)) { return False; } - ListT o = (ListT) other; if (size != o.size().intValue()) { return False; } - for (long i = 0; i < size; i++) { - IntT idx = intOf(i); - Val e1 = get(idx); + for (int i = 0; i < size; i++) { + Val e1 = getUnchecked(i); if (isError(e1)) { return e1; } - Val e2 = o.get(idx); + Val e2 = elementAt(o, i); if (isError(e2)) { return e2; } @@ -228,8 +246,8 @@ public Val equal(Val other) { @Override public Val contains(Val value) { - for (long i = 0; i < size; i++) { - Val elem = get(intOf(i)); + for (int i = 0; i < size; i++) { + Val elem = getUnchecked(i); if (value.equal(elem) == True) { return True; } @@ -242,22 +260,27 @@ public Val size() { return intOf(size); } + @Override + public int nativeSize() { + return Math.toIntExact(size); + } + @Override public boolean equals(Object o) { if (this == o) { return true; } - if (!(o instanceof Val)) { + if (!(o instanceof Val val)) { return false; } - return equal((Val) o) == True; + return equal(val) == True; } @Override public int hashCode() { int result = 1; - for (long i = 0; i < size; i++) { - result = 31 * result + get(intOf(i)).hashCode(); + for (int i = 0; i < size; i++) { + result = 31 * result + getUnchecked(i).hashCode(); } return result; } @@ -277,17 +300,18 @@ int checkedIndex(Val index, int size) { throw new InvalidIndexException( valOrErr(index, "unsupported index type '%s' in list", index.type())); } - int i = (int) index.intValue(); - if (i < 0 || i >= size) { + long longIndex = index.intValue(); + if (longIndex < 0 || longIndex >= size) { // Note: the conformance tests assert on 'invalid_argument' throw new InvalidIndexException( - newErr("invalid_argument: index '%d' out of range in list of size '%d'", i, size)); + newErr( + "invalid_argument: index '%d' out of range in list of size '%d'", longIndex, size)); } - return i; + return (int) longIndex; } private final class ArrayListIteratorT extends BaseVal implements IteratorT { - private long index; + private int index; @Override public Val hasNext() { @@ -297,12 +321,13 @@ public Val hasNext() { @Override public Val next() { if (index < size) { - return get(intOf(index++)); + return getUnchecked(index++); } return noMoreElements(); } @Override + @SuppressWarnings("removal") public T convertToNative(Class typeDesc) { throw new UnsupportedOperationException("IMPLEMENT ME??"); } @@ -357,19 +382,18 @@ public Object value() { @Override public Val add(Val other) { - if (!(other instanceof Lister)) { + if (!(other instanceof Lister otherList)) { return noSuchOverload(this, "add", other); } - Lister otherList = (Lister) other; - int otherSize = (int) otherList.size().intValue(); + int otherSize = otherList.nativeSize(); Object[] newArray = Arrays.copyOf(array, array.length + otherSize); Class componentType = array.getClass().getComponentType(); for (int i = 0; i < otherSize; i++) { - Val otherValue = otherList.get(intOf(i)); + Val otherValue = elementAt(otherList, i); newArray[array.length + i] = - componentType.isInstance(otherValue) + componentType != Object.class && componentType.isInstance(otherValue) ? otherValue - : otherValue.convertToNative(componentType); + : nativeListElement(otherValue, componentType); } return new GenericListT(adapter, newArray); } @@ -383,7 +407,12 @@ public Val get(Val index) { return e.error; } - return adapter.nativeToValue(array[i]); + return getUnchecked(i); + } + + @Override + Val getUnchecked(int index) { + return adapter.nativeToValue(array[index]); } @Override @@ -414,17 +443,16 @@ public Object value() { @Override public Val add(Val other) { - if (!(other instanceof Lister)) { + if (!(other instanceof Lister otherList)) { return noSuchOverload(this, "add", other); } - Lister otherList = (Lister) other; - int otherSize = (int) otherList.size().intValue(); + int otherSize = otherList.nativeSize(); Object[] newArray = new Object[list.size() + otherSize]; for (int i = 0; i < list.size(); i++) { newArray[i] = list.get(i); } for (int i = 0; i < otherSize; i++) { - newArray[list.size() + i] = otherList.get(intOf(i)); + newArray[list.size() + i] = nativeListElement(elementAt(otherList, i), Object.class); } return new GenericListT(adapter, newArray); } @@ -438,7 +466,12 @@ public Val get(Val index) { return e.error; } - return adapter.nativeToValue(list.get(i)); + return getUnchecked(i); + } + + @Override + Val getUnchecked(int index) { + return adapter.nativeToValue(list.get(index)); } } @@ -461,20 +494,19 @@ public Object value() { @Override public Val add(Val other) { - if (!(other instanceof Lister)) { + if (!(other instanceof Lister otherLister)) { return noSuchOverload(this, "add", other); } - if (other instanceof ValListT) { - Val[] otherArray = ((ValListT) other).array; + if (other instanceof ValListT valListT) { + Val[] otherArray = valListT.array; Val[] newArray = Arrays.copyOf(array, array.length + otherArray.length); System.arraycopy(otherArray, 0, newArray, array.length, otherArray.length); return new ValListT(adapter, newArray); } else { - Lister otherLister = (Lister) other; - int otherSIze = (int) otherLister.size().intValue(); - Val[] newArray = Arrays.copyOf(array, array.length + otherSIze); - for (int i = 0; i < otherSIze; i++) { - newArray[array.length + i] = otherLister.get(intOf(i)); + int otherSize = otherLister.nativeSize(); + Val[] newArray = Arrays.copyOf(array, array.length + otherSize); + for (int i = 0; i < otherSize; i++) { + newArray[array.length + i] = elementAt(otherLister, i); } return new ValListT(adapter, newArray); } @@ -488,7 +520,12 @@ public Val get(Val index) { } catch (InvalidIndexException e) { return e.error; } - return array[i]; + return getUnchecked(i); + } + + @Override + Val getUnchecked(int index) { + return array[index]; } @Override @@ -511,18 +548,17 @@ abstract static class PrimitiveArrayListT extends BaseListT { @Override public Val add(Val other) { - if (!(other instanceof Lister)) { + if (!(other instanceof Lister otherLister)) { return noSuchOverload(this, "add", other); } - Lister otherLister = (Lister) other; int thisSize = (int) size; - int otherSize = (int) otherLister.size().intValue(); + int otherSize = otherLister.nativeSize(); Val[] newArray = new Val[thisSize + otherSize]; for (int i = 0; i < thisSize; i++) { - newArray[i] = get(intOf(i)); + newArray[i] = getUnchecked(i); } for (int i = 0; i < otherSize; i++) { - newArray[thisSize + i] = otherLister.get(intOf(i)); + newArray[thisSize + i] = elementAt(otherLister, i); } return new ValListT(adapter, newArray); } @@ -549,7 +585,49 @@ public Val get(Val index) { } catch (InvalidIndexException e) { return e.error; } - return intOf(array[i]); + return getUnchecked(i); + } + + @Override + Val getUnchecked(int index) { + return intOf(array[index]); + } + + @Override + public Val contains(Val value) { + if (value instanceof IntT intValue) { + long needle = intValue.intValue(); + if (needle >= Integer.MIN_VALUE && needle <= Integer.MAX_VALUE) { + int intNeedle = (int) needle; + for (int element : array) { + if (element == intNeedle) { + return True; + } + } + return False; + } + return False; + } + return super.contains(value); + } + + @Override + public Val equal(Val other) { + if (other instanceof IntArrayListT intList) { + return boolOf(Arrays.equals(array, intList.array)); + } + if (other instanceof LongArrayListT longList) { + if (array.length != longList.array.length) { + return False; + } + for (int i = 0; i < array.length; i++) { + if (array[i] != longList.array[i]) { + return False; + } + } + return True; + } + return super.equal(other); } } @@ -574,7 +652,37 @@ public Val get(Val index) { } catch (InvalidIndexException e) { return e.error; } - return intOf(array[i]); + return getUnchecked(i); + } + + @Override + Val getUnchecked(int index) { + return intOf(array[index]); + } + + @Override + public Val contains(Val value) { + if (value instanceof IntT intValue) { + long needle = intValue.intValue(); + for (long element : array) { + if (element == needle) { + return True; + } + } + return False; + } + return super.contains(value); + } + + @Override + public Val equal(Val other) { + if (other instanceof LongArrayListT longList) { + return boolOf(Arrays.equals(array, longList.array)); + } + if (other instanceof IntArrayListT intList) { + return intList.equal(this); + } + return super.equal(other); } } @@ -599,7 +707,42 @@ public Val get(Val index) { } catch (InvalidIndexException e) { return e.error; } - return doubleOf(array[i]); + return getUnchecked(i); + } + + @Override + Val getUnchecked(int index) { + return doubleOf(array[index]); + } + + @Override + public Val contains(Val value) { + if (value instanceof DoubleT doubleValue) { + double needle = doubleValue.doubleValue(); + for (double element : array) { + if (element == needle) { + return True; + } + } + return False; + } + return super.contains(value); + } + + @Override + public Val equal(Val other) { + if (other instanceof DoubleArrayListT doubleList) { + if (array.length != doubleList.array.length) { + return False; + } + for (int i = 0; i < array.length; i++) { + if (array[i] != doubleList.array[i]) { + return False; + } + } + return True; + } + return super.equal(other); } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/MapT.java b/core/src/main/java/org/projectnessie/cel/common/types/MapT.java index e15b7c43..c6a264c6 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/MapT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/MapT.java @@ -20,16 +20,15 @@ import static org.projectnessie.cel.common.types.Err.isError; import static org.projectnessie.cel.common.types.Err.newErr; import static org.projectnessie.cel.common.types.Err.newTypeConversionError; -import static org.projectnessie.cel.common.types.StringT.StringType; import static org.projectnessie.cel.common.types.TypeT.TypeType; import static org.projectnessie.cel.common.types.Types.boolOf; +import static org.projectnessie.cel.common.types.Util.isUnknownOrError; import com.google.protobuf.Any; import com.google.protobuf.Struct; import com.google.protobuf.Value; import java.util.HashMap; import java.util.Map; -import java.util.Objects; import org.projectnessie.cel.common.types.ref.BaseVal; import org.projectnessie.cel.common.types.ref.Type; import org.projectnessie.cel.common.types.ref.TypeAdapter; @@ -59,11 +58,10 @@ public static Val newWrappedMap(TypeAdapter adapter, Map value) { public static Val newMaybeWrappedMap(TypeAdapter adapter, Map value) { boolean alreadyWrapped = true; for (Map.Entry entry : value.entrySet()) { - if (!(entry.getKey() instanceof Val) || !(entry.getValue() instanceof Val)) { + if (!(entry.getKey() instanceof Val key) || !(entry.getValue() instanceof Val)) { alreadyWrapped = false; break; } - Val key = (Val) entry.getKey(); if (key.type().typeEnum() == TypeEnum.Null) { return newErr("unsupported key type"); } @@ -72,19 +70,52 @@ public static Val newMaybeWrappedMap(TypeAdapter adapter, Map value) { return newWrappedMap(adapter, (Map) value); } - Map newMap = new HashMap<>(value.size() * 4 / 3 + 1); + Map newMap = new HashMap<>(value.size() * 4 / 3 + 1); for (Map.Entry entry : value.entrySet()) { Val k = adapter.nativeToValue(entry.getKey()); - Val v = adapter.nativeToValue(entry.getValue()); if (k.type().typeEnum() == TypeEnum.Null) { return newErr("unsupported key type"); } - if (newMap.putIfAbsent(k, v) != null) { + int previousSize = newMap.size(); + newMap.put(k, entry.getValue()); + if (newMap.size() == previousSize) { // Prevent duplicate keys, error out. return newErr("Failed with repeated key"); } } - return newWrappedMap(adapter, newMap); + return new NativeMapT(adapter, newMap); + } + + /** + * Creates a checked map whose keys and values use independently checked materialization. + * + *

Keys are adapted eagerly and values remain raw until selected or traversed. + */ + public static Val newCheckedMap( + TypeAdapter keyAdapter, TypeAdapter valueAdapter, Map value) { + Map newMap = new HashMap<>(value.size() * 4 / 3 + 1); + for (Map.Entry entry : value.entrySet()) { + Val key = keyAdapter.nativeToValue(entry.getKey()); + if (key instanceof Err) { + return key; + } + if (key.type().typeEnum() == TypeEnum.Null) { + return newErr("unsupported key type"); + } + int previousSize = newMap.size(); + newMap.put(key, entry.getValue()); + if (newMap.size() == previousSize) { + return newErr("Failed with repeated key"); + } + } + return new NativeMapT(valueAdapter, newMap); + } + + public static boolean isSupportedLiteralKeyType(Val key) { + return switch (key.type().typeEnum()) { + case Bool, Int, String, Uint -> true; + default -> false; + }; } @Override @@ -92,6 +123,110 @@ public Type type() { return MapType; } + protected final Val mapEqual(Val other) { + if (!(other instanceof MapT o)) { + return False; + } + if (nativeSize() != o.nativeSize()) { + return False; + } + + Iterable entries = mapEntries(); + if (entries != null) { + for (Object entry : entries) { + Val key = mapEntryKey(entry); + Val value = mapEntryValue(entry); + Val oVal = o.find(key); + if (oVal == null) { + return False; + } + Val equal = mapValueEqual(value, oVal); + if (equal != True) { + return equal; + } + } + return True; + } + + IteratorT myIter = iterator(); + while (myIter.hasNext() == True) { + Val key = myIter.next(); + + Val val = get(key); + Val oVal = o.find(key); + if (oVal == null) { + return False; + } + Val eq = mapValueEqual(val, oVal); + if (eq == True) { + continue; + } + return eq; + } + return True; + } + + /** Returns backing entries when this implementation can traverse them directly. */ + protected Iterable mapEntries() { + return null; + } + + /** Adapts the key of an entry returned by {@link #mapEntries()}. */ + protected Val mapEntryKey(Object entry) { + throw new UnsupportedOperationException(); + } + + /** Adapts the value of an entry returned by {@link #mapEntries()}. */ + protected Val mapEntryValue(Object entry) { + throw new UnsupportedOperationException(); + } + + protected static Val mapValueEqual(Val value, Val otherValue) { + if (isError(value)) { + return value; + } + if (isError(otherValue)) { + return otherValue; + } + + Val equal = value.equal(otherValue); + if (equal == True || equal == False) { + return equal; + } + if (!value.type().equals(otherValue.type())) { + return False; + } + equal = value.equal(otherValue); + return equal instanceof Err ? equal : boolOf(equal == True); + } + + @Override + public final boolean equals(Object other) { + return other instanceof MapT o && mapEqual(o) == True; + } + + @Override + public final int hashCode() { + int hash = 0; + Iterable entries = mapEntries(); + if (entries != null) { + for (Object entry : entries) { + Val key = mapEntryKey(entry); + Val value = mapEntryValue(entry); + hash += key.hashCode() ^ (value != null ? value.hashCode() : 0); + } + return hash; + } + + IteratorT keys = iterator(); + while (keys.hasNext() == True) { + Val key = keys.next(); + Val value = find(key); + hash += key.hashCode() ^ (value != null ? value.hashCode() : 0); + } + return hash; + } + static final class ValMapT extends MapT { private final TypeAdapter adapter; @@ -102,7 +237,7 @@ static final class ValMapT extends MapT { this.map = map; } - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (Map.class.isAssignableFrom(typeDesc) || typeDesc == Object.class) { @@ -136,9 +271,12 @@ private Value toPbValue() { private Struct toPbStruct() { Struct.Builder struct = Struct.newBuilder(); map.forEach( - (k, v) -> - struct.putFields( - k.convertToType(StringType).value().toString(), v.convertToNative(Value.class))); + (k, v) -> { + if (k.type().typeEnum() != TypeEnum.String) { + throw new IllegalArgumentException("bad key type"); + } + struct.putFields(k.value().toString(), adapter.valueToNative(v, Value.class)); + }); return struct.build(); } @@ -167,50 +305,7 @@ public IteratorT iterator() { @Override public Val equal(Val other) { - // TODO this is expensive :( - if (!(other instanceof MapT)) { - return False; - } - MapT o = (MapT) other; - if (!size().equal(o.size()).booleanValue()) { - return False; - } - IteratorT myIter = iterator(); - while (myIter.hasNext() == True) { - Val key = myIter.next(); - - Val val = get(key); - Val oVal = o.find(key); - if (oVal == null) { - return False; - } - if (isError(val)) { - return val; - } - if (isError(oVal)) { - return val; - } - - Val eq = val.equal(oVal); - if (eq == True) { - continue; - } - if (eq == False) { - return False; - } - - if (!val.type().equals(oVal.type())) { - return False; - } - eq = val.equal(oVal); - if (eq instanceof Err) { - return eq; - } - if (eq != True) { - return False; - } - } - return True; + return mapEqual(other); } @Override @@ -222,6 +317,9 @@ public Object value() { @Override public Val contains(Val value) { + if (isUnknownOrError(value)) { + return value; + } return boolOf(find(value) != null); } @@ -235,6 +333,26 @@ public Val size() { return IntT.intOf(map.size()); } + @Override + public int nativeSize() { + return map.size(); + } + + @Override + protected Iterable mapEntries() { + return map.entrySet(); + } + + @Override + protected Val mapEntryKey(Object entry) { + return (Val) ((Map.Entry) entry).getKey(); + } + + @Override + protected Val mapEntryValue(Object entry) { + return (Val) ((Map.Entry) entry).getValue(); + } + @Override public Val find(Val key) { // Note: no special handling for heterogenous numeric map keys needed, the Val type @@ -243,25 +361,128 @@ public Val find(Val key) { } @Override - public boolean equals(Object o) { - if (this == o) { - return true; + public String toString() { + return "JavaMapT{" + "adapter=" + adapter + ", map=" + map + '}'; + } + } + + static final class NativeMapT extends MapT { + private final TypeAdapter adapter; + private final Map map; + + NativeMapT(TypeAdapter adapter, Map map) { + this.adapter = adapter; + this.map = map; + } + + @SuppressWarnings("removal") + @Override + public T convertToNative(Class typeDesc) { + return adapter.valueToNative(newWrappedMap(adapter, adaptedMap()), typeDesc); + } + + @Override + public Val convertToType(Type typeValue) { + if (typeValue == MapType) { + return this; } - if (o == null || getClass() != o.getClass()) { - return false; + if (typeValue == TypeType) { + return MapType; } - ValMapT valMapT = (ValMapT) o; - return Objects.equals(map, valMapT.map); + return newTypeConversionError(MapType, typeValue); } @Override - public int hashCode() { - return Objects.hash(super.hashCode(), map); + public IteratorT iterator() { + return IteratorT.javaIterator(adapter, map.keySet().iterator()); + } + + @Override + public Val equal(Val other) { + if (other instanceof NativeMapT nativeOther) { + if (map.size() != nativeOther.map.size()) { + return False; + } + for (Map.Entry entry : map.entrySet()) { + Object otherRawValue = nativeOther.map.get(entry.getKey()); + if (otherRawValue == null && !nativeOther.map.containsKey(entry.getKey())) { + return False; + } + Val equal = + mapValueEqual( + adapter.nativeToValue(entry.getValue()), + nativeOther.adapter.nativeToValue(otherRawValue)); + if (equal != True) { + return equal; + } + } + return True; + } + return mapEqual(other); + } + + @Override + public Object value() { + Map nativeMap = new HashMap<>(map.size() * 4 / 3 + 1); + map.forEach( + (key, rawValue) -> nativeMap.put(key.value(), adapter.nativeToValue(rawValue).value())); + return nativeMap; + } + + @Override + public Val contains(Val value) { + if (isUnknownOrError(value)) { + return value; + } + Object rawValue = map.get(value); + return boolOf(rawValue != null || map.containsKey(value)); + } + + @Override + public Val get(Val index) { + return find(index); + } + + @Override + public Val size() { + return IntT.intOf(map.size()); + } + + @Override + public int nativeSize() { + return map.size(); + } + + @Override + protected Iterable mapEntries() { + return map.entrySet(); + } + + @Override + protected Val mapEntryKey(Object entry) { + return (Val) ((Map.Entry) entry).getKey(); + } + + @Override + protected Val mapEntryValue(Object entry) { + return adapter.nativeToValue(((Map.Entry) entry).getValue()); + } + + @Override + public Val find(Val key) { + Object rawValue = map.get(key); + return rawValue != null || map.containsKey(key) ? adapter.nativeToValue(rawValue) : null; + } + + private Map adaptedMap() { + Map adapted = new HashMap<>(map.size() * 4 / 3 + 1); + map.forEach((key, rawValue) -> adapted.put(key, adapter.nativeToValue(rawValue))); + return adapted; } @Override public String toString() { - return "JavaMapT{" + "adapter=" + adapter + ", map=" + map + '}'; + return "NativeMapT{" + "adapter=" + adapter + ", map=" + map + '}'; } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/NullT.java b/core/src/main/java/org/projectnessie/cel/common/types/NullT.java index 415d479f..423b56bc 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/NullT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/NullT.java @@ -19,7 +19,6 @@ import static org.projectnessie.cel.common.types.BoolT.True; import static org.projectnessie.cel.common.types.Err.newTypeConversionError; import static org.projectnessie.cel.common.types.Err.noSuchOverload; -import static org.projectnessie.cel.common.types.StringT.stringOf; import com.google.protobuf.Any; import com.google.protobuf.Value; @@ -42,7 +41,7 @@ public final class NullT extends BaseVal { private static final Any PbAny = Any.pack(PbValue); /** ConvertToNative implements ref.Val.ConvertToNative. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc == Integer.class || typeDesc == int.class) { @@ -82,37 +81,22 @@ public T convertToNative(Class typeDesc) { /** ConvertToType implements ref.Val.ConvertToType. */ @Override public Val convertToType(Type typeValue) { - switch (typeValue.typeEnum()) { - case String: - return stringOf("null"); - case Null: - return this; - case Type: - return NullType; - } - return newTypeConversionError(NullType, typeValue); + return switch (typeValue.typeEnum()) { + case String -> StringT.NULL; + case Null -> this; + case Type -> NullType; + default -> newTypeConversionError(NullType, typeValue); + }; } /** Equal implements ref.Val.Equal. */ @Override public Val equal(Val other) { - switch (other.type().typeEnum()) { - case Null: - return True; - case Int: - case Uint: - case Double: - case String: - case Bytes: - case Bool: - case List: - case Map: - case Object: - case Type: - return False; - default: - return noSuchOverload(this, "equal", other); - } + return switch (other.type().typeEnum()) { + case Null -> True; + case Int, Uint, Double, String, Bytes, Bool, List, Map, Object, Type -> False; + default -> noSuchOverload(this, "equal", other); + }; } /** Type implements ref.Val.Type. */ diff --git a/core/src/main/java/org/projectnessie/cel/common/types/NumericComparison.java b/core/src/main/java/org/projectnessie/cel/common/types/NumericComparison.java new file mode 100644 index 00000000..2381e337 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/common/types/NumericComparison.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.common.types; + +import java.math.BigInteger; + +/** Pure comparison and equality semantics shared by CEL's built-in numeric values. */ +final class NumericComparison { + private NumericComparison() {} + + static int compareInt(long left, long right) { + return Long.compare(left, right); + } + + static int compareIntUint(long left, long right) { + if (left < 0L || right < 0L) { + return -1; + } + return Long.compare(left, right); + } + + static int compareUintInt(long left, long right) { + return -compareIntUint(right, left); + } + + static int compareUint(long left, long right) { + return Long.compareUnsigned(left, right); + } + + static int compareIntDouble(long left, double right) { + return compareDouble((double) left, right); + } + + static int compareDoubleInt(double left, long right) { + return compareDouble(left, (double) right); + } + + static int compareUintDouble(long left, double right) { + if (right < 0.0d) { + return 1; + } + return compareDouble(unsignedLongToDouble(left), right); + } + + static int compareDoubleUint(double left, long right) { + return compareDouble(left, unsignedLongToDouble(right)); + } + + static int compareDouble(double left, double right) { + if (left == right) { + // CEL treats positive and negative zero as equal. + return 0; + } + return Double.compare(left, right); + } + + static boolean equalInt(long left, long right) { + return left == right; + } + + static boolean equalIntUint(long left, long right) { + return left >= 0L && right >= 0L && left == right; + } + + static boolean equalUintInt(long left, long right) { + return equalIntUint(right, left); + } + + static boolean equalUint(long left, long right) { + return left == right; + } + + static boolean equalIntDouble(long left, double right) { + return (double) left == right; + } + + static boolean equalDoubleInt(double left, long right) { + return left == (double) right; + } + + static boolean equalUintDouble(long left, double right) { + return unsignedLongToDouble(left) == right; + } + + static boolean equalDoubleUint(double left, long right) { + return left == unsignedLongToDouble(right); + } + + static boolean equalDouble(double left, double right) { + return left == right; + } + + private static double unsignedLongToDouble(long value) { + return value >= 0L + ? (double) value + : new BigInteger(Long.toUnsignedString(value)).doubleValue(); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ObjectT.java b/core/src/main/java/org/projectnessie/cel/common/types/ObjectT.java index e7f8c100..44c3f2cc 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/ObjectT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/ObjectT.java @@ -29,7 +29,7 @@ import org.projectnessie.cel.common.types.traits.FieldTester; import org.projectnessie.cel.common.types.traits.Indexer; -public abstract class ObjectT extends BaseVal implements FieldTester, Indexer, TypeAdapter { +public abstract class ObjectT extends BaseVal implements FieldTester, Indexer { protected final TypeAdapter adapter; protected final Object value; protected final TypeDescription typeDesc; @@ -78,11 +78,6 @@ public Object value() { return value; } - @Override - public Val nativeToValue(Object value) { - return adapter.nativeToValue(value); - } - @Override public boolean equals(Object o) { if (this == o) { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/OptionalT.java b/core/src/main/java/org/projectnessie/cel/common/types/OptionalT.java new file mode 100644 index 00000000..677a9d17 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/common/types/OptionalT.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.common.types; + +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.Err.newTypeConversionError; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.IntT.IntZero; +import static org.projectnessie.cel.common.types.TypeT.TypeType; +import static org.projectnessie.cel.common.types.TypeT.newObjectTypeValue; + +import com.google.protobuf.Message; +import java.util.Objects; +import org.projectnessie.cel.common.types.ref.BaseVal; +import org.projectnessie.cel.common.types.ref.Type; +import org.projectnessie.cel.common.types.ref.TypeEnum; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Container; +import org.projectnessie.cel.common.types.traits.FieldTester; +import org.projectnessie.cel.common.types.traits.Indexer; +import org.projectnessie.cel.common.types.traits.Mapper; +import org.projectnessie.cel.common.types.traits.Receiver; +import org.projectnessie.cel.common.types.traits.Sizer; +import org.projectnessie.cel.common.types.traits.Trait; + +/** Runtime value for CEL optional_type values. */ +public final class OptionalT extends BaseVal implements FieldTester, Indexer, Receiver { + public static final String OptionalTypeName = "optional_type"; + public static final Type OptionalType = + newObjectTypeValue( + OptionalTypeName, Trait.FieldTesterType, Trait.IndexerType, Trait.ReceiverType); + + private static final OptionalT None = new OptionalT(null, false); + + private final Val value; + private final boolean present; + + private OptionalT(Val value, boolean present) { + this.value = value; + this.present = present; + } + + public static OptionalT none() { + return None; + } + + public static OptionalT of(Val value) { + return new OptionalT(Objects.requireNonNull(value, "value"), true); + } + + public static OptionalT ofNonZeroValue(Val value) { + return isZeroValue(value) ? none() : of(value); + } + + public static Val optionalSelect(Val operand, Val field) { + return optionalAccess(operand, field); + } + + public static Val optionalIndex(Val operand, Val index) { + return optionalAccess(operand, index); + } + + public boolean hasValue() { + return present; + } + + public Val getValue() { + return value; + } + + @Override + @SuppressWarnings("removal") + public T convertToNative(Class typeDesc) { + if (typeDesc == Val.class || typeDesc == OptionalT.class) { + return typeDesc.cast(this); + } + if (typeDesc == Object.class) { + return typeDesc.cast(value()); + } + throw new RuntimeException( + String.format( + "native type conversion error from '%s' to '%s'", OptionalType, typeDesc.getName())); + } + + @Override + public Val convertToType(Type typeValue) { + if (typeValue.equals(OptionalType)) { + return this; + } + if (typeValue == TypeType) { + return OptionalType; + } + return newTypeConversionError(OptionalType, typeValue); + } + + @Override + public Val equal(Val other) { + if (!(other instanceof OptionalT optional)) { + return False; + } + if (!present || !optional.present) { + return present == optional.present ? True : False; + } + return value.equal(optional.value); + } + + @Override + public Type type() { + return OptionalType; + } + + @Override + public Object value() { + return present ? value.value() : null; + } + + @Override + public Val isSet(Val field) { + if (!present) { + return False; + } + if (value instanceof OptionalT optionalT) { + return optionalT.isSet(field); + } + if (value instanceof FieldTester fieldTester) { + Val present = fieldTester.isSet(field); + return isMissingAccess(present) ? False : present; + } + if (value instanceof Container container) { + return container.contains(field); + } + return noSuchOverload(value, "has", field); + } + + @Override + public Val get(Val index) { + return present ? optionalAccess(value, index) : none(); + } + + @Override + public Val receive(String function, String overload, Val... args) { + return switch (function) { + case "hasValue" -> + args.length == 0 + ? (present ? True : False) + : noSuchOverload(this, function, overload, args); + case "value" -> value(args, function, overload); + case "or" -> or(args, function, overload); + case "orValue" -> orValue(args, function, overload); + default -> noSuchOverload(this, function, overload, args); + }; + } + + private Val value(Val[] args, String function, String overload) { + if (args.length != 0) { + return noSuchOverload(this, function, overload, args); + } + return present ? value : newErr("optional.none() has no value"); + } + + private Val or(Val[] args, String function, String overload) { + if (args.length != 1 || !(args[0] instanceof OptionalT)) { + return noSuchOverload(this, function, overload, args); + } + return present ? this : args[0]; + } + + private Val orValue(Val[] args, String function, String overload) { + if (args.length != 1) { + return noSuchOverload(this, function, overload, args); + } + return present ? value : args[0]; + } + + private static boolean isZeroValue(Val value) { + return switch (value.type().typeEnum()) { + case Null -> true; + case Bool -> value == False || !value.booleanValue(); + case Int, Uint -> value.intValue() == 0L; + case Double -> value.doubleValue() == 0.0d; + case String, Bytes, List, Map -> + value instanceof Sizer sizer && sizer.size().equal(IntZero) == True; + case Object -> value.value() instanceof Message message && message.getAllFields().isEmpty(); + default -> false; + }; + } + + private static Val optionalAccess(Val operand, Val index) { + if (operand instanceof OptionalT optionalT) { + return optionalT.get(index); + } + if (operand instanceof FieldTester fieldTester && index.type().typeEnum() == TypeEnum.String) { + Val present = fieldTester.isSet(index); + if (present == False) { + return none(); + } + if (present != True) { + return isMissingAccess(present) ? none() : present; + } + } + if (operand instanceof Mapper mapper) { + Val value = mapper.find(index); + return value == null ? none() : of(value); + } + if (operand instanceof Indexer indexer) { + Val value = indexer.get(index); + return isMissingAccess(value) ? none() : of(value); + } + return noSuchOverload(operand, "optional access", index); + } + + private static boolean isMissingAccess(Val value) { + if (!(value instanceof Err)) { + return false; + } + String error = value.toString(); + return error.startsWith("no such key") + || error.startsWith("no such field") + || error.startsWith("invalid_argument") + || error.startsWith("index out of bounds"); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Val)) { + return false; + } + return equal((Val) o) == True; + } + + @Override + public int hashCode() { + return present ? Objects.hash(OptionalType, value) : Objects.hash(OptionalType); + } + + @Override + public String toString() { + return present ? String.format("optional.of(%s)", value) : "optional.none()"; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/common/types/Overloads.java b/core/src/main/java/org/projectnessie/cel/common/types/Overloads.java index 805167f6..3ad5a035 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/Overloads.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/Overloads.java @@ -236,20 +236,19 @@ private Overloads() {} // IsTypeConversionFunction returns whether the input function is a standard library type // conversion function. public static boolean isTypeConversionFunction(String function) { - switch (function) { - case TypeConvertBool: - case TypeConvertBytes: - case TypeConvertDouble: - case TypeConvertDuration: - case TypeConvertDyn: - case TypeConvertInt: - case TypeConvertString: - case TypeConvertTimestamp: - case TypeConvertType: - case TypeConvertUint: - return true; - default: - return false; - } + return switch (function) { + case TypeConvertBool, + TypeConvertBytes, + TypeConvertDouble, + TypeConvertDuration, + TypeConvertDyn, + TypeConvertInt, + TypeConvertString, + TypeConvertTimestamp, + TypeConvertType, + TypeConvertUint -> + true; + default -> false; + }; } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/StringT.java b/core/src/main/java/org/projectnessie/cel/common/types/StringT.java index db308eb4..3587bec8 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/StringT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/StringT.java @@ -70,6 +70,10 @@ public final class StringT extends BaseVal implements Adder, Comparer, Matcher, Overloads.StartsWith, StringT::stringStartsWith); + static final StringT TRUE = stringOf("true"); + static final StringT FALSE = stringOf("false"); + static final StringT NULL = stringOf("null"); + public static StringT stringOf(String s) { return new StringT(s); } @@ -83,14 +87,14 @@ private StringT(String s) { /** Add implements traits.Adder.Add. */ @Override public Val add(Val other) { - if (!(other instanceof StringT)) { + if (!(other instanceof StringT o)) { return noSuchOverload(this, "add", other); } - return new StringT(s + ((StringT) other).s); + return new StringT(s + o.s); } /** ConvertToNative implements ref.Val.ConvertToNative. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc == String.class || typeDesc == Object.class) { @@ -128,11 +132,19 @@ public Val convertToType(Type typeVal) { case Double: return doubleOf(Double.parseDouble(s)); case Bool: - if ("true".equalsIgnoreCase(s)) { - return True; - } - if ("false".equalsIgnoreCase(s)) { - return False; + switch (s) { + case "1": + case "t": + case "true": + case "TRUE": + case "True": + return True; + case "0": + case "f": + case "false": + case "FALSE": + case "False": + return False; } break; case Bytes: @@ -156,46 +168,31 @@ public Val convertToType(Type typeVal) { /** Compare implements traits.Comparer.Compare. */ @Override public Val compare(Val other) { - switch (other.type().typeEnum()) { - case String: - return intOfCompare(s.compareTo(((StringT) other).s)); - case Null: - return False; - default: - return noSuchOverload(this, "compare", other); - } + return switch (other.type().typeEnum()) { + case String -> intOfCompare(s.compareTo(((StringT) other).s)); + case Null -> False; + default -> noSuchOverload(this, "compare", other); + }; } /** Equal implements ref.Val.Equal. */ @Override public Val equal(Val other) { - switch (other.type().typeEnum()) { - case String: - return boolOf(s.equals(((StringT) other).s)); - case Null: - case Bool: - case Bytes: - case Int: - case Uint: - case Double: - case List: - case Map: - case Object: - case Type: - return False; - default: - return noSuchOverload(this, "equal", other); - } + return switch (other.type().typeEnum()) { + case String -> boolOf(s.equals(((StringT) other).s)); + case Null, Bool, Bytes, Int, Uint, Double, List, Map, Object, Type -> False; + default -> noSuchOverload(this, "equal", other); + }; } /** Match implements traits.Matcher.Match. */ @Override public Val match(Val pattern) { - if (!(pattern instanceof StringT)) { + if (!(pattern instanceof StringT o)) { return noSuchOverload(this, "match", pattern); } try { - Pattern p = Pattern.compile(((StringT) pattern).s); + Pattern p = Pattern.compile(o.s); java.util.regex.Matcher m = p.matcher(s); return boolOf(m.find()); } catch (Exception e) { @@ -221,6 +218,11 @@ public Val size() { return intOf(s.codePointCount(0, s.length())); } + @Override + public int nativeSize() { + return s.codePointCount(0, s.length()); + } + /** Type implements ref.Val.Type. */ @Override public Type type() { @@ -251,23 +253,23 @@ public int hashCode() { } static Val stringContains(String s, Val sub) { - if (!(sub instanceof StringT)) { + if (!(sub instanceof StringT str)) { return noSuchOverload(StringType, "contains", sub); } - return boolOf(s.contains(((StringT) sub).s)); + return boolOf(s.contains(str.s)); } static Val stringEndsWith(String s, Val suf) { - if (!(suf instanceof StringT)) { + if (!(suf instanceof StringT str)) { return noSuchOverload(StringType, "endsWith", suf); } - return boolOf(s.endsWith(((StringT) suf).s)); + return boolOf(s.endsWith(str.s)); } static Val stringStartsWith(String s, Val pre) { - if (!(pre instanceof StringT)) { + if (!(pre instanceof StringT str)) { return noSuchOverload(StringType, "startsWith", pre); } - return boolOf(s.startsWith(((StringT) pre).s)); + return boolOf(s.startsWith(str.s)); } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/TimestampT.java b/core/src/main/java/org/projectnessie/cel/common/types/TimestampT.java index b99caac0..ecb49b5d 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/TimestampT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/TimestampT.java @@ -31,7 +31,6 @@ import static org.projectnessie.cel.common.types.Types.boolOf; import com.google.protobuf.Any; -import com.google.protobuf.StringValue; import com.google.protobuf.Timestamp; import com.google.protobuf.Value; import java.time.DateTimeException; @@ -208,7 +207,7 @@ public Val compare(Val other) { } /** ConvertToNative implements ref.Val.ConvertToNative. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc == ZonedDateTime.class) { @@ -249,7 +248,8 @@ public T convertToNative(Class typeDesc) { } if (typeDesc == Value.class) { // CEL follows the proto3 to JSON conversion which formats as an RFC 3339 encoded JSON string. - return (T) StringValue.of(jsonFormatter.format(t)); + DateTimeFormatter df = (t.getNano() > 0L) ? rfc3339nanoFormatter : rfc3339formatter; + return (T) Value.newBuilder().setStringValue(df.format(t)).build(); } throw new RuntimeException( @@ -269,36 +269,18 @@ private Timestamp toPbTimestamp() { /** ConvertToType implements ref.Val.ConvertToType. */ @Override public Val convertToType(Type typeValue) { - switch (typeValue.typeEnum()) { - case String: + return switch (typeValue.typeEnum()) { + case String -> { DateTimeFormatter df = (t.getNano() > 0L) ? rfc3339nanoFormatter : rfc3339formatter; - return stringOf(df.format(t)); - case Int: - return intOf(t.toEpochSecond()); - case Timestamp: - return this; - case Type: - return TimestampType; - } - return newTypeConversionError(TimestampType, typeValue); + yield stringOf(df.format(t)); + } + case Int -> intOf(t.toEpochSecond()); + case Timestamp -> this; + case Type -> TimestampType; + default -> newTypeConversionError(TimestampType, typeValue); + }; } - private static final DateTimeFormatter jsonFormatter = - new DateTimeFormatterBuilder() - .appendValue(ChronoField.YEAR, 4, 5, SignStyle.EXCEEDS_PAD) - .appendLiteral('-') - .appendValue(ChronoField.MONTH_OF_YEAR, 2) - .appendLiteral('-') - .appendValue(ChronoField.DAY_OF_MONTH, 2) - .appendLiteral('T') - .appendValue(ChronoField.HOUR_OF_DAY, 2) - .appendLiteral(':') - .appendValue(ChronoField.MINUTE_OF_HOUR, 2) - .appendLiteral(':') - .appendValue(ChronoField.SECOND_OF_MINUTE, 2) - .appendLiteral('Z') - .toFormatter(); - /** Only used for format a string, never for parsing. */ private static final DateTimeFormatter rfc3339formatter = new DateTimeFormatterBuilder() @@ -361,14 +343,11 @@ public Val convertToType(Type typeValue) { /** Equal implements ref.Val.Equal. */ @Override public Val equal(Val other) { - switch (other.type().typeEnum()) { - case Timestamp: - return boolOf(t.equals(((TimestampT) other).t)); - case Null: - return False; - default: - return noSuchOverload(this, "equal", other); - } + return switch (other.type().typeEnum()) { + case Timestamp -> boolOf(t.equals(((TimestampT) other).t)); + case Null -> False; + default -> noSuchOverload(this, "equal", other); + }; } /** Receive implements traits.Reciever.Receive. */ diff --git a/core/src/main/java/org/projectnessie/cel/common/types/TypeT.java b/core/src/main/java/org/projectnessie/cel/common/types/TypeT.java index 56ddacef..f167c08e 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/TypeT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/TypeT.java @@ -55,11 +55,20 @@ public static Type newObjectTypeValue(String name) { return new ObjectTypeT(name); } + /** NewObjectTypeValue returns a *TypeValue with the supplied traits for a qualified type name. */ + public static Type newObjectTypeValue(String name, Trait... traits) { + return new ObjectTypeT(name, traits); + } + static final class ObjectTypeT extends TypeT { private final String typeName; ObjectTypeT(String typeName) { - super(TypeEnum.Object, Trait.FieldTesterType, Trait.IndexerType); + this(typeName, Trait.FieldTesterType, Trait.IndexerType); + } + + ObjectTypeT(String typeName, Trait... traits) { + super(TypeEnum.Object, traits); this.typeName = typeName; } @@ -86,6 +95,7 @@ public double doubleValue() { /** ConvertToNative implements ref.Val.ConvertToNative. */ @Override + @SuppressWarnings("removal") public T convertToNative(Class typeDesc) { throw new UnsupportedOperationException("type conversion not supported for 'type'"); } @@ -93,13 +103,11 @@ public T convertToNative(Class typeDesc) { /** ConvertToType implements ref.Val.ConvertToType. */ @Override public Val convertToType(Type typeVal) { - switch (typeVal.typeEnum()) { - case Type: - return TypeType; - case String: - return stringOf(typeName()); - } - return newTypeConversionError(TypeType, typeVal); + return switch (typeVal.typeEnum()) { + case Type -> TypeType; + case String -> stringOf(typeName()); + default -> newTypeConversionError(TypeType, typeVal); + }; } /** Equal implements ref.Val.Equal. */ @@ -154,11 +162,10 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + if (!(o instanceof Type typeValue)) { return false; } - Type typeValue = (Type) o; - return typeEnum == typeValue.typeEnum() && typeName().equals(typeValue.typeName()); + return typeName().equals(typeValue.typeName()); } @Override diff --git a/core/src/main/java/org/projectnessie/cel/common/types/Types.java b/core/src/main/java/org/projectnessie/cel/common/types/Types.java index 36d6b63b..e4eafce6 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/Types.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/Types.java @@ -55,4 +55,20 @@ public static Type getTypeByName(String typeName) { public static BoolT boolOf(boolean b) { return b ? BoolT.True : BoolT.False; } + + static int numericHashCode(long value) { + return numericHashCode((double) value); + } + + static int unsignedNumericHashCode(long value) { + double doubleValue = value >= 0 ? (double) value : (double) (value & Long.MAX_VALUE) + 0x1.0p63; + return numericHashCode(doubleValue); + } + + static int numericHashCode(double value) { + // CEL numeric equality compares signed and unsigned integers after conversion to double when + // either operand is a double. Use the same canonical representation for Java hash-based maps. + // Canonicalize signed zero because CEL considers -0.0 and 0.0 equal. + return Double.hashCode(value == 0.0d ? 0.0d : value); + } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/UintT.java b/core/src/main/java/org/projectnessie/cel/common/types/UintT.java index f0fe8b7b..8a456e75 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/UintT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/UintT.java @@ -16,7 +16,6 @@ package org.projectnessie.cel.common.types; import static org.projectnessie.cel.common.types.BoolT.False; -import static org.projectnessie.cel.common.types.DoubleT.DoubleType; import static org.projectnessie.cel.common.types.DoubleT.doubleOf; import static org.projectnessie.cel.common.types.Err.divideByZero; import static org.projectnessie.cel.common.types.Err.errUintOverflow; @@ -24,7 +23,6 @@ import static org.projectnessie.cel.common.types.Err.newTypeConversionError; import static org.projectnessie.cel.common.types.Err.noSuchOverload; import static org.projectnessie.cel.common.types.Err.rangeError; -import static org.projectnessie.cel.common.types.IntT.IntOne; import static org.projectnessie.cel.common.types.IntT.intOf; import static org.projectnessie.cel.common.types.IntT.intOfCompare; import static org.projectnessie.cel.common.types.IntT.maxIntJSON; @@ -36,6 +34,7 @@ import com.google.protobuf.UInt64Value; import com.google.protobuf.Value; import java.math.BigInteger; +import java.util.stream.IntStream; import org.projectnessie.cel.common.ULong; import org.projectnessie.cel.common.types.Overflow.OverflowException; import org.projectnessie.cel.common.types.ref.BaseVal; @@ -65,16 +64,19 @@ public final class UintT extends BaseVal Trait.MultiplierType, Trait.SubtractorType); + private static final UintT[] CONST = + IntStream.rangeClosed(0, 10).mapToObj(UintT::new).toArray(UintT[]::new); + /** Uint constants */ - public static final UintT UintZero = new UintT(0); + public static final UintT UintZero = CONST[0]; public static UintT uintOf(ULong i) { return uintOf(i.longValue()); } public static UintT uintOf(long i) { - if (i == 0L) { - return UintZero; + if (i >= 0L && i <= 10) { + return CONST[(int) i]; } return new UintT(i); } @@ -96,7 +98,7 @@ public double doubleValue() { } /** ConvertToNative implements ref.Val.ConvertToNative. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc == Long.class || typeDesc == long.class || typeDesc == Object.class) { @@ -120,13 +122,16 @@ public T convertToNative(Class typeDesc) { return (T) UInt64Value.of(i); } if (typeDesc == UInt32Value.class) { + if (Long.compareUnsigned(i, 0xffffffffL) > 0) { + Err.throwErrorAsIllegalStateException(rangeError(Long.toUnsignedString(i), "uint32")); + } return (T) UInt32Value.of((int) i); } if (typeDesc == Val.class || typeDesc == UintT.class) { return (T) this; } if (typeDesc == Value.class) { - if (i <= maxIntJSON) { + if (Long.compareUnsigned(i, maxIntJSON) <= 0) { // JSON can accurately represent 32-bit uints as floating point values. return (T) Value.newBuilder().setNumberValue(i).build(); } else { @@ -144,102 +149,47 @@ public T convertToNative(Class typeDesc) { /** ConvertToType implements ref.Val.ConvertToType. */ @Override public Val convertToType(Type typeValue) { - switch (typeValue.typeEnum()) { - case Int: + return switch (typeValue.typeEnum()) { + case Int -> { if (i < 0L) { - return rangeError(Long.toUnsignedString(i), "int"); + yield rangeError(Long.toUnsignedString(i), "int"); } - return intOf(i); - case Uint: - return this; - case Double: + yield intOf(i); + } + case Uint -> this; + case Double -> { if (i < 0L) { - return doubleOf(new BigInteger(Long.toUnsignedString(i)).doubleValue()); + yield doubleOf(new BigInteger(Long.toUnsignedString(i)).doubleValue()); } - return doubleOf(i); - case String: - return stringOf(Long.toUnsignedString(i)); - case Type: - return UintType; - } - return newTypeConversionError(UintType, typeValue); + yield doubleOf(i); + } + case String -> stringOf(Long.toUnsignedString(i)); + case Type -> UintType; + default -> newTypeConversionError(UintType, typeValue); + }; } /** Compare implements traits.Comparer.Compare. */ @Override public Val compare(Val other) { - switch (other.type().typeEnum()) { - case Int: - if (other.type().typeEnum() == TypeEnum.Err) { - return other; - } - if (other.intValue() < 0L) { - // the other int is < 0, so any uint is greater - return IntOne; - } - if (i < 0L) { - // this uint is > Long.MAX_VALUE, so it MUST be greater than any signed int - return IntOne; - } - return intOfCompare(Long.compareUnsigned(i, other.intValue())); - case Double: - if (other.type().typeEnum() == TypeEnum.Err) { - return other; - } - if (other.doubleValue() < 0d) { - // the other int is < 0, so any uint is greater - return IntOne; - } - DoubleT cmp = (DoubleT) convertToType(DoubleType); - return cmp.compare(other); - case Uint: - Val converted = other.convertToType(type()); - if (converted.type().typeEnum() == TypeEnum.Err) { - return converted; - } - return intOfCompare(Long.compareUnsigned(i, ((UintT) converted).i)); - default: - return noSuchOverload(this, "compare", other); - } + return switch (other.type().typeEnum()) { + case Int -> intOfCompare(NumericComparison.compareUintInt(i, other.intValue())); + case Double -> intOfCompare(NumericComparison.compareUintDouble(i, other.doubleValue())); + case Uint -> intOfCompare(NumericComparison.compareUint(i, other.intValue())); + default -> noSuchOverload(this, "compare", other); + }; } /** Equal implements ref.Val.Equal. */ @Override public Val equal(Val other) { - switch (other.type().typeEnum()) { - case Int: - if (other.type().typeEnum() == TypeEnum.Err) { - return other; - } - if (other.intValue() < 0L) { - // the other int is < 0, so no uint can be equal - return False; - } - if (i < 0L) { - // this uint is > Long.MAX_VALUE, so it CANNOT be equal - return False; - } - return boolOf(i == other.intValue()); - case Double: - return other.equal(this); - case Uint: - Val converted = other.convertToType(type()); - if (converted.type().typeEnum() == TypeEnum.Err) { - return converted; - } - return boolOf(i == converted.intValue()); - case Null: - case Bool: - case Bytes: - case List: - case Map: - case Object: - case String: - case Type: - return False; - default: - return noSuchOverload(this, "equal", other); - } + return switch (other.type().typeEnum()) { + case Int -> boolOf(NumericComparison.equalUintInt(i, other.intValue())); + case Double -> boolOf(NumericComparison.equalUintDouble(i, other.doubleValue())); + case Uint -> boolOf(NumericComparison.equalUint(i, other.intValue())); + case Null, Bool, Bytes, List, Map, Object, String, Type -> False; + default -> noSuchOverload(this, "equal", other); + }; } /** Divide implements traits.Divider.Divide. */ @@ -324,17 +274,16 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (!(o instanceof Val)) { + if (!(o instanceof Val val)) { return false; } // Defer to CEL's equal functionality to allow heterogeneous numeric map keys - return equal((Val) o).booleanValue(); + return equal(val).booleanValue(); } @Override public int hashCode() { - // Used to allow heterogeneous numeric map keys - return (int) i; + return Types.unsignedNumericHashCode(i); } public String toString() { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/UnknownT.java b/core/src/main/java/org/projectnessie/cel/common/types/UnknownT.java index 7630d266..f31d71a2 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/UnknownT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/UnknownT.java @@ -42,7 +42,7 @@ private UnknownT(long value) { } /** ConvertToNative implements ref.Val.ConvertToNative. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (Long.class.isAssignableFrom(typeDesc) diff --git a/core/src/main/java/org/projectnessie/cel/common/types/Util.java b/core/src/main/java/org/projectnessie/cel/common/types/Util.java index 4eea3dc0..6d737461 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/Util.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/Util.java @@ -21,12 +21,10 @@ public final class Util { /** IsUnknownOrError returns whether the input element ref.Val is an ErrType or UnknonwType. */ public static boolean isUnknownOrError(Val val) { - switch (val.type().typeEnum()) { - case Unknown: - case Err: - return true; - } - return false; + return switch (val.type().typeEnum()) { + case Unknown, Err -> true; + default -> false; + }; } /** @@ -34,15 +32,9 @@ public static boolean isUnknownOrError(Val val) { * types do not include well-known types such as Duration and Timestamp. */ public static boolean isPrimitiveType(Val val) { - switch (val.type().typeEnum()) { - case Bool: - case Bytes: - case Double: - case Int: - case String: - case Uint: - return true; - } - return false; + return switch (val.type().typeEnum()) { + case Bool, Bytes, Double, Int, String, Uint -> true; + default -> false; + }; } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/Checked.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/Checked.java index 04b5b232..0a0f3dee 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/Checked.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/Checked.java @@ -92,6 +92,8 @@ public final class Checked { // Well-known types. CheckedWellKnowns.put("google.protobuf.Any", checkedAny); CheckedWellKnowns.put("google.protobuf.Duration", checkedDuration); + CheckedWellKnowns.put( + "google.protobuf.FieldMask", checkedMessageType("google.protobuf.FieldMask")); CheckedWellKnowns.put("google.protobuf.Timestamp", checkedTimestamp); // Json types. CheckedWellKnowns.put("google.protobuf.ListValue", checkedListDyn); diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/Db.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/Db.java index ced0befa..2fb01465 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/Db.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/Db.java @@ -24,6 +24,8 @@ import com.google.protobuf.Descriptors.FileDescriptor; import com.google.protobuf.Duration; import com.google.protobuf.Empty; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.FieldMask; import com.google.protobuf.Message; import com.google.protobuf.Timestamp; import com.google.protobuf.Value; @@ -49,6 +51,8 @@ public final class Db { /** files contains the deduped set of FileDescriptions whose types are contained in the pb.Db. */ private final List files; + private volatile ExtensionRegistry extensionRegistry; + /** DefaultDb used at evaluation time or unless overridden at check time. */ public static final Db defaultDb = new Db(new HashMap<>(), new ArrayList<>()); @@ -62,6 +66,7 @@ public final class Db { defaultDb.registerMessage(Any.getDefaultInstance()); defaultDb.registerMessage(Duration.getDefaultInstance()); defaultDb.registerMessage(Empty.getDefaultInstance()); + defaultDb.registerMessage(FieldMask.getDefaultInstance()); defaultDb.registerMessage(Timestamp.getDefaultInstance()); defaultDb.registerMessage(Value.getDefaultInstance()); defaultDb.registerMessage(BoolValue.getDefaultInstance()); @@ -121,10 +126,14 @@ public FileDescription registerDescriptor(FileDescriptor fileDesc) { for (String enumValName : fd.getEnumNames()) { revFileDescriptorMap.put(enumValName, fd); } + for (String extensionName : fd.getExtensionNames()) { + revFileDescriptorMap.put(extensionName, fd); + } for (String msgTypeName : fd.getTypeNames()) { revFileDescriptorMap.put(msgTypeName, fd); } revFileDescriptorMap.put(path, fd); + extensionRegistry = null; // Return the specific file descriptor registered. files.add(fd); @@ -169,6 +178,47 @@ public PbTypeDescription describeType(String typeName) { return fd != null ? fd.getTypeDescription(typeName) : null; } + public FieldDescription describeExtension(String messageType, String extensionName) { + extensionName = sanitizeProtoName(extensionName); + FieldDescription extension = describeExtension(extensionName); + if (extension == null + || !sanitizeProtoName(messageType) + .equals(extension.descriptor().getContainingType().getFullName())) { + return null; + } + return extension; + } + + public FieldDescription describeExtension(String extensionName) { + extensionName = sanitizeProtoName(extensionName); + FileDescription fd = revFileDescriptorMap.get(extensionName); + return fd != null ? fd.getExtensionDescription(extensionName) : null; + } + + ExtensionRegistry extensionRegistry() { + ExtensionRegistry registry = extensionRegistry; + if (registry != null) { + return registry; + } + synchronized (this) { + registry = extensionRegistry; + if (registry == null) { + registry = ExtensionRegistry.newInstance(); + for (FileDescription file : files) { + for (FieldDescription extension : file.getExtensionDescriptions()) { + if (extension.isMessage()) { + registry.add(extension.descriptor(), extension.zero()); + } else { + registry.add(extension.descriptor()); + } + } + } + extensionRegistry = registry; + } + return registry; + } + } + /** * CollectFileDescriptorSet builds a file descriptor set associated with the file where the input * message is declared. diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/DefaultTypeAdapter.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/DefaultTypeAdapter.java index f20c5d45..42dc5537 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/DefaultTypeAdapter.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/DefaultTypeAdapter.java @@ -26,11 +26,13 @@ import org.projectnessie.cel.common.ULong; import org.projectnessie.cel.common.types.Err; import org.projectnessie.cel.common.types.Types; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeAdapterSupport; import org.projectnessie.cel.common.types.ref.Val; /** defaultTypeAdapter converts go native types to CEL values. */ -public final class DefaultTypeAdapter implements TypeAdapter { +public final class DefaultTypeAdapter implements StandardScalarTypeAdapter { /** DefaultTypeAdapter adapts canonical CEL types from their equivalent Go values. */ public static final DefaultTypeAdapter Instance = new DefaultTypeAdapter(Db.defaultDb); @@ -50,6 +52,41 @@ public Val nativeToValue(Object value) { return Err.unsupportedRefValConversionErr(value); } + @Override + public Val nativeToValue(boolean value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(byte value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(short value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(int value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(long value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(float value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(double value) { + return TypeAdapterSupport.nativeToValue(value); + } + /** * nativeToValue returns the converted (ref.Val, true) of a conversion is found, otherwise (nil, * false) @@ -64,8 +101,7 @@ public static Val nativeToValue(Db db, TypeAdapter a, Object value) { if (value instanceof Val) { return (Val) value; } - if (value instanceof Message) { - Message msg = (Message) value; + if (value instanceof Message msg) { String typeName = typeNameFromMessage(msg); if (typeName.isEmpty()) { return anyWithEmptyType(); @@ -84,8 +120,7 @@ public static Val nativeToValue(Db db, TypeAdapter a, Object value) { } static Object maybeUnwrapValue(Object value) { - if (value instanceof Value) { - Value v = (Value) value; + if (value instanceof Value v) { switch (v.getKindCase()) { case BOOL_VALUE: return v.getBoolValue(); diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java index 9eb6ea86..4f4aaac6 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java @@ -15,12 +15,14 @@ */ package org.projectnessie.cel.common.types.pb; +import static org.projectnessie.cel.common.types.BoolT.False; import static org.projectnessie.cel.common.types.BoolT.True; import static org.projectnessie.cel.common.types.Err.newTypeConversionError; import static org.projectnessie.cel.common.types.Err.noMoreElements; import static org.projectnessie.cel.common.types.IntT.intOf; import static org.projectnessie.cel.common.types.TypeT.TypeType; import static org.projectnessie.cel.common.types.Types.boolOf; +import static org.projectnessie.cel.common.types.Util.isUnknownOrError; import static org.projectnessie.cel.common.types.pb.PbTypeDescription.reflectTypeOf; import static org.projectnessie.cel.common.types.pb.PbTypeDescription.unwrapDynamic; @@ -47,10 +49,12 @@ import org.projectnessie.cel.common.types.MapT; import org.projectnessie.cel.common.types.ref.BaseVal; import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeEnum; import org.projectnessie.cel.common.types.ref.Val; /** FieldDescription holds metadata related to fields declared within a type. */ public final class FieldDescription extends Description { + private static final Object NO_NATIVE_MAP_KEY = new Object(); /** KeyType holds the key FieldDescription for map fields. */ final FieldDescription keyType; @@ -135,34 +139,18 @@ public static FieldDescription newFieldDescription(FieldDescriptor fieldDesc) { } private static Class reflectTypeOfField(FieldDescriptor fieldDesc) { - switch (fieldDesc.getType()) { - case DOUBLE: - return Double.class; - case FLOAT: - return Float.class; - case STRING: - return String.class; - case BOOL: - return Boolean.class; - case BYTES: - return ByteString.class; - case INT32: - case SFIXED32: - case SINT32: - return Integer.class; - case INT64: - case SFIXED64: - case SINT64: - return Long.class; - case UINT32: - case UINT64: - case FIXED32: - case FIXED64: - return ULong.class; - case ENUM: - return Enum.class; - } - return reflectTypeOf(fieldDesc.getDefaultValue()); + return switch (fieldDesc.getType()) { + case DOUBLE -> Double.class; + case FLOAT -> Float.class; + case STRING -> String.class; + case BOOL -> Boolean.class; + case BYTES -> ByteString.class; + case INT32, SFIXED32, SINT32 -> Integer.class; + case INT64, SFIXED64, SINT64 -> Long.class; + case UINT32, UINT64, FIXED32, FIXED64 -> ULong.class; + case ENUM -> Enum.class; + default -> reflectTypeOf(fieldDesc.getDefaultValue()); + }; } private FieldDescription( @@ -209,18 +197,9 @@ public FieldDescriptor descriptor() { * on more than just protobuf field accesses; however, the target here must be a protobuf.Message. */ public boolean isSet(Object target) { - if (target instanceof Message) { - Message v = (Message) target; - // pbRef = v.ProtoReflect() - Descriptor pbDesc = v.getDescriptorForType(); - if (pbDesc == desc.getContainingType()) { - // When the target protobuf shares the same message descriptor instance as the field - // descriptor, use the cached field descriptor value. - return FieldDescription.hasValueForField(desc, v); - } - // Otherwise, fallback to a dynamic lookup of the field descriptor from the target - // instance as an attempt to use the cached field descriptor will result in a panic. - return FieldDescription.hasValueForField(pbDesc.findFieldByName(name()), v); + if (target instanceof Message v) { + FieldDescriptor fd = fieldDescriptorFor(v); + return fd != null && FieldDescription.hasValueForField(fd, v); } return false; } @@ -235,27 +214,14 @@ public boolean isSet(Object target) { * protobuf.Message. */ public Object getFrom(Db db, Object target) { - if (!(target instanceof Message)) { + if (!(target instanceof Message v)) { throw new IllegalArgumentException( String.format( "unsupported field selection target: (%s)%s", target.getClass().getName(), target)); } - Message v = (Message) target; // pbRef = v.protoReflect(); - Descriptor pbDesc = v.getDescriptorForType(); - Object fieldVal; - - FieldDescriptor fd; - if (pbDesc == desc.getContainingType()) { - // When the target protobuf shares the same message descriptor instance as the field - // descriptor, use the cached field descriptor value. - fd = desc; - } else { - // Otherwise, fallback to a dynamic lookup of the field descriptor from the target - // instance as an attempt to use the cached field descriptor will result in a panic. - fd = pbDesc.findFieldByName(name()); - } - fieldVal = getValueFromField(fd, v); + FieldDescriptor fd = fieldDescriptorFor(v); + Object fieldVal = getValueFromField(fd, v); Class fieldType = fieldVal.getClass(); if (fd.getJavaType() != JavaType.MESSAGE @@ -352,26 +318,16 @@ public Class reflectType() { if (r && desc.isMapField()) { return Map.class; } - switch (desc.getJavaType()) { - case ENUM: - case MESSAGE: - return reflectType; - case BOOLEAN: - return r ? Boolean[].class : Boolean.class; - case BYTE_STRING: - return r ? ByteString[].class : ByteString.class; - case DOUBLE: - return r ? Double[].class : Double.class; - case FLOAT: - return r ? Float[].class : Float.class; - case INT: - return r ? Integer[].class : Integer.class; - case LONG: - return r ? Long[].class : Long.class; - case STRING: - return r ? String[].class : String.class; - } - return reflectType; + return switch (desc.getJavaType()) { + case ENUM, MESSAGE -> reflectType; + case BOOLEAN -> r ? Boolean[].class : Boolean.class; + case BYTE_STRING -> r ? ByteString[].class : ByteString.class; + case DOUBLE -> r ? Double[].class : Double.class; + case FLOAT -> r ? Float[].class : Float.class; + case INT -> r ? Integer[].class : Integer.class; + case LONG -> r ? Long[].class : Long.class; + case STRING -> r ? String[].class : String.class; + }; } /** @@ -447,7 +403,11 @@ public int hashCode() { } public boolean hasField(Object target) { - return hasValueForField(desc, (Message) target); + if (!(target instanceof Message message)) { + return false; + } + FieldDescriptor fd = fieldDescriptorFor(message); + return fd != null && hasValueForField(fd, message); } public Object getField(Object target) { @@ -464,9 +424,42 @@ public Object getField(Object target, TypeAdapter adapter) { return getValueFromField(fd, message); } + Object adaptGeneratedValue(Object value, TypeAdapter adapter) { + if (desc.isMapField() && value instanceof Map) { + return new ProtoMapT(adapter, desc, (Map) value); + } + if (desc.isRepeated()) { + FieldDescriptor.Type type = desc.getType(); + if (value instanceof List + && (type == FieldDescriptor.Type.UINT32 + || type == FieldDescriptor.Type.UINT64 + || type == FieldDescriptor.Type.FIXED32 + || type == FieldDescriptor.Type.FIXED64)) { + return new UnsignedLongList((List) value); + } + return value; + } + return normalizeUnsignedValue(desc, value); + } + + boolean isWrapper() { + return isWellKnownType(desc); + } + + boolean generatedValueNeedsAdaptation() { + if (desc.isMapField()) { + return true; + } + FieldDescriptor.Type type = desc.getType(); + return type == FieldDescriptor.Type.UINT32 + || type == FieldDescriptor.Type.UINT64 + || type == FieldDescriptor.Type.FIXED32 + || type == FieldDescriptor.Type.FIXED64; + } + public static Object getValueFromField(FieldDescriptor desc, Message message) { - if (isWellKnownType(desc) && !message.hasField(desc)) { + if (!desc.isRepeated() && isWellKnownType(desc) && !message.hasField(desc)) { return NullValue.NULL_VALUE; } @@ -489,8 +482,7 @@ public static Object getValueFromField(FieldDescriptor desc, Message message) { // is very inefficient. // There is no way to do a "message.getMapField(desc, key)" (aka a "reflective counterpart" // for the generated map accessor methods like 'getXXXTypeOrThrow()'), too. - if (v instanceof List) { - List lst = (List) v; + if (v instanceof List lst) { Map map = new HashMap<>(lst.size() * 4 / 3 + 1); FieldDescriptor keyDesc = desc.getMessageType().findFieldByNumber(1); FieldDescriptor valueDesc = desc.getMessageType().findFieldByNumber(2); @@ -500,8 +492,7 @@ public static Object getValueFromField(FieldDescriptor desc, Message message) { if (e instanceof MapEntry) { key = normalizeUnsignedValue(keyDesc, ((MapEntry) e).getKey()); value = normalizeUnsignedValue(valueDesc, ((MapEntry) e).getValue()); - } else if (e instanceof DynamicMessage) { - DynamicMessage dynMsg = (DynamicMessage) e; + } else if (e instanceof DynamicMessage dynMsg) { List fields = dynMsg.getDescriptorForType().getFields(); if (fields.size() == 2) { FieldDescriptor dynKeyDesc = fields.get(0); @@ -541,7 +532,18 @@ private FieldDescriptor fieldDescriptorFor(Message message) { if (messageDesc == desc.getContainingType()) { return desc; } - return messageDesc.findFieldByName(name()); + if (!desc.isExtension()) { + return messageDesc.findFieldByName(name()); + } + for (FieldDescriptor field : message.getAllFields().keySet()) { + if (field.getFullName().equals(desc.getFullName())) { + return field; + } + } + if (messageDesc.getFullName().equals(desc.getContainingType().getFullName())) { + return desc; + } + return null; } private static final class UnsignedLongList extends AbstractList { @@ -565,6 +567,7 @@ public int size() { private static final class ProtoMapT extends MapT { private final TypeAdapter adapter; private final List entries; + private final Map map; private final FieldDescriptor keyDesc; private final FieldDescriptor valueDesc; private volatile Map indexedEntries; @@ -573,14 +576,23 @@ private static final class ProtoMapT extends MapT { private ProtoMapT(TypeAdapter adapter, FieldDescriptor field, List entries) { this.adapter = adapter; this.entries = entries; + this.map = null; this.keyDesc = field.getMessageType().findFieldByNumber(1); this.valueDesc = field.getMessageType().findFieldByNumber(2); } - @SuppressWarnings("unchecked") + private ProtoMapT(TypeAdapter adapter, FieldDescriptor field, Map map) { + this.adapter = adapter; + this.entries = null; + this.map = map; + this.keyDesc = field.getMessageType().findFieldByNumber(1); + this.valueDesc = field.getMessageType().findFieldByNumber(2); + } + + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { - return (T) MapT.newMaybeWrappedMap(adapter, toJavaMap()).convertToNative(typeDesc); + return adapter.valueToNative(MapT.newMaybeWrappedMap(adapter, toJavaMap()), typeDesc); } @Override @@ -601,7 +613,31 @@ public IteratorT iterator() { @Override public Val equal(Val other) { - return MapT.newMaybeWrappedMap(adapter, toJavaMap()).equal(other); + if (other instanceof ProtoMapT protoOther + && map != null + && protoOther.map != null + && keyDesc.getType() == protoOther.keyDesc.getType()) { + if (nativeSize() != protoOther.nativeSize()) { + return False; + } + for (Map.Entry entry : map.entrySet()) { + Object key = entry.getKey(); + Object otherRawValue = protoOther.map.get(key); + if (otherRawValue == null && !protoOther.map.containsKey(key)) { + return False; + } + Val equal = + mapValueEqual( + adapter.nativeToValue(normalizeUnsignedValue(valueDesc, entry.getValue())), + protoOther.adapter.nativeToValue( + normalizeUnsignedValue(protoOther.valueDesc, otherRawValue))); + if (equal != True) { + return equal; + } + } + return True; + } + return mapEqual(other); } @Override @@ -611,6 +647,15 @@ public Object value() { @Override public Val contains(Val value) { + if (isUnknownOrError(value)) { + return value; + } + if (map != null) { + Object nativeKey = nativeMapKey(value); + if (nativeKey != NO_NATIVE_MAP_KEY) { + return boolOf(map.containsKey(nativeKey)); + } + } return boolOf(find(value) != null); } @@ -621,27 +666,91 @@ public Val get(Val index) { @Override public Val size() { - return intOf(entries.size()); + return intOf(entryCount()); + } + + @Override + public int nativeSize() { + return entryCount(); + } + + @Override + protected Iterable mapEntries() { + return allEntries(); + } + + @Override + protected Val mapEntryKey(Object entry) { + return adapter.nativeToValue(nativeEntryKey(entry)); + } + + @Override + protected Val mapEntryValue(Object entry) { + return adapter.nativeToValue(nativeEntryValue(entry)); } @Override public Val find(Val key) { + if (map != null) { + Object nativeKey = nativeMapKey(key); + if (nativeKey != NO_NATIVE_MAP_KEY) { + Object value = map.get(nativeKey); + return value != null || map.containsKey(nativeKey) + ? adapter.nativeToValue(normalizeUnsignedValue(valueDesc, value)) + : null; + } + } Map index = indexedEntries; if (index != null) { return index.get(key); } - if (!scanned || entries.size() <= 1) { + if (!scanned || entryCount() <= 1) { scanned = true; return scan(key); } return indexedEntries().get(key); } + private Object nativeMapKey(Val key) { + switch (keyDesc.getType()) { + case BOOL: + return key.type().typeEnum() == TypeEnum.Bool ? key.booleanValue() : NO_NATIVE_MAP_KEY; + case STRING: + return key.type().typeEnum() == TypeEnum.String ? key.value() : NO_NATIVE_MAP_KEY; + case INT32: + case SINT32: + case SFIXED32: + if (key.type().typeEnum() != TypeEnum.Int) { + return NO_NATIVE_MAP_KEY; + } + long int32 = key.intValue(); + return int32 >= Integer.MIN_VALUE && int32 <= Integer.MAX_VALUE + ? (int) int32 + : NO_NATIVE_MAP_KEY; + case INT64: + case SINT64: + case SFIXED64: + return key.type().typeEnum() == TypeEnum.Int ? key.intValue() : NO_NATIVE_MAP_KEY; + case UINT32: + case FIXED32: + if (key.type().typeEnum() != TypeEnum.Uint) { + return NO_NATIVE_MAP_KEY; + } + long uint32 = key.intValue(); + return Long.compareUnsigned(uint32, 0xffff_ffffL) <= 0 ? (int) uint32 : NO_NATIVE_MAP_KEY; + case UINT64: + case FIXED64: + return key.type().typeEnum() == TypeEnum.Uint ? key.intValue() : NO_NATIVE_MAP_KEY; + default: + return NO_NATIVE_MAP_KEY; + } + } + private Val scan(Val key) { - for (Object entry : entries) { - Val candidate = adapter.nativeToValue(mapEntryKey(entry)); + for (Object entry : allEntries()) { + Val candidate = adapter.nativeToValue(nativeEntryKey(entry)); if (candidate.equal(key) == True) { - return adapter.nativeToValue(mapEntryValue(entry)); + return adapter.nativeToValue(nativeEntryValue(entry)); } } return null; @@ -655,10 +764,10 @@ private Map indexedEntries() { synchronized (this) { index = indexedEntries; if (index == null) { - index = new HashMap<>(entries.size() * 4 / 3 + 1); - for (Object entry : entries) { - Val key = adapter.nativeToValue(mapEntryKey(entry)); - Val value = adapter.nativeToValue(mapEntryValue(entry)); + index = new HashMap<>(entryCount() * 4 / 3 + 1); + for (Object entry : allEntries()) { + Val key = adapter.nativeToValue(nativeEntryKey(entry)); + Val value = adapter.nativeToValue(nativeEntryValue(entry)); index.putIfAbsent(key, value); } indexedEntries = index; @@ -668,22 +777,25 @@ private Map indexedEntries() { } private Map toJavaMap() { - Map map = new HashMap<>(entries.size() * 4 / 3 + 1); - for (Object entry : entries) { - map.put(mapEntryKey(entry), mapEntryValue(entry)); + Map javaMap = new HashMap<>(entryCount() * 4 / 3 + 1); + for (Object entry : allEntries()) { + javaMap.put(nativeEntryKey(entry), nativeEntryValue(entry)); } - return map; + return javaMap; } - private Object mapEntryKey(Object entry) { + private Object nativeEntryKey(Object entry) { return normalizeUnsignedValue(entryKeyDescriptor(entry), rawMapEntryValue(entry, 1)); } - private Object mapEntryValue(Object entry) { + private Object nativeEntryValue(Object entry) { return normalizeUnsignedValue(entryValueDescriptor(entry), rawMapEntryValue(entry, 2)); } private FieldDescriptor entryKeyDescriptor(Object entry) { + if (entry instanceof Map.Entry) { + return keyDesc; + } if (entry instanceof DynamicMessage) { List fields = ((DynamicMessage) entry).getDescriptorForType().getFields(); if (fields.size() == 2) { @@ -694,6 +806,9 @@ private FieldDescriptor entryKeyDescriptor(Object entry) { } private FieldDescriptor entryValueDescriptor(Object entry) { + if (entry instanceof Map.Entry) { + return valueDesc; + } if (entry instanceof DynamicMessage) { List fields = ((DynamicMessage) entry).getDescriptorForType().getFields(); if (fields.size() == 2) { @@ -704,12 +819,13 @@ private FieldDescriptor entryValueDescriptor(Object entry) { } private Object rawMapEntryValue(Object entry, int fieldNumber) { - if (entry instanceof MapEntry) { - MapEntry mapEntry = (MapEntry) entry; + if (entry instanceof Map.Entry mapEntry) { return fieldNumber == 1 ? mapEntry.getKey() : mapEntry.getValue(); } - if (entry instanceof DynamicMessage) { - DynamicMessage dynMsg = (DynamicMessage) entry; + if (entry instanceof MapEntry mapEntry) { + return fieldNumber == 1 ? mapEntry.getKey() : mapEntry.getValue(); + } + if (entry instanceof DynamicMessage dynMsg) { List fields = dynMsg.getDescriptorForType().getFields(); if (fields.size() == 2) { return dynMsg.getField(fields.get(fieldNumber - 1)); @@ -719,23 +835,32 @@ private Object rawMapEntryValue(Object entry, int fieldNumber) { String.format("Unexpected %s (%s) in list of map fields", entry.getClass(), entry)); } + private int entryCount() { + return entries != null ? entries.size() : map.size(); + } + + private Iterable allEntries() { + return entries != null ? entries : map.entrySet(); + } + private final class EntryKeyIterator extends BaseVal implements IteratorT { - private int index; + private final java.util.Iterator iterator = allEntries().iterator(); @Override public Val hasNext() { - return boolOf(index < entries.size()); + return boolOf(iterator.hasNext()); } @Override public Val next() { - if (index < entries.size()) { - return adapter.nativeToValue(mapEntryKey(entries.get(index++))); + if (iterator.hasNext()) { + return adapter.nativeToValue(nativeEntryKey(iterator.next())); } return noMoreElements(); } @Override + @SuppressWarnings("removal") public T convertToNative(Class typeDesc) { throw new UnsupportedOperationException(); } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/FileDescription.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/FileDescription.java index 3e554c1c..c9d229d9 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/FileDescription.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/FileDescription.java @@ -18,6 +18,7 @@ import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.EnumDescriptor; import com.google.protobuf.Descriptors.EnumValueDescriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.FileDescriptor; import java.util.HashMap; import java.util.List; @@ -29,11 +30,15 @@ public final class FileDescription { private final Map types; private final Map enums; + private final Map extensions; private FileDescription( - Map types, Map enums) { + Map types, + Map enums, + Map extensions) { this.types = types; this.enums = enums; + this.extensions = extensions; } @Override @@ -45,12 +50,14 @@ public boolean equals(Object o) { return false; } FileDescription that = (FileDescription) o; - return Objects.equals(types, that.types) && Objects.equals(enums, that.enums); + return Objects.equals(types, that.types) + && Objects.equals(enums, that.enums) + && Objects.equals(extensions, that.extensions); } @Override public int hashCode() { - return Objects.hash(types, enums); + return Objects.hash(types, enums, extensions); } /** @@ -66,7 +73,10 @@ public static FileDescription newFileDescription(FileDescriptor fileDesc) { Map types = new HashMap<>(); metadata.msgTypes.forEach( (name, msgType) -> types.put(name, PbTypeDescription.newTypeDescription(name, msgType))); - return new FileDescription(types, enums); + Map extensions = new HashMap<>(); + metadata.extensions.forEach( + (name, extension) -> extensions.put(name, FieldDescription.newFieldDescription(extension))); + return new FileDescription(types, enums, extensions); } /** @@ -82,6 +92,21 @@ public String[] getEnumNames() { return enums.keySet().toArray(new String[0]); } + /** GetExtensionDescription returns a field description for a qualified extension name. */ + public FieldDescription getExtensionDescription(String extensionName) { + return extensions.get(sanitizeProtoName(extensionName)); + } + + /** GetExtensionNames returns the string names of all extensions in the file. */ + public String[] getExtensionNames() { + return extensions.keySet().toArray(new String[0]); + } + + /** GetExtensionDescriptions returns all extension field descriptions in the file. */ + public Iterable getExtensionDescriptions() { + return extensions.values(); + } + /** * GetTypeDescription returns a TypeDescription for a qualified protobuf message type name * declared within the .proto file. @@ -111,12 +136,18 @@ static final class FileMetadata { /** enumValues maps from fully-qualified enum value to enum value descriptor. */ final Map enumValues; + /** extensions maps from fully-qualified extension name to field descriptor. */ + final Map extensions; + // TODO: support enum type definitions for use in future type-check enhancements. private FileMetadata( - Map msgTypes, Map enumValues) { + Map msgTypes, + Map enumValues, + Map extensions) { this.msgTypes = msgTypes; this.enumValues = enumValues; + this.extensions = extensions; } /** @@ -126,10 +157,12 @@ private FileMetadata( static FileMetadata collectFileMetadata(FileDescriptor fileDesc) { Map msgTypes = new HashMap<>(); Map enumValues = new HashMap<>(); + Map extensions = new HashMap<>(); - collectMsgTypes(fileDesc.getMessageTypes(), msgTypes, enumValues); + collectMsgTypes(fileDesc.getMessageTypes(), msgTypes, enumValues, extensions); collectEnumValues(fileDesc.getEnumTypes(), enumValues); - return new FileMetadata(msgTypes, enumValues); + collectExtensions(fileDesc.getExtensions(), extensions); + return new FileMetadata(msgTypes, enumValues, extensions); } /** @@ -139,17 +172,26 @@ static FileMetadata collectFileMetadata(FileDescriptor fileDesc) { private static void collectMsgTypes( List msgTypes, Map msgTypeMap, - Map enumValueMap) { + Map enumValueMap, + Map extensionMap) { for (Descriptor msgType : msgTypes) { msgTypeMap.put(msgType.getFullName(), msgType); List nestedMsgTypes = msgType.getNestedTypes(); if (!nestedMsgTypes.isEmpty()) { - collectMsgTypes(nestedMsgTypes, msgTypeMap, enumValueMap); + collectMsgTypes(nestedMsgTypes, msgTypeMap, enumValueMap, extensionMap); } List nestedEnumTypes = msgType.getEnumTypes(); if (!nestedEnumTypes.isEmpty()) { collectEnumValues(nestedEnumTypes, enumValueMap); } + collectExtensions(msgType.getExtensions(), extensionMap); + } + } + + private static void collectExtensions( + List extensions, Map extensionMap) { + for (FieldDescriptor extension : extensions) { + extensionMap.put(extension.getFullName(), extension); } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/GeneratedFieldAccessor.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/GeneratedFieldAccessor.java new file mode 100644 index 00000000..dffa712c --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/GeneratedFieldAccessor.java @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.common.types.pb; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Descriptors.EnumValueDescriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.Message; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.List; +import java.util.Map; +import org.projectnessie.cel.common.types.ref.FieldGetter; +import org.projectnessie.cel.common.types.ref.FieldGetter.Primitive; +import org.projectnessie.cel.common.types.ref.FieldTester; + +final class GeneratedFieldAccessor { + + private static final MethodType OBJECT_GETTER = MethodType.methodType(Object.class, Object.class); + + private GeneratedFieldAccessor() {} + + static FieldGetter create(PbTypeDescription type, FieldDescription field) { + return create(type, field, false); + } + + static FieldGetter createForObject(PbTypeDescription type, FieldDescription field) { + return create(type, field, true); + } + + private static FieldGetter create( + PbTypeDescription type, FieldDescription field, boolean allowOrdinaryMessages) { + FieldDescriptor descriptor = field.descriptor(); + Message zero = type.zero(); + Class messageClass = type.reflectType(); + if (descriptor.isExtension() + || zero instanceof DynamicMessage + || !messageClass.isInstance(zero)) { + return null; + } + + String suffix = accessorSuffix(descriptor.getName()); + String methodName; + Class returnType; + if (descriptor.isMapField()) { + FieldDescriptor valueDescriptor = descriptor.getMessageType().findFieldByNumber(2); + methodName = + valueDescriptor.getType() == FieldDescriptor.Type.ENUM + ? "get" + suffix + "ValueMap" + : "get" + suffix + "Map"; + returnType = Map.class; + } else if (descriptor.isRepeated()) { + methodName = + descriptor.getType() == FieldDescriptor.Type.ENUM + ? "get" + suffix + "ValueList" + : "get" + suffix + "List"; + returnType = List.class; + } else if (descriptor.getType() == FieldDescriptor.Type.ENUM) { + if (isNullValue(descriptor.getEnumType().getFullName())) { + return null; + } + methodName = "get" + suffix + "Value"; + returnType = int.class; + } else { + methodName = "get" + suffix; + returnType = generatedReturnType(descriptor, zero, allowOrdinaryMessages); + } + if (returnType == null) { + return null; + } + + for (String candidate : new String[] {methodName, methodName + '_'}) { + try { + MethodHandle getter = + MethodHandles.publicLookup() + .findVirtual(messageClass, candidate, MethodType.methodType(returnType)); + if (!matchesField(getter, zero, descriptor)) { + continue; + } + MethodHandle objectGetter = getter.asType(OBJECT_GETTER); + Primitive primitiveGetter = primitiveGetter(messageClass, descriptor, getter, objectGetter); + if (primitiveGetter != null) { + return primitiveGetter; + } + return target -> invoke(objectGetter, target); + } catch (IllegalAccessException | NoSuchMethodException | RuntimeException e) { + // Generated-name collisions and inaccessible generated classes use descriptor access. + } + } + return null; + } + + private static Primitive primitiveGetter( + Class targetType, + FieldDescriptor descriptor, + MethodHandle getter, + MethodHandle objectGetter) { + if (descriptor.isRepeated()) { + return null; + } + MethodHandle booleanGetter = null; + MethodHandle longGetter = null; + MethodHandle doubleGetter = null; + switch (descriptor.getType()) { + case BOOL -> + booleanGetter = getter.asType(MethodType.methodType(boolean.class, Object.class)); + case INT32, SINT32, SFIXED32, INT64, SINT64, SFIXED64, ENUM -> + longGetter = getter.asType(MethodType.methodType(long.class, Object.class)); + case FLOAT, DOUBLE -> + doubleGetter = getter.asType(MethodType.methodType(double.class, Object.class)); + default -> { + return null; + } + } + return new PrimitiveGetter(targetType, objectGetter, booleanGetter, longGetter, doubleGetter); + } + + private record PrimitiveGetter( + Class targetType, + MethodHandle objectGetter, + MethodHandle booleanGetter, + MethodHandle longGetter, + MethodHandle doubleGetter) + implements Primitive { + + @Override + public Class optimizedTargetType() { + return targetType; + } + + @Override + public Object getFrom(Object target) { + return invoke(objectGetter, target); + } + + @Override + public boolean getBooleanFrom(Object target) { + if (booleanGetter == null) { + return Primitive.super.getBooleanFrom(target); + } + return invokeBoolean(booleanGetter, target); + } + + @Override + public long getLongFrom(Object target) { + if (longGetter == null) { + return Primitive.super.getLongFrom(target); + } + return invokeLong(longGetter, target); + } + + @Override + public double getDoubleFrom(Object target) { + if (doubleGetter == null) { + return Primitive.super.getDoubleFrom(target); + } + return invokeDouble(doubleGetter, target); + } + } + + static FieldTester createTester(PbTypeDescription type, FieldDescription field) { + FieldDescriptor descriptor = field.descriptor(); + Message zero = type.zero(); + Class messageClass = type.reflectType(); + if (descriptor.isExtension() + || descriptor.isRepeated() + || !descriptor.hasPresence() + || zero instanceof DynamicMessage + || !messageClass.isInstance(zero)) { + return null; + } + + String methodName = "has" + accessorSuffix(descriptor.getName()); + for (String candidate : new String[] {methodName, methodName + '_'}) { + try { + MethodHandle tester = + MethodHandles.publicLookup() + .findVirtual(messageClass, candidate, MethodType.methodType(boolean.class)); + if (!matchesPresence(tester, zero, descriptor)) { + continue; + } + MethodHandle objectTester = + tester.asType(MethodType.methodType(boolean.class, Object.class)); + return target -> invokeBoolean(objectTester, target); + } catch (IllegalAccessException | NoSuchMethodException | RuntimeException e) { + // Generated-name collisions and inaccessible generated classes use descriptor access. + } + } + return null; + } + + private static Class generatedReturnType( + FieldDescriptor descriptor, Message zero, boolean allowOrdinaryMessages) { + return switch (descriptor.getType()) { + case BOOL -> boolean.class; + case STRING -> String.class; + case BYTES -> ByteString.class; + case INT32, SINT32, SFIXED32, UINT32, FIXED32 -> int.class; + case INT64, SINT64, SFIXED64, UINT64, FIXED64 -> long.class; + case FLOAT -> float.class; + case DOUBLE -> double.class; + case MESSAGE -> { + Object value = + allowOrdinaryMessages + || Checked.CheckedWellKnowns.containsKey( + descriptor.getMessageType().getFullName()) + ? zero.getField(descriptor) + : null; + yield value instanceof Message ? value.getClass() : null; + } + default -> null; + }; + } + + private static boolean matchesField( + MethodHandle getter, Message zero, FieldDescriptor descriptor) { + try { + Message.Builder builder = zero.newBuilderForType(); + if (descriptor.isMapField()) { + Message.Builder entry = builder.newBuilderForField(descriptor); + for (FieldDescriptor entryField : descriptor.getMessageType().getFields()) { + Object value = validationValue(entryField, entry); + if (value == null) { + return false; + } + entry.setField(entryField, value); + } + builder.addRepeatedField(descriptor, entry.buildPartial()); + return ((Map) getter.invoke(builder.buildPartial())).size() == 1; + } + + Object expected = validationValue(descriptor, builder); + if (expected == null) { + return false; + } + if (descriptor.isRepeated()) { + builder.addRepeatedField(descriptor, expected); + List actual = (List) getter.invoke(builder.buildPartial()); + if (actual.size() != 1) { + return false; + } + Object actualValue = actual.get(0); + return descriptor.getType() == FieldDescriptor.Type.ENUM + ? actualValue.equals(((EnumValueDescriptor) expected).getNumber()) + : expected.equals(actualValue); + } + + builder.setField(descriptor, expected); + Object actual = getter.invoke(builder.buildPartial()); + return descriptor.getType() == FieldDescriptor.Type.ENUM + ? actual.equals(((EnumValueDescriptor) expected).getNumber()) + : expected.equals(actual); + } catch (Throwable e) { + return false; + } + } + + private static boolean matchesPresence( + MethodHandle tester, Message zero, FieldDescriptor descriptor) { + try { + if ((boolean) tester.invoke(zero)) { + return false; + } + Message.Builder builder = zero.newBuilderForType(); + builder.setField(descriptor, builder.getField(descriptor)); + return (boolean) tester.invoke(builder.buildPartial()); + } catch (Throwable e) { + return false; + } + } + + private static Object validationValue( + FieldDescriptor descriptor, Message.Builder containingBuilder) { + return switch (descriptor.getType()) { + case BOOL -> true; + case STRING -> "cel_generated_field_accessor"; + case BYTES -> ByteString.copyFromUtf8("cel_generated_field_accessor"); + case INT32, SINT32, SFIXED32, UINT32, FIXED32 -> 0x51a7; + case INT64, SINT64, SFIXED64, UINT64, FIXED64 -> 0x51a7_19b3_42c5L; + case FLOAT -> 123.25f; + case DOUBLE -> 123.25d; + case ENUM -> { + List values = descriptor.getEnumType().getValues(); + yield values.get(Math.min(1, values.size() - 1)); + } + case MESSAGE -> nonDefaultMessage(containingBuilder.newBuilderForField(descriptor)); + default -> null; + }; + } + + private static Message nonDefaultMessage(Message.Builder builder) { + for (FieldDescriptor field : builder.getDescriptorForType().getFields()) { + if (field.isRepeated() || field.getType() == FieldDescriptor.Type.MESSAGE) { + continue; + } + Object value = validationValue(field, builder); + if (value != null) { + return builder.setField(field, value).buildPartial(); + } + } + return null; + } + + private static String accessorSuffix(String fieldName) { + StringBuilder result = new StringBuilder(fieldName.length()); + boolean capitalizeNext = true; + for (int i = 0; i < fieldName.length(); i++) { + char c = fieldName.charAt(i); + if (c >= 'a' && c <= 'z') { + result.append(capitalizeNext ? Character.toUpperCase(c) : c); + capitalizeNext = false; + } else if ((c >= 'A' && c <= 'Z')) { + result.append(c); + capitalizeNext = false; + } else if (c >= '0' && c <= '9') { + result.append(c); + capitalizeNext = true; + } else { + capitalizeNext = true; + } + } + return result.toString(); + } + + private static boolean isNullValue(String enumTypeName) { + return enumTypeName.equals("google.protobuf.NullValue"); + } + + private static Object invoke(MethodHandle getter, Object target) { + try { + return (Object) getter.invokeExact(target); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + private static boolean invokeBoolean(MethodHandle tester, Object target) { + try { + return (boolean) tester.invokeExact(target); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + private static long invokeLong(MethodHandle getter, Object target) { + try { + return (long) getter.invokeExact(target); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + private static double invokeDouble(MethodHandle getter, Object target) { + try { + return (double) getter.invokeExact(target); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } +} diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/PbObjectT.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/PbObjectT.java index d139b618..8df95c48 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/PbObjectT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/PbObjectT.java @@ -21,11 +21,18 @@ import static org.projectnessie.cel.common.types.Types.boolOf; import com.google.protobuf.Any; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Descriptors.FieldDescriptor.JavaType; import com.google.protobuf.DynamicMessage; +import com.google.protobuf.Empty; +import com.google.protobuf.FieldMask; import com.google.protobuf.Message; +import com.google.protobuf.Struct; +import com.google.protobuf.Timestamp; import com.google.protobuf.Value; import org.projectnessie.cel.common.types.ObjectT; import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.ref.FieldType; import org.projectnessie.cel.common.types.ref.Type; import org.projectnessie.cel.common.types.ref.TypeAdapter; import org.projectnessie.cel.common.types.ref.Val; @@ -57,7 +64,11 @@ public Val isSet(Val field) { return noSuchOverload(this, "isSet", field); } String protoFieldStr = (String) field.value(); - FieldDescription fd = typeDesc().fieldByName(protoFieldStr); + FieldType fieldType = fieldType(protoFieldStr, true); + if (fieldType != null) { + return boolOf(fieldType.isSet.isSet(value)); + } + FieldDescription fd = fieldDescription(protoFieldStr); if (fd == null) { return noSuchField(protoFieldStr); } @@ -70,14 +81,33 @@ public Val get(Val index) { return noSuchOverload(this, "get", index); } String protoFieldStr = (String) index.value(); - FieldDescription fd = typeDesc().fieldByName(protoFieldStr); + FieldType fieldType = fieldType(protoFieldStr, false); + if (fieldType != null) { + return adapter.nativeToValue(fieldType.getFrom.getFrom(value)); + } + FieldDescription fd = fieldDescription(protoFieldStr); if (fd == null) { return noSuchField(protoFieldStr); } - return nativeToValue(fd.getField(value, adapter)); + return adapter.nativeToValue(fd.getField(value, adapter)); } - @SuppressWarnings("unchecked") + @Override + public Val equal(Val other) { + if (!(other instanceof PbObjectT otherObject)) { + return super.equal(other); + } + + if (!typeDesc().name().equals(otherObject.typeDesc().name())) { + return boolOf(false); + } + if (containsNaN(message()) || containsNaN(otherObject.message())) { + return boolOf(false); + } + return boolOf(message().equals(otherObject.message())); + } + + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc.isAssignableFrom(value.getClass())) { @@ -86,9 +116,6 @@ public T convertToNative(Class typeDesc) { if (typeDesc.isAssignableFrom(getClass())) { return (T) this; } - if (typeDesc.isAssignableFrom(value.getClass())) { - return (T) value; - } if (typeDesc == DynamicMessage.class) { return (T) DynamicMessage.newBuilder(message().getDescriptorForType()).mergeFrom(message()).build(); @@ -102,20 +129,15 @@ public T convertToNative(Class typeDesc) { } if (typeDesc == Value.class) { // jsonValueType - throw new UnsupportedOperationException("IMPLEMENT proto-to-json"); - // TODO proto-to-json - // // Marshal the proto to JSON first, and then rehydrate as protobuf.Value as there is no - // // support for direct conversion from proto.Message to protobuf.Value. - // bytes, err := protojson.Marshal(pb) - // if err != nil { - // return nil, err - // } - // json := &structpb.Value{} - // err = protojson.Unmarshal(bytes, json) - // if err != nil { - // return nil, err - // } - // return json, nil + if (value instanceof Empty) { + return (T) Value.newBuilder().setStructValue(Struct.getDefaultInstance()).build(); + } + if (value instanceof FieldMask) { + return (T) Value.newBuilder().setStringValue(fieldMaskJsonValue((FieldMask) value)).build(); + } + if (value instanceof Timestamp) { + return adapter.valueToNative(adapter.nativeToValue(value), typeDesc); + } } if (typeDesc.isAssignableFrom(this.typeDesc.reflectType()) || typeDesc == Object.class) { if (value instanceof Any || value instanceof DynamicMessage) { @@ -141,10 +163,80 @@ private Message message() { return (Message) value; } + private static boolean containsNaN(Message message) { + for (FieldDescriptor field : message.getAllFields().keySet()) { + Object fieldValue = message.getField(field); + if (field.isRepeated()) { + for (Object element : (Iterable) fieldValue) { + if (containsNaN(field, element)) { + return true; + } + } + } else if (containsNaN(field, fieldValue)) { + return true; + } + } + return false; + } + + private static boolean containsNaN(FieldDescriptor field, Object value) { + JavaType javaType = field.getJavaType(); + if (javaType == JavaType.DOUBLE) { + return Double.isNaN((Double) value); + } + if (javaType == JavaType.FLOAT) { + return Float.isNaN((Float) value); + } + return javaType == JavaType.MESSAGE && containsNaN((Message) value); + } + + private static String fieldMaskJsonValue(FieldMask fieldMask) { + StringBuilder value = new StringBuilder(); + for (String path : fieldMask.getPathsList()) { + if (!value.isEmpty()) { + value.append(','); + } + value.append(fieldMaskPathJsonValue(path)); + } + return value.toString(); + } + + private static String fieldMaskPathJsonValue(String path) { + StringBuilder value = new StringBuilder(path.length()); + boolean upperNext = false; + for (int i = 0; i < path.length(); i++) { + char c = path.charAt(i); + if (c == '_') { + upperNext = true; + } else if (upperNext) { + value.append(Character.toUpperCase(c)); + upperNext = false; + } else { + value.append(c); + } + } + return value.toString(); + } + private PbTypeDescription typeDesc() { return (PbTypeDescription) typeDesc; } + private FieldDescription fieldDescription(String fieldName) { + FieldDescription field = typeDesc().fieldByName(fieldName); + if (field != null || !(adapter instanceof ProtoTypeRegistry)) { + return field; + } + return ((ProtoTypeRegistry) adapter).findFieldDescription(typeDesc().name(), fieldName); + } + + private FieldType fieldType(String fieldName, boolean presenceTest) { + if (adapter instanceof ProtoTypeRegistry registry) { + return registry.findFieldTypeForObjectAccess(typeDesc().name(), fieldName, presenceTest); + } + return null; + } + @SuppressWarnings("unchecked") private T buildFrom(Class typeDesc) { try { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/PbTypeDescription.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/PbTypeDescription.java index fd1fc2d4..09bb4dcb 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/PbTypeDescription.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/PbTypeDescription.java @@ -119,15 +119,13 @@ public Object maybeUnwrap(Db db, Object m) { if (this.reflectType == Any.class) { String realTypeUrl; ByteString realValue; - if (msg instanceof DynamicMessage) { - DynamicMessage dyn = (DynamicMessage) msg; + if (msg instanceof DynamicMessage dyn) { Descriptor dynDesc = dyn.getDescriptorForType(); FieldDescriptor fTypeUrl = dynDesc.findFieldByName("type_url"); FieldDescriptor fValue = dynDesc.findFieldByName("value"); realTypeUrl = (String) dyn.getField(fTypeUrl); realValue = (ByteString) dyn.getField(fValue); - } else if (msg instanceof Any) { - Any any = (Any) msg; + } else if (msg instanceof Any any) { realTypeUrl = any.getTypeUrl(); realValue = any.getValue(); } else { @@ -138,17 +136,17 @@ public Object maybeUnwrap(Db db, Object m) { return anyWithEmptyType(); } PbTypeDescription realTypeDescriptor = db.describeType(realTypeName); - Message realMsg = realTypeDescriptor.zeroMsg.getParserForType().parseFrom(realValue); + Message realMsg = + DynamicMessage.parseFrom( + realTypeDescriptor.getDescriptor(), realValue, db.extensionRegistry()); return realTypeDescriptor.maybeUnwrap(db, realMsg); } if (!(zeroMsg instanceof DynamicMessage)) { - if (msg instanceof Any) { - Any any = (Any) msg; - msg = zeroMsg.getParserForType().parseFrom(any.getValue()); - } else if (msg instanceof DynamicMessage) { - DynamicMessage dyn = (DynamicMessage) msg; - msg = zeroMsg.getParserForType().parseFrom(dyn.toByteString()); + if (msg instanceof Any any) { + msg = DynamicMessage.parseFrom(getDescriptor(), any.getValue(), db.extensionRegistry()); + } else if (msg instanceof DynamicMessage dyn && !hasExtensions(msg)) { + msg = zeroMsg.getParserForType().parseFrom(dyn.toByteString(), db.extensionRegistry()); } } } catch (InvalidProtocolBufferException e) { @@ -244,37 +242,37 @@ static Object unwrap(Db db, Description desc, Message msg) { return conv.apply(msg); } - if (msg instanceof Any) { - Any v = (Any) msg; + if (msg instanceof Any v) { DynamicMessage dyn = DynamicMessage.newBuilder(v).build(); return unwrapDynamic(db, desc, dyn); } if (msg instanceof DynamicMessage) { return unwrapDynamic(db, desc, msg); } - if (msg instanceof Value) { - Value v = (Value) msg; - switch (v.getKindCase()) { - case BOOL_VALUE: - return v.getBoolValue(); - case LIST_VALUE: - return v.getListValue(); - case NULL_VALUE: - return v.getNullValue(); - case NUMBER_VALUE: - return v.getNumberValue(); - case STRING_VALUE: - return v.getStringValue(); - case STRUCT_VALUE: - return v.getStructValue(); - default: - return NullValue.NULL_VALUE; - } + if (msg instanceof Value v) { + return switch (v.getKindCase()) { + case BOOL_VALUE -> v.getBoolValue(); + case LIST_VALUE -> v.getListValue(); + case NULL_VALUE -> v.getNullValue(); + case NUMBER_VALUE -> v.getNumberValue(); + case STRING_VALUE -> v.getStringValue(); + case STRUCT_VALUE -> v.getStructValue(); + default -> NullValue.NULL_VALUE; + }; } return msg; } + private static boolean hasExtensions(Message message) { + for (FieldDescriptor field : message.getAllFields().keySet()) { + if (field.isExtension()) { + return true; + } + } + return false; + } + private static java.time.Duration asJavaDuration(Duration d) { return java.time.Duration.ofSeconds(d.getSeconds(), d.getNanos()); } @@ -371,16 +369,14 @@ private static Object unwrapDynamicAny(Db db, Description desc, Message refMsg) } public static String typeNameFromMessage(Message message) { - if (message instanceof DynamicMessage) { - DynamicMessage dyn = (DynamicMessage) message; + if (message instanceof DynamicMessage dyn) { Descriptor dynDesc = dyn.getDescriptorForType(); if (dynDesc.getFullName().equals("google.protobuf.Any")) { FieldDescriptor f = dynDesc.findFieldByName("type_url"); String typeUrl = (String) dyn.getField(f); return typeNameFromUrl(typeUrl); } - } else if (message instanceof Any) { - Any any = (Any) message; + } else if (message instanceof Any any) { String typeUrl = any.getTypeUrl(); return typeNameFromUrl(typeUrl); } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/ProtoTypeRegistry.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/ProtoTypeRegistry.java index 18aa9fe1..64e1220e 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/ProtoTypeRegistry.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/ProtoTypeRegistry.java @@ -16,6 +16,7 @@ package org.projectnessie.cel.common.types.pb; import static org.projectnessie.cel.common.types.BoolT.BoolType; +import static org.projectnessie.cel.common.types.BoolT.True; import static org.projectnessie.cel.common.types.BytesT.BytesType; import static org.projectnessie.cel.common.types.DoubleT.DoubleType; import static org.projectnessie.cel.common.types.DurationT.DurationType; @@ -30,6 +31,7 @@ import static org.projectnessie.cel.common.types.MapT.MapType; import static org.projectnessie.cel.common.types.NullT.NullType; import static org.projectnessie.cel.common.types.StringT.StringType; +import static org.projectnessie.cel.common.types.StringT.stringOf; import static org.projectnessie.cel.common.types.TimestampT.TimestampType; import static org.projectnessie.cel.common.types.TypeT.TypeType; import static org.projectnessie.cel.common.types.TypeT.newObjectTypeValue; @@ -44,6 +46,7 @@ import com.google.api.expr.v1alpha1.Type; 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.EnumDescriptor; @@ -54,6 +57,7 @@ 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; @@ -79,16 +83,27 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.types.IteratorT; +import org.projectnessie.cel.common.types.MapT; +import org.projectnessie.cel.common.types.NullT; import org.projectnessie.cel.common.types.TypeT; +import org.projectnessie.cel.common.types.ref.FieldGetter; +import org.projectnessie.cel.common.types.ref.FieldTester; import org.projectnessie.cel.common.types.ref.FieldType; +import org.projectnessie.cel.common.types.ref.StandardScalarFieldProvider; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeAdapterSupport; import org.projectnessie.cel.common.types.ref.TypeRegistry; import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; -public final class ProtoTypeRegistry implements TypeRegistry { +public final class ProtoTypeRegistry + implements TypeRegistry, StandardScalarTypeAdapter, StandardScalarFieldProvider { private static final ProtoTypeRegistry DEFAULT_REGISTRY = newDefaultRegistry(); private final Map revTypeMap; - private final Map fieldTypeCache; + private final Map> fieldTypeCache; private final Db pbdb; private ProtoTypeRegistry( @@ -132,6 +147,7 @@ private static ProtoTypeRegistry newDefaultRegistry() { Arrays.asList( DoubleValue.getDescriptor().getFile(), Empty.getDescriptor().getFile(), + FieldMask.getDescriptor().getFile(), Timestamp.getDescriptor().getFile(), UInt64Value.getDescriptor().getFile(), Any.getDescriptor().getFile(), @@ -200,21 +216,135 @@ public Val enumValue(String enumName) { @Override public FieldType findFieldType(String messageType, String fieldName) { - String cacheKey = messageType + '\n' + fieldName; - return fieldTypeCache.computeIfAbsent(cacheKey, key -> loadFieldType(messageType, fieldName)); + Map messageFields = fieldTypeCache.get(messageType); + if (messageFields == null) { + Map newFields = new ConcurrentHashMap<>(); + Map existing = fieldTypeCache.putIfAbsent(messageType, newFields); + messageFields = existing != null ? existing : newFields; + } + FieldType fieldType = messageFields.get(fieldName); + if (fieldType != null) { + return fieldType; + } + FieldType loaded = loadFieldType(messageType, fieldName); + if (loaded == null) { + return null; + } + FieldType existing = messageFields.putIfAbsent(fieldName, loaded); + return existing != null ? existing : loaded; } private FieldType loadFieldType(String messageType, String fieldName) { + FieldDescription field = findFieldDescription(messageType, fieldName); + if (field == null) { + return null; + } + FieldTester descriptorTester = field::hasField; + FieldGetter descriptorGetter = target -> field.getField(target, this); + PbTypeDescription type = pbdb.describeType(messageType); + FieldGetter generatedGetter = type != null ? GeneratedFieldAccessor.create(type, field) : null; + FieldGetter objectGetter = generatedGetter; + if (objectGetter == null && type != null) { + objectGetter = GeneratedFieldAccessor.createForObject(type, field); + } + FieldTester generatedTester = + type != null ? GeneratedFieldAccessor.createTester(type, field) : null; + FieldTester tester = descriptorTester; + if (generatedTester != null) { + Class generatedType = type.reflectType(); + tester = + target -> + generatedType.isInstance(target) + ? generatedTester.isSet(target) + : field.hasField(target); + } + FieldGetter getter = + generatedGetter != null + ? bindGeneratedGetter(type, field, generatedGetter, tester) + : descriptorGetter; + FieldGetter optimizedObjectGetter = + objectGetter != null ? bindGeneratedGetter(type, field, objectGetter, tester) : null; + return new ProtoFieldType( + field.checkedType(), tester, getter, generatedTester, optimizedObjectGetter); + } + + private FieldGetter bindGeneratedGetter( + PbTypeDescription type, + FieldDescription field, + FieldGetter generatedGetter, + FieldTester tester) { + Class generatedType = type.reflectType(); + if (field.isWrapper()) { + return target -> + generatedType.isInstance(target) + ? !tester.isSet(target) + ? com.google.protobuf.NullValue.NULL_VALUE + : generatedGetter.getFrom(target) + : field.getField(target, this); + } + if (field.generatedValueNeedsAdaptation()) { + return target -> + generatedType.isInstance(target) + ? field.adaptGeneratedValue(generatedGetter.getFrom(target), this) + : field.getField(target, this); + } + if (generatedGetter instanceof FieldGetter.Primitive primitiveGetter) { + return new FieldGetter.Primitive() { + @Override + public Class optimizedTargetType() { + return primitiveGetter.optimizedTargetType(); + } + + @Override + public Object getFrom(Object target) { + return generatedType.isInstance(target) + ? primitiveGetter.getFrom(target) + : field.getField(target, ProtoTypeRegistry.this); + } + + @Override + public boolean getBooleanFrom(Object target) { + return primitiveGetter.getBooleanFrom(target); + } + + @Override + public long getLongFrom(Object target) { + return primitiveGetter.getLongFrom(target); + } + + @Override + public double getDoubleFrom(Object target) { + return primitiveGetter.getDoubleFrom(target); + } + }; + } + return target -> + generatedType.isInstance(target) + ? generatedGetter.getFrom(target) + : field.getField(target, this); + } + + FieldType findFieldTypeForObjectAccess( + String messageType, String fieldName, boolean presenceTest) { + FieldType fieldType = findFieldType(messageType, fieldName); + if (!(fieldType instanceof ProtoFieldType protoFieldType)) { + return null; + } + return presenceTest + ? protoFieldType.objectPresenceFieldType + : protoFieldType.objectGetterFieldType; + } + + FieldDescription findFieldDescription(String messageType, String fieldName) { PbTypeDescription msgType = pbdb.describeType(messageType); if (msgType == null) { return null; } FieldDescription field = msgType.fieldByName(fieldName); if (field == null) { - return null; + field = pbdb.describeExtension(messageType, fieldName); } - return new FieldType( - field.checkedType(), field::hasField, target -> field.getField(target, this)); + return field; } @Override @@ -227,6 +357,9 @@ public Val findIdent(String identName) { if (enumVal != null) { return intOf(enumVal.value()); } + if (pbdb.describeExtension(identName) != null) { + return stringOf(identName); + } return null; } @@ -265,27 +398,156 @@ private Val newValueSetFields(Map fields, PbTypeDescription td, Bui return noSuchField(name); } - // TODO resolve inefficiency for maps: first converted from a MapT to a native Java map and - // then to a protobuf struct. The intermediate step (the Java map) could be omitted. + FieldDescriptor pbDesc = field.descriptor(); + if (nv.getValue() == org.projectnessie.cel.common.types.NullT.NullValue + && isNullClearedField(pbDesc)) { + continue; + } + + try { + Object value; + if (pbDesc.isMapField() && nv.getValue() instanceof MapT map) { + value = toProtoMapStructure(field, map); + } else { + value = toNativeFieldValue(nv.getValue(), field); + if (value.getClass().isArray()) { + value = Arrays.asList((Object[]) value); + } + + if (pbDesc.getJavaType() == JavaType.ENUM) { + value = intToProtoEnumValues(field, value); + } + + if (pbDesc.isMapField()) { + value = toProtoMapStructure(pbDesc, value); + } + } - Object value = nv.getValue().convertToNative(field.reflectType()); - if (value.getClass().isArray()) { - value = Arrays.asList((Object[]) value); + builder.setField(pbDesc, value); + } catch (RuntimeException e) { + return newErr(e, "invalid value for field '%s': %s", name, e.getMessage()); } + } + return null; + } - FieldDescriptor pbDesc = field.descriptor(); + private Object toNativeFieldValue(Val value, FieldDescription field) { + FieldDescriptor fieldDesc = field.descriptor(); + if (fieldDesc.isRepeated() + && !fieldDesc.isMapField() + && isNullPrunedMessageField(fieldDesc) + && value instanceof Lister) { + return toNativeRepeatedFieldValue((Lister) value, fieldDesc); + } + return valueToNative(value, field.reflectType()); + } - if (pbDesc.getJavaType() == JavaType.ENUM) { - value = intToProtoEnumValues(field, value); + private Object toNativeRepeatedFieldValue(Lister value, FieldDescriptor fieldDesc) { + Class elementType = messageNativeType(fieldDesc); + int size = value.nativeSize(); + List converted = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + Val element = value.nativeGetAt(i); + if (element == NullT.NullValue && isNullPrunedMessageField(fieldDesc)) { + continue; } + converted.add(valueToNative(element, elementType)); + } + return converted; + } + + private static boolean isNullClearedField(FieldDescriptor field) { + if (field.getJavaType() != JavaType.MESSAGE || field.isRepeated() || field.isMapField()) { + return false; + } + Type wellKnownType = Checked.CheckedWellKnowns.get(field.getMessageType().getFullName()); + return wellKnownType == null || isNullPrunedMessageField(field); + } + + private static boolean isNullPrunedMessageField(FieldDescriptor field) { + if (field.getJavaType() != JavaType.MESSAGE) { + return false; + } + String typeName = field.getMessageType().getFullName(); + Type wellKnownType = Checked.CheckedWellKnowns.get(typeName); + if (wellKnownType == null) { + return false; + } + return wellKnownType.hasWrapper() + || typeName.equals("google.protobuf.Duration") + || typeName.equals("google.protobuf.Timestamp"); + } + + private static Class messageNativeType(FieldDescriptor field) { + return switch (field.getMessageType().getFullName()) { + case "google.protobuf.Any" -> Any.class; + case "google.protobuf.BoolValue" -> BoolValue.class; + case "google.protobuf.BytesValue" -> BytesValue.class; + case "google.protobuf.DoubleValue" -> DoubleValue.class; + case "google.protobuf.Duration" -> Duration.class; + case "google.protobuf.FieldMask" -> FieldMask.class; + case "google.protobuf.FloatValue" -> FloatValue.class; + case "google.protobuf.Int32Value" -> Int32Value.class; + case "google.protobuf.Int64Value" -> Int64Value.class; + case "google.protobuf.StringValue" -> StringValue.class; + case "google.protobuf.Timestamp" -> Timestamp.class; + case "google.protobuf.UInt32Value" -> UInt32Value.class; + case "google.protobuf.UInt64Value" -> UInt64Value.class; + case "google.protobuf.Value" -> Value.class; + default -> Message.class; + }; + } - if (pbDesc.isMapField()) { - value = toProtoMapStructure(pbDesc, value); + /** Converts a CEL map directly to protobuf map entries without an intermediate Java map. */ + private Object toProtoMapStructure(FieldDescription field, MapT value) { + FieldDescriptor fieldDesc = field.descriptor(); + Descriptor entryType = fieldDesc.getMessageType(); + FieldDescriptor keyType = field.keyType.descriptor(); + FieldDescriptor valueType = field.valueType.descriptor(); + WireFormat.FieldType keyFieldType = WireFormat.FieldType.valueOf(keyType.getType().name()); + WireFormat.FieldType valueFieldType = WireFormat.FieldType.valueOf(valueType.getType().name()); + List> entries = new ArrayList<>(value.nativeSize()); + + IteratorT iterator = value.iterator(); + while (iterator.hasNext() == True) { + Val key = iterator.next(); + Val mapValue = value.find(key); + if (mapValue == NullT.NullValue && isNullPrunedMessageField(valueType)) { + continue; } - builder.setField(pbDesc, value); + Object nativeKey = toNativeMapEntryValue(key, keyType); + Object nativeValue = toNativeMapEntryValue(mapValue, valueType); + entries.add( + MapEntry.newDefaultInstance( + entryType, keyFieldType, nativeKey, valueFieldType, nativeValue)); } - return null; + return entries; + } + + private Object toNativeMapEntryValue(Val value, FieldDescriptor field) { + return switch (field.getType()) { + case DOUBLE -> valueToDouble(value); + case FLOAT -> valueToNative(value, Float.class); + case INT64, SINT64, SFIXED64 -> valueToLong(value); + case UINT64, FIXED64 -> valueToNative(value, ULong.class).longValue(); + case INT32, SINT32, SFIXED32 -> valueToInt(value); + case UINT32, FIXED32 -> valueToNative(value, ULong.class).intValue(); + case BOOL -> valueToBoolean(value); + case STRING -> valueToNative(value, String.class); + case BYTES -> valueToNative(value, ByteString.class); + case ENUM -> { + if (value == NullT.NullValue) { + if (field.getEnumType().getFullName().equals("google.protobuf.NullValue")) { + yield 0; + } + throw new IllegalArgumentException("null is only valid for google.protobuf.NullValue"); + } + yield valueToInt(value); + } + case MESSAGE -> valueToNative(value, messageNativeType(field)); + case GROUP -> throw new IllegalArgumentException("protobuf maps cannot contain group values"); + }; } /** @@ -309,8 +571,13 @@ private Object toProtoMapStructure(FieldDescriptor fieldDesc, Object value) { // if (!(k instanceof String)) { // return Err.newTypeConversionError(k.getClass().getName(), String.class.getName()); // } - if (valueFieldType == WireFormat.FieldType.MESSAGE && !(v instanceof Message)) { - v = nativeToValue(v).convertToNative(Value.class); + if (valueFieldType == WireFormat.FieldType.MESSAGE) { + if (isNullNativeValue(v) && isNullPrunedMessageField(valueType)) { + continue; + } + if (!(v instanceof Message)) { + v = valueToNative(nativeToValue(v), messageNativeType(valueType)); + } } MapEntry newEntry = @@ -323,6 +590,10 @@ private Object toProtoMapStructure(FieldDescriptor fieldDesc, Object value) { return value; } + private static boolean isNullNativeValue(Object value) { + return value == null || value == com.google.protobuf.NullValue.NULL_VALUE; + } + /** * Converts a value of type {@link Number} to {@link EnumValueDescriptor}, also works for arrays * and {@link List}s containing {@link Number}s. @@ -333,8 +604,7 @@ private Object intToProtoEnumValues(FieldDescription field, Object value) { if (value instanceof Number) { int enumValue = ((Number) value).intValue(); value = enumType.findValueByNumberCreatingIfUnknown(enumValue); - } else if (value instanceof List) { - List list = (List) value; + } else if (value instanceof List list) { List newList = new ArrayList(list.size()); for (Object o : list) { int enumValue = ((Number) o).intValue(); @@ -362,6 +632,7 @@ public void registerDescriptor(FileDescriptor fileDesc) { /** RegisterMessage registers a protocol buffer message and its dependencies. */ public void registerMessage(Message message) { FileDescription fd = pbdb.registerMessage(message); + fieldTypeCache.remove(typeNameFromMessage(message)); registerAllTypes(fd); } @@ -381,8 +652,7 @@ public void registerType(org.projectnessie.cel.common.types.ref.Type... types) { */ @Override public Val nativeToValue(Object value) { - if (value instanceof Message) { - Message v = (Message) value; + if (value instanceof Message v) { String typeName = typeNameFromMessage(v); if (typeName.isEmpty()) { return anyWithEmptyType(); @@ -422,6 +692,41 @@ public Val nativeToValue(Object value) { return unsupportedRefValConversionErr(value); } + @Override + public Val nativeToValue(boolean value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(byte value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(short value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(int value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(long value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(float value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(double value) { + return TypeAdapterSupport.nativeToValue(value); + } + void registerAllTypes(FileDescription fd) { for (String typeName : fd.getTypeNames()) { registerType(newObjectTypeValue(typeName)); @@ -433,6 +738,24 @@ public String toString() { return "ProtoTypeRegistry{" + "revTypeMap.size=" + revTypeMap.size() + ", pbdb=" + pbdb + '}'; } + private static final class ProtoFieldType extends FieldType { + private final FieldType objectPresenceFieldType; + private final FieldType objectGetterFieldType; + + private ProtoFieldType( + Type type, + FieldTester isSet, + FieldGetter getFrom, + FieldTester generatedTester, + FieldGetter objectGetter) { + super(type, isSet, getFrom); + this.objectPresenceFieldType = + generatedTester != null ? new FieldType(type, isSet, getFrom) : null; + this.objectGetterFieldType = + objectGetter != null ? new FieldType(type, isSet, objectGetter) : null; + } + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ref/ExactAggregateFieldProvider.java b/core/src/main/java/org/projectnessie/cel/common/types/ref/ExactAggregateFieldProvider.java new file mode 100644 index 00000000..b5c7c59e --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/common/types/ref/ExactAggregateFieldProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.common.types.ref; + +/** + * Type provider whose aggregate field accessors return certified exact aggregate representations. + * + *

For fields described as checked CEL list or map types, the associated {@link + * FieldType#getFrom} accessor returns a Java representation satisfying {@link + * ExactAggregateTypeAdapter}'s homogeneous representation, checked-kind provenance, null, + * encounter-order, equality/hashing, immutability-during-evaluation, cycle, and contract-violation + * requirements. This marker does not change scalar field semantics and exposes no evaluator or + * planner implementation type. + * + *

Implementations should declare this contract only on visibly distinct opt-in provider + * instances. Existing default providers do not acquire the stricter input contract implicitly. In + * the initial contract, field specialization recognizes this marker only when the same configured + * object is also the {@link ExactAggregateTypeAdapter}; independently configured provider and + * adapter instances remain on the general field path. Field presence behavior and accessor + * invocation order are unchanged. + */ +public interface ExactAggregateFieldProvider extends TypeProvider {} diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ref/ExactAggregateTypeAdapter.java b/core/src/main/java/org/projectnessie/cel/common/types/ref/ExactAggregateTypeAdapter.java new file mode 100644 index 00000000..0d70e177 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/common/types/ref/ExactAggregateTypeAdapter.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.common.types.ref; + +import com.google.api.expr.v1alpha1.Type; +import java.util.Objects; + +/** + * Type adapter that can materialize certified Java aggregate representations using their checked + * CEL type. + * + *

This is an explicit, stricter opt-in contract applying to every instance of an implementing + * class, independently of mutable evaluation context. It defines checked materialization in both + * native-enabled and native-disabled planning; implementing it does not itself permit a native + * island. + * + *

The canonical aggregate representations are {@code int[]}, {@code long[]}, {@code double[]}, + * {@code String[]}, {@code Object[]}, {@link java.util.List}, non-list {@link java.util.Collection} + * including {@link java.util.Set}, and {@link java.util.Map}, recursively. Java {@code byte[]} + * remains CEL bytes rather than a list. Embedded {@link Val} values and {@code Val[]} are not + * conforming exact aggregate elements. The {@code checkedType} is authoritative for element, key, + * and value kinds, including whether Java {@code long} bits represent CEL {@code int} or {@code + * uint}; implementations must not infer signedness from the Java class or sign bit, and do not + * infer implicit signed/unsigned mixing. + * + *

Inputs contain homogeneous values compatible with the checked type. Null list elements and map + * values are permitted only when the nested checked type permits CEL null, dynamic, or a nullable + * wrapper value. Present-null map values remain distinct from absent keys. Map keys must not be + * null, and distinct Java keys must not be equal under CEL key equality. Exact lookup is permitted + * only for a planner-certified domain whose Java equality and hashing agree with CEL. Cross-numeric + * lookup is not inferred; double lookup must preserve CEL NaN inequality and signed-zero equality. + * Sets advertised through this contract use the canonical homogeneous boxed representation needed + * by direct CEL membership: {@link Boolean}, {@link String}, {@link Long} for CEL int, either all + * {@link Long} or all {@link org.projectnessie.cel.common.ULong} for CEL uint, and {@link Double} + * for CEL double. Other numeric wrappers and mixed boxed representations must be exposed through a + * general adapter instead. This restriction lets a direct hash miss remain definitive without an + * element-proportional validation scan. + * + *

List materialization preserves the established encounter-order and snapshot behavior of each + * representation. Arrays use index order and lists use list order. Non-list collections are + * snapshotted with {@code toArray()}, and exact inputs require that order to agree with iterator + * encounter order. Sets are encounter-ordered CEL lists for indexing, traversal, equality, and + * terminal conversion; unordered Java set equality is not CEL list equality. + * + *

The caller must not mutate an advertised aggregate during one CEL evaluation and must not + * supply cycles traversed recursively by materialization. Implementations need not perform an + * element-proportional validation pass before a constant-time operation. Cheaply detected contract + * violations return a deterministic CEL error value, never Java {@code null}, a general-path + * fallback, or a request to evaluate again. O(1) operations need not pre-scan unvisited elements; + * undetected violations are caller contract breaches and must never cause expression replay or + * corrupt reusable plan state. + * + *

Exact aggregate field materialization initially requires the same configured object to + * implement both this interface and {@link ExactAggregateFieldProvider} and to be supplied as both + * the CEL type adapter and provider. Activation values require only this adapter contract. + * + *

This contract exposes no evaluator, planner, node, capability, or internal result type. + * Implementations may provide checked materialization independently of scalar native + * specialization. + */ +public interface ExactAggregateTypeAdapter extends TypeAdapter { + /** + * Converts a certified Java aggregate to a CEL value using the authoritative checked CEL type. + * + * @param value certified Java aggregate representation + * @param checkedType non-null checked CEL list or map type + * @return the materialized CEL aggregate or a deterministic CEL error for a cheaply detected + * contract violation + * @throws NullPointerException if {@code checkedType} is null + * @throws IllegalArgumentException if {@code checkedType} is not a CEL list or map type + */ + default Val nativeAggregateToValue(Object value, Type checkedType) { + Objects.requireNonNull(checkedType, "checkedType"); + if (checkedType.getTypeKindCase() != Type.TypeKindCase.LIST_TYPE + && checkedType.getTypeKindCase() != Type.TypeKindCase.MAP_TYPE) { + throw new IllegalArgumentException("checkedType must be a CEL list or map type"); + } + return TypeAdapterSupport.nativeAggregateToValue(this, value, checkedType); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ref/FieldGetter.java b/core/src/main/java/org/projectnessie/cel/common/types/ref/FieldGetter.java index 3dad1f5b..4acb12fd 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/ref/FieldGetter.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/ref/FieldGetter.java @@ -19,4 +19,25 @@ @FunctionalInterface public interface FieldGetter { Object getFrom(Object target); + + /** Optional allocation-free primitive access implemented by trusted generated-field getters. */ + interface Primitive extends FieldGetter { + /** Runtime type for which the primitive accessors are valid. */ + Class optimizedTargetType(); + + /** Reads a CEL boolean without boxing. */ + default boolean getBooleanFrom(Object target) { + throw new UnsupportedOperationException("boolean field access is not supported"); + } + + /** Reads a CEL signed integer without boxing. */ + default long getLongFrom(Object target) { + throw new UnsupportedOperationException("integer field access is not supported"); + } + + /** Reads a CEL double without boxing. */ + default double getDoubleFrom(Object target) { + throw new UnsupportedOperationException("double field access is not supported"); + } + } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ref/StandardScalarFieldProvider.java b/core/src/main/java/org/projectnessie/cel/common/types/ref/StandardScalarFieldProvider.java new file mode 100644 index 00000000..b8d46341 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/common/types/ref/StandardScalarFieldProvider.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.common.types.ref; + +/** + * Type provider whose scalar field accessors return canonical Java scalar representations. + * + *

For fields described as CEL boolean, signed integer, double, string, or null, the associated + * {@link FieldType#getFrom} accessor returns the corresponding Java scalar, Java {@code null}, or + * an existing terminal {@link Val}. Access must not depend on mutable evaluation context. + * + *

This semantic marker permits evaluators to consume supported scalar fields before adapting a + * final result. Implementations should only declare it when those guarantees apply to every + * instance. + */ +public interface StandardScalarFieldProvider extends TypeProvider {} diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ref/StandardScalarTypeAdapter.java b/core/src/main/java/org/projectnessie/cel/common/types/ref/StandardScalarTypeAdapter.java new file mode 100644 index 00000000..23801e0a --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/common/types/ref/StandardScalarTypeAdapter.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.common.types.ref; + +/** + * Type adapter whose Java scalar conversions have the standard CEL semantics. + * + *

Implementations preserve existing {@link Val} values and convert Java {@code null}, boolean, + * signed integral, floating-point, and string values exactly as {@link TypeAdapterSupport}. The + * primitive overloads must agree with {@link #nativeToValue(Object)}, and these conversions must + * not depend on mutable evaluation context. + * + *

This semantic marker permits evaluators to carry the corresponding Java scalars internally + * before adapting the final result. Implementations should only declare it when those guarantees + * apply to every instance. + */ +public interface StandardScalarTypeAdapter extends TypeAdapter {} diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapter.java b/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapter.java index 81c88fea..a4c7b3c1 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapter.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapter.java @@ -15,10 +15,79 @@ */ package org.projectnessie.cel.common.types.ref; -/** - * TypeAdapter converts native Go values of varying type and complexity to equivalent CEL values. - */ +/** Converts native Java values of varying type and complexity to and from equivalent CEL values. */ +@FunctionalInterface public interface TypeAdapter { - /** NativeToValue converts the input `value` to a CEL `ref.Val`. */ + /** Converts the input {@code value} to a CEL {@link Val}. */ Val nativeToValue(Object value); + + /** Converts a primitive boolean to a CEL value. */ + default Val nativeToValue(boolean value) { + return nativeToValue(Boolean.valueOf(value)); + } + + /** Converts a primitive byte to a CEL value. */ + default Val nativeToValue(byte value) { + return nativeToValue(Byte.valueOf(value)); + } + + /** Converts a primitive short to a CEL value. */ + default Val nativeToValue(short value) { + return nativeToValue(Short.valueOf(value)); + } + + /** Converts a primitive int to a CEL value. */ + default Val nativeToValue(int value) { + return nativeToValue(Integer.valueOf(value)); + } + + /** Converts a primitive char to a CEL value. */ + default Val nativeToValue(char value) { + return nativeToValue(Character.valueOf(value)); + } + + /** Converts a primitive long to a CEL value. */ + default Val nativeToValue(long value) { + return nativeToValue(Long.valueOf(value)); + } + + /** Converts a primitive float to a CEL value. */ + default Val nativeToValue(float value) { + return nativeToValue(Float.valueOf(value)); + } + + /** Converts a primitive double to a CEL value. */ + default Val nativeToValue(double value) { + return nativeToValue(Double.valueOf(value)); + } + + /** Converts a CEL value to the requested native Java representation. */ + default T valueToNative(Val value, Class targetType) { + return TypeAdapterSupport.valueToNative(this, value, targetType); + } + + /** Converts a CEL boolean value to a primitive boolean. */ + default boolean valueToBoolean(Val value) { + return TypeAdapterSupport.valueToBoolean(value); + } + + /** Converts a CEL integer value to a range-checked primitive int. */ + default int valueToInt(Val value) { + return TypeAdapterSupport.valueToInt(value); + } + + /** + * Converts a CEL integer value to its primitive long representation. + * + *

CEL signed integers use a Java {@code long}. Unsigned values preserve their raw unsigned + * bits in the returned {@code long}. + */ + default long valueToLong(Val value) { + return TypeAdapterSupport.valueToLong(value); + } + + /** Converts a CEL double value to a primitive double. */ + default double valueToDouble(Val value) { + return TypeAdapterSupport.valueToDouble(value); + } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java b/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java index a4ed052f..76b420cc 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java @@ -18,6 +18,8 @@ import static org.projectnessie.cel.common.types.BytesT.bytesOf; import static org.projectnessie.cel.common.types.DoubleT.doubleOf; import static org.projectnessie.cel.common.types.DurationT.durationOf; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.Err.rangeError; import static org.projectnessie.cel.common.types.IntT.intOf; import static org.projectnessie.cel.common.types.ListT.newDoubleArrayList; import static org.projectnessie.cel.common.types.ListT.newGenericArrayList; @@ -27,6 +29,7 @@ import static org.projectnessie.cel.common.types.ListT.newLongArrayList; import static org.projectnessie.cel.common.types.ListT.newStringArrayList; import static org.projectnessie.cel.common.types.ListT.newValArrayList; +import static org.projectnessie.cel.common.types.MapT.newCheckedMap; import static org.projectnessie.cel.common.types.MapT.newJSONStruct; import static org.projectnessie.cel.common.types.MapT.newMaybeWrappedMap; import static org.projectnessie.cel.common.types.StringT.stringOf; @@ -35,6 +38,7 @@ import static org.projectnessie.cel.common.types.Types.boolOf; import static org.projectnessie.cel.common.types.UintT.uintOf; +import com.google.api.expr.v1alpha1.Type; import com.google.protobuf.ByteString; import com.google.protobuf.Descriptors.EnumValueDescriptor; import com.google.protobuf.EnumValue; @@ -56,11 +60,15 @@ import java.util.Optional; import java.util.function.BiFunction; import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.types.BoolT; +import org.projectnessie.cel.common.types.DoubleT; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.IntT; import org.projectnessie.cel.common.types.NullT; +import org.projectnessie.cel.common.types.UintT; +import org.projectnessie.cel.common.types.UnknownT; -/** - * Helper class for {@link TypeAdapter} implementations to convert from a Java type to a CEL type. - */ +/** Helper class for {@link TypeAdapter} implementations to convert between Java and CEL types. */ public final class TypeAdapterSupport { private TypeAdapterSupport() {} @@ -125,8 +133,7 @@ public static Val maybeNativeToValue(TypeAdapter a, Object value) { if (value instanceof Collection) { return newGenericArrayList(a, ((Collection) value).toArray()); } - if (value instanceof Optional) { - Optional optional = (Optional) value; + if (value instanceof Optional optional) { return optional.map(a::nativeToValue).orElse(NullT.NullValue); } if (value instanceof Map) { @@ -152,4 +159,243 @@ public static Val maybeNativeToValue(TypeAdapter a, Object value) { return null; } + + static Val nativeAggregateToValue(TypeAdapter adapter, Object value, Type checkedType) { + if (checkedType == null) { + return newErr("exact aggregate materialization requires a checked CEL type"); + } + return switch (checkedType.getTypeKindCase()) { + case LIST_TYPE -> + checkedList(adapter, value, checkedType.getListType().getElemType(), checkedType); + case MAP_TYPE -> + checkedMap( + adapter, + value, + checkedType.getMapType().getKeyType(), + checkedType.getMapType().getValueType(), + checkedType); + default -> checkedScalar(adapter, value, checkedType); + }; + } + + private static Val checkedList( + TypeAdapter adapter, Object value, Type elementType, Type checkedType) { + if (value == null) { + return incompatible(null, checkedType); + } + if (value instanceof long[] values) { + if (elementType.getTypeKindCase() != Type.TypeKindCase.PRIMITIVE) { + return incompatible(value, checkedType); + } + return switch (elementType.getPrimitive()) { + case INT64 -> newLongArrayList(adapter, values); + case UINT64 -> newGenericArrayList(adapter, unsignedValues(values)); + default -> incompatible(value, checkedType); + }; + } + TypeAdapter elementAdapter = new CheckedTypeAdapter(adapter, elementType); + if (value instanceof int[] values + && elementType.getTypeKindCase() == Type.TypeKindCase.PRIMITIVE + && elementType.getPrimitive() == Type.PrimitiveType.INT64) { + return newIntArrayList(adapter, values); + } + if (value instanceof double[] values + && elementType.getTypeKindCase() == Type.TypeKindCase.PRIMITIVE + && elementType.getPrimitive() == Type.PrimitiveType.DOUBLE) { + return newDoubleArrayList(adapter, values); + } + if (value instanceof String[] values + && elementType.getTypeKindCase() == Type.TypeKindCase.PRIMITIVE + && elementType.getPrimitive() == Type.PrimitiveType.STRING) { + return newGenericArrayList(elementAdapter, values); + } + if (value instanceof Val[]) { + return incompatible(value, checkedType); + } + if (value instanceof Object[] values) { + return newGenericArrayList(elementAdapter, values); + } + if (value instanceof List values) { + return newGenericList(elementAdapter, values); + } + if (value instanceof Collection values) { + return newGenericArrayList(elementAdapter, values.toArray()); + } + return incompatible(value, checkedType); + } + + private static ULong[] unsignedValues(long[] values) { + ULong[] unsigned = new ULong[values.length]; + for (int i = 0; i < values.length; i++) { + unsigned[i] = ULong.valueOf(values[i]); + } + return unsigned; + } + + private static Val checkedMap( + TypeAdapter adapter, Object value, Type keyType, Type valueType, Type checkedType) { + if (value == null) { + return incompatible(null, checkedType); + } + if (!(value instanceof Map values)) { + return incompatible(value, checkedType); + } + return newCheckedMap( + new CheckedTypeAdapter(adapter, keyType), + new CheckedTypeAdapter(adapter, valueType), + values); + } + + private static Val checkedScalar(TypeAdapter adapter, Object value, Type checkedType) { + if (value instanceof Val) { + return incompatible(value, checkedType); + } + return switch (checkedType.getTypeKindCase()) { + case NULL -> value == null ? NullT.NullValue : incompatible(value, checkedType); + case DYN -> adapter.nativeToValue(value); + case WRAPPER -> + value == null + ? NullT.NullValue + : checkedPrimitive(value, checkedType.getWrapper(), checkedType); + case PRIMITIVE -> checkedPrimitive(value, checkedType.getPrimitive(), checkedType); + case LIST_TYPE, MAP_TYPE -> throw new IllegalStateException("aggregate handled separately"); + case WELL_KNOWN, MESSAGE_TYPE, TYPE, ABSTRACT_TYPE -> + value != null ? adapter.nativeToValue(value) : incompatible(null, checkedType); + case FUNCTION, TYPE_PARAM, ERROR, TYPEKIND_NOT_SET -> incompatible(value, checkedType); + }; + } + + private static Val checkedPrimitive( + Object value, Type.PrimitiveType primitive, Type checkedType) { + return switch (primitive) { + case BOOL -> value instanceof Boolean bool ? boolOf(bool) : incompatible(value, checkedType); + case INT64 -> + value instanceof Byte + || value instanceof Short + || value instanceof Integer + || value instanceof Long + ? intOf(((Number) value).longValue()) + : incompatible(value, checkedType); + case UINT64 -> + value instanceof ULong unsigned + ? uintOf(unsigned.longValue()) + : value instanceof Long bits ? uintOf(bits) : incompatible(value, checkedType); + case DOUBLE -> + value instanceof Float || value instanceof Double + ? doubleOf(((Number) value).doubleValue()) + : incompatible(value, checkedType); + case STRING -> + value instanceof String string ? stringOf(string) : incompatible(value, checkedType); + case BYTES -> + value instanceof byte[] bytes + ? bytesOf(bytes) + : value instanceof ByteString bytes + ? bytesOf(bytes) + : incompatible(value, checkedType); + default -> incompatible(value, checkedType); + }; + } + + private static Val incompatible(Object value, Type checkedType) { + return newErr( + "exact aggregate value of Java type '%s' is incompatible with checked CEL type '%s'", + value == null ? "null" : value.getClass().getName(), checkedType); + } + + private static final class CheckedTypeAdapter implements TypeAdapter { + private final TypeAdapter adapter; + private final Type checkedType; + + private CheckedTypeAdapter(TypeAdapter adapter, Type checkedType) { + this.adapter = adapter; + this.checkedType = checkedType; + } + + @Override + public Val nativeToValue(Object value) { + return nativeAggregateToValue(adapter, value, checkedType); + } + } + + public static Val nativeToValue(boolean value) { + return boolOf(value); + } + + public static Val nativeToValue(byte value) { + return intOf(value); + } + + public static Val nativeToValue(short value) { + return intOf(value); + } + + public static Val nativeToValue(int value) { + return intOf(value); + } + + public static Val nativeToValue(long value) { + return intOf(value); + } + + public static Val nativeToValue(float value) { + return doubleOf(value); + } + + public static Val nativeToValue(double value) { + return doubleOf(value); + } + + @SuppressWarnings("unchecked") + public static T valueToNative(TypeAdapter adapter, Val value, Class targetType) { + if (targetType == boolean.class) { + return (T) Boolean.valueOf(adapter.valueToBoolean(value)); + } + if (targetType == int.class) { + return (T) Integer.valueOf(adapter.valueToInt(value)); + } + if (targetType == long.class) { + return (T) Long.valueOf(adapter.valueToLong(value)); + } + if (targetType == double.class) { + return (T) Double.valueOf(adapter.valueToDouble(value)); + } + return legacyValueToNative(value, targetType); + } + + public static boolean valueToBoolean(Val value) { + if (value instanceof BoolT boolValue) { + return boolValue.booleanValue(); + } + return legacyValueToNative(value, boolean.class); + } + + public static int valueToInt(Val value) { + if (value instanceof IntT || value instanceof UintT) { + long longValue = value.intValue(); + if (longValue < Integer.MIN_VALUE || longValue > Integer.MAX_VALUE) { + Err.throwErrorAsIllegalStateException(rangeError(longValue, "Java int")); + } + return (int) longValue; + } + return legacyValueToNative(value, int.class); + } + + public static long valueToLong(Val value) { + if (value instanceof IntT || value instanceof UintT || value instanceof UnknownT) { + return value.intValue(); + } + return legacyValueToNative(value, long.class); + } + + public static double valueToDouble(Val value) { + if (value instanceof DoubleT doubleValue) { + return doubleValue.doubleValue(); + } + return legacyValueToNative(value, double.class); + } + + @SuppressWarnings("removal") + private static T legacyValueToNative(Val value, Class targetType) { + return value.convertToNative(targetType); + } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ref/Val.java b/core/src/main/java/org/projectnessie/cel/common/types/ref/Val.java index 113285ba..eb15e35f 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/ref/Val.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/ref/Val.java @@ -21,9 +21,14 @@ */ public interface Val { /** - * ConvertToNative converts the Value to a native Go struct according to the reflected type - * description, or error if the conversion is not feasible. + * Converts the value to a native Java value according to the requested type, or reports an error + * if the conversion is not feasible. + * + * @deprecated Use {@link TypeAdapter#valueToNative(Val, Class)} with the adapter associated with + * the evaluation environment. {@code DefaultTypeAdapter.Instance} can be used for + * context-free built-in values. */ + @Deprecated(forRemoval = true) T convertToNative(Class typeDesc); /** diff --git a/core/src/main/java/org/projectnessie/cel/common/types/traits/Indexer.java b/core/src/main/java/org/projectnessie/cel/common/types/traits/Indexer.java index 6132eb68..54db9129 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/traits/Indexer.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/traits/Indexer.java @@ -15,10 +15,20 @@ */ package org.projectnessie.cel.common.types.traits; +import static org.projectnessie.cel.common.types.IntT.intOf; + import org.projectnessie.cel.common.types.ref.Val; /** Indexer permits random access of elements by index 'a[b()]'. */ public interface Indexer { /** Get the value at the specified index or error. */ Val get(Val index); + + /** + * Get the value at a native integer index or return the same error as {@link #get(Val)}. + * Implementations may override this method to avoid constructing a CEL integer index. + */ + default Val nativeGetAt(int index) { + return get(intOf(index)); + } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/traits/Sizer.java b/core/src/main/java/org/projectnessie/cel/common/types/traits/Sizer.java index 86fb786c..3bc5fd3b 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/traits/Sizer.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/traits/Sizer.java @@ -21,4 +21,13 @@ public interface Sizer { /** Size returns the number of elements or length of the value. */ Val size(); + + /** + * Returns the size as a native integer for internal traversal and capacity calculations. + * + * @throws ArithmeticException if the size cannot be represented as an {@code int} + */ + default int nativeSize() { + return Math.toIntExact(size().intValue()); + } } diff --git a/core/src/main/java/org/projectnessie/cel/extension/EncodersLib.java b/core/src/main/java/org/projectnessie/cel/extension/EncodersLib.java new file mode 100644 index 00000000..1b90817b --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/extension/EncodersLib.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.extension; + +import static java.util.Collections.singletonList; +import static org.projectnessie.cel.common.types.BytesT.bytesOf; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.StringT.stringOf; + +import java.util.Base64; +import java.util.List; +import org.projectnessie.cel.EnvOption; +import org.projectnessie.cel.Library; +import org.projectnessie.cel.ProgramOption; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.BytesT; +import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.functions.Overload; + +/** EncodersLib provides CEL helper functions for common binary-to-text encodings. */ +public final class EncodersLib implements Library { + private static final String BASE64_ENCODE = "base64.encode"; + private static final String BASE64_DECODE = "base64.decode"; + private static final String BASE64_ENCODE_OVERLOAD = "base64_encode_bytes"; + private static final String BASE64_DECODE_OVERLOAD = "base64_decode_string"; + + private EncodersLib() {} + + public static EnvOption encoders() { + return Library.Lib(new EncodersLib()); + } + + @Override + public List getCompileOptions() { + return List.of( + EnvOption.declarations( + Decls.newFunction( + BASE64_ENCODE, + Decls.newOverload( + BASE64_ENCODE_OVERLOAD, singletonList(Decls.Bytes), Decls.String)), + Decls.newFunction( + BASE64_DECODE, + Decls.newOverload( + BASE64_DECODE_OVERLOAD, singletonList(Decls.String), Decls.Bytes)))); + } + + @Override + public List getProgramOptions() { + return List.of( + ProgramOption.functions( + Overload.unary(BASE64_ENCODE, EncodersLib::base64Encode), + Overload.unary(BASE64_ENCODE_OVERLOAD, EncodersLib::base64Encode), + Overload.unary(BASE64_DECODE, EncodersLib::base64Decode), + Overload.unary(BASE64_DECODE_OVERLOAD, EncodersLib::base64Decode))); + } + + private static Val base64Encode(Val value) { + if (!(value instanceof BytesT)) { + return noSuchOverload(value, BASE64_ENCODE, BASE64_ENCODE_OVERLOAD, new Val[] {value}); + } + byte[] bytes = DefaultTypeAdapter.Instance.valueToNative(value, byte[].class); + return stringOf(Base64.getEncoder().encodeToString(bytes)); + } + + private static Val base64Decode(Val value) { + if (!(value instanceof StringT)) { + return noSuchOverload(value, BASE64_DECODE, BASE64_DECODE_OVERLOAD, new Val[] {value}); + } + String text = DefaultTypeAdapter.Instance.valueToNative(value, String.class); + try { + return bytesOf(Base64.getDecoder().decode(text)); + } catch (IllegalArgumentException e) { + return newErr(e, "invalid base64 string"); + } + } +} diff --git a/core/src/main/java/org/projectnessie/cel/extension/Guards.java b/core/src/main/java/org/projectnessie/cel/extension/Guards.java index b1c66506..3102f68b 100644 --- a/core/src/main/java/org/projectnessie/cel/extension/Guards.java +++ b/core/src/main/java/org/projectnessie/cel/extension/Guards.java @@ -25,6 +25,8 @@ import org.projectnessie.cel.common.types.StringT; import org.projectnessie.cel.interpreter.functions.BinaryOp; import org.projectnessie.cel.interpreter.functions.FunctionOp; +import org.projectnessie.cel.interpreter.functions.QuaternaryOp; +import org.projectnessie.cel.interpreter.functions.TernaryOp; import org.projectnessie.cel.interpreter.functions.UnaryOp; /** function invocation guards for common call signatures within extension functions. */ @@ -57,6 +59,19 @@ public static FunctionOp callInStrIntIntOutStr( }; } + public static TernaryOp callInStrIntIntOutStrTernary( + TriFunction func) { + return (first, second, third) -> { + try { + return StringT.stringOf( + func.apply( + (String) first.value(), getIntValue((IntT) second), getIntValue((IntT) third))); + } catch (RuntimeException e) { + return Err.newErr(e, "%s", e.getMessage()); + } + }; + } + public static BinaryOp callInStrStrOutInt(BiFunction func) { return (lhs, rhs) -> { try { @@ -82,6 +97,18 @@ public static FunctionOp callInStrStrIntOutInt( }; } + public static TernaryOp callInStrStrIntOutIntTernary( + TriFunction func) { + return (first, second, third) -> { + try { + return IntT.intOf( + func.apply((String) first.value(), (String) second.value(), getIntValue((IntT) third))); + } catch (RuntimeException e) { + return Err.newErr(e, "%s", e.getMessage()); + } + }; + } + public static BinaryOp callInStrStrOutStrArr(BiFunction func) { return (lhs, rhs) -> { try { @@ -107,6 +134,18 @@ public static FunctionOp callInStrStrIntOutStrArr( }; } + public static TernaryOp callInStrStrIntOutStrArrTernary( + TriFunction func) { + return (first, second, third) -> { + try { + return ListT.newStringArrayList( + func.apply((String) first.value(), (String) second.value(), getIntValue((IntT) third))); + } catch (RuntimeException e) { + return Err.newErr(e, "%s", e.getMessage()); + } + }; + } + public static FunctionOp callInStrStrStrOutStr(TriFunction func) { return values -> { try { @@ -121,6 +160,18 @@ public static FunctionOp callInStrStrStrOutStr(TriFunction func) { + return (first, second, third) -> { + try { + return StringT.stringOf( + func.apply((String) first.value(), (String) second.value(), (String) third.value())); + } catch (RuntimeException e) { + return Err.newErr(e, "%s", e.getMessage()); + } + }; + } + public static FunctionOp callInStrStrStrIntOutStr( QuadFunction func) { return values -> { @@ -137,6 +188,22 @@ public static FunctionOp callInStrStrStrIntOutStr( }; } + public static QuaternaryOp callInStrStrStrIntOutStrQuaternary( + QuadFunction func) { + return (first, second, third, fourth) -> { + try { + return StringT.stringOf( + func.apply( + (String) first.value(), + (String) second.value(), + (String) third.value(), + getIntValue((IntT) fourth))); + } catch (RuntimeException e) { + return Err.newErr(e, "%s", e.getMessage()); + } + }; + } + public static UnaryOp callInStrOutStr(UnaryOperator func) { return val -> { try { diff --git a/core/src/main/java/org/projectnessie/cel/extension/MathLib.java b/core/src/main/java/org/projectnessie/cel/extension/MathLib.java new file mode 100644 index 00000000..adca603e --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/extension/MathLib.java @@ -0,0 +1,540 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.extension; + +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.Err.errIntOverflow; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; + +import com.google.api.expr.v1alpha1.Decl; +import com.google.api.expr.v1alpha1.Type; +import java.util.ArrayList; +import java.util.List; +import org.projectnessie.cel.EnvOption; +import org.projectnessie.cel.Library; +import org.projectnessie.cel.ProgramOption; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.DoubleT; +import org.projectnessie.cel.common.types.IntT; +import org.projectnessie.cel.common.types.UintT; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; +import org.projectnessie.cel.interpreter.functions.FunctionOp; +import org.projectnessie.cel.interpreter.functions.Overload; +import org.projectnessie.cel.interpreter.functions.QuaternaryOp; +import org.projectnessie.cel.interpreter.functions.QuinaryOp; +import org.projectnessie.cel.interpreter.functions.TernaryOp; + +/** MathLib provides CEL helper functions from the standard math extension library. */ +public final class MathLib implements Library { + private static final String GREATEST = "math.greatest"; + private static final String LEAST = "math.least"; + private static final String CEIL = "math.ceil"; + private static final String FLOOR = "math.floor"; + private static final String ROUND = "math.round"; + private static final String TRUNC = "math.trunc"; + private static final String ABS = "math.abs"; + private static final String SIGN = "math.sign"; + private static final String IS_NAN = "math.isNaN"; + private static final String IS_INF = "math.isInf"; + private static final String IS_FINITE = "math.isFinite"; + private static final String BIT_AND = "math.bitAnd"; + private static final String BIT_OR = "math.bitOr"; + private static final String BIT_XOR = "math.bitXor"; + private static final String BIT_NOT = "math.bitNot"; + private static final String BIT_SHIFT_LEFT = "math.bitShiftLeft"; + private static final String BIT_SHIFT_RIGHT = "math.bitShiftRight"; + + private MathLib() {} + + public static EnvOption math() { + return Library.Lib(new MathLib()); + } + + @Override + public List getCompileOptions() { + List declarations = new ArrayList<>(); + declarations.add(minMaxDeclaration(GREATEST)); + declarations.add(minMaxDeclaration(LEAST)); + declarations.add(unaryDeclaration(CEIL, Decls.Double)); + declarations.add(unaryDeclaration(FLOOR, Decls.Double)); + declarations.add(unaryDeclaration(ROUND, Decls.Double)); + declarations.add(unaryDeclaration(TRUNC, Decls.Double)); + declarations.add(unaryDeclaration(ABS, Decls.Dyn)); + declarations.add(unaryDeclaration(SIGN, Decls.Dyn)); + declarations.add(unaryDeclaration(IS_NAN, Decls.Bool)); + declarations.add(unaryDeclaration(IS_INF, Decls.Bool)); + declarations.add(unaryDeclaration(IS_FINITE, Decls.Bool)); + declarations.add(binaryDeclaration(BIT_AND, Decls.Dyn)); + declarations.add(binaryDeclaration(BIT_OR, Decls.Dyn)); + declarations.add(binaryDeclaration(BIT_XOR, Decls.Dyn)); + declarations.add(unaryDeclaration(BIT_NOT, Decls.Dyn)); + declarations.add(binaryDeclaration(BIT_SHIFT_LEFT, Decls.Dyn)); + declarations.add(binaryDeclaration(BIT_SHIFT_RIGHT, Decls.Dyn)); + return List.of(EnvOption.declarations(declarations)); + } + + @Override + public List getProgramOptions() { + List overloads = new ArrayList<>(); + overloads.add( + Overload.overload( + GREATEST, + null, + MathLib::greatest, + MathLib::greatest, + MathLib::greatest, + MathLib::greatest, + MathLib::greatest, + MathLib::greatest)); + overloads.add( + Overload.overload( + LEAST, + null, + MathLib::least, + MathLib::least, + MathLib::least, + MathLib::least, + MathLib::least, + MathLib::least)); + addArityOverloads( + overloads, + GREATEST, + MathLib::greatest, + MathLib::greatest, + MathLib::greatest, + MathLib::greatest); + addArityOverloads( + overloads, LEAST, MathLib::least, MathLib::least, MathLib::least, MathLib::least); + overloads.add(Overload.unary(CEIL, MathLib::ceil)); + overloads.add(Overload.unary(overloadId(CEIL, 1), MathLib::ceil)); + overloads.add(Overload.unary(FLOOR, MathLib::floor)); + overloads.add(Overload.unary(overloadId(FLOOR, 1), MathLib::floor)); + overloads.add(Overload.unary(ROUND, MathLib::round)); + overloads.add(Overload.unary(overloadId(ROUND, 1), MathLib::round)); + overloads.add(Overload.unary(TRUNC, MathLib::trunc)); + overloads.add(Overload.unary(overloadId(TRUNC, 1), MathLib::trunc)); + overloads.add(Overload.unary(ABS, MathLib::abs)); + overloads.add(Overload.unary(overloadId(ABS, 1), MathLib::abs)); + overloads.add(Overload.unary(SIGN, MathLib::sign)); + overloads.add(Overload.unary(overloadId(SIGN, 1), MathLib::sign)); + overloads.add(Overload.unary(IS_NAN, MathLib::isNaN)); + overloads.add(Overload.unary(overloadId(IS_NAN, 1), MathLib::isNaN)); + overloads.add(Overload.unary(IS_INF, MathLib::isInf)); + overloads.add(Overload.unary(overloadId(IS_INF, 1), MathLib::isInf)); + overloads.add(Overload.unary(IS_FINITE, MathLib::isFinite)); + overloads.add(Overload.unary(overloadId(IS_FINITE, 1), MathLib::isFinite)); + overloads.add(Overload.binary(BIT_AND, MathLib::bitAnd)); + overloads.add(Overload.binary(overloadId(BIT_AND, 2), MathLib::bitAnd)); + overloads.add(Overload.binary(BIT_OR, MathLib::bitOr)); + overloads.add(Overload.binary(overloadId(BIT_OR, 2), MathLib::bitOr)); + overloads.add(Overload.binary(BIT_XOR, MathLib::bitXor)); + overloads.add(Overload.binary(overloadId(BIT_XOR, 2), MathLib::bitXor)); + overloads.add(Overload.unary(BIT_NOT, MathLib::bitNot)); + overloads.add(Overload.unary(overloadId(BIT_NOT, 1), MathLib::bitNot)); + overloads.add(Overload.binary(BIT_SHIFT_LEFT, MathLib::bitShiftLeft)); + overloads.add(Overload.binary(overloadId(BIT_SHIFT_LEFT, 2), MathLib::bitShiftLeft)); + overloads.add(Overload.binary(BIT_SHIFT_RIGHT, MathLib::bitShiftRight)); + overloads.add(Overload.binary(overloadId(BIT_SHIFT_RIGHT, 2), MathLib::bitShiftRight)); + return List.of(ProgramOption.functions(overloads.toArray(Overload[]::new))); + } + + private static Decl minMaxDeclaration(String function) { + List overloads = new ArrayList<>(); + overloads.add(Decls.newOverload(overloadId(function, "int"), List.of(Decls.Int), Decls.Int)); + overloads.add(Decls.newOverload(overloadId(function, "uint"), List.of(Decls.Uint), Decls.Uint)); + overloads.add( + Decls.newOverload(overloadId(function, "double"), List.of(Decls.Double), Decls.Double)); + for (int arity = 2; arity <= 5; arity++) { + overloads.add(Decls.newOverload(overloadId(function, arity), dynArgs(arity), Decls.Dyn)); + } + overloads.add( + Decls.newOverload( + overloadId(function, "list"), List.of(Decls.newListType(Decls.Dyn)), Decls.Dyn)); + return Decls.newFunction(function, overloads); + } + + private static Decl unaryDeclaration(String function, Type result) { + return Decls.newFunction( + function, Decls.newOverload(overloadId(function, 1), List.of(Decls.Dyn), result)); + } + + private static Decl binaryDeclaration(String function, Type result) { + return Decls.newFunction( + function, Decls.newOverload(overloadId(function, 2), dynArgs(2), result)); + } + + private static List dynArgs(int count) { + List args = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + args.add(Decls.Dyn); + } + return args; + } + + private static void addArityOverloads( + List overloads, + String function, + FunctionOp op, + TernaryOp ternaryOp, + QuaternaryOp quaternaryOp, + QuinaryOp quinaryOp) { + overloads.add(Overload.unary(overloadId(function, "int"), op::invoke)); + overloads.add(Overload.unary(overloadId(function, "uint"), op::invoke)); + overloads.add(Overload.unary(overloadId(function, "double"), op::invoke)); + overloads.add( + Overload.binary(overloadId(function, 2), (left, right) -> op.invoke(left, right))); + overloads.add(Overload.ternary(overloadId(function, 3), ternaryOp)); + overloads.add(Overload.quaternary(overloadId(function, 4), quaternaryOp)); + overloads.add(Overload.quinary(overloadId(function, 5), quinaryOp)); + overloads.add(Overload.unary(overloadId(function, "list"), op::invoke)); + } + + private static String overloadId(String function, int arity) { + return function.replace('.', '_') + "_" + arity; + } + + private static String overloadId(String function, String suffix) { + return function.replace('.', '_') + "_" + suffix; + } + + private static Val greatest(Val... values) { + return minMax(values, true); + } + + private static Val greatest(Val first, Val second, Val third) { + return minMax(first, second, third, true); + } + + private static Val greatest(Val first, Val second, Val third, Val fourth) { + return minMax(first, second, third, fourth, true); + } + + private static Val greatest(Val first, Val second, Val third, Val fourth, Val fifth) { + return minMax(first, second, third, fourth, fifth, true); + } + + private static Val least(Val... values) { + return minMax(values, false); + } + + private static Val least(Val first, Val second, Val third) { + return minMax(first, second, third, false); + } + + private static Val least(Val first, Val second, Val third, Val fourth) { + return minMax(first, second, third, fourth, false); + } + + private static Val least(Val first, Val second, Val third, Val fourth, Val fifth) { + return minMax(first, second, third, fourth, fifth, false); + } + + private static Val minMax( + Val first, Val second, Val third, Val fourth, Val fifth, boolean greatest) { + Val result = minMax(first, second, third, fourth, greatest); + if (!isNumber(result)) { + return result; + } + if (!isNumber(fifth)) { + return noSuchOverload(); + } + int cmp = compareNumbers(fifth, result); + if ((greatest && cmp > 0) || (!greatest && cmp < 0)) { + result = fifth; + } + return result; + } + + private static Val minMax(Val first, Val second, Val third, Val fourth, boolean greatest) { + Val result = minMax(first, second, third, greatest); + if (!isNumber(result)) { + return result; + } + if (!isNumber(fourth)) { + return noSuchOverload(); + } + int cmp = compareNumbers(fourth, result); + if ((greatest && cmp > 0) || (!greatest && cmp < 0)) { + result = fourth; + } + return result; + } + + private static Val minMax(Val first, Val second, Val third, boolean greatest) { + if (!isNumber(first)) { + return noSuchOverload(); + } + Val result = first; + if (!isNumber(second)) { + return noSuchOverload(); + } + int cmp = compareNumbers(second, result); + if ((greatest && cmp > 0) || (!greatest && cmp < 0)) { + result = second; + } + if (!isNumber(third)) { + return noSuchOverload(); + } + cmp = compareNumbers(third, result); + if ((greatest && cmp > 0) || (!greatest && cmp < 0)) { + result = third; + } + return result; + } + + private static Val minMax(Val[] values, boolean greatest) { + List candidates = candidates(values); + if (candidates.isEmpty()) { + return newErr("empty argument list"); + } + + Val result = candidates.get(0); + if (!isNumber(result)) { + return noSuchOverload(); + } + for (int i = 1; i < candidates.size(); i++) { + Val candidate = candidates.get(i); + if (!isNumber(candidate)) { + return noSuchOverload(); + } + int cmp = compareNumbers(candidate, result); + if ((greatest && cmp > 0) || (!greatest && cmp < 0)) { + result = candidate; + } + } + return result; + } + + private static List candidates(Val[] values) { + if (values.length == 1 && values[0] instanceof Lister list) { + int size = list.nativeSize(); + List elements = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + elements.add(list.nativeGetAt(i)); + } + return elements; + } + return List.of(values); + } + + private static int compareNumbers(Val left, Val right) { + if (left instanceof DoubleT || right instanceof DoubleT) { + return Double.compare(asDouble(left), asDouble(right)); + } + if (left instanceof UintT && right instanceof UintT) { + return Long.compareUnsigned(left.intValue(), right.intValue()); + } + if (left instanceof UintT && right instanceof IntT) { + long rightValue = right.intValue(); + return rightValue < 0 ? 1 : Long.compareUnsigned(left.intValue(), rightValue); + } + if (left instanceof IntT && right instanceof UintT) { + long leftValue = left.intValue(); + return leftValue < 0 ? -1 : Long.compareUnsigned(leftValue, right.intValue()); + } + return Long.compare(left.intValue(), right.intValue()); + } + + private static double asDouble(Val value) { + if (value instanceof DoubleT) { + return value.doubleValue(); + } + if (value instanceof UintT) { + return Long.toUnsignedString(value.intValue()).equals("18446744073709551615") + ? 18446744073709551615.0 + : Double.parseDouble(Long.toUnsignedString(value.intValue())); + } + return value.intValue(); + } + + private static Val ceil(Val value) { + if (!(value instanceof DoubleT)) { + return noSuchOverload(); + } + return doubleOf(Math.ceil(value.doubleValue())); + } + + private static Val floor(Val value) { + if (!(value instanceof DoubleT)) { + return noSuchOverload(); + } + return doubleOf(Math.floor(value.doubleValue())); + } + + private static Val round(Val value) { + if (!(value instanceof DoubleT)) { + return noSuchOverload(); + } + double d = value.doubleValue(); + if (Double.isNaN(d) || Double.isInfinite(d)) { + return doubleOf(d); + } + return doubleOf(d < 0 ? Math.ceil(d - 0.5d) : Math.floor(d + 0.5d)); + } + + private static Val trunc(Val value) { + if (!(value instanceof DoubleT)) { + return noSuchOverload(); + } + double d = value.doubleValue(); + if (Double.isNaN(d) || Double.isInfinite(d)) { + return doubleOf(d); + } + return doubleOf(d < 0 ? Math.ceil(d) : Math.floor(d)); + } + + private static Val abs(Val value) { + if (value instanceof UintT) { + return value; + } + if (value instanceof IntT) { + long v = value.intValue(); + if (v == Long.MIN_VALUE) { + return errIntOverflow; + } + return intOf(Math.abs(v)); + } + if (value instanceof DoubleT) { + return doubleOf(Math.abs(value.doubleValue())); + } + return noSuchOverload(); + } + + private static Val sign(Val value) { + if (value instanceof UintT) { + return uintOf(value.intValue() == 0 ? 0 : 1); + } + if (value instanceof IntT) { + return intOf(Long.compare(value.intValue(), 0)); + } + if (value instanceof DoubleT) { + return doubleOf(Double.compare(value.doubleValue(), 0.0d)); + } + return noSuchOverload(); + } + + private static Val isNaN(Val value) { + if (!(value instanceof DoubleT)) { + return noSuchOverload(); + } + return Double.isNaN(value.doubleValue()) ? True : False; + } + + private static Val isInf(Val value) { + if (!(value instanceof DoubleT)) { + return noSuchOverload(); + } + return Double.isInfinite(value.doubleValue()) ? True : False; + } + + private static Val isFinite(Val value) { + if (!(value instanceof DoubleT)) { + return noSuchOverload(); + } + double d = value.doubleValue(); + return !Double.isNaN(d) && !Double.isInfinite(d) ? True : False; + } + + private static Val bitAnd(Val left, Val right) { + return bitwise(left, right, (a, b) -> a & b); + } + + private static Val bitOr(Val left, Val right) { + return bitwise(left, right, (a, b) -> a | b); + } + + private static Val bitXor(Val left, Val right) { + return bitwise(left, right, (a, b) -> a ^ b); + } + + private static Val bitwise(Val left, Val right, LongOperator op) { + if (left instanceof IntT && right instanceof IntT) { + return intOf(op.apply(left.intValue(), right.intValue())); + } + if (left instanceof UintT && right instanceof UintT) { + return uintOf(op.apply(left.intValue(), right.intValue())); + } + return noSuchOverload(); + } + + private static Val bitNot(Val value) { + if (value instanceof IntT) { + return intOf(~value.intValue()); + } + if (value instanceof UintT) { + return uintOf(~value.intValue()); + } + return noSuchOverload(); + } + + private static Val bitShiftLeft(Val left, Val right) { + if (!(right instanceof IntT)) { + return noSuchOverload(); + } + long shift = right.intValue(); + if (shift < 0) { + return newErr("negative offset"); + } + if (shift >= Long.SIZE) { + return left instanceof UintT ? uintOf(0) : left instanceof IntT ? intOf(0) : noSuchOverload(); + } + if (left instanceof IntT) { + return intOf(left.intValue() << shift); + } + if (left instanceof UintT) { + return uintOf(left.intValue() << shift); + } + return noSuchOverload(); + } + + private static Val bitShiftRight(Val left, Val right) { + if (!(right instanceof IntT)) { + return noSuchOverload(); + } + long shift = right.intValue(); + if (shift < 0) { + return newErr("negative offset"); + } + if (shift >= Long.SIZE) { + return left instanceof UintT ? uintOf(0) : left instanceof IntT ? intOf(0) : noSuchOverload(); + } + if (left instanceof IntT) { + return intOf(left.intValue() >>> shift); + } + if (left instanceof UintT) { + return uintOf(left.intValue() >>> shift); + } + return noSuchOverload(); + } + + private static boolean isNumber(Val value) { + return value instanceof IntT || value instanceof UintT || value instanceof DoubleT; + } + + private static Val noSuchOverload() { + return newErr("no such overload"); + } + + @FunctionalInterface + private interface LongOperator { + long apply(long left, long right); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/extension/NetworkLib.java b/core/src/main/java/org/projectnessie/cel/extension/NetworkLib.java new file mode 100644 index 00000000..693e13ec --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/extension/NetworkLib.java @@ -0,0 +1,539 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.extension; + +import static java.util.Collections.singletonList; +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.Err.newTypeConversionError; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.StringT.StringType; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.TypeT.newObjectTypeValue; +import static org.projectnessie.cel.common.types.Types.boolOf; + +import com.google.api.expr.v1alpha1.Type; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.projectnessie.cel.EnvOption; +import org.projectnessie.cel.Library; +import org.projectnessie.cel.ProgramOption; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.ref.BaseVal; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Receiver; +import org.projectnessie.cel.common.types.traits.Trait; +import org.projectnessie.cel.interpreter.functions.Overload; + +/** NetworkLib provides CEL helper functions from the standard network extension library. */ +public final class NetworkLib implements Library { + private static final String IP = "ip"; + private static final String CIDR = "cidr"; + private static final String IS_IP = "isIP"; + private static final String IP_IS_CANONICAL = "ip.isCanonical"; + private static final String NET_IP = "net.IP"; + private static final String NET_CIDR = "net.CIDR"; + + private static final Type IP_TYPE = Decls.newObjectType(NET_IP); + private static final Type CIDR_TYPE = Decls.newObjectType(NET_CIDR); + + private static final org.projectnessie.cel.common.types.ref.Type IP_TYPE_VALUE = + newObjectTypeValue(NET_IP, Trait.ReceiverType); + private static final org.projectnessie.cel.common.types.ref.Type CIDR_TYPE_VALUE = + newObjectTypeValue(NET_CIDR, Trait.ReceiverType); + + private NetworkLib() {} + + public static EnvOption network() { + return Library.Lib(new NetworkLib()); + } + + @Override + public List getCompileOptions() { + return List.of( + EnvOption.types(IP_TYPE_VALUE, CIDR_TYPE_VALUE), + EnvOption.declarations( + Decls.newVar(NET_IP, Decls.newTypeType(IP_TYPE)), + Decls.newVar(NET_CIDR, Decls.newTypeType(CIDR_TYPE)), + Decls.newFunction( + IP, Decls.newOverload("ip_string", singletonList(Decls.String), IP_TYPE)), + Decls.newFunction( + CIDR, Decls.newOverload("cidr_string", singletonList(Decls.String), CIDR_TYPE)), + Decls.newFunction( + IS_IP, + Decls.newOverload("is_ip_string", singletonList(Decls.String), Decls.Bool), + Decls.newOverload("is_ip_cidr", singletonList(CIDR_TYPE), Decls.Bool)), + Decls.newFunction( + IP_IS_CANONICAL, + Decls.newOverload( + "ip_is_canonical_string", singletonList(Decls.String), Decls.Bool)), + Decls.newFunction( + "string", + Decls.newOverload("string_ip", singletonList(IP_TYPE), Decls.String), + Decls.newOverload("string_cidr", singletonList(CIDR_TYPE), Decls.String)), + Decls.newFunction( + "family", + Decls.newInstanceOverload("ip_family", singletonList(IP_TYPE), Decls.Int)), + Decls.newFunction( + "isUnspecified", + Decls.newInstanceOverload("ip_is_unspecified", singletonList(IP_TYPE), Decls.Bool)), + Decls.newFunction( + "isLoopback", + Decls.newInstanceOverload("ip_is_loopback", singletonList(IP_TYPE), Decls.Bool)), + Decls.newFunction( + "isGlobalUnicast", + Decls.newInstanceOverload( + "ip_is_global_unicast", singletonList(IP_TYPE), Decls.Bool)), + Decls.newFunction( + "isLinkLocalMulticast", + Decls.newInstanceOverload( + "ip_is_link_local_multicast", singletonList(IP_TYPE), Decls.Bool)), + Decls.newFunction( + "isLinkLocalUnicast", + Decls.newInstanceOverload( + "ip_is_link_local_unicast", singletonList(IP_TYPE), Decls.Bool)), + Decls.newFunction( + "containsIP", + Decls.newInstanceOverload( + "cidr_contains_ip", List.of(CIDR_TYPE, IP_TYPE), Decls.Bool), + Decls.newInstanceOverload( + "cidr_contains_ip_string", List.of(CIDR_TYPE, Decls.String), Decls.Bool)), + Decls.newFunction( + "containsCIDR", + Decls.newInstanceOverload( + "cidr_contains_cidr", List.of(CIDR_TYPE, CIDR_TYPE), Decls.Bool), + Decls.newInstanceOverload( + "cidr_contains_cidr_string", List.of(CIDR_TYPE, Decls.String), Decls.Bool)), + Decls.newFunction( + "ip", Decls.newInstanceOverload("cidr_ip", singletonList(CIDR_TYPE), IP_TYPE)), + Decls.newFunction( + "masked", + Decls.newInstanceOverload("cidr_masked", singletonList(CIDR_TYPE), CIDR_TYPE)), + Decls.newFunction( + "prefixLength", + Decls.newInstanceOverload( + "cidr_prefix_length", singletonList(CIDR_TYPE), Decls.Int)))); + } + + @Override + public List getProgramOptions() { + return List.of( + ProgramOption.functions( + Overload.unary(IP, NetworkLib::ip), + Overload.unary("ip_string", NetworkLib::ip), + Overload.unary(CIDR, NetworkLib::cidr), + Overload.unary("cidr_string", NetworkLib::cidr), + Overload.unary(IS_IP, NetworkLib::isIp), + Overload.unary("is_ip_string", NetworkLib::isIp), + Overload.unary("is_ip_cidr", value -> newErr("no such overload")), + Overload.unary(IP_IS_CANONICAL, NetworkLib::ipIsCanonical), + Overload.unary("ip_is_canonical_string", NetworkLib::ipIsCanonical), + Overload.unary("string_ip", value -> value.convertToType(StringType)), + Overload.unary("string_cidr", value -> value.convertToType(StringType)), + Overload.unary("cidr_ip", value -> ((CidrT) value).receive("ip", "cidr_ip"))), + ProgramOption.globals(Map.of(NET_IP, IP_TYPE_VALUE, NET_CIDR, CIDR_TYPE_VALUE))); + } + + private static Val ip(Val value) { + if (value instanceof CidrT cidr) { + return cidr.ip; + } + if (!isString(value)) { + return noSuchOverload(null, IP, value); + } + return parseIp(value.value().toString()); + } + + private static Val cidr(Val value) { + if (!isString(value)) { + return noSuchOverload(null, CIDR, value); + } + return parseCidr(value.value().toString()); + } + + private static Val isIp(Val value) { + if (!isString(value)) { + return noSuchOverload(null, IS_IP, value); + } + return parseIp(value.value().toString()) instanceof IpT ? True : False; + } + + private static Val ipIsCanonical(Val value) { + if (!isString(value)) { + return noSuchOverload(null, IP_IS_CANONICAL, value); + } + Val parsed = parseIp(value.value().toString()); + if (!(parsed instanceof IpT ip)) { + return parsed; + } + return boolOf(value.value().toString().equals(ip.canonical)); + } + + private static boolean isString(Val value) { + return value.type().typeEnum() == org.projectnessie.cel.common.types.ref.TypeEnum.String; + } + + private static Val parseIp(String text) { + if (text.contains("%")) { + return newErr("IP Address with zone value is not allowed"); + } + if (text.indexOf(':') >= 0 && text.indexOf('.') >= 0) { + return newErr("IPv4-mapped IPv6 address is not allowed"); + } + try { + if (text.indexOf(':') >= 0) { + InetAddress address = InetAddress.getByName(text); + byte[] bytes = address.getAddress(); + if (bytes.length == 4) { + return new IpT(bytes, 4); + } + if (bytes.length == 16) { + return new IpT(bytes, 6); + } + } else { + return new IpT(parseIpv4(text), 4); + } + } catch (IllegalArgumentException | UnknownHostException e) { + // fall through + } + return newErr("IP Address '%s' parse error during conversion from string", text); + } + + private static byte[] parseIpv4(String text) { + String[] parts = text.split("\\.", -1); + if (parts.length != 4) { + throw new IllegalArgumentException("invalid IPv4 address"); + } + byte[] bytes = new byte[4]; + for (int i = 0; i < 4; i++) { + String part = parts[i]; + if (part.isEmpty() || (part.length() > 1 && part.charAt(0) == '0')) { + throw new IllegalArgumentException("invalid IPv4 address"); + } + int value = Integer.parseInt(part); + if (value < 0 || value > 255) { + throw new IllegalArgumentException("invalid IPv4 address"); + } + bytes[i] = (byte) value; + } + return bytes; + } + + private static Val parseCidr(String text) { + int slash = text.indexOf('/'); + if (slash < 0 || slash != text.lastIndexOf('/') || slash == text.length() - 1) { + return newErr("network address parse error during conversion from string"); + } + String ipText = text.substring(0, slash); + String prefixText = text.substring(slash + 1); + if (ipText.contains("%")) { + return newErr("CIDR with zone value is not allowed"); + } + Val parsedIp = parseIp(ipText); + if (!(parsedIp instanceof IpT ip)) { + return parsedIp; + } + try { + int prefix = Integer.parseInt(prefixText); + int bits = ip.family == 4 ? 32 : 128; + if (prefix < 0 || prefix > bits) { + return newErr("network address parse error during conversion from string"); + } + return new CidrT(ip, prefix); + } catch (NumberFormatException e) { + return newErr("network address parse error during conversion from string"); + } + } + + private static BigInteger unsigned(byte[] bytes) { + return new BigInteger(1, bytes); + } + + private static byte[] bytes(BigInteger value, int length) { + byte[] source = value.toByteArray(); + byte[] target = new byte[length]; + int copy = Math.min(source.length, length); + System.arraycopy(source, source.length - copy, target, length - copy, copy); + return target; + } + + private static byte[] mask(byte[] address, int prefix) { + int bits = address.length * Byte.SIZE; + if (prefix == bits) { + return address.clone(); + } + BigInteger value = unsigned(address); + BigInteger mask = BigInteger.ONE.shiftLeft(bits).subtract(BigInteger.ONE); + mask = mask.xor(BigInteger.ONE.shiftLeft(bits - prefix).subtract(BigInteger.ONE)); + return bytes(value.and(mask), address.length); + } + + private static String canonicalIpv4(byte[] bytes) { + return (bytes[0] & 0xff) + + "." + + (bytes[1] & 0xff) + + "." + + (bytes[2] & 0xff) + + "." + + (bytes[3] & 0xff); + } + + private static String canonicalIpv6(byte[] bytes) { + int[] words = new int[8]; + for (int i = 0; i < words.length; i++) { + words[i] = ((bytes[i * 2] & 0xff) << 8) | (bytes[i * 2 + 1] & 0xff); + } + + int bestStart = -1; + int bestLength = 0; + for (int i = 0; i < words.length; ) { + if (words[i] != 0) { + i++; + continue; + } + int start = i; + while (i < words.length && words[i] == 0) { + i++; + } + int length = i - start; + if (length > bestLength && length > 1) { + bestStart = start; + bestLength = length; + } + } + + StringBuilder result = new StringBuilder(); + for (int i = 0; i < words.length; i++) { + if (i == bestStart) { + if (!result.isEmpty() && result.charAt(result.length() - 1) != ':') { + result.append(':'); + } + result.append(':'); + i += bestLength - 1; + continue; + } + if (!result.isEmpty() && result.charAt(result.length() - 1) != ':') { + result.append(':'); + } + result.append(Integer.toHexString(words[i])); + } + return result.toString(); + } + + private static final class IpT extends BaseVal implements Receiver { + private final byte[] bytes; + private final int family; + private final String canonical; + + private IpT(byte[] bytes, int family) { + this.bytes = bytes.clone(); + this.family = family; + this.canonical = family == 4 ? canonicalIpv4(bytes) : canonicalIpv6(bytes); + } + + @Override + @SuppressWarnings({"removal", "unchecked"}) + public T convertToNative(Class typeDesc) { + if (typeDesc == String.class || typeDesc == Object.class) { + return (T) canonical; + } + if (typeDesc == byte[].class) { + return (T) bytes.clone(); + } + throw new IllegalArgumentException( + String.format("Unsupported conversion of '%s' to '%s'", NET_IP, typeDesc.getName())); + } + + @Override + public Val convertToType(org.projectnessie.cel.common.types.ref.Type typeVal) { + if (typeVal == StringType) { + return stringOf(canonical); + } + if (typeVal.typeName().equals(org.projectnessie.cel.common.types.TypeT.TypeType.typeName())) { + return IP_TYPE_VALUE; + } + if (typeVal.typeName().equals(NET_IP)) { + return this; + } + return newTypeConversionError(NET_IP, typeVal); + } + + @Override + public Val equal(Val other) { + if (!(other instanceof IpT otherIp)) { + return False; + } + return boolOf(Arrays.equals(bytes, otherIp.bytes)); + } + + @Override + public Val receive(String function, String overload, Val... args) { + if (args.length != 0) { + return noSuchOverload(this, function, overload, args); + } + return switch (function) { + case "family" -> intOf(family); + case "isUnspecified" -> boolOf(unsigned(bytes).signum() == 0); + case "isLoopback" -> + boolOf(family == 4 ? (bytes[0] & 0xff) == 127 : unsigned(bytes).equals(BigInteger.ONE)); + case "isGlobalUnicast" -> + boolOf(!isMulticast() && unsigned(bytes).signum() != 0 && !isBroadcast()); + case "isLinkLocalMulticast" -> + boolOf( + family == 4 + ? canonical.startsWith("224.0.0.") + : (bytes[0] & 0xff) == 0xff && (bytes[1] & 0xff) == 0x02); + case "isLinkLocalUnicast" -> + boolOf( + family == 4 + ? (bytes[0] & 0xff) == 169 && (bytes[1] & 0xff) == 254 + : (bytes[0] & 0xff) == 0xfe && ((bytes[1] & 0xc0) == 0x80)); + default -> noSuchOverload(this, function, overload, args); + }; + } + + private boolean isMulticast() { + return family == 4 ? (bytes[0] & 0xf0) == 0xe0 : (bytes[0] & 0xff) == 0xff; + } + + private boolean isBroadcast() { + return family == 4 + && unsigned(bytes).equals(BigInteger.ONE.shiftLeft(32).subtract(BigInteger.ONE)); + } + + @Override + public org.projectnessie.cel.common.types.ref.Type type() { + return IP_TYPE_VALUE; + } + + @Override + public Object value() { + return canonical; + } + } + + private static final class CidrT extends BaseVal implements Receiver { + private final IpT ip; + private final int prefix; + private final String canonical; + + private CidrT(IpT ip, int prefix) { + this.ip = ip; + this.prefix = prefix; + this.canonical = ip.canonical + "/" + prefix; + } + + @Override + @SuppressWarnings({"removal", "unchecked"}) + public T convertToNative(Class typeDesc) { + if (typeDesc == String.class || typeDesc == Object.class) { + return (T) canonical; + } + throw new IllegalArgumentException( + String.format("Unsupported conversion of '%s' to '%s'", NET_CIDR, typeDesc.getName())); + } + + @Override + public Val convertToType(org.projectnessie.cel.common.types.ref.Type typeVal) { + if (typeVal == StringType) { + return stringOf(canonical); + } + if (typeVal.typeName().equals(org.projectnessie.cel.common.types.TypeT.TypeType.typeName())) { + return CIDR_TYPE_VALUE; + } + if (typeVal.typeName().equals(NET_CIDR)) { + return this; + } + return newTypeConversionError(NET_CIDR, typeVal); + } + + @Override + public Val equal(Val other) { + if (!(other instanceof CidrT otherCidr)) { + return False; + } + return boolOf(prefix == otherCidr.prefix && ip.equal(otherCidr.ip) == True); + } + + @Override + public Val receive(String function, String overload, Val... args) { + return switch (function) { + case "containsIP" -> containsIp(args); + case "containsCIDR" -> containsCidr(args); + case "ip" -> args.length == 0 ? ip : noSuchOverload(this, function, overload, args); + case "masked" -> + args.length == 0 ? masked() : noSuchOverload(this, function, overload, args); + case "prefixLength" -> + args.length == 0 ? intOf(prefix) : noSuchOverload(this, function, overload, args); + default -> noSuchOverload(this, function, overload, args); + }; + } + + private Val containsIp(Val[] args) { + if (args.length != 1) { + return noSuchOverload(this, "containsIP", "", args); + } + Val candidate = + args[0] instanceof IpT + ? args[0] + : isString(args[0]) ? parseIp(args[0].value().toString()) : null; + if (!(candidate instanceof IpT candidateIp)) { + return candidate != null ? candidate : noSuchOverload(this, "containsIP", "", args); + } + if (candidateIp.family != ip.family) { + return False; + } + return boolOf(Arrays.equals(mask(candidateIp.bytes, prefix), mask(ip.bytes, prefix))); + } + + private Val containsCidr(Val[] args) { + if (args.length != 1) { + return noSuchOverload(this, "containsCIDR", "", args); + } + Val candidate = + args[0] instanceof CidrT + ? args[0] + : isString(args[0]) ? parseCidr(args[0].value().toString()) : null; + if (!(candidate instanceof CidrT candidateCidr)) { + return candidate != null ? candidate : noSuchOverload(this, "containsCIDR", "", args); + } + if (candidateCidr.ip.family != ip.family || candidateCidr.prefix < prefix) { + return False; + } + return boolOf(Arrays.equals(mask(candidateCidr.ip.bytes, prefix), mask(ip.bytes, prefix))); + } + + private CidrT masked() { + return new CidrT(new IpT(mask(ip.bytes, prefix), ip.family), prefix); + } + + @Override + public org.projectnessie.cel.common.types.ref.Type type() { + return CIDR_TYPE_VALUE; + } + + @Override + public Object value() { + return canonical; + } + } +} diff --git a/core/src/main/java/org/projectnessie/cel/extension/OptionalLib.java b/core/src/main/java/org/projectnessie/cel/extension/OptionalLib.java new file mode 100644 index 00000000..f56a8e59 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/extension/OptionalLib.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.extension; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.projectnessie.cel.common.types.OptionalT.OptionalType; + +import com.google.api.expr.v1alpha1.Expr; +import com.google.api.expr.v1alpha1.Expr.ExprKindCase; +import java.util.List; +import org.projectnessie.cel.EnvOption; +import org.projectnessie.cel.Library; +import org.projectnessie.cel.ProgramOption; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.ErrorWithLocation; +import org.projectnessie.cel.common.Location; +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.OptionalT; +import org.projectnessie.cel.interpreter.functions.Overload; +import org.projectnessie.cel.parser.ExprHelper; +import org.projectnessie.cel.parser.Macro; + +/** + * OptionalLib provides CEL optional helper functions. + * + *

This library provides runtime optional values, ordinary optional constructors/receiver + * methods, optional access operators, and lazy optMap/optFlatMap macro expansion. + */ +public final class OptionalLib implements Library { + private static final String OPTIONAL_TYPE = "optional_type"; + private static final String OPTIONAL_NONE = "optional.none"; + private static final String OPTIONAL_OF = "optional.of"; + private static final String OPTIONAL_OF_NON_ZERO_VALUE = "optional.ofNonZeroValue"; + private static final String OPTIONAL_HAS_VALUE = "hasValue"; + private static final String OPTIONAL_VALUE = "value"; + private static final String OPTIONAL_OR = "or"; + private static final String OPTIONAL_OR_VALUE = "orValue"; + private static final String OPTIONAL_OPT_MAP = "optMap"; + private static final String OPTIONAL_OPT_FLAT_MAP = "optFlatMap"; + private static final String OPTIONAL_NONE_OVERLOAD = "optional_none"; + private static final String OPTIONAL_OF_OVERLOAD = "optional_of"; + private static final String OPTIONAL_OF_NON_ZERO_VALUE_OVERLOAD = "optional_of_non_zero_value"; + private static final String OPTIONAL_SELECT_OVERLOAD = "optional_select"; + private static final String OPTIONAL_INDEX_OVERLOAD = "optional_index"; + private static final String OPTIONAL_INDEX_OPTIONAL_OVERLOAD = "optional_index_optional"; + private static final String OPTIONAL_HAS_VALUE_OVERLOAD = "optional_has_value"; + private static final String OPTIONAL_VALUE_OVERLOAD = "optional_value"; + private static final String OPTIONAL_OR_OVERLOAD = "optional_or"; + private static final String OPTIONAL_OR_VALUE_OVERLOAD = "optional_or_value"; + private static final String TYPE_PARAM_A = "A"; + private static final String OPTIONAL_MACRO_TARGET = "@optional_target"; + private static final String OPTIONAL_MACRO_RESULT = "@optional_result"; + + private OptionalLib() {} + + public static EnvOption optionals() { + return Library.Lib(new OptionalLib()); + } + + @Override + public List getCompileOptions() { + var typeParamA = Decls.newTypeParamType(TYPE_PARAM_A); + var optionalA = Decls.newAbstractType(OPTIONAL_TYPE, singletonList(typeParamA)); + var typeParams = singletonList(TYPE_PARAM_A); + + return List.of( + EnvOption.types(singletonList(OptionalType)), + EnvOption.macros( + Macro.newReceiverMacro(OPTIONAL_OPT_MAP, 2, OptionalLib::makeOptMap), + Macro.newReceiverMacro(OPTIONAL_OPT_FLAT_MAP, 2, OptionalLib::makeOptFlatMap)), + EnvOption.declarations( + Decls.newVar(OPTIONAL_TYPE, Decls.newTypeType(optionalA)), + Decls.newFunction( + OPTIONAL_NONE, + Decls.newParameterizedOverload( + OPTIONAL_NONE_OVERLOAD, emptyList(), optionalA, typeParams)), + Decls.newFunction( + OPTIONAL_OF, + Decls.newParameterizedOverload( + OPTIONAL_OF_OVERLOAD, singletonList(typeParamA), optionalA, typeParams)), + Decls.newFunction( + OPTIONAL_OF_NON_ZERO_VALUE, + Decls.newParameterizedOverload( + OPTIONAL_OF_NON_ZERO_VALUE_OVERLOAD, + singletonList(typeParamA), + optionalA, + typeParams)), + Decls.newFunction( + Operator.OptionalSelect.id, + Decls.newOverload( + OPTIONAL_SELECT_OVERLOAD, + List.of(Decls.Dyn, Decls.String), + Decls.newAbstractType(OPTIONAL_TYPE, singletonList(Decls.Dyn)))), + Decls.newFunction( + Operator.OptionalIndex.id, + Decls.newOverload( + OPTIONAL_INDEX_OVERLOAD, + List.of(Decls.Dyn, Decls.Dyn), + Decls.newAbstractType(OPTIONAL_TYPE, singletonList(Decls.Dyn)))), + Decls.newFunction( + Operator.Index.id, + Decls.newParameterizedOverload( + OPTIONAL_INDEX_OPTIONAL_OVERLOAD, + List.of(optionalA, Decls.Dyn), + Decls.newAbstractType(OPTIONAL_TYPE, singletonList(Decls.Dyn)), + typeParams)), + Decls.newFunction( + OPTIONAL_HAS_VALUE, + Decls.newParameterizedInstanceOverload( + OPTIONAL_HAS_VALUE_OVERLOAD, singletonList(optionalA), Decls.Bool, typeParams)), + Decls.newFunction( + OPTIONAL_VALUE, + Decls.newParameterizedInstanceOverload( + OPTIONAL_VALUE_OVERLOAD, singletonList(optionalA), typeParamA, typeParams)), + Decls.newFunction( + OPTIONAL_OR, + Decls.newParameterizedInstanceOverload( + OPTIONAL_OR_OVERLOAD, List.of(optionalA, optionalA), optionalA, typeParams)), + Decls.newFunction( + OPTIONAL_OR_VALUE, + Decls.newParameterizedInstanceOverload( + OPTIONAL_OR_VALUE_OVERLOAD, + List.of(optionalA, typeParamA), + typeParamA, + typeParams)))); + } + + @Override + public List getProgramOptions() { + return List.of( + ProgramOption.functions( + Overload.function(OPTIONAL_NONE, args -> OptionalT.none()), + Overload.function(OPTIONAL_NONE_OVERLOAD, args -> OptionalT.none()), + Overload.unary(OPTIONAL_OF, OptionalT::of), + Overload.unary(OPTIONAL_OF_OVERLOAD, OptionalT::of), + Overload.unary(OPTIONAL_OF_NON_ZERO_VALUE, OptionalT::ofNonZeroValue), + Overload.unary(OPTIONAL_OF_NON_ZERO_VALUE_OVERLOAD, OptionalT::ofNonZeroValue), + Overload.binary(Operator.OptionalSelect.id, OptionalT::optionalSelect), + Overload.binary(OPTIONAL_SELECT_OVERLOAD, OptionalT::optionalSelect), + Overload.binary(Operator.OptionalIndex.id, OptionalT::optionalIndex), + Overload.binary(OPTIONAL_INDEX_OVERLOAD, OptionalT::optionalIndex))); + } + + private static Expr makeOptMap(ExprHelper eh, Expr target, List args) { + return makeOptionalMap(eh, target, args, true); + } + + private static Expr makeOptFlatMap(ExprHelper eh, Expr target, List args) { + return makeOptionalMap(eh, target, args, false); + } + + private static Expr makeOptionalMap( + ExprHelper eh, Expr target, List args, boolean wrapResult) { + String variable = extractIdent(args.get(0)); + if (variable == null) { + Location location = eh.offsetLocation(args.get(0).getId()); + throw new ErrorWithLocation(location, "argument must be a simple name"); + } + + Expr boundTarget = eh.ident(OPTIONAL_MACRO_TARGET); + Expr value = eh.receiverCall(OPTIONAL_VALUE, boundTarget, emptyList()); + Expr iterRange = + eh.globalCall( + Operator.Conditional.id, + eh.receiverCall(OPTIONAL_HAS_VALUE, boundTarget, emptyList()), + eh.newList(value), + eh.newList()); + Expr init = eh.globalCall(OPTIONAL_NONE); + Expr step = wrapResult ? eh.globalCall(OPTIONAL_OF, args.get(1)) : args.get(1); + Expr accuIdent = eh.ident(Macro.AccumulatorName); + Expr result = + eh.fold( + variable, + iterRange, + Macro.AccumulatorName, + init, + eh.literalBool(true), + step, + accuIdent); + + Expr outerAccu = eh.ident(OPTIONAL_MACRO_RESULT); + Expr dynNull = eh.globalCall("dyn", eh.literalNull()); + return eh.fold( + OPTIONAL_MACRO_TARGET, + eh.newList(target), + OPTIONAL_MACRO_RESULT, + dynNull, + eh.literalBool(true), + result, + outerAccu); + } + + private static String extractIdent(Expr expression) { + if (expression.getExprKindCase() == ExprKindCase.IDENT_EXPR) { + return expression.getIdentExpr().getName(); + } + return null; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/extension/ProtoLib.java b/core/src/main/java/org/projectnessie/cel/extension/ProtoLib.java new file mode 100644 index 00000000..1fa2ebc1 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/extension/ProtoLib.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.extension; + +import static java.util.Arrays.asList; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; + +import java.util.List; +import org.projectnessie.cel.EnvOption; +import org.projectnessie.cel.Library; +import org.projectnessie.cel.ProgramOption; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.FieldTester; +import org.projectnessie.cel.common.types.traits.Indexer; +import org.projectnessie.cel.interpreter.functions.Overload; + +/** + * ProtoLib provides CEL protobuf extension helper functions. + * + *

The extension-name argument is a fully-qualified protobuf extension field name. Expressions + * that use extension identifiers instead of string literals need those identifiers registered with + * the type provider, for example through {@link EnvOption#types(Object...)} with protobuf + * descriptors that contain the extensions. + */ +public final class ProtoLib implements Library { + private static final String HAS_EXT = "proto.hasExt"; + private static final String GET_EXT = "proto.getExt"; + private static final String HAS_EXT_OVERLOAD = "proto_has_ext"; + private static final String GET_EXT_OVERLOAD = "proto_get_ext"; + + private ProtoLib() {} + + public static EnvOption proto() { + return Library.Lib(new ProtoLib()); + } + + @Override + public List getCompileOptions() { + return List.of( + EnvOption.declarations( + Decls.newFunction( + HAS_EXT, + Decls.newOverload(HAS_EXT_OVERLOAD, asList(Decls.Dyn, Decls.String), Decls.Bool)), + Decls.newFunction( + GET_EXT, + Decls.newOverload(GET_EXT_OVERLOAD, asList(Decls.Dyn, Decls.String), Decls.Dyn)))); + } + + @Override + public List getProgramOptions() { + return List.of( + ProgramOption.functions( + Overload.binary(HAS_EXT, ProtoLib::hasExt), + Overload.binary(HAS_EXT_OVERLOAD, ProtoLib::hasExt), + Overload.binary(GET_EXT, ProtoLib::getExt), + Overload.binary(GET_EXT_OVERLOAD, ProtoLib::getExt))); + } + + private static Val hasExt(Val object, Val extensionName) { + if (!(object instanceof FieldTester) || !(extensionName instanceof StringT)) { + return noSuchOverload(object, HAS_EXT, HAS_EXT_OVERLOAD, new Val[] {object, extensionName}); + } + return ((FieldTester) object).isSet(extensionName); + } + + private static Val getExt(Val object, Val extensionName) { + if (!(object instanceof Indexer) || !(extensionName instanceof StringT)) { + return noSuchOverload(object, GET_EXT, GET_EXT_OVERLOAD, new Val[] {object, extensionName}); + } + return ((Indexer) object).get(extensionName); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/extension/StringsLib.java b/core/src/main/java/org/projectnessie/cel/extension/StringsLib.java index 4469648c..b6124e1e 100644 --- a/core/src/main/java/org/projectnessie/cel/extension/StringsLib.java +++ b/core/src/main/java/org/projectnessie/cel/extension/StringsLib.java @@ -15,6 +15,10 @@ */ package org.projectnessie.cel.extension; +import static java.math.RoundingMode.HALF_EVEN; +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.math.BigDecimal; import java.util.*; import java.util.regex.Pattern; import org.projectnessie.cel.EnvOption; @@ -22,6 +26,11 @@ import org.projectnessie.cel.ProgramOption; import org.projectnessie.cel.checker.Decls; import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Indexer; +import org.projectnessie.cel.common.types.traits.Sizer; import org.projectnessie.cel.interpreter.functions.Overload; /** @@ -239,10 +248,13 @@ public class StringsLib implements Library { private static final String LAST_INDEX_OF = "lastIndexOf"; private static final String LOWER_ASCII = "lowerAscii"; private static final String REPLACE = "replace"; + private static final String REVERSE = "reverse"; private static final String SPLIT = "split"; private static final String SUBSTR = "substring"; private static final String TRIM_SPACE = "trim"; private static final String UPPER_ASCII = "upperAscii"; + private static final String FORMAT = "format"; + private static final String QUOTE = "strings.quote"; // whitespace characters definition from // https://en.wikipedia.org/wiki/Whitespace_character#Unicode @@ -285,71 +297,79 @@ public List getCompileOptions() { Decls.newFunction( CHAR_AT, Decls.newInstanceOverload( - "string_char_at_int", Arrays.asList(Decls.String, Decls.Int), Decls.String)), + "string_char_at_int", List.of(Decls.String, Decls.Int), Decls.String)), Decls.newFunction( INDEX_OF, Decls.newInstanceOverload( - "string_index_of_string", Arrays.asList(Decls.String, Decls.String), Decls.Int), + "string_index_of_string", List.of(Decls.String, Decls.String), Decls.Int), Decls.newInstanceOverload( "string_index_of_string_int", - Arrays.asList(Decls.String, Decls.String, Decls.Int), + List.of(Decls.String, Decls.String, Decls.Int), Decls.Int)), Decls.newFunction( JOIN, Decls.newInstanceOverload( - "list_join", Arrays.asList(Decls.newListType(Decls.String)), Decls.String), + "list_join", List.of(Decls.newListType(Decls.String)), Decls.String), Decls.newInstanceOverload( "list_join_string", - Arrays.asList(Decls.newListType(Decls.String), Decls.String), + List.of(Decls.newListType(Decls.String), Decls.String), Decls.String)), Decls.newFunction( LAST_INDEX_OF, Decls.newInstanceOverload( - "string_last_index_of_string", - Arrays.asList(Decls.String, Decls.String), - Decls.Int), + "string_last_index_of_string", List.of(Decls.String, Decls.String), Decls.Int), Decls.newInstanceOverload( "string_last_index_of_string_int", - Arrays.asList(Decls.String, Decls.String, Decls.Int), + List.of(Decls.String, Decls.String, Decls.Int), Decls.Int)), Decls.newFunction( LOWER_ASCII, Decls.newInstanceOverload( - "string_lower_ascii", Arrays.asList(Decls.String), Decls.String)), + "string_lower_ascii", List.of(Decls.String), Decls.String)), Decls.newFunction( REPLACE, Decls.newInstanceOverload( "string_replace_string_string", - Arrays.asList(Decls.String, Decls.String, Decls.String), + List.of(Decls.String, Decls.String, Decls.String), Decls.String), Decls.newInstanceOverload( "string_replace_string_string_int", - Arrays.asList(Decls.String, Decls.String, Decls.String, Decls.Int), + List.of(Decls.String, Decls.String, Decls.String, Decls.Int), Decls.String)), + Decls.newFunction( + REVERSE, + Decls.newInstanceOverload("string_reverse", List.of(Decls.String), Decls.String)), Decls.newFunction( SPLIT, Decls.newInstanceOverload( - "string_split_string", Arrays.asList(Decls.String, Decls.String), Decls.Dyn), + "string_split_string", List.of(Decls.String, Decls.String), Decls.Dyn), Decls.newInstanceOverload( "string_split_string_int", - Arrays.asList(Decls.String, Decls.String, Decls.Int), + List.of(Decls.String, Decls.String, Decls.Int), Decls.Dyn)), Decls.newFunction( SUBSTR, Decls.newInstanceOverload( - "string_substring_int", Arrays.asList(Decls.String, Decls.Int), Decls.String), + "string_substring_int", List.of(Decls.String, Decls.Int), Decls.String), Decls.newInstanceOverload( "string_substring_int_int", - Arrays.asList(Decls.String, Decls.Int, Decls.Int), + List.of(Decls.String, Decls.Int, Decls.Int), Decls.String)), Decls.newFunction( TRIM_SPACE, - Decls.newInstanceOverload( - "string_trim", Arrays.asList(Decls.String), Decls.String)), + Decls.newInstanceOverload("string_trim", List.of(Decls.String), Decls.String)), Decls.newFunction( UPPER_ASCII, Decls.newInstanceOverload( - "string_upper_ascii", Arrays.asList(Decls.String), Decls.String))); + "string_upper_ascii", List.of(Decls.String), Decls.String)), + Decls.newFunction( + FORMAT, + Decls.newInstanceOverload( + "string_format", + List.of(Decls.String, Decls.newListType(Decls.Dyn)), + Decls.String)), + Decls.newFunction( + QUOTE, Decls.newOverload("strings_quote", List.of(Decls.String), Decls.String))); return List.of(option); } @@ -363,7 +383,11 @@ public List getProgramOptions() { null, null, Guards.callInStrStrOutInt(StringsLib::indexOf), - Guards.callInStrStrIntOutInt(StringsLib::indexOfOffset)), + Guards.callInStrStrIntOutIntTernary(StringsLib::indexOfOffset), + values -> + values.length == 3 + ? Guards.callInStrStrIntOutInt(StringsLib::indexOfOffset).invoke(values) + : Err.maybeNoSuchOverloadErr(null)), Overload.overload( JOIN, null, @@ -375,13 +399,19 @@ public List getProgramOptions() { null, null, Guards.callInStrStrOutInt(StringsLib::lastIndexOf), - Guards.callInStrStrIntOutInt(StringsLib::lastIndexOfOffset)), + Guards.callInStrStrIntOutIntTernary(StringsLib::lastIndexOfOffset), + values -> + values.length == 3 + ? Guards.callInStrStrIntOutInt(StringsLib::lastIndexOfOffset).invoke(values) + : Err.maybeNoSuchOverloadErr(null)), Overload.unary(LOWER_ASCII, Guards.callInStrOutStr(StringsLib::lowerASCII)), Overload.overload( REPLACE, null, null, null, + Guards.callInStrStrStrOutStrTernary(StringsLib::replace), + Guards.callInStrStrStrIntOutStrQuaternary(StringsLib::replaceN), values -> { if (values.length == 3) { return Guards.callInStrStrStrOutStr(StringsLib::replace).invoke(values); @@ -391,20 +421,31 @@ public List getProgramOptions() { } return Err.maybeNoSuchOverloadErr(null); }), + Overload.unary(REVERSE, Guards.callInStrOutStr(StringsLib::reverse)), Overload.overload( SPLIT, null, null, Guards.callInStrStrOutStrArr(StringsLib::split), - Guards.callInStrStrIntOutStrArr(StringsLib::splitN)), + Guards.callInStrStrIntOutStrArrTernary(StringsLib::splitN), + values -> + values.length == 3 + ? Guards.callInStrStrIntOutStrArr(StringsLib::splitN).invoke(values) + : Err.maybeNoSuchOverloadErr(null)), Overload.overload( SUBSTR, null, null, Guards.callInStrIntOutStr(StringsLib::substr), - Guards.callInStrIntIntOutStr(StringsLib::substrRange)), + Guards.callInStrIntIntOutStrTernary(StringsLib::substrRange), + values -> + values.length == 3 + ? Guards.callInStrIntIntOutStr(StringsLib::substrRange).invoke(values) + : Err.maybeNoSuchOverloadErr(null)), Overload.unary(TRIM_SPACE, Guards.callInStrOutStr(StringsLib::trimSpace)), - Overload.unary(UPPER_ASCII, Guards.callInStrOutStr(StringsLib::upperASCII))); + Overload.unary(UPPER_ASCII, Guards.callInStrOutStr(StringsLib::upperASCII)), + Overload.binary(FORMAT, StringsLib::format), + Overload.unary(QUOTE, Guards.callInStrOutStr(StringsLib::quote))); return List.of(functions); } @@ -463,6 +504,51 @@ static String replace(String str, String old, String replacement) { return str.replace(old, replacement); } + static String reverse(String str) { + return new StringBuilder(str).reverse().toString(); + } + + static String quote(String str) { + StringBuilder quoted = new StringBuilder(str.length() + 2); + quoted.append('"'); + for (int offset = 0; offset < str.length(); ) { + int codePoint = str.codePointAt(offset); + offset += Character.charCount(codePoint); + switch (codePoint) { + case '\u0007': + quoted.append("\\a"); + break; + case '\b': + quoted.append("\\b"); + break; + case '\f': + quoted.append("\\f"); + break; + case '\n': + quoted.append("\\n"); + break; + case '\r': + quoted.append("\\r"); + break; + case '\t': + quoted.append("\\t"); + break; + case '\u000b': + quoted.append("\\v"); + break; + case '\\': + quoted.append("\\\\"); + break; + case '"': + quoted.append("\\\""); + break; + default: + quoted.appendCodePoint(codePoint); + } + } + return quoted.append('"').toString(); + } + /** * replace first n non-overlapping instance of {old} replaced by {replacement}. It works as strings.Replace in Go to have consistent behavior @@ -487,7 +573,7 @@ static String replaceN(String str, String old, String replacement, int n) { int count = 0; for (; count < n && index < str.length(); count++) { - if (old.length() == 0) { + if (old.isEmpty()) { stringBuilder.append(replacement).append(str, index, index + 1); index++; } else { @@ -538,7 +624,7 @@ static String[] splitN(String s, String sep, int n) { if (n == 1) { return new String[] {s}; } - if (sep.length() == 0) { + if (sep.isEmpty()) { return explode(s, n); } @@ -647,4 +733,242 @@ static String upperASCII(String str) { } return stringBuilder.toString(); } + + private static Val format(Val pattern, Val args) { + if (!(pattern instanceof StringT) || !(args instanceof Sizer) || !(args instanceof Indexer)) { + return Err.maybeNoSuchOverloadErr(null); + } + try { + return StringT.stringOf( + formatPattern((String) pattern.value(), (Sizer) args, (Indexer) args)); + } catch (FormatException e) { + return Err.newErr("%s", e.getMessage()); + } + } + + private static String formatPattern(String pattern, Sizer argsSizer, Indexer argsIndexer) { + int argCount = argsSizer.nativeSize(); + int argIndex = 0; + StringBuilder out = new StringBuilder(pattern.length()); + for (int i = 0; i < pattern.length(); i++) { + char ch = pattern.charAt(i); + if (ch != '%') { + out.append(ch); + continue; + } + if (++i >= pattern.length()) { + throw new FormatException("could not parse formatting clause: missing formatting clause"); + } + ch = pattern.charAt(i); + if (ch == '%') { + out.append('%'); + continue; + } + int precision = -1; + if (ch == '.') { + int precisionStart = ++i; + while (i < pattern.length() && Character.isDigit(pattern.charAt(i))) { + i++; + } + if (precisionStart == i || i >= pattern.length()) { + throw new FormatException("could not parse formatting clause: malformed precision"); + } + precision = Integer.parseInt(pattern.substring(precisionStart, i)); + ch = pattern.charAt(i); + } + if ("sdboxXfe".indexOf(ch) < 0) { + throw new FormatException( + "could not parse formatting clause: unrecognized formatting clause \"%s\"", ch); + } + if (argIndex >= argCount) { + throw new FormatException("index %d out of range", argIndex); + } + Val arg = argsIndexer.nativeGetAt(argIndex++); + out.append(formatValue(ch, precision, arg)); + } + return out.toString(); + } + + private static String formatValue(char clause, int precision, Val arg) { + return switch (clause) { + case 's' -> renderStringClause(arg); + case 'd' -> renderDecimalClause(arg); + case 'b' -> renderBinaryClause(arg); + case 'o' -> renderOctalClause(arg); + case 'x', 'X' -> renderHexClause(arg, clause == 'X'); + case 'f' -> renderFixedPointClause(arg, precision >= 0 ? precision : 6); + case 'e' -> renderScientificClause(arg, precision >= 0 ? precision : 6); + default -> + throw new FormatException( + "could not parse formatting clause: unrecognized formatting clause \"%s\"", clause); + }; + } + + private static String renderStringClause(Val value) { + return switch (value.type().typeEnum()) { + case String -> value.value().toString(); + case Bool -> Boolean.toString(value.booleanValue()); + case Bytes -> + new String(DefaultTypeAdapter.Instance.valueToNative(value, byte[].class), UTF_8); + case Int -> Long.toString(value.intValue()); + case Uint -> Long.toUnsignedString(value.intValue()); + case Double -> renderDouble(value.doubleValue()); + case Null -> "null"; + case Type, Duration, Timestamp -> value.convertToType(StringT.StringType).value().toString(); + case List -> renderList(value); + case Map -> renderMap(value); + default -> + throw new FormatException( + "error during formatting: string clause can only be used on strings, bools, bytes, ints, doubles, maps, lists, types, durations, and timestamps, was given %s", + value.type().typeName()); + }; + } + + private static String renderDecimalClause(Val value) { + switch (value.type().typeEnum()) { + case Int: + return Long.toString(value.intValue()); + case Uint: + return Long.toUnsignedString(value.intValue()); + case Double: + double d = value.doubleValue(); + if (Double.isNaN(d) || Double.isInfinite(d)) { + return renderDouble(d); + } + break; + default: + } + throw new FormatException( + "error during formatting: decimal clause can only be used on integers, was given %s", + value.type().typeName()); + } + + private static String renderBinaryClause(Val value) { + return switch (value.type().typeEnum()) { + case Int -> Long.toBinaryString(value.intValue()); + case Uint -> Long.toUnsignedString(value.intValue(), 2); + case Bool -> value.booleanValue() ? "1" : "0"; + default -> + throw new FormatException( + "error during formatting: only integers and bools can be formatted as binary, was given %s", + value.type().typeName()); + }; + } + + private static String renderOctalClause(Val value) { + return switch (value.type().typeEnum()) { + case Int -> Long.toOctalString(value.intValue()); + case Uint -> Long.toUnsignedString(value.intValue(), 8); + default -> + throw new FormatException( + "error during formatting: octal clause can only be used on integers, was given %s", + value.type().typeName()); + }; + } + + private static String renderHexClause(Val value, boolean upperCase) { + String hex = + switch (value.type().typeEnum()) { + case Int -> Long.toHexString(value.intValue()); + case Uint -> Long.toUnsignedString(value.intValue(), 16); + case String -> bytesToHex(value.value().toString().getBytes(UTF_8)); + case Bytes -> bytesToHex(DefaultTypeAdapter.Instance.valueToNative(value, byte[].class)); + default -> + throw new FormatException( + "error during formatting: only integers, byte buffers, and strings can be formatted as hex, was given %s", + value.type().typeName()); + }; + return upperCase ? hex.toUpperCase(Locale.ROOT) : hex; + } + + private static String renderFixedPointClause(Val value, int precision) { + switch (value.type().typeEnum()) { + case Int: + case Uint: + case Double: + double d = value.doubleValue(); + if (Double.isNaN(d) || Double.isInfinite(d)) { + return renderDouble(d); + } + return BigDecimal.valueOf(d).setScale(precision, HALF_EVEN).toPlainString(); + default: + throw new FormatException( + "error during formatting: fixed-point clause can only be used on doubles, was given %s", + value.type().typeName()); + } + } + + private static String renderScientificClause(Val value, int precision) { + switch (value.type().typeEnum()) { + case Int: + case Uint: + case Double: + double d = value.doubleValue(); + if (Double.isNaN(d) || Double.isInfinite(d)) { + return renderDouble(d); + } + return String.format(Locale.ROOT, "%." + precision + "e", d); + default: + throw new FormatException( + "error during formatting: scientific clause can only be used on doubles, was given %s", + value.type().typeName()); + } + } + + private static String renderList(Val value) { + Sizer sizer = (Sizer) value; + Indexer indexer = (Indexer) value; + int size = sizer.nativeSize(); + StringBuilder out = new StringBuilder("["); + for (int i = 0; i < size; i++) { + if (i > 0) { + out.append(", "); + } + out.append(renderStringClause(indexer.nativeGetAt(i))); + } + return out.append(']').toString(); + } + + private static String renderMap(Val value) { + org.projectnessie.cel.common.types.IteratorT iterator = + ((org.projectnessie.cel.common.types.IterableT) value).iterator(); + Indexer indexer = (Indexer) value; + List entries = new ArrayList<>(); + while (iterator.hasNext().booleanValue()) { + Val key = iterator.next(); + entries.add(renderStringClause(key) + ": " + renderStringClause(indexer.get(key))); + } + Collections.sort(entries); + return "{" + String.join(", ", entries) + "}"; + } + + private static String renderDouble(double value) { + if (Double.isNaN(value)) { + return "NaN"; + } + if (value == Double.POSITIVE_INFINITY) { + return "Infinity"; + } + if (value == Double.NEGATIVE_INFINITY) { + return "-Infinity"; + } + return Double.toString(value); + } + + private static String bytesToHex(byte[] bytes) { + char[] hex = new char[bytes.length * 2]; + char[] digits = "0123456789abcdef".toCharArray(); + for (int i = 0; i < bytes.length; i++) { + int b = bytes[i] & 0xff; + hex[i * 2] = digits[b >>> 4]; + hex[i * 2 + 1] = digits[b & 0x0f]; + } + return new String(hex); + } + + private static final class FormatException extends RuntimeException { + FormatException(String message, Object... args) { + super(String.format(Locale.ROOT, message, args)); + } + } } diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/Activation.java b/core/src/main/java/org/projectnessie/cel/interpreter/Activation.java index ff0ea5e2..a8e9d57a 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/Activation.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Activation.java @@ -15,35 +15,46 @@ */ package org.projectnessie.cel.interpreter; -import java.util.Arrays; -import java.util.HashMap; import java.util.Map; import java.util.function.Function; -import java.util.function.Supplier; -import org.projectnessie.cel.common.types.ref.Val; /** * Activation used to resolve identifiers by name and references by id. * *

An Activation is the primary mechanism by which a caller supplies input into a CEL program. */ -public interface Activation { +public interface Activation extends ActivationFunction { + @SuppressWarnings("removal") + @Override + default Object resolve(String name) { + var resolved = resolveName(name); + if (resolved != null) { + return resolved.present() ? resolved.value() : ABSENT; + } + return ABSENT; + } + /** - * ResolveName returns a value from the activation by qualified name, or false if the name could - * not be found. + * Deprecated for removal. + * + *

Replace with {@link #resolve(String)}. */ + @SuppressWarnings({"DeprecatedIsStillUsed", "removal"}) + @Deprecated(forRemoval = true) ResolvedValue resolveName(String name); /** * Parent returns the parent of the current activation, may be nil. If non-nil, the parent will be * searched during resolve calls. */ - Activation parent(); + default Activation parent() { + return null; + } /** EmptyActivation returns a variable free activation. */ static Activation emptyActivation() { // This call cannot fail. - return newActivation(new HashMap()); + return newActivation(Map.of()); } /** @@ -60,18 +71,22 @@ static Activation emptyActivation() { *

Values which are not represented as ref.Val types on input may be adapted to a ref.Val using * the ref.TypeAdapter configured in the environment. */ + @SuppressWarnings({"rawtypes", "unchecked", "removal"}) static Activation newActivation(Object bindings) { if (bindings == null) { throw new NullPointerException("bindings must be non-nil"); } - if (bindings instanceof Activation) { - return (Activation) bindings; + if (bindings instanceof Activation activation) { + return activation; + } + if (bindings instanceof Map map) { + return new MapActivation(map); } - if (bindings instanceof Map) { - return new MapActivation((Map) bindings); + if (bindings instanceof Function func) { + bindings = (ActivationFunction) name -> ResolvedValue.mapLegacy(func.apply(name)); } - if (bindings instanceof Function) { - return new FunctionActivation((Function) bindings); + if (bindings instanceof ActivationFunction activationFunction) { + return new FunctionActivation(activationFunction); } throw new IllegalArgumentException( String.format( @@ -79,116 +94,6 @@ static Activation newActivation(Object bindings) { bindings.getClass().getName())); } - /** - * mapActivation which implements Activation and maps of named values. - * - *

Named bindings may lazily supply values by providing a function which accepts no arguments - * and produces an interface value. - */ - final class MapActivation implements Activation { - private final Map bindings; - - MapActivation(Map bindings) { - this.bindings = bindings; - } - - /** Parent implements the Activation interface method. */ - @Override - public Activation parent() { - return null; - } - - /** ResolveName implements the Activation interface method. */ - @Override - public ResolvedValue resolveName(String name) { - Object obj = bindings.get(name); - if (obj == null) { - if (!bindings.containsKey(name)) { - return ResolvedValue.ABSENT; - } - return ResolvedValue.NULL_VALUE; - } - - if (obj instanceof Supplier) { - obj = ((Supplier) obj).get(); - bindings.put(name, obj); - } - return ResolvedValue.resolvedValue(obj); - } - - @Override - public String toString() { - return "MapActivation{" + "bindings=" + bindings + '}'; - } - } - - /** functionActivation which implements Activation and a provider of named values. */ - final class FunctionActivation implements Activation { - private final Function provider; - - FunctionActivation(Function provider) { - this.provider = provider; - } - - /** Parent implements the Activation interface method. */ - @Override - public Activation parent() { - return null; - } - - /** ResolveName implements the Activation interface method. */ - @Override - public ResolvedValue resolveName(String name) { - Object result = provider.apply(name); - if (result instanceof ResolvedValue) { - return (ResolvedValue) result; - } else if (result == null) { - return ResolvedValue.ABSENT; - } else { - return ResolvedValue.resolvedValue(result); - } - } - - @Override - public String toString() { - return "FunctionActivation{" + "provider=" + provider + '}'; - } - } - - /** - * hierarchicalActivation which implements Activation and contains a parent and child activation. - */ - final class HierarchicalActivation implements Activation { - private final Activation parent; - private final Activation child; - - HierarchicalActivation(Activation parent, Activation child) { - this.parent = parent; - this.child = child; - } - - /** Parent implements the Activation interface method. */ - @Override - public Activation parent() { - return parent; - } - - /** ResolveName implements the Activation interface method. */ - @Override - public ResolvedValue resolveName(String name) { - ResolvedValue object = child.resolveName(name); - if (object.present()) { - return object; - } - return parent.resolveName(name); - } - - @Override - public String toString() { - return "HierarchicalActivation{" + "parent=" + parent + ", child=" + child + '}'; - } - } - /** * NewHierarchicalActivation takes two activations and produces a new one which prioritizes * resolution in the child first and parent(s) second. @@ -218,83 +123,4 @@ interface PartialActivation extends Activation { */ AttributePattern[] unknownAttributePatterns(); } - - /** partActivation is the default implementations of the PartialActivation interface. */ - final class PartActivation implements PartialActivation { - private final Activation delegate; - private final AttributePattern[] unknowns; - - PartActivation(Activation delegate, AttributePattern[] unknowns) { - this.delegate = delegate; - this.unknowns = unknowns; - } - - @Override - public Activation parent() { - return delegate.parent(); - } - - @Override - public ResolvedValue resolveName(String name) { - return delegate.resolveName(name); - } - - /** UnknownAttributePatterns implements the PartialActivation interface method. */ - @Override - public AttributePattern[] unknownAttributePatterns() { - return unknowns; - } - - @Override - public String toString() { - return "PartActivation{" - + "delegate=" - + delegate - + ", unknowns=" - + Arrays.toString(unknowns) - + '}'; - } - } - - /** - * varActivation represents a single mutable variable binding. - * - *

This activation type should only be used within folds as the fold loop controls the object - * life-cycle. - */ - final class VarActivation implements Activation { - Activation parent; - String name; - Val val; - - VarActivation() {} - - /** Parent implements the Activation interface method. */ - @Override - public Activation parent() { - return parent; - } - - /** ResolveName implements the Activation interface method. */ - @Override - public ResolvedValue resolveName(String name) { - if (name.equals(this.name)) { - return ResolvedValue.resolvedValue(val); - } - return parent.resolveName(name); - } - - @Override - public String toString() { - return "VarActivation{" - + "parent=" - + parent - + ", name='" - + name - + '\'' - + ", val=" - + val - + '}'; - } - } } diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/ActivationFunction.java b/core/src/main/java/org/projectnessie/cel/interpreter/ActivationFunction.java new file mode 100644 index 00000000..620b2dc1 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/ActivationFunction.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +@FunctionalInterface +public interface ActivationFunction { + Object ABSENT = + new Object() { + @Override + public String toString() { + return ""; + } + }; + + /** + * Returns a value from the activation by qualified name. + * + *

Return the exact ABSENT singleton when unresolved; consumers compare it by identity. + * + * @return the resolved value, {@code null} if null or the constant/sentinel value {@link #ABSENT} + * if the name/value is absent. + */ + Object resolve(String name); +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/ActivationImplementations.java b/core/src/main/java/org/projectnessie/cel/interpreter/ActivationImplementations.java new file mode 100644 index 00000000..90468b45 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/ActivationImplementations.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Supplier; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.Activation.PartialActivation; + +/** + * Activation backed by a map of named values. + * + *

Named bindings may lazily supply values through a no-argument {@link Supplier}. + */ +final class MapActivation implements Activation { + private final Map bindings; + + MapActivation(Map bindings) { + this.bindings = bindings; + } + + @Override + public Object resolve(String name) { + if (name.startsWith(".")) { + name = name.substring(1); + } + Object obj = bindings.get(name); + if (obj == null) { + if (!bindings.containsKey(name)) { + return ABSENT; + } + return null; + } + + if (obj instanceof Supplier) { + obj = ((Supplier) obj).get(); + bindings.put(name, obj); + } + return obj; + } + + @SuppressWarnings("removal") + @Override + public ResolvedValue resolveName(String name) { + return ResolvedValue.mapTo(resolve(name)); + } + + @Override + public String toString() { + return "MapActivation{" + "bindings=" + bindings + '}'; + } +} + +/** Activation backed by a provider of named values. */ +final class FunctionActivation implements Activation { + private final ActivationFunction provider; + + FunctionActivation(ActivationFunction provider) { + this.provider = provider; + } + + @Override + public Object resolve(String name) { + if (name.startsWith(".")) { + name = name.substring(1); + } + return provider.resolve(name); + } + + @SuppressWarnings("removal") + @Override + public ResolvedValue resolveName(String name) { + return ResolvedValue.mapTo(resolve(name)); + } + + @Override + public String toString() { + return "FunctionActivation{" + "provider=" + provider + '}'; + } +} + +/** Activation which resolves against its child before its parent. */ +final class HierarchicalActivation implements Activation { + private final Activation parent; + private final Activation child; + + HierarchicalActivation(Activation parent, Activation child) { + this.parent = parent; + this.child = child; + } + + @Override + public Activation parent() { + return parent; + } + + @Override + public Object resolve(String name) { + if (name.startsWith(".")) { + return parent.resolve(name.substring(1)); + } + var resolvedName = child.resolve(name); + if (ABSENT != resolvedName) { + return resolvedName; + } + return parent.resolve(name); + } + + @SuppressWarnings("removal") + @Override + public ResolvedValue resolveName(String name) { + return ResolvedValue.mapTo(resolve(name)); + } + + @Override + public String toString() { + return "HierarchicalActivation{" + "parent=" + parent + ", child=" + child + '}'; + } +} + +/** Default implementation of {@link PartialActivation}. */ +final class PartActivation implements PartialActivation { + private final Activation delegate; + private final AttributePattern[] unknowns; + + PartActivation(Activation delegate, AttributePattern[] unknowns) { + this.delegate = delegate; + this.unknowns = unknowns; + } + + @Override + public Activation parent() { + return delegate.parent(); + } + + @Override + public Object resolve(String name) { + return delegate.resolve(name); + } + + @SuppressWarnings("removal") + @Override + public ResolvedValue resolveName(String name) { + return ResolvedValue.mapTo(resolve(name)); + } + + @Override + public AttributePattern[] unknownAttributePatterns() { + return unknowns; + } + + @Override + public String toString() { + return "PartActivation{" + + "delegate=" + + delegate + + ", unknowns=" + + Arrays.toString(unknowns) + + '}'; + } +} + +/** + * A single mutable variable binding. + * + *

This activation type is used only within folds, whose loop controls its lifecycle. + */ +final class VarActivation implements Activation { + Activation parent; + String name; + Val val; + + VarActivation() {} + + @Override + public Activation parent() { + return parent; + } + + @Override + public Object resolve(String name) { + if (name.startsWith(".")) { + return parent.resolve(name.substring(1)); + } + if (name.equals(this.name)) { + return val; + } + return parent.resolve(name); + } + + @SuppressWarnings("removal") + @Override + public ResolvedValue resolveName(String name) { + return ResolvedValue.mapTo(resolve(name)); + } + + @Override + public String toString() { + return "VarActivation{" + "parent=" + parent + ", name='" + name + '\'' + ", val=" + val + '}'; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/AstPruner.java b/core/src/main/java/org/projectnessie/cel/interpreter/AstPruner.java index 47f16cc9..00d2a542 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/AstPruner.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/AstPruner.java @@ -20,7 +20,6 @@ // common subexpressions. import static org.projectnessie.cel.common.types.BoolT.True; -import static org.projectnessie.cel.common.types.IntT.intOf; import static org.projectnessie.cel.common.types.UnknownT.isUnknown; import static org.projectnessie.cel.common.types.Util.isUnknownOrError; @@ -78,20 +77,17 @@ * prepare the overloads accordingly. */ public final class AstPruner { - private final Expr expr; private final EvalState state; private long nextExprID; - private AstPruner(Expr expr, EvalState state, long nextExprID) { - this.expr = expr; + private AstPruner(EvalState state, long nextExprID) { this.state = state; this.nextExprID = nextExprID; } public static Expr pruneAst(Expr expr, EvalState state) { - AstPruner pruner = new AstPruner(expr, state, 1); - Expr newExpr = pruner.prune(expr); - return newExpr; + AstPruner pruner = new AstPruner(state, 1); + return pruner.prune(expr); } static Expr createLiteral(long id, Constant val) { @@ -124,12 +120,11 @@ Expr maybeCreateLiteral(long id, Val v) { } // Attempt to build a list literal. - if (v instanceof Lister) { - Lister list = (Lister) v; - int sz = (int) list.size().intValue(); + if (v instanceof Lister list) { + int sz = list.nativeSize(); List elemExprs = new ArrayList<>(sz); for (int i = 0; i < sz; i++) { - Val elem = list.get(intOf(i)); + Val elem = list.nativeGetAt(i); if (isUnknownOrError(elem)) { return null; } @@ -146,10 +141,9 @@ Expr maybeCreateLiteral(long id, Val v) { } // Create a map literal if possible. - if (v instanceof Mapper) { - Mapper mp = (Mapper) v; + if (v instanceof Mapper mp) { IteratorT it = mp.iterator(); - List entries = new ArrayList<>((int) mp.size().intValue()); + List entries = new ArrayList<>(mp.nativeSize()); while (it.hasNext() == True) { Val key = it.next(); Val val = mp.get(key); @@ -321,33 +315,32 @@ Expr prune(Expr node) { boolean prunedStruct = false; CreateStruct struct = node.getStructExpr(); List entries = struct.getEntriesList(); - String messageType = struct.getMessageName(); List newEntries = new ArrayList<>(entries.size()); for (int i = 0; i < entries.size(); i++) { Entry entry = entries.get(i); newEntries.add(entry); Expr mapKey = entry.getMapKey(); - Expr newKey = mapKey != Entry.getDefaultInstance().getMapKey() ? prune(mapKey) : null; + Expr newKey = entry.hasMapKey() ? prune(mapKey) : null; Expr newValue = prune(entry.getValue()); if ((newKey == null || newKey == mapKey) && (newValue == null || newValue == entry.getValue())) { continue; } prunedStruct = true; - Entry newEntry; - if (!messageType.isEmpty()) { - newEntry = - Entry.newBuilder().setFieldKey(entry.getFieldKey()).setValue(newValue).build(); - } else { - newEntry = Entry.newBuilder().setMapKey(newKey).setValue(newValue).build(); + Entry.Builder newEntry = Entry.newBuilder(entry); + if (newKey != null && newKey != mapKey) { + newEntry.setMapKey(newKey); } - newEntries.set(i, newEntry); + if (newValue != null && newValue != entry.getValue()) { + newEntry.setValue(newValue); + } + newEntries.set(i, newEntry.build()); } if (prunedStruct) { return Expr.newBuilder() .setId(node.getId()) .setStructExpr( - CreateStruct.newBuilder().setMessageName(messageType).addAllEntries(entries)) + CreateStruct.newBuilder(struct).clearEntries().addAllEntries(newEntries)) .build(); } break; @@ -363,6 +356,7 @@ Expr prune(Expr node) { .setComprehensionExpr( Comprehension.newBuilder() .setIterVar(compre.getIterVar()) + .setIterVar2(compre.getIterVar2()) .setIterRange(newRange) .setAccuVar(compre.getAccuVar()) .setAccuInit(compre.getAccuInit()) diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/AttributeFactory.java b/core/src/main/java/org/projectnessie/cel/interpreter/AttributeFactory.java index 6eecaa5b..beb6403a 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/AttributeFactory.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/AttributeFactory.java @@ -33,6 +33,8 @@ import static org.projectnessie.cel.common.types.Types.boolOf; import static org.projectnessie.cel.common.types.UintT.uintOf; import static org.projectnessie.cel.common.types.UnknownT.isUnknown; +import static org.projectnessie.cel.interpreter.AttributeFactory.newQualifierStatic; +import static org.projectnessie.cel.interpreter.AttributeFactory.refResolve; import static org.projectnessie.cel.interpreter.Coster.costOf; import com.google.api.expr.v1alpha1.Type; @@ -50,7 +52,14 @@ import org.projectnessie.cel.common.types.ref.TypeProvider; import org.projectnessie.cel.common.types.ref.Val; import org.projectnessie.cel.common.types.traits.Indexer; +import org.projectnessie.cel.common.types.traits.Lister; import org.projectnessie.cel.common.types.traits.Mapper; +import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; +import org.projectnessie.cel.interpreter.AttributeFactory.ConstantQualifier; +import org.projectnessie.cel.interpreter.AttributeFactory.ConstantQualifierEquator; +import org.projectnessie.cel.interpreter.AttributeFactory.NamespacedAttribute; +import org.projectnessie.cel.interpreter.AttributeFactory.Qualifier; +import org.projectnessie.cel.interpreter.AttributeFactory.ValQualifier; import org.projectnessie.cel.interpreter.AttributePattern.QualifierValueEquator; /** AttributeFactory provides methods creating Attribute and Qualifier values. */ @@ -177,1248 +186,1260 @@ static AttributeFactory newAttributeFactory(Container cont, TypeAdapter a, TypeP return new AttrFactory(cont, a, p); } - final class AttrFactory implements AttributeFactory { - private final Container container; - private final TypeAdapter adapter; - private final TypeProvider provider; - - AttrFactory(Container container, TypeAdapter adapter, TypeProvider provider) { - this.container = container; - this.adapter = adapter; - this.provider = provider; + static Qualifier newQualifierStatic(TypeAdapter adapter, long id, Object v) { + if (v instanceof Attribute) { + return new AttrQualifier(id, (Attribute) v); } - /** - * AbsoluteAttribute refers to a variable value and an optional qualifier path. - * - *

The namespaceNames represent the names the variable could have based on namespace - * resolution rules. - */ - @Override - public NamespacedAttribute absoluteAttribute(long id, String... names) { - return new AbsoluteAttribute(id, names, new ArrayList<>(), adapter, provider, this); - } + Class c = v.getClass(); - /** - * ConditionalAttribute supports the case where an attribute selection may occur on a - * conditional expression, e.g. (cond ? a : b).c - */ - @Override - public AttributeFactory.Attribute conditionalAttribute( - long id, Interpretable expr, AttributeFactory.Attribute t, AttributeFactory.Attribute f) { - return new ConditionalAttribute(id, expr, t, f, adapter, this); + if (v instanceof Val val) { + switch (val.type().typeEnum()) { + case String: + return new StringQualifier(id, (String) val.value(), val, adapter); + case Double: + return new DoubleQualifier(id, (double) val.value(), val, adapter); + case Int: + return new IntQualifier(id, val.intValue(), val, adapter); + case Uint: + return new UintQualifier(id, val.intValue(), val, adapter); + case Bool: + return new BoolQualifier(id, val.booleanValue(), val, adapter); + case Null: + // Not actually a qualifier, but conformance-tests require this, although it's actually an + // error condition. + return new NullQualifier(id, val, adapter); + } } - /** - * MaybeAttribute collects variants of unchecked AbsoluteAttribute values which could either be - * direct variable accesses or some combination of variable access with qualification. - */ - @Override - public Attribute maybeAttribute(long id, String name) { - List attrs = new ArrayList<>(); - attrs.add(absoluteAttribute(id, container.resolveCandidateNames(name))); - return new MaybeAttribute(id, attrs, adapter, provider, this); + if (c == String.class) { + return new StringQualifier(id, (String) v, stringOf((String) v), adapter); } - - /** RelativeAttribute refers to an expression and an optional qualifier path. */ - @Override - public Attribute relativeAttribute(long id, Interpretable operand) { - return new RelativeAttribute(id, operand, new ArrayList<>(), adapter, this); + if (c == ULong.class) { + long l = ((ULong) v).longValue(); + return new UintQualifier(id, l, uintOf(l), adapter); + } + if ((c == Byte.class) || (c == Short.class) || (c == Integer.class) || (c == Long.class)) { + long i = ((Number) v).longValue(); + return new IntQualifier(id, i, intOf(i), adapter); + } + if (c == Double.class) { + double b = (Double) v; + return new DoubleQualifier(id, b, doubleOf(b), adapter); + } + if (c == Boolean.class) { + boolean b = (Boolean) v; + return new BoolQualifier(id, b, boolOf(b), adapter); } - /** NewQualifier is an implementation of the AttributeFactory interface. */ - @Override - public AttributeFactory.Qualifier newQualifier(Type objType, long qualID, Object val) { - // Before creating a new qualifier check to see if this is a protobuf message field access. - // If so, use the precomputed GetFrom qualification method rather than the standard - // stringQualifier. - if (val instanceof String) { - String str = (String) val; - if (objType != null && !objType.getMessageType().isEmpty()) { - FieldType ft = provider.findFieldType(objType.getMessageType(), str); - if (ft != null && ft.isSet != null && ft.getFrom != null) { - return new FieldQualifier(qualID, str, ft, adapter); + throw new IllegalStateException( + String.format("invalid qualifier type: %s", v.getClass().getName())); + } + + /** + * refResolve attempts to convert the value to a CEL value and then uses reflection methods to try + * and resolve the qualifier. + */ + static Val refResolve(TypeAdapter adapter, Val idx, Object obj) { + Val celVal = adapter.nativeToValue(obj); + if (celVal instanceof Mapper mapper) { + Val elem = mapper.find(idx); + if (elem == null) { + return noSuchKey(idx); + } + return elem; + } + if (celVal instanceof Lister list) { + switch (idx.type().typeEnum()) { + case Int: + long intIndex = idx.intValue(); + if (intIndex >= Integer.MIN_VALUE && intIndex <= Integer.MAX_VALUE) { + return list.nativeGetAt((int) intIndex); } - } + break; + case Uint: + long uintIndex = idx.intValue(); + if (uintIndex >= 0 && uintIndex <= Integer.MAX_VALUE) { + return list.nativeGetAt((int) uintIndex); + } + break; + case Double: + double doubleIndex = idx.doubleValue(); + if (doubleIndex >= Integer.MIN_VALUE + && doubleIndex <= Integer.MAX_VALUE + && Math.rint(doubleIndex) == doubleIndex) { + return list.nativeGetAt((int) doubleIndex); + } + break; + default: + break; } - return newQualifierStatic(adapter, qualID, val); } - - @Override - public String toString() { - return "AttrFactory{" - + "container=" - + container - + ", adapter=" - + adapter - + ", provider=" - + provider - + '}'; + if (celVal instanceof Indexer indexer) { + return indexer.get(idx); } + if (isUnknown(celVal)) { + return celVal; + } + // TODO: If the types.Err value contains more than just an error message at some point in the + // future, then it would be reasonable to return error values as ref.Val types rather than + // simple go error types. + throwErrorAsIllegalStateException(celVal); + return noSuchOverload(celVal, "ref-resolve", null); } +} - final class AbsoluteAttribute implements Qualifier, NamespacedAttribute, Coster { - final long id; +/** Default {@link AttributeFactory} implementation. */ +final class AttrFactory implements AttributeFactory { + private final Container container; + private final TypeAdapter adapter; + private final TypeProvider provider; - /** - * namespaceNames represent the names the variable could have based on declared container - * (package) of the expression. - */ - final String[] namespaceNames; - - final List qualifiers; - final TypeAdapter adapter; - final TypeProvider provider; - final AttributeFactory fac; - - AbsoluteAttribute( - long id, - String[] namespaceNames, - List qualifiers, - TypeAdapter adapter, - TypeProvider provider, - AttributeFactory fac) { - this.id = id; - this.namespaceNames = Objects.requireNonNull(namespaceNames); - this.qualifiers = Objects.requireNonNull(qualifiers); - this.adapter = Objects.requireNonNull(adapter); - this.provider = Objects.requireNonNull(provider); - this.fac = Objects.requireNonNull(fac); - } + AttrFactory(Container container, TypeAdapter adapter, TypeProvider provider) { + this.container = container; + this.adapter = adapter; + this.provider = provider; + } - /** ID implements the Attribute interface method. */ - @Override - public long id() { - return id; - } + /** + * AbsoluteAttribute refers to a variable value and an optional qualifier path. + * + *

The namespaceNames represent the names the variable could have based on namespace resolution + * rules. + */ + @Override + public NamespacedAttribute absoluteAttribute(long id, String... names) { + return new AbsoluteAttribute(id, names, new ArrayList<>(), adapter, provider, this); + } + + /** + * ConditionalAttribute supports the case where an attribute selection may occur on a conditional + * expression, e.g. (cond ? a : b).c + */ + @Override + public AttributeFactory.Attribute conditionalAttribute( + long id, Interpretable expr, AttributeFactory.Attribute t, AttributeFactory.Attribute f) { + return new ConditionalAttribute(id, expr, t, f, adapter, this); + } + + /** + * MaybeAttribute collects variants of unchecked AbsoluteAttribute values which could either be + * direct variable accesses or some combination of variable access with qualification. + */ + @Override + public Attribute maybeAttribute(long id, String name) { + List attrs = new ArrayList<>(); + attrs.add( + name.startsWith(".") + ? absoluteAttribute(id, name) + : absoluteAttribute(id, container.resolveCandidateNames(name))); + return new MaybeAttribute(id, attrs, adapter, provider, this); + } + + /** RelativeAttribute refers to an expression and an optional qualifier path. */ + @Override + public Attribute relativeAttribute(long id, Interpretable operand) { + return new RelativeAttribute(id, operand, new ArrayList<>(), adapter, this); + } - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - long min = 0L; - long max = 0L; - for (Qualifier q : qualifiers) { - Cost qc = Cost.estimateCost(q); - min += qc.min; - max += qc.max; + /** NewQualifier is an implementation of the AttributeFactory interface. */ + @Override + public AttributeFactory.Qualifier newQualifier(Type objType, long qualID, Object val) { + // Before creating a new qualifier check to see if this is a protobuf message field access. + // If so, use the precomputed GetFrom qualification method rather than the standard + // stringQualifier. + if (val instanceof String str) { + if (objType != null && !objType.getMessageType().isEmpty()) { + FieldType ft = provider.findFieldType(objType.getMessageType(), str); + if (ft != null && ft.isSet != null && ft.getFrom != null) { + return new FieldQualifier(qualID, str, ft, adapter); + } } - min++; // For object retrieval. - max++; - return costOf(min, max); } + return newQualifierStatic(adapter, qualID, val); + } - /** AddQualifier implements the Attribute interface method. */ - @Override - public Attribute addQualifier(AttributeFactory.Qualifier q) { - qualifiers.add(q); - return this; - } + @Override + public String toString() { + return "AttrFactory{" + + "container=" + + container + + ", adapter=" + + adapter + + ", provider=" + + provider + + '}'; + } +} - /** CandidateVariableNames implements the NamespaceAttribute interface method. */ - @Override - public String[] candidateVariableNames() { - return namespaceNames; - } +/** + * @param namespaceNames namespaceNames represent the names the variable could have based on + * declared container (package) of the expression. + */ +record AbsoluteAttribute( + long id, + String[] namespaceNames, + List qualifiers, + TypeAdapter adapter, + TypeProvider provider, + AttributeFactory fac) + implements Qualifier, NamespacedAttribute, Coster { + AbsoluteAttribute( + long id, + String[] namespaceNames, + List qualifiers, + TypeAdapter adapter, + TypeProvider provider, + AttributeFactory fac) { + this.id = id; + this.namespaceNames = Objects.requireNonNull(namespaceNames); + this.qualifiers = Objects.requireNonNull(qualifiers); + this.adapter = Objects.requireNonNull(adapter); + this.provider = Objects.requireNonNull(provider); + this.fac = Objects.requireNonNull(fac); + } - /** - * Qualifiers returns the list of Qualifier instances associated with the namespaced attribute. - */ - @Override - public List qualifiers() { - return qualifiers; - } + /** ID implements the Attribute interface method. */ + @Override + public long id() { + return id; + } - /** Qualify is an implementation of the Qualifier interface method. */ - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - Object val = resolve(vars); - if (isUnknown(val)) { - return val; - } - Qualifier qual = fac.newQualifier(null, id, val); - return qual.qualify(vars, obj); - } + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + long min = 0L; + long max = 0L; + for (Qualifier q : qualifiers) { + Cost qc = Cost.estimateCost(q); + min += qc.min; + max += qc.max; + } + min++; // For object retrieval. + max++; + return costOf(min, max); + } - /** - * Resolve returns the resolved Attribute value given the Activation, or error if the Attribute - * variable is not found, or if its Qualifiers cannot be applied successfully. - */ - @Override - public Object resolve(org.projectnessie.cel.interpreter.Activation vars) { - return tryResolve(vars); + /** AddQualifier implements the Attribute interface method. */ + @Override + public Attribute addQualifier(Qualifier q) { + qualifiers.add(q); + return this; + } + + /** CandidateVariableNames implements the NamespaceAttribute interface method. */ + @Override + public String[] candidateVariableNames() { + return namespaceNames; + } + + /** + * Qualifiers returns the list of Qualifier instances associated with the namespaced attribute. + */ + @Override + public List qualifiers() { + return qualifiers; + } + + /** Qualify is an implementation of the Qualifier interface method. */ + @Override + public Object qualify(Activation vars, Object obj) { + Object val = resolve(vars); + if (isUnknown(val)) { + return val; } + Qualifier qual = fac.newQualifier(null, id, val); + return qual.qualify(vars, obj); + } - /** - * TryResolve iterates through the namespaced variable names until one is found within the - * Activation or TypeProvider. - * - *

If the variable name cannot be found as an Activation variable or in the TypeProvider as a - * type, then the result is `nil`, `false`, `nil` per the interface requirement. - */ - @Override - public Object tryResolve(org.projectnessie.cel.interpreter.Activation vars) { - for (String nm : namespaceNames) { - // If the variable is found, process it. Otherwise, wait until the checks to - // determine whether the type is unknown before returning. - ResolvedValue obj = vars.resolveName(nm); - if (obj.present()) { - Object op = obj.value(); - for (int i = 0; i < qualifiers.size(); i++) { - Qualifier qual = qualifiers.get(i); - Object op2 = qualify(vars, op, qual, i == qualifiers.size() - 1); - if (op2 instanceof Err) { - return op2; - } - if (op2 == null) { - break; - } - op = op2; - } - return op; - } - // Attempt to resolve the qualified type name if the name is not a variable identifier. - Val typ = provider.findIdent(nm); - if (typ != null) { - if (qualifiers.isEmpty()) { - return typ; - } - throw noSuchAttributeException(this); + /** + * Resolve returns the resolved Attribute value given the Activation, or error if the Attribute + * variable is not found, or if its Qualifiers cannot be applied successfully. + */ + @Override + public Object resolve(Activation vars) { + return tryResolve(vars); + } + + /** + * TryResolve iterates through the namespaced variable names until one is found within the + * Activation or TypeProvider. + * + *

If the variable name cannot be found as an Activation variable or in the TypeProvider as a + * type, then the result is `nil`, `false`, `nil` per the interface requirement. + */ + @Override + public Object tryResolve(Activation vars) { + Object local = tryResolveCurrentVar(vars); + if (local != null) { + return local; + } + for (String nm : namespaceNames) { + // If the variable is found, process it. Otherwise, wait until the checks to + // determine whether the type is unknown before returning. + Object obj = vars.resolve(nm); + if (Activation.ABSENT != obj) { + return resolveQualifiers(vars, obj); + } + // Attempt to resolve the qualified type name if the name is not a variable identifier. + Val typ = provider.findIdent(nm); + if (typ != null) { + if (qualifiers.isEmpty()) { + return typ; } + throw noSuchAttributeException(this); } - throw noSuchAttributeException(this); } + throw noSuchAttributeException(this); + } - private Object qualify( - org.projectnessie.cel.interpreter.Activation vars, - Object obj, - Qualifier qualifier, - boolean last) { - if (last && qualifier instanceof ValQualifier) { - return ((ValQualifier) qualifier).qualifyToVal(vars, obj); + Object tryResolveCurrentVar(Activation vars) { + if (vars instanceof VarActivation var && (namespaceNames.length > 1 || !qualifiers.isEmpty())) { + String localName = namespaceNames[namespaceNames.length - 1]; + if (localName.equals(var.name)) { + return resolveQualifiers(vars, var.val); } - return qualifier.qualify(vars, obj); } + return null; + } - /** String implements the Stringer interface method. */ - @Override - public String toString() { - return "id: " + id + ", names: " + Arrays.toString(namespaceNames); + private Object resolveQualifiers(Activation vars, Object obj) { + Object op = obj; + for (int i = 0; i < qualifiers.size(); i++) { + Qualifier qual = qualifiers.get(i); + Object op2 = qualify(vars, op, qual, i == qualifiers.size() - 1); + if (op2 instanceof Err) { + return op2; + } + if (op2 == null) { + break; + } + op = op2; } + return op; } - final class ConditionalAttribute implements Qualifier, Attribute, Coster { - final long id; - final Interpretable expr; - final Attribute truthy; - final Attribute falsy; - final TypeAdapter adapter; - final AttributeFactory fac; - - ConditionalAttribute( - long id, - Interpretable expr, - Attribute truthy, - Attribute falsy, - TypeAdapter adapter, - AttributeFactory fac) { - this.id = id; - this.expr = expr; - this.truthy = truthy; - this.falsy = falsy; - this.adapter = adapter; - this.fac = fac; + private Object qualify(Activation vars, Object obj, Qualifier qualifier, boolean last) { + if (last && qualifier instanceof ValQualifier) { + return ((ValQualifier) qualifier).qualifyToVal(vars, obj); } + return qualifier.qualify(vars, obj); + } - /** ID is an implementation of the Attribute interface method. */ - @Override - public long id() { - return id; - } + /** String implements the Stringer interface method. */ + @Override + public String toString() { + return "id: " + id + ", names: " + Arrays.toString(namespaceNames); + } +} - /** - * Cost provides the heuristic cost of a ternary operation {@code <expr> ? <t> : - * <f>}. The cost is computed as {@code cost(expr)} plus the min/max costs of evaluating - * either `t` or `f`. - */ - @Override - public Cost cost() { - Cost t = Cost.estimateCost(truthy); - Cost f = Cost.estimateCost(falsy); - Cost e = Cost.estimateCost(expr); - return costOf(e.min + Math.min(t.min, f.min), e.max + Math.max(t.max, f.max)); - } +record ConditionalAttribute( + long id, + Interpretable expr, + Attribute truthy, + Attribute falsy, + TypeAdapter adapter, + AttributeFactory fac) + implements Qualifier, Attribute, Coster { + + /** ID is an implementation of the Attribute interface method. */ + @Override + public long id() { + return id; + } - /** - * AddQualifier appends the same qualifier to both sides of the conditional, in effect managing - * the qualification of alternate attributes. - */ - @Override - public Attribute addQualifier(AttributeFactory.Qualifier qual) { - truthy.addQualifier(qual); // just do - falsy.addQualifier(qual); // just do - return this; - } + /** + * Cost provides the heuristic cost of a ternary operation {@code <expr> ? <t> : + * <f>}. The cost is computed as {@code cost(expr)} plus the min/max costs of evaluating + * either `t` or `f`. + */ + @Override + public Cost cost() { + Cost t = Cost.estimateCost(truthy); + Cost f = Cost.estimateCost(falsy); + Cost e = Cost.estimateCost(expr); + return costOf(e.min + Math.min(t.min, f.min), e.max + Math.max(t.max, f.max)); + } - /** Qualify is an implementation of the Qualifier interface method. */ - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - Object val = resolve(vars); - if (isUnknown(val)) { - return val; - } - Qualifier qual = fac.newQualifier(null, id, val); - return qual.qualify(vars, obj); - } + /** + * AddQualifier appends the same qualifier to both sides of the conditional, in effect managing + * the qualification of alternate attributes. + */ + @Override + public Attribute addQualifier(Qualifier qual) { + truthy.addQualifier(qual); // just do + falsy.addQualifier(qual); // just do + return this; + } - /** - * Resolve evaluates the condition, and then resolves the truthy or falsy branch accordingly. - */ - @Override - public Object resolve(org.projectnessie.cel.interpreter.Activation vars) { - Val val = expr.eval(vars); - if (val == null) { - throw noSuchAttributeException(this); - } - if (isError(val)) { - throw new ErrException("message: %s", val); - } - if (val == True) { - return truthy.resolve(vars); - } - if (val == False) { - return falsy.resolve(vars); - } - if (isUnknown(val)) { - return val; - } - return maybeNoSuchOverloadErr(val); + /** Qualify is an implementation of the Qualifier interface method. */ + @Override + public Object qualify(Activation vars, Object obj) { + Object val = resolve(vars); + if (isUnknown(val)) { + return val; } + Qualifier qual = fac.newQualifier(null, id, val); + return qual.qualify(vars, obj); + } - /** String is an implementation of the Stringer interface method. */ - @Override - public String toString() { - return String.format("id: %d, truthy attribute: %s, falsy attribute: %s", id, truthy, falsy); + /** Resolve evaluates the condition, and then resolves the truthy or falsy branch accordingly. */ + @Override + public Object resolve(Activation vars) { + Val val = expr.eval(vars); + if (val == null) { + throw noSuchAttributeException(this); + } + if (isError(val)) { + throw new ErrException("message: %s", val); + } + if (val == True) { + return truthy.resolve(vars); + } + if (val == False) { + return falsy.resolve(vars); } + if (isUnknown(val)) { + return val; + } + return maybeNoSuchOverloadErr(val); } - final class MaybeAttribute implements Coster, Attribute, Qualifier { - final long id; - final List attrs; - final TypeAdapter adapter; - final TypeProvider provider; - final AttributeFactory fac; - - MaybeAttribute( - long id, - List attrs, - TypeAdapter adapter, - TypeProvider provider, - AttributeFactory fac) { - this.id = id; - this.attrs = attrs; - this.adapter = adapter; - this.provider = provider; - this.fac = fac; - } + /** String is an implementation of the Stringer interface method. */ + @Override + public String toString() { + return String.format("id: %d, truthy attribute: %s, falsy attribute: %s", id, truthy, falsy); + } +} - /** ID is an implementation of the Attribute interface method. */ - @Override - public long id() { - return id; - } +record MaybeAttribute( + long id, + List attrs, + TypeAdapter adapter, + TypeProvider provider, + AttributeFactory fac) + implements Coster, Attribute, Qualifier { + + /** ID is an implementation of the Attribute interface method. */ + @Override + public long id() { + return id; + } - /** - * Cost implements the Coster interface method. The min cost is computed as the minimal cost - * among all the possible attributes, the max cost ditto. - */ - @Override - public Cost cost() { - long min = Long.MAX_VALUE; - long max = 0L; - for (NamespacedAttribute a : attrs) { - Cost ac = Cost.estimateCost(a); - min = Long.min(min, ac.min); - max = Long.max(max, ac.max); - } - return costOf(min, max); - } + /** + * Cost implements the Coster interface method. The min cost is computed as the minimal cost among + * all the possible attributes, the max cost ditto. + */ + @Override + public Cost cost() { + long min = Long.MAX_VALUE; + long max = 0L; + for (NamespacedAttribute a : attrs) { + Cost ac = Cost.estimateCost(a); + min = Long.min(min, ac.min); + max = Long.max(max, ac.max); + } + return costOf(min, max); + } - /** - * AddQualifier adds a qualifier to each possible attribute variant, and also creates a new - * namespaced variable from the qualified value. - * - *

The algorithm for building the maybe attribute is as follows: - * - *

    - *
  1. Create a maybe attribute from a simple identifier when it occurs in a parsed-only - * expression
    - *
    - * {@code mb = MaybeAttribute(<id>, "a")}
    - *
    - * Initializing the maybe attribute creates an absolute attribute internally which - * includes the possible namespaced names of the attribute. In this example, let's assume - * we are in namespace 'ns', then the maybe is either one of the following variable names: - *
    - *
    - * possible variables names -- ns.a, a - *
  2. Adding a qualifier to the maybe means that the variable name could be a longer - * qualified name, or a field selection on one of the possible variable names produced - * earlier:
    - *
    - * {@code mb.AddQualifier("b")}
    - *
    - * possible variables names -- ns.a.b, a.b
    - * possible field selection -- ns.a['b'], a['b'] - *
- * - * If none of the attributes within the maybe resolves a value, the result is an error. - */ - @Override - public Attribute addQualifier(AttributeFactory.Qualifier qual) { - String str = ""; - boolean isStr = false; - if (qual instanceof ConstantQualifier) { - ConstantQualifier cq = (ConstantQualifier) qual; - Object cqv = cq.value().value(); - if (cqv instanceof String) { - str = (String) cqv; - isStr = true; - } + /** + * AddQualifier adds a qualifier to each possible attribute variant, and also creates a new + * namespaced variable from the qualified value. + * + *

The algorithm for building the maybe attribute is as follows: + * + *

    + *
  1. Create a maybe attribute from a simple identifier when it occurs in a parsed-only + * expression
    + *
    + * {@code mb = MaybeAttribute(<id>, "a")}
    + *
    + * Initializing the maybe attribute creates an absolute attribute internally which includes + * the possible namespaced names of the attribute. In this example, let's assume we are in + * namespace 'ns', then the maybe is either one of the following variable names:
    + *
    + * possible variables names -- ns.a, a + *
  2. Adding a qualifier to the maybe means that the variable name could be a longer qualified + * name, or a field selection on one of the possible variable names produced earlier:
    + *
    + * {@code mb.AddQualifier("b")}
    + *
    + * possible variables names -- ns.a.b, a.b
    + * possible field selection -- ns.a['b'], a['b'] + *
+ * + * If none of the attributes within the maybe resolves a value, the result is an error. + */ + @Override + public Attribute addQualifier(Qualifier qual) { + String str = ""; + boolean isStr = false; + if (qual instanceof ConstantQualifier cq) { + Object cqv = cq.value().value(); + if (cqv instanceof String) { + str = (String) cqv; + isStr = true; } - String[] augmentedNames = new String[0]; - // First add the qualifier to all existing attributes in the oneof. - for (NamespacedAttribute attr : attrs) { - if (isStr && attr.qualifiers().isEmpty()) { - String[] candidateVars = attr.candidateVariableNames(); - augmentedNames = new String[candidateVars.length]; - for (int i = 0; i < candidateVars.length; i++) { - String name = candidateVars[i]; - augmentedNames[i] = String.format("%s.%s", name, str); - } + } + String[] augmentedNames = new String[0]; + // First add the qualifier to all existing attributes in the oneof. + for (NamespacedAttribute attr : attrs) { + if (isStr && attr.qualifiers().isEmpty()) { + String[] candidateVars = attr.candidateVariableNames(); + augmentedNames = new String[candidateVars.length]; + for (int i = 0; i < candidateVars.length; i++) { + String name = candidateVars[i]; + augmentedNames[i] = String.format("%s.%s", name, str); } - attr.addQualifier(qual); } - // Next, ensure the most specific variable / type reference is searched first. - if (attrs.isEmpty()) { - attrs.add(fac.absoluteAttribute(qual.id(), augmentedNames)); - } else { - attrs.add(0, fac.absoluteAttribute(qual.id(), augmentedNames)); - } - return this; + attr.addQualifier(qual); } + // Next, ensure the most specific variable / type reference is searched first. + if (attrs.isEmpty()) { + attrs.add(fac.absoluteAttribute(qual.id(), augmentedNames)); + } else { + attrs.add(0, fac.absoluteAttribute(qual.id(), augmentedNames)); + } + return this; + } - /** Qualify is an implementation of the Qualifier interface method. */ - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - Object val = resolve(vars); - if (isUnknown(val)) { - return val; - } - Qualifier qual = fac.newQualifier(null, id, val); - return qual.qualify(vars, obj); + /** Qualify is an implementation of the Qualifier interface method. */ + @Override + public Object qualify(Activation vars, Object obj) { + Object val = resolve(vars); + if (isUnknown(val)) { + return val; } + Qualifier qual = fac.newQualifier(null, id, val); + return qual.qualify(vars, obj); + } - /** - * Resolve follows the variable resolution rules to determine whether the attribute is a - * variable or a field selection. - */ - @Override - public Object resolve(org.projectnessie.cel.interpreter.Activation vars) { - for (NamespacedAttribute attr : attrs) { - try { - return attr.tryResolve(vars); - } catch (ErrException ignore) { + /** + * Resolve follows the variable resolution rules to determine whether the attribute is a variable + * or a field selection. + */ + @Override + public Object resolve(Activation vars) { + for (NamespacedAttribute attr : attrs) { + if (attr instanceof AbsoluteAttribute) { + Object result = ((AbsoluteAttribute) attr).tryResolveCurrentVar(vars); + if (result != null) { + return result; } } - // Else, produce a no such attribute error. - throw noSuchAttributeException(this); } - - /** String is an implementation of the Stringer interface method. */ - @Override - public String toString() { - return String.format("id: %s, attributes: %s", id, attrs); + for (NamespacedAttribute attr : attrs) { + try { + return attr.tryResolve(vars); + } catch (ErrException ignore) { + } } + // Else, produce a no such attribute error. + throw noSuchAttributeException(this); } - final class RelativeAttribute implements Coster, Qualifier, Attribute { - final long id; - final Interpretable operand; - final List qualifiers; - final TypeAdapter adapter; - final AttributeFactory fac; - - RelativeAttribute( - long id, - Interpretable operand, - List qualifiers, - TypeAdapter adapter, - AttributeFactory fac) { - this.id = id; - this.operand = operand; - this.qualifiers = qualifiers; - this.adapter = adapter; - this.fac = fac; - } + /** String is an implementation of the Stringer interface method. */ + @Override + public String toString() { + return String.format("id: %s, attributes: %s", id, attrs); + } +} - /** ID is an implementation of the Attribute interface method. */ - @Override - public long id() { - return id; - } +record RelativeAttribute( + long id, + Interpretable operand, + List qualifiers, + TypeAdapter adapter, + AttributeFactory fac) + implements Coster, Qualifier, Attribute { + + /** ID is an implementation of the Attribute interface method. */ + @Override + public long id() { + return id; + } - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - Cost c = Cost.estimateCost(operand); - long min = c.min; - long max = c.max; - for (Qualifier qual : qualifiers) { - Cost q = Cost.estimateCost(qual); - min += q.min; - max += q.max; - } - return costOf(min, max); - } + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + Cost c = Cost.estimateCost(operand); + long min = c.min; + long max = c.max; + for (Qualifier qual : qualifiers) { + Cost q = Cost.estimateCost(qual); + min += q.min; + max += q.max; + } + return costOf(min, max); + } - /** AddQualifier implements the Attribute interface method. */ - @Override - public Attribute addQualifier(AttributeFactory.Qualifier qual) { - qualifiers.add(qual); - return this; - } + /** AddQualifier implements the Attribute interface method. */ + @Override + public Attribute addQualifier(Qualifier qual) { + qualifiers.add(qual); + return this; + } - /** Qualify is an implementation of the Qualifier interface method. */ - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - Object val = resolve(vars); - if (isUnknown(val)) { - return val; - } - Qualifier qual = fac.newQualifier(null, id, val); - return qual.qualify(vars, obj); + /** Qualify is an implementation of the Qualifier interface method. */ + @Override + public Object qualify(Activation vars, Object obj) { + Object val = resolve(vars); + if (isUnknown(val)) { + return val; } + Qualifier qual = fac.newQualifier(null, id, val); + return qual.qualify(vars, obj); + } - /** Resolve expression value and qualifier relative to the expression result. */ - @Override - public Object resolve(org.projectnessie.cel.interpreter.Activation vars) { - // First, evaluate the operand. - Val v = operand.eval(vars); - if (isError(v)) { - throw new ErrException("message: %s", v); - } - if (isUnknown(v)) { - return v; - } - // Next, qualify it. Qualification handles unkonwns as well, so there's no need to recheck. - Object obj = v; - for (int i = 0; i < qualifiers.size(); i++) { - Qualifier qual = qualifiers.get(i); - if (obj == null) { - throw noSuchAttributeException(this); - } - if (i == qualifiers.size() - 1 && qual instanceof ValQualifier) { - obj = ((ValQualifier) qual).qualifyToVal(vars, obj); - } else { - obj = qual.qualify(vars, obj); - } - if (obj instanceof Err) { - return obj; - } - } + /** Resolve expression value and qualifier relative to the expression result. */ + @Override + public Object resolve(Activation vars) { + // First, evaluate the operand. + Val v = operand.eval(vars); + if (isError(v)) { + throw new ErrException("message: %s", v); + } + if (isUnknown(v)) { + return v; + } + // Next, qualify it. Qualification handles unkonwns as well, so there's no need to recheck. + Object obj = v; + for (int i = 0; i < qualifiers.size(); i++) { + Qualifier qual = qualifiers.get(i); if (obj == null) { throw noSuchAttributeException(this); } - return obj; + if (i == qualifiers.size() - 1 && qual instanceof ValQualifier) { + obj = ((ValQualifier) qual).qualifyToVal(vars, obj); + } else { + obj = qual.qualify(vars, obj); + } + if (obj instanceof Err) { + return obj; + } } - - /** String is an implementation of the Stringer interface method. */ - @Override - public String toString() { - return String.format("id: %d, operand: %s", id, operand); + if (obj == null) { + throw noSuchAttributeException(this); } + return obj; } - static Qualifier newQualifierStatic(TypeAdapter adapter, long id, Object v) { - if (v instanceof Attribute) { - return new AttrQualifier(id, (Attribute) v); - } + /** String is an implementation of the Stringer interface method. */ + @Override + public String toString() { + return String.format("id: %d, operand: %s", id, operand); + } +} - Class c = v.getClass(); +record AttrQualifier(long id, Attribute attribute) implements Coster, Attribute { - if (v instanceof Val) { - Val val = (Val) v; - switch (val.type().typeEnum()) { - case String: - return new StringQualifier(id, (String) val.value(), val, adapter); - case Double: - return new DoubleQualifier(id, (double) val.value(), val, adapter); - case Int: - return new IntQualifier(id, val.intValue(), val, adapter); - case Uint: - return new UintQualifier(id, val.intValue(), val, adapter); - case Bool: - return new BoolQualifier(id, val.booleanValue(), val, adapter); - case Null: - // Not actually a qualifier, but conformance-tests require this, although it's actually an - // error condition. - return new NullQualifier(id, val, adapter); - } - } + /** Cost returns zero for constant field qualifiers */ + @Override + public Cost cost() { + return Cost.estimateCost(attribute); + } - if (c == String.class) { - return new StringQualifier(id, (String) v, stringOf((String) v), adapter); - } - if (c == ULong.class) { - long l = ((ULong) v).longValue(); - return new UintQualifier(id, l, uintOf(l), adapter); - } - if ((c == Byte.class) || (c == Short.class) || (c == Integer.class) || (c == Long.class)) { - long i = ((Number) v).longValue(); - return new IntQualifier(id, i, intOf(i), adapter); - } - if (c == Double.class) { - double b = (Double) v; - return new DoubleQualifier(id, b, doubleOf(b), adapter); - } - if (c == Boolean.class) { - boolean b = (Boolean) v; - return new BoolQualifier(id, b, boolOf(b), adapter); - } + @Override + public Attribute addQualifier(Qualifier q) { + return attribute.addQualifier(q); + } - throw new IllegalStateException( - String.format("invalid qualifier type: %s", v.getClass().getName())); + @Override + public Object resolve(Activation a) { + return attribute.resolve(a); } - final class AttrQualifier implements Coster, Attribute { - final long id; - final Attribute attribute; + @Override + public Object qualify(Activation vars, Object obj) { + return attribute.qualify(vars, obj); + } - AttrQualifier(long id, Attribute attribute) { - this.id = id; - this.attribute = attribute; - } + @Override + public String toString() { + return "AttrQualifier{" + "id=" + id + ", attribute=" + attribute + '}'; + } +} - @Override - public long id() { - return id; - } +final class StringQualifier implements Coster, ConstantQualifierEquator, QualifierValueEquator { + final long id; + final String value; + final Val celValue; + final TypeAdapter adapter; + + StringQualifier(long id, String value, Val celValue, TypeAdapter adapter) { + this.id = id; + this.value = value; + this.celValue = celValue; + this.adapter = adapter; + } - /** Cost returns zero for constant field qualifiers */ - @Override - public Cost cost() { - return Cost.estimateCost(attribute); - } + /** ID is an implementation of the Qualifier interface method. */ + @Override + public long id() { + return id; + } - @Override - public Attribute addQualifier(Qualifier q) { - return attribute.addQualifier(q); + /** Qualify implements the Qualifier interface method. */ + @SuppressWarnings("rawtypes") + @Override + public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { + String s = value; + if (obj instanceof Map m) { + obj = m.get(s); + if (obj == null) { + if (m.containsKey(s)) { + return NullValue; + } + throw noSuchKeyException(s); + } + } else if (isUnknown(obj)) { + return obj; + } else { + return refResolve(adapter, celValue, obj); } + return obj; + } - @Override - public Object resolve(Activation a) { - return attribute.resolve(a); - } + /** Value implements the ConstantQualifier interface */ + @Override + public Val value() { + return celValue; + } - @Override - public Object qualify(Activation vars, Object obj) { - return attribute.qualify(vars, obj); - } + /** Cost returns zero for constant field qualifiers */ + @Override + public Cost cost() { + return Cost.None; + } - @Override - public String toString() { - return "AttrQualifier{" + "id=" + id + ", attribute=" + attribute + '}'; + @Override + public boolean qualifierValueEquals(Object value) { + if (value instanceof String) { + return this.value.equals(value); } + return false; } - final class StringQualifier implements Coster, ConstantQualifierEquator, QualifierValueEquator { - final long id; - final String value; - final Val celValue; - final TypeAdapter adapter; + @Override + public String toString() { + return "StringQualifier{" + + "id=" + + id + + ", value='" + + value + + '\'' + + ", celValue=" + + celValue + + ", adapter=" + + adapter + + '}'; + } +} - StringQualifier(long id, String value, Val celValue, TypeAdapter adapter) { - this.id = id; - this.value = value; - this.celValue = celValue; - this.adapter = adapter; - } +final class DoubleQualifier implements Coster, ConstantQualifierEquator { + final long id; + final double value; + final Val celValue; + final TypeAdapter adapter; + + DoubleQualifier(long id, double value, Val celValue, TypeAdapter adapter) { + this.id = id; + this.value = value; + this.celValue = celValue; + this.adapter = adapter; + } - /** ID is an implementation of the Qualifier interface method. */ - @Override - public long id() { - return id; - } + /** ID is an implementation of the Qualifier interface method. */ + @Override + public long id() { + return id; + } - /** Qualify implements the Qualifier interface method. */ - @SuppressWarnings("rawtypes") - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - String s = value; - if (obj instanceof Map) { - Map m = (Map) obj; - obj = m.get(s); - if (obj == null) { - if (m.containsKey(s)) { - return NullValue; - } - throw noSuchKeyException(s); + /** Qualify implements the Qualifier interface method. */ + @SuppressWarnings({"rawtypes", "DuplicatedCode"}) + @Override + public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { + double i = value; + if (obj instanceof Map m) { + obj = m.get(i); + if (obj == null) { + obj = m.get((int) i); + } + if (obj == null) { + if (m.containsKey(i) || m.containsKey((int) i)) { + return null; } - } else if (isUnknown(obj)) { - return obj; - } else { - return refResolve(adapter, celValue, obj); + throw noSuchKeyException(i); } return obj; } - - /** Value implements the ConstantQualifier interface */ - @Override - public Val value() { - return celValue; + if (obj.getClass().isArray()) { + int l = Array.getLength(obj); + if (i < 0 || i >= l) { + throw indexOutOfBoundsException(i); + } + obj = Array.get(obj, (int) i); + return obj; } - - /** Cost returns zero for constant field qualifiers */ - @Override - public Cost cost() { - return Cost.None; + if (obj instanceof List list) { + int l = list.size(); + if (i < 0 || i >= l) { + throw indexOutOfBoundsException(i); + } + obj = list.get((int) i); + return obj; + } + if (isUnknown(obj)) { + return obj; } + return refResolve(adapter, celValue, obj); + } - @Override - public boolean qualifierValueEquals(Object value) { - if (value instanceof String) { - return this.value.equals(value); - } + /** Value implements the ConstantQualifier interface */ + @Override + public Val value() { + return celValue; + } + + /** Cost returns zero for constant field qualifiers */ + @Override + public Cost cost() { + return Cost.None; + } + + @Override + public boolean qualifierValueEquals(Object value) { + if (value instanceof ULong) { return false; } - - @Override - public String toString() { - return "StringQualifier{" - + "id=" - + id - + ", value='" - + value - + '\'' - + ", celValue=" - + celValue - + ", adapter=" - + adapter - + '}'; + if (value instanceof Number) { + return this.value == ((Number) value).doubleValue(); } + return false; } - final class DoubleQualifier implements Coster, ConstantQualifierEquator { - final long id; - final double value; - final Val celValue; - final TypeAdapter adapter; + @Override + public String toString() { + return "DoubleQualifier{" + + "id=" + + id + + ", value=" + + value + + ", celValue=" + + celValue + + ", adapter=" + + adapter + + '}'; + } +} - DoubleQualifier(long id, double value, Val celValue, TypeAdapter adapter) { - this.id = id; - this.value = value; - this.celValue = celValue; - this.adapter = adapter; - } +final class IntQualifier implements Coster, ConstantQualifierEquator { + final long id; + final long value; + final Val celValue; + final TypeAdapter adapter; + + IntQualifier(long id, long value, Val celValue, TypeAdapter adapter) { + this.id = id; + this.value = value; + this.celValue = celValue; + this.adapter = adapter; + } - /** ID is an implementation of the Qualifier interface method. */ - @Override - public long id() { - return id; - } + /** ID is an implementation of the Qualifier interface method. */ + @Override + public long id() { + return id; + } - /** Qualify implements the Qualifier interface method. */ - @SuppressWarnings("rawtypes") - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - double i = value; - if (obj instanceof Map) { - Map m = (Map) obj; - obj = m.get(i); - if (obj == null) { - obj = m.get((int) i); - } - if (obj == null) { - if (m.containsKey(i) || m.containsKey((int) i)) { - return null; - } - throw noSuchKeyException(i); - } - return obj; + /** Qualify implements the Qualifier interface method. */ + @SuppressWarnings({"rawtypes", "DuplicatedCode"}) + @Override + public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { + long i = value; + if (obj instanceof Map m) { + obj = m.get(i); + if (obj == null) { + obj = m.get((int) i); } - if (obj.getClass().isArray()) { - int l = Array.getLength(obj); - if (i < 0 || i >= l) { - throw indexOutOfBoundsException(i); + if (obj == null) { + if (m.containsKey(i) || m.containsKey((int) i)) { + return null; } - obj = Array.get(obj, (int) i); - return obj; + throw noSuchKeyException(i); } - if (obj instanceof List) { - List list = (List) obj; - int l = list.size(); - if (i < 0 || i >= l) { - throw indexOutOfBoundsException(i); - } - obj = list.get((int) i); - return obj; + return obj; + } + if (obj.getClass().isArray()) { + int l = Array.getLength(obj); + if (i < 0 || i >= l) { + throw indexOutOfBoundsException(i); } - if (isUnknown(obj)) { - return obj; + obj = Array.get(obj, (int) i); + return obj; + } + if (obj instanceof List list) { + int l = list.size(); + if (i < 0 || i >= l) { + throw indexOutOfBoundsException(i); } - return refResolve(adapter, celValue, obj); + obj = list.get((int) i); + return obj; } - - /** Value implements the ConstantQualifier interface */ - @Override - public Val value() { - return celValue; + if (isUnknown(obj)) { + return obj; } + return refResolve(adapter, celValue, obj); + } - /** Cost returns zero for constant field qualifiers */ - @Override - public Cost cost() { - return Cost.None; - } + /** Value implements the ConstantQualifier interface */ + @Override + public Val value() { + return celValue; + } - @Override - public boolean qualifierValueEquals(Object value) { - if (value instanceof ULong) { - return false; - } - if (value instanceof Number) { - return this.value == ((Number) value).doubleValue(); - } + /** Cost returns zero for constant field qualifiers */ + @Override + public Cost cost() { + return Cost.None; + } + + @Override + public boolean qualifierValueEquals(Object value) { + if (value instanceof ULong) { return false; } - - @Override - public String toString() { - return "DoubleQualifier{" - + "id=" - + id - + ", value=" - + value - + ", celValue=" - + celValue - + ", adapter=" - + adapter - + '}'; + if (value instanceof Number) { + return this.value == ((Number) value).longValue(); } + return false; } - final class IntQualifier implements Coster, ConstantQualifierEquator { - final long id; - final long value; - final Val celValue; - final TypeAdapter adapter; + @Override + public String toString() { + return "IntQualifier{" + + "id=" + + id + + ", value=" + + value + + ", celValue=" + + celValue + + ", adapter=" + + adapter + + '}'; + } +} - IntQualifier(long id, long value, Val celValue, TypeAdapter adapter) { - this.id = id; - this.value = value; - this.celValue = celValue; - this.adapter = adapter; - } +final class UintQualifier implements Coster, ConstantQualifierEquator { + final long id; + final long value; + final Val celValue; + final TypeAdapter adapter; + + UintQualifier(long id, long value, Val celValue, TypeAdapter adapter) { + this.id = id; + this.value = value; + this.celValue = celValue; + this.adapter = adapter; + } - /** ID is an implementation of the Qualifier interface method. */ - @Override - public long id() { - return id; - } + /** ID is an implementation of the Qualifier interface method. */ + @Override + public long id() { + return id; + } - /** Qualify implements the Qualifier interface method. */ - @SuppressWarnings("rawtypes") - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - long i = value; - if (obj instanceof Map) { - Map m = (Map) obj; - obj = m.get(i); - if (obj == null) { - obj = m.get((int) i); - } - if (obj == null) { - if (m.containsKey(i) || m.containsKey((int) i)) { - return null; - } - throw noSuchKeyException(i); - } - return obj; - } - if (obj.getClass().isArray()) { - int l = Array.getLength(obj); - if (i < 0 || i >= l) { - throw indexOutOfBoundsException(i); - } - obj = Array.get(obj, (int) i); - return obj; - } - if (obj instanceof List) { - List list = (List) obj; - int l = list.size(); - if (i < 0 || i >= l) { - throw indexOutOfBoundsException(i); - } - obj = list.get((int) i); - return obj; + /** Qualify implements the Qualifier interface method. */ + @SuppressWarnings("rawtypes") + @Override + public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { + long i = value; + if (obj instanceof Map m) { + obj = m.get(ULong.valueOf(i)); + if (obj == null) { + throw noSuchKeyException(i); } - if (isUnknown(obj)) { - return obj; + return obj; + } + if (obj.getClass().isArray()) { + int l = Array.getLength(obj); + if (i < 0 && i >= l) { + throw indexOutOfBoundsException(i); } - return refResolve(adapter, celValue, obj); + obj = Array.get(obj, (int) i); + return obj; } - - /** Value implements the ConstantQualifier interface */ - @Override - public Val value() { - return celValue; + if (isUnknown(obj)) { + return obj; } + return refResolve(adapter, celValue, obj); + } - /** Cost returns zero for constant field qualifiers */ - @Override - public Cost cost() { - return Cost.None; - } + /** Value implements the ConstantQualifier interface */ + @Override + public Val value() { + return celValue; + } - @Override - public boolean qualifierValueEquals(Object value) { - if (value instanceof ULong) { - return false; - } - if (value instanceof Number) { - return this.value == ((Number) value).longValue(); - } - return false; - } + /** Cost returns zero for constant field qualifiers */ + @Override + public Cost cost() { + return Cost.None; + } - @Override - public String toString() { - return "IntQualifier{" - + "id=" - + id - + ", value=" - + value - + ", celValue=" - + celValue - + ", adapter=" - + adapter - + '}'; + @Override + public boolean qualifierValueEquals(Object value) { + if (value instanceof ULong) { + return this.value == ((ULong) value).longValue(); } + return false; } - final class UintQualifier implements Coster, ConstantQualifierEquator { - final long id; - final long value; - final Val celValue; - final TypeAdapter adapter; + @Override + public String toString() { + return "UintQualifier{" + + "id=" + + id + + ", value=" + + value + + ", celValue=" + + celValue + + ", adapter=" + + adapter + + '}'; + } +} - UintQualifier(long id, long value, Val celValue, TypeAdapter adapter) { - this.id = id; - this.value = value; - this.celValue = celValue; - this.adapter = adapter; - } +final class BoolQualifier implements Coster, ConstantQualifierEquator { + final long id; + final boolean value; + final Val celValue; + final TypeAdapter adapter; + + BoolQualifier(long id, boolean value, Val celValue, TypeAdapter adapter) { + this.id = id; + this.value = value; + this.celValue = celValue; + this.adapter = adapter; + } - /** ID is an implementation of the Qualifier interface method. */ - @Override - public long id() { - return id; - } + /** ID is an implementation of the Qualifier interface method. */ + @Override + public long id() { + return id; + } - /** Qualify implements the Qualifier interface method. */ - @SuppressWarnings("rawtypes") - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - long i = value; - if (obj instanceof Map) { - Map m = (Map) obj; - obj = m.get(ULong.valueOf(i)); - if (obj == null) { - throw noSuchKeyException(i); - } - return obj; - } - if (obj.getClass().isArray()) { - int l = Array.getLength(obj); - if (i < 0 && i >= l) { - throw indexOutOfBoundsException(i); + /** Qualify implements the Qualifier interface method. */ + @SuppressWarnings("rawtypes") + @Override + public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { + boolean b = value; + if (obj instanceof Map m) { + obj = m.get(b); + if (obj == null) { + if (m.containsKey(b)) { + return null; } - obj = Array.get(obj, (int) i); - return obj; - } - if (isUnknown(obj)) { - return obj; + throw noSuchKeyException(b); } + } else if (isUnknown(obj)) { + return obj; + } else { return refResolve(adapter, celValue, obj); } + return obj; + } - /** Value implements the ConstantQualifier interface */ - @Override - public Val value() { - return celValue; - } - - /** Cost returns zero for constant field qualifiers */ - @Override - public Cost cost() { - return Cost.None; - } + /** Value implements the ConstantQualifier interface */ + @Override + public Val value() { + return celValue; + } - @Override - public boolean qualifierValueEquals(Object value) { - if (value instanceof ULong) { - return this.value == ((ULong) value).longValue(); - } - return false; - } + /** Cost returns zero for constant field qualifiers */ + @Override + public Cost cost() { + return Cost.None; + } - @Override - public String toString() { - return "UintQualifier{" - + "id=" - + id - + ", value=" - + value - + ", celValue=" - + celValue - + ", adapter=" - + adapter - + '}'; + @Override + public boolean qualifierValueEquals(Object value) { + if (value instanceof Boolean) { + return this.value == (Boolean) value; } + return false; } - final class BoolQualifier implements Coster, ConstantQualifierEquator { - final long id; - final boolean value; - final Val celValue; - final TypeAdapter adapter; + @Override + public String toString() { + return "BoolQualifier{" + + "id=" + + id + + ", value=" + + value + + ", celValue=" + + celValue + + ", adapter=" + + adapter + + '}'; + } +} - BoolQualifier(long id, boolean value, Val celValue, TypeAdapter adapter) { - this.id = id; - this.value = value; - this.celValue = celValue; - this.adapter = adapter; - } +/** + * Not actually a qualifier, but conformance-tests require this, although it's actually an error + * condition. + */ +record NullQualifier(long id, Val celValue, TypeAdapter adapter) + implements Coster, ConstantQualifierEquator { - /** ID is an implementation of the Qualifier interface method. */ - @Override - public long id() { - return id; - } + /** ID is an implementation of the Qualifier interface method. */ + @Override + public long id() { + return id; + } - /** Qualify implements the Qualifier interface method. */ - @SuppressWarnings("rawtypes") - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - boolean b = value; - if (obj instanceof Map) { - Map m = (Map) obj; - obj = m.get(b); - if (obj == null) { - if (m.containsKey(b)) { - return null; - } - throw noSuchKeyException(b); - } - } else if (isUnknown(obj)) { - return obj; - } else { - return refResolve(adapter, celValue, obj); - } - return obj; - } + /** Qualify implements the Qualifier interface method. */ + @Override + public Object qualify(Activation vars, Object obj) { + return null; + } - /** Value implements the ConstantQualifier interface */ - @Override - public Val value() { - return celValue; - } + /** Value implements the ConstantQualifier interface */ + @Override + public Val value() { + return NullValue; + } - /** Cost returns zero for constant field qualifiers */ - @Override - public Cost cost() { - return Cost.None; - } + /** Cost returns zero for constant field qualifiers */ + @Override + public Cost cost() { + return Cost.None; + } - @Override - public boolean qualifierValueEquals(Object value) { - if (value instanceof Boolean) { - return this.value == (Boolean) value; - } - return false; - } + @Override + public boolean qualifierValueEquals(Object value) { + return value == null || value == NullValue; + } - @Override - public String toString() { - return "BoolQualifier{" - + "id=" - + id - + ", value=" - + value - + ", celValue=" - + celValue - + ", adapter=" - + adapter - + '}'; - } + @Override + public String toString() { + return "NullQualifier{" + "id=" + id + ", celValue=" + celValue + ", adapter=" + adapter + '}'; } +} - /** - * Not actually a qualifier, but conformance-tests require this, although it's actually an error - * condition. - */ - final class NullQualifier implements Coster, ConstantQualifierEquator { - final long id; - final Val celValue; - final TypeAdapter adapter; - - NullQualifier(long id, Val celValue, TypeAdapter adapter) { - this.id = id; - this.celValue = celValue; - this.adapter = adapter; - } +/** + * fieldQualifier indicates that the qualification is a well-defined field with a known field type. + * When the field type is known this can be used to improve the speed and efficiency of field + * resolution. + */ +record FieldQualifier( + long id, + String name, + FieldType fieldType, + TypeAdapter adapter, + CheckedAggregateMaterializer aggregateMaterializer) + implements Coster, ConstantQualifierEquator, ValQualifier { + FieldQualifier(long id, String name, FieldType fieldType, TypeAdapter adapter) { + this(id, name, fieldType, adapter, null); + } - /** ID is an implementation of the Qualifier interface method. */ - @Override - public long id() { - return id; - } + /** ID is an implementation of the Qualifier interface method. */ + @Override + public long id() { + return id; + } - /** Qualify implements the Qualifier interface method. */ - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - return null; + /** Qualify implements the Qualifier interface method. */ + @Override + public Object qualify(Activation vars, Object obj) { + if (obj instanceof Val) { + obj = ((Val) obj).value(); } + return fieldType.getFrom.getFrom(obj); + } - /** Value implements the ConstantQualifier interface */ - @Override - public Val value() { - return NullValue; - } + @Override + public Val qualifyToVal(Activation vars, Object obj) { + Object value = qualify(vars, obj); + return aggregateMaterializer != null + ? aggregateMaterializer.materialize(value) + : adapter.nativeToValue(value); + } - /** Cost returns zero for constant field qualifiers */ - @Override - public Cost cost() { - return Cost.None; - } + /** Value implements the ConstantQualifier interface */ + @Override + public Val value() { + return stringOf(name); + } - @Override - public boolean qualifierValueEquals(Object value) { - return value == null || value == NullValue; - } + /** Cost returns zero for constant field qualifiers */ + @Override + public Cost cost() { + return Cost.None; + } - @Override - public String toString() { - return "NullQualifier{" - + "id=" - + id - + ", celValue=" - + celValue - + ", adapter=" - + adapter - + '}'; + @Override + public boolean qualifierValueEquals(Object value) { + if (value instanceof String) { + return this.name.equals(value); } + return false; } - /** - * fieldQualifier indicates that the qualification is a well-defined field with a known field - * type. When the field type is known this can be used to improve the speed and efficiency of - * field resolution. - */ - final class FieldQualifier implements Coster, ConstantQualifierEquator, ValQualifier { - final long id; - final String name; - final FieldType fieldType; - final TypeAdapter adapter; - - FieldQualifier(long id, String name, FieldType fieldType, TypeAdapter adapter) { - this.id = id; - this.name = name; - this.fieldType = fieldType; - this.adapter = adapter; - } + @Override + public String toString() { + return "FieldQualifier{" + + "id=" + + id + + ", name='" + + name + + '\'' + + ", fieldType=" + + fieldType + + ", adapter=" + + adapter + + '}'; + } +} - /** ID is an implementation of the Qualifier interface method. */ - @Override - public long id() { - return id; - } +/** + * Exact aggregate field qualifier that deliberately retains the raw host value for a checked + * aggregate source node. + */ +final class RawExactAggregateFieldQualifier implements Coster, ConstantQualifierEquator { + private final FieldQualifier delegate; - /** Qualify implements the Qualifier interface method. */ - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - if (obj instanceof Val) { - obj = ((Val) obj).value(); - } - return fieldType.getFrom.getFrom(obj); - } + RawExactAggregateFieldQualifier(long id, String name, FieldType fieldType, TypeAdapter adapter) { + this.delegate = new FieldQualifier(id, name, fieldType, adapter); + } - @Override - public Val qualifyToVal(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - return adapter.nativeToValue(qualify(vars, obj)); - } + @Override + public long id() { + return delegate.id(); + } - /** Value implements the ConstantQualifier interface */ - @Override - public Val value() { - return stringOf(name); - } + @Override + public Object qualify(Activation vars, Object obj) { + return delegate.qualify(vars, obj); + } - /** Cost returns zero for constant field qualifiers */ - @Override - public Cost cost() { - return Cost.None; - } + @Override + public Val value() { + return delegate.value(); + } - @Override - public boolean qualifierValueEquals(Object value) { - if (value instanceof String) { - return this.name.equals(value); - } - return false; - } + @Override + public Cost cost() { + return delegate.cost(); + } - @Override - public String toString() { - return "FieldQualifier{" - + "id=" - + id - + ", name='" - + name - + '\'' - + ", fieldType=" - + fieldType - + ", adapter=" - + adapter - + '}'; - } + @Override + public boolean qualifierValueEquals(Object value) { + return delegate.qualifierValueEquals(value); } - /** - * refResolve attempts to convert the value to a CEL value and then uses reflection methods to try - * and resolve the qualifier. - */ - static Val refResolve(TypeAdapter adapter, Val idx, Object obj) { - Val celVal = adapter.nativeToValue(obj); - if (celVal instanceof Mapper) { - Mapper mapper = (Mapper) celVal; - Val elem = mapper.find(idx); - if (elem == null) { - return noSuchKey(idx); - } - return elem; - } - if (celVal instanceof Indexer) { - Indexer indexer = (Indexer) celVal; - return indexer.get(idx); - } - if (isUnknown(celVal)) { - return celVal; - } - // TODO: If the types.Err value contains more than just an error message at some point in the - // future, then it would be reasonable to return error values as ref.Val types rather than - // simple go error types. - throwErrorAsIllegalStateException(celVal); - return noSuchOverload(celVal, "ref-resolve", null); + @Override + public String toString() { + return "RawExactAggregateFieldQualifier{" + "delegate=" + delegate + '}'; } } diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/AttributePattern.java b/core/src/main/java/org/projectnessie/cel/interpreter/AttributePattern.java index 44788c08..b8281d38 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/AttributePattern.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/AttributePattern.java @@ -165,8 +165,7 @@ public boolean matches(Qualifier q) { if (wildcard) { return true; } - if (q instanceof QualifierValueEquator) { - QualifierValueEquator qve = (QualifierValueEquator) q; + if (q instanceof QualifierValueEquator qve) { return qve.qualifierValueEquals(value); } return false; @@ -419,8 +418,7 @@ public Object resolve(org.projectnessie.cel.interpreter.Activation vars) { @Override public Object tryResolve(org.projectnessie.cel.interpreter.Activation vars) { long id = attr.id(); - if (vars instanceof PartialActivation) { - PartialActivation partial = (PartialActivation) vars; + if (vars instanceof PartialActivation partial) { Object unk = fac.matchesUnknownPatterns(partial, id, candidateVariableNames(), qualifiers); if (unk != null) { return unk; diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/CheckedAggregateMaterializer.java b/core/src/main/java/org/projectnessie/cel/interpreter/CheckedAggregateMaterializer.java new file mode 100644 index 00000000..e991e9ba --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/CheckedAggregateMaterializer.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.Err.newErr; + +import com.google.api.expr.v1alpha1.Type; +import java.util.Objects; +import org.projectnessie.cel.common.types.ref.ExactAggregateTypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; + +/** Evaluation-independent checked aggregate materialization selected by the planner. */ +final class CheckedAggregateMaterializer { + private final ExactAggregateTypeAdapter adapter; + private final Type checkedType; + + CheckedAggregateMaterializer(ExactAggregateTypeAdapter adapter, Type checkedType) { + this.adapter = Objects.requireNonNull(adapter, "adapter"); + this.checkedType = Objects.requireNonNull(checkedType, "checkedType"); + } + + Val materialize(Object value) { + try { + return adapter.nativeAggregateToValue(value, checkedType); + } catch (Exception failure) { + return newErr(failure, failure.toString()); + } + } + + Val materializeListElement(Object value) { + Val list = materialize(new Object[] {value}); + return list instanceof Lister lister ? lister.nativeGetAt(0) : list; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/DefaultDispatcher.java b/core/src/main/java/org/projectnessie/cel/interpreter/DefaultDispatcher.java new file mode 100644 index 00000000..17380a2d --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/DefaultDispatcher.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.projectnessie.cel.interpreter.functions.Overload; + +/** Default dispatcher backed by an overload map. */ +final class DefaultDispatcher implements Dispatcher { + private final Dispatcher parent; + private final Map overloads; + + DefaultDispatcher(Dispatcher parent, Map overloads) { + this.parent = parent; + this.overloads = overloads; + } + + @Override + public void add(Overload... overloads) { + for (Overload overload : overloads) { + if (this.overloads.containsKey(overload.operator)) { + throw new IllegalArgumentException( + String.format("overload already exists '%s'", overload.operator)); + } + this.overloads.put(overload.operator, overload); + } + } + + @Override + public Overload findOverload(String overload) { + Overload result = overloads.get(overload); + if (result != null) { + return result; + } + return parent != null ? parent.findOverload(overload) : null; + } + + @Override + public String[] overloadIds() { + List result = new ArrayList<>(overloads.keySet()); + if (parent != null) { + Collections.addAll(result, parent.overloadIds()); + } + return result.toArray(new String[0]); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/Dispatcher.java b/core/src/main/java/org/projectnessie/cel/interpreter/Dispatcher.java index 2b7e1cae..db39d18b 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/Dispatcher.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Dispatcher.java @@ -15,11 +15,7 @@ */ package org.projectnessie.cel.interpreter; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; -import java.util.List; -import java.util.Map; import org.projectnessie.cel.interpreter.functions.Overload; /** Dispatcher resolves function calls to their appropriate overload. */ @@ -46,50 +42,4 @@ static Dispatcher newDispatcher() { static Dispatcher extendDispatcher(Dispatcher parent) { return new DefaultDispatcher(parent, new HashMap<>()); } - - /** defaultDispatcher struct which contains an overload map. */ - final class DefaultDispatcher implements Dispatcher { - private final Dispatcher parent; - private final Map overloads; - - DefaultDispatcher(Dispatcher parent, Map overloads) { - this.parent = parent; - this.overloads = overloads; - } - - /** Add implements the Dispatcher.Add interface method. */ - @Override - public void add(Overload... overloads) { - for (Overload o : overloads) { - - // add the overload unless an overload of the same name has already been provided. - if (this.overloads.containsKey(o.operator)) { - throw new IllegalArgumentException( - String.format("overload already exists '%s'", o.operator)); - } - // index the overload by function name. - this.overloads.put(o.operator, o); - } - } - - /** FindOverload implements the Dispatcher.FindOverload interface method. */ - @Override - public Overload findOverload(String overload) { - Overload o = overloads.get(overload); - if (o != null) { - return o; - } - return parent != null ? parent.findOverload(overload) : null; - } - - /** OverloadIds implements the Dispatcher interface method. */ - @Override - public String[] overloadIds() { - List r = new ArrayList<>(overloads.keySet()); - if (parent != null) { - Collections.addAll(r, parent.overloadIds()); - } - return r.toArray(new String[0]); - } - } } diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedAggregateNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedAggregateNodes.java new file mode 100644 index 00000000..919825ce --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedAggregateNodes.java @@ -0,0 +1,398 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.Err.valOrErr; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.Util.isUnknownOrError; +import static org.projectnessie.cel.interpreter.Activation.emptyActivation; +import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; +import static org.projectnessie.cel.interpreter.Coster.costOf; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.IterableT; +import org.projectnessie.cel.common.types.IteratorT; +import org.projectnessie.cel.common.types.ListT; +import org.projectnessie.cel.common.types.OptionalT; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; +import org.projectnessie.cel.common.types.traits.Mapper; +import org.projectnessie.cel.common.types.traits.Sizer; +import org.projectnessie.cel.common.types.traits.Trait; + +class EvalList extends AbstractEval implements Coster { + final Interpretable[] elems; + final boolean[] optionalIndices; + private final TypeAdapter adapter; + + EvalList(long id, Interpretable[] elems, TypeAdapter adapter) { + this(id, elems, new boolean[elems.length], adapter); + } + + EvalList(long id, Interpretable[] elems, boolean[] optionalIndices, TypeAdapter adapter) { + super(id); + this.elems = elems; + this.optionalIndices = optionalIndices; + this.adapter = adapter; + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(Activation ctx) { + List elemVals = new ArrayList<>(elems.length); + // If any argument is unknown or error early terminate. + for (int i = 0; i < elems.length; i++) { + Interpretable elem = elems[i]; + Val elemVal = elem.eval(ctx); + if (isUnknownOrError(elemVal)) { + return elemVal; + } + if (optionalIndices[i]) { + if (!(elemVal instanceof OptionalT optional)) { + return newErr("optional list element is not optional"); + } + if (!optional.hasValue()) { + continue; + } + elemVal = optional.getValue(); + } + elemVals.add(elemVal); + } + return adapter.nativeToValue(elemVals.toArray(Val[]::new)); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return Interpretable.sumOfCost(elems); + } + + @Override + public String toString() { + return "EvalList{" + "id=" + id + ", elems=" + Arrays.toString(elems) + '}'; + } +} + +class EvalListFold extends AbstractEval implements Coster { + final String iterVar; + final String iterVar2; + final Interpretable iterRange; + final Interpretable filter; + final Interpretable transform; + final TypeAdapter adapter; + + EvalListFold( + long id, + String iterVar, + String iterVar2, + Interpretable iterRange, + Interpretable filter, + Interpretable transform, + TypeAdapter adapter) { + super(id); + this.iterVar = iterVar; + this.iterVar2 = iterVar2; + this.iterRange = iterRange; + this.filter = filter; + this.transform = transform; + this.adapter = adapter; + } + + @SuppressWarnings("DuplicatedCode") + @Override + public Val eval(Activation ctx) { + Val foldRange = iterRange.eval(ctx); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return valOrErr( + foldRange, "got '%s', expected iterable type", foldRange.getClass().getName()); + } + + VarActivation iterCtx = new VarActivation(); + iterCtx.parent = ctx; + iterCtx.name = iterVar; + VarActivation iterCtx2 = null; + if (!iterVar2.isEmpty()) { + iterCtx2 = new VarActivation(); + iterCtx2.parent = iterCtx; + iterCtx2.name = iterVar2; + } + List values = new ArrayList<>(listCapacity(foldRange)); + IteratorT it = ((IterableT) foldRange).iterator(); + long index = 0L; + var isLister = foldRange instanceof Lister; + var mapper = (foldRange instanceof Mapper m) ? m : null; + while (it.hasNext() == True) { + Val next = it.next(); + Activation loopCtx = iterCtx; + if (iterCtx2 != null) { + if (isLister) { + iterCtx.val = intOf(index); + iterCtx2.val = next; + } else if (mapper != null) { + iterCtx.val = next; + iterCtx2.val = mapper.get(next); + } else { + return valOrErr( + foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); + } + loopCtx = iterCtx2; + } else { + iterCtx.val = next; + } + index++; + + if (filter != null) { + Val include = filter.eval(loopCtx); + if (include == False) { + continue; + } + if (include != True) { + return noSuchOverload(null, Operator.Conditional.id, include); + } + } + + Val value = transform.eval(loopCtx); + if (isUnknownOrError(value)) { + return value; + } + values.add(value); + } + return ListT.newValArrayList(adapter, values.toArray(new Val[0])); + } + + int listCapacity(Val foldRange) { + if (foldRange.type().hasTrait(Trait.SizerType)) { + int size = ((Sizer) foldRange).nativeSize(); + if (size > 0) { + return size; + } + } + return 0; + } + + @SuppressWarnings("DuplicatedCode") + @Override + public Cost cost() { + Cost range = estimateCost(iterRange); + Cost result = estimateCost(transform); + if (filter != null) { + result = result.add(estimateCost(filter)); + } + Val foldRange = iterRange.eval(emptyActivation()); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return Cost.Unknown; + } + long rangeCnt; + if (foldRange instanceof Sizer sizer) { + rangeCnt = sizer.nativeSize(); + } else { + rangeCnt = 0L; + IteratorT it = ((IterableT) foldRange).iterator(); + while (it.hasNext() == True) { + it.next(); + rangeCnt++; + } + } + return range.add(result.multiply(rangeCnt)); + } + + @Override + public String toString() { + return "EvalListFold{" + + "id=" + + id + + ", iterVar='" + + iterVar + + '\'' + + ", iterVar2='" + + iterVar2 + + '\'' + + ", iterRange=" + + iterRange + + ", filter=" + + filter + + ", transform=" + + transform + + '}'; + } +} + +class EvalFold extends AbstractEval implements Coster { + // TODO combine with EvalExhaustiveFold + final String accuVar; + final String iterVar; + final String iterVar2; + final Interpretable iterRange; + final Interpretable accu; + final Interpretable cond; + final Interpretable step; + final Interpretable result; + + EvalFold( + long id, + String accuVar, + Interpretable accu, + String iterVar, + String iterVar2, + Interpretable iterRange, + Interpretable cond, + Interpretable step, + Interpretable result) { + super(id); + this.accuVar = accuVar; + this.iterVar = iterVar; + this.iterVar2 = iterVar2; + this.iterRange = iterRange; + this.accu = accu; + this.cond = cond; + this.step = step; + this.result = result; + } + + /** Eval implements the Interpretable interface method. */ + @SuppressWarnings("DuplicatedCode") + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val foldRange = iterRange.eval(ctx); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return valOrErr( + foldRange, "got '%s', expected iterable type", foldRange.getClass().getName()); + } + // Configure the fold activation with the accumulator initial value. + VarActivation accuCtx = new VarActivation(); + accuCtx.parent = ctx; + accuCtx.name = accuVar; + accuCtx.val = accu.eval(ctx); + VarActivation iterCtx = new VarActivation(); + iterCtx.parent = accuCtx; + iterCtx.name = iterVar; + VarActivation iterCtx2 = null; + if (!iterVar2.isEmpty()) { + iterCtx2 = new VarActivation(); + iterCtx2.parent = iterCtx; + iterCtx2.name = iterVar2; + } + IteratorT it = ((IterableT) foldRange).iterator(); + long index = 0L; + var isLister = foldRange instanceof Lister; + var mapper = (foldRange instanceof Mapper m) ? m : null; + while (it.hasNext() == True) { + // Modify the iter var in the fold activation. + Val next = it.next(); + Activation loopCtx = iterCtx; + if (iterCtx2 != null) { + if (isLister) { + iterCtx.val = intOf(index); + iterCtx2.val = next; + } else if (mapper != null) { + iterCtx.val = next; + iterCtx2.val = mapper.get(next); + } else { + return valOrErr( + foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); + } + loopCtx = iterCtx2; + } else { + iterCtx.val = next; + } + index++; + + // Evaluate the condition, terminate the loop if false. + Val c = cond.eval(loopCtx); + if (c == False) { + break; + } + + // Evalute the evaluation step into accu var. + accuCtx.val = step.eval(loopCtx); + } + // Compute the result. + return result.eval(accuCtx); + } + + /** Cost implements the Coster interface method. */ + @SuppressWarnings("DuplicatedCode") + @Override + public Cost cost() { + // Compute the cost for evaluating iterRange. + Cost i = estimateCost(iterRange); + + // Compute the size of iterRange. If the size depends on the input, return the maximum + // possible + // cost range. + Val foldRange = iterRange.eval(emptyActivation()); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return Cost.Unknown; + } + long rangeCnt; + if (foldRange instanceof Sizer sizer) { + rangeCnt = sizer.nativeSize(); + } else { + rangeCnt = 0L; + IteratorT it = ((IterableT) foldRange).iterator(); + while (it.hasNext() == True) { + it.next(); + rangeCnt++; + } + } + Cost a = estimateCost(accu); + Cost c = estimateCost(cond); + Cost s = estimateCost(step); + Cost r = estimateCost(result); + + // The cond and step costs are multiplied by size(iterRange). The minimum possible cost incurs + // when the evaluation result can be determined by the first iteration. + return i.add(a) + .add(r) + .add(costOf(c.min, c.max * rangeCnt)) + .add(costOf(s.min, s.max * rangeCnt)); + } + + @Override + public String toString() { + return "EvalFold{" + + "id=" + + id + + ", accuVar='" + + accuVar + + '\'' + + ", iterVar='" + + iterVar + + '\'' + + ", iterVar2='" + + iterVar2 + + '\'' + + ", iterRange=" + + iterRange + + ", accu=" + + accu + + ", cond=" + + cond + + ", step=" + + step + + ", result=" + + result + + '}'; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedCallNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedCallNodes.java new file mode 100644 index 00000000..d5fe665e --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedCallNodes.java @@ -0,0 +1,706 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.Err.valOrErr; +import static org.projectnessie.cel.common.types.Util.isUnknownOrError; +import static org.projectnessie.cel.interpreter.Coster.Cost.OneOne; +import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; +import static org.projectnessie.cel.interpreter.Interpretable.receiveVarArgs; +import static org.projectnessie.cel.interpreter.Interpretable.sumOfCost; + +import java.util.Arrays; +import java.util.Objects; +import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.ref.FieldType; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Container; +import org.projectnessie.cel.common.types.traits.FieldTester; +import org.projectnessie.cel.common.types.traits.Receiver; +import org.projectnessie.cel.common.types.traits.Trait; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableCall; +import org.projectnessie.cel.interpreter.functions.FunctionOp; +import org.projectnessie.cel.interpreter.functions.QuaternaryOp; +import org.projectnessie.cel.interpreter.functions.QuinaryOp; +import org.projectnessie.cel.interpreter.functions.TernaryOp; + +/** Package-private established call nodes. */ +final class EvalTestOnly implements Interpretable, Coster { + private final long id; + private final Interpretable op; + private final StringT field; + private final FieldType fieldType; + + EvalTestOnly(long id, Interpretable op, StringT field, FieldType fieldType) { + this.id = id; + this.op = Objects.requireNonNull(op); + this.field = Objects.requireNonNull(field); + this.fieldType = fieldType; + } + + /** ID implements the Interpretable interface method. */ + @Override + public long id() { + return id; + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + // Handle field selection on a proto in the most efficient way possible. + if (fieldType != null) { + if (op instanceof InterpretableAttribute opAttr) { + Object opVal = opAttr.resolve(ctx); + if (opVal instanceof Val refVal) { + opVal = refVal.value(); + } + if (fieldType.isSet.isSet(opVal)) { + return True; + } + return False; + } + } + + Val obj = op.eval(ctx); + if (obj instanceof FieldTester tester) { + return tester.isSet(field); + } + if (obj instanceof Container container) { + return container.contains(field); + } + return valOrErr(obj, "invalid type for field selection."); + } + + /** + * Cost provides the heuristic cost of a `has(field)` macro. The cost has at least 1 for + * determining if the field exists, apart from the cost of accessing the field. + */ + @Override + public Cost cost() { + Cost c = estimateCost(op); + return c.add(OneOne); + } + + @Override + public String toString() { + return "EvalTestOnly{" + "id=" + id + ", field=" + field + '}'; + } +} + +final class EvalZeroArity extends AbstractEval implements InterpretableCall, Coster { + private final String function; + private final String overload; + private final FunctionOp impl; + + EvalZeroArity(long id, String function, String overload, FunctionOp impl) { + super(id); + this.function = Objects.requireNonNull(function); + this.overload = Objects.requireNonNull(overload); + this.impl = impl; + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation activation) { + return impl.invoke(); + } + + /** Cost returns 1 representing the heuristic cost of the function. */ + @Override + public Cost cost() { + return Cost.OneOne; + } + + /** Function implements the InterpretableCall interface method. */ + @Override + public String function() { + return function; + } + + /** OverloadID implements the InterpretableCall interface method. */ + @Override + public String overloadID() { + return overload; + } + + /** Args returns the argument to the unary function. */ + @Override + public Interpretable[] args() { + return new Interpretable[0]; + } + + @Override + public String toString() { + return "EvalZeroArity{" + + "id=" + + id + + ", function='" + + function + + '\'' + + ", overload='" + + overload + + '\'' + + ", impl=" + + impl + + '}'; + } +} + +final class EvalTernary extends AbstractEval implements Coster, InterpretableCall { + private final String function; + private final String overload; + private final Interpretable first; + private final Interpretable second; + private final Interpretable third; + private final Trait trait; + private final TernaryOp impl; + + EvalTernary( + long id, + String function, + String overload, + Interpretable first, + Interpretable second, + Interpretable third, + Trait trait, + TernaryOp impl) { + super(id); + this.function = Objects.requireNonNull(function); + this.overload = Objects.requireNonNull(overload); + this.first = Objects.requireNonNull(first); + this.second = Objects.requireNonNull(second); + this.third = Objects.requireNonNull(third); + this.trait = trait; + this.impl = Objects.requireNonNull(impl); + } + + @SuppressWarnings("DuplicatedCode") + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val firstVal = first.eval(ctx); + if (isUnknownOrError(firstVal)) { + return firstVal; + } + Val secondVal = second.eval(ctx); + if (isUnknownOrError(secondVal)) { + return secondVal; + } + Val thirdVal = third.eval(ctx); + if (isUnknownOrError(thirdVal)) { + return thirdVal; + } + if (trait == null || firstVal.type().hasTrait(trait)) { + return impl.invoke(firstVal, secondVal, thirdVal); + } + if (firstVal.type().hasTrait(Trait.ReceiverType)) { + return ((Receiver) firstVal).receive(function, overload, secondVal, thirdVal); + } + return noSuchOverload(firstVal, function, overload, new Val[] {firstVal, secondVal, thirdVal}); + } + + @Override + public Cost cost() { + return estimateCost(first).add(estimateCost(second)).add(estimateCost(third)).add(OneOne); + } + + @Override + public String function() { + return function; + } + + @Override + public String overloadID() { + return overload; + } + + @Override + public Interpretable[] args() { + return new Interpretable[] {first, second, third}; + } + + @Override + public String toString() { + return "EvalTernary{" + + "id=" + + id + + ", first=" + + first + + ", second=" + + second + + ", third=" + + third + + ", function='" + + function + + '\'' + + ", overload='" + + overload + + '\'' + + ", trait=" + + trait + + ", impl=" + + impl + + '}'; + } +} + +final class EvalQuaternary extends AbstractEval implements Coster, InterpretableCall { + private final String function; + private final String overload; + private final Interpretable first; + private final Interpretable second; + private final Interpretable third; + private final Interpretable fourth; + private final Trait trait; + private final QuaternaryOp impl; + + EvalQuaternary( + long id, + String function, + String overload, + Interpretable first, + Interpretable second, + Interpretable third, + Interpretable fourth, + Trait trait, + QuaternaryOp impl) { + super(id); + this.function = Objects.requireNonNull(function); + this.overload = Objects.requireNonNull(overload); + this.first = Objects.requireNonNull(first); + this.second = Objects.requireNonNull(second); + this.third = Objects.requireNonNull(third); + this.fourth = Objects.requireNonNull(fourth); + this.trait = trait; + this.impl = Objects.requireNonNull(impl); + } + + @SuppressWarnings("DuplicatedCode") + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val firstVal = first.eval(ctx); + if (isUnknownOrError(firstVal)) { + return firstVal; + } + Val secondVal = second.eval(ctx); + if (isUnknownOrError(secondVal)) { + return secondVal; + } + Val thirdVal = third.eval(ctx); + if (isUnknownOrError(thirdVal)) { + return thirdVal; + } + Val fourthVal = fourth.eval(ctx); + if (isUnknownOrError(fourthVal)) { + return fourthVal; + } + if (trait == null || firstVal.type().hasTrait(trait)) { + return impl.invoke(firstVal, secondVal, thirdVal, fourthVal); + } + if (firstVal.type().hasTrait(Trait.ReceiverType)) { + return ((Receiver) firstVal).receive(function, overload, secondVal, thirdVal, fourthVal); + } + return noSuchOverload( + firstVal, function, overload, new Val[] {firstVal, secondVal, thirdVal, fourthVal}); + } + + @Override + public Cost cost() { + return estimateCost(first) + .add(estimateCost(second)) + .add(estimateCost(third)) + .add(estimateCost(fourth)) + .add(OneOne); + } + + @Override + public String function() { + return function; + } + + @Override + public String overloadID() { + return overload; + } + + @Override + public Interpretable[] args() { + return new Interpretable[] {first, second, third, fourth}; + } + + @Override + public String toString() { + return "EvalQuaternary{" + + "id=" + + id + + ", first=" + + first + + ", second=" + + second + + ", third=" + + third + + ", fourth=" + + fourth + + ", function='" + + function + + '\'' + + ", overload='" + + overload + + '\'' + + ", trait=" + + trait + + ", impl=" + + impl + + '}'; + } +} + +final class EvalQuinary extends AbstractEval implements Coster, InterpretableCall { + private final String function; + private final String overload; + private final Interpretable first; + private final Interpretable second; + private final Interpretable third; + private final Interpretable fourth; + private final Interpretable fifth; + private final Trait trait; + private final QuinaryOp impl; + + EvalQuinary( + long id, + String function, + String overload, + Interpretable first, + Interpretable second, + Interpretable third, + Interpretable fourth, + Interpretable fifth, + Trait trait, + QuinaryOp impl) { + super(id); + this.function = Objects.requireNonNull(function); + this.overload = Objects.requireNonNull(overload); + this.first = Objects.requireNonNull(first); + this.second = Objects.requireNonNull(second); + this.third = Objects.requireNonNull(third); + this.fourth = Objects.requireNonNull(fourth); + this.fifth = Objects.requireNonNull(fifth); + this.trait = trait; + this.impl = Objects.requireNonNull(impl); + } + + @SuppressWarnings("DuplicatedCode") + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val firstVal = first.eval(ctx); + if (isUnknownOrError(firstVal)) { + return firstVal; + } + Val secondVal = second.eval(ctx); + if (isUnknownOrError(secondVal)) { + return secondVal; + } + Val thirdVal = third.eval(ctx); + if (isUnknownOrError(thirdVal)) { + return thirdVal; + } + Val fourthVal = fourth.eval(ctx); + if (isUnknownOrError(fourthVal)) { + return fourthVal; + } + Val fifthVal = fifth.eval(ctx); + if (isUnknownOrError(fifthVal)) { + return fifthVal; + } + if (trait == null || firstVal.type().hasTrait(trait)) { + return impl.invoke(firstVal, secondVal, thirdVal, fourthVal, fifthVal); + } + if (firstVal.type().hasTrait(Trait.ReceiverType)) { + return ((Receiver) firstVal) + .receive(function, overload, secondVal, thirdVal, fourthVal, fifthVal); + } + return noSuchOverload( + firstVal, + function, + overload, + new Val[] {firstVal, secondVal, thirdVal, fourthVal, fifthVal}); + } + + @Override + public Cost cost() { + return estimateCost(first) + .add(estimateCost(second)) + .add(estimateCost(third)) + .add(estimateCost(fourth)) + .add(estimateCost(fifth)) + .add(OneOne); + } + + @Override + public String function() { + return function; + } + + @Override + public String overloadID() { + return overload; + } + + @Override + public Interpretable[] args() { + return new Interpretable[] {first, second, third, fourth, fifth}; + } + + @Override + public String toString() { + return "EvalQuinary{" + + "id=" + + id + + ", first=" + + first + + ", second=" + + second + + ", third=" + + third + + ", fourth=" + + fourth + + ", fifth=" + + fifth + + ", function='" + + function + + '\'' + + ", overload='" + + overload + + '\'' + + ", trait=" + + trait + + ", impl=" + + impl + + '}'; + } +} + +final class EvalVarArgs extends AbstractEval implements Coster, InterpretableCall { + private final String function; + private final String overload; + private final Interpretable[] args; + private final Trait trait; + private final FunctionOp impl; + + public EvalVarArgs( + long id, + String function, + String overload, + Interpretable[] args, + Trait trait, + FunctionOp impl) { + super(id); + this.function = Objects.requireNonNull(function); + this.overload = Objects.requireNonNull(overload); + this.args = Objects.requireNonNull(args); + this.trait = trait; + this.impl = impl; + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val[] argVals = new Val[args.length]; + // Early return if any argument to the function is unknown or error. + for (int i = 0; i < args.length; i++) { + Interpretable arg = args[i]; + argVals[i] = arg.eval(ctx); + if (isUnknownOrError(argVals[i])) { + return argVals[i]; + } + } + // If the implementation is bound and the argument value has the right traits required to + // invoke it, then call the implementation. + Val arg0 = argVals[0]; + if (impl != null && (trait == null || arg0.type().hasTrait(trait))) { + return impl.invoke(argVals); + } + // Otherwise, if the argument is a ReceiverType attempt to invoke the receiver method on the + // operand (arg0). + if (arg0.type().hasTrait(Trait.ReceiverType)) { + return receiveVarArgs((Receiver) arg0, function, overload, argVals); + } + return noSuchOverload(arg0, function, overload, argVals); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + Cost c = sumOfCost(args); + return c.add(OneOne); // add cost for function + } + + /** Function implements the InterpretableCall interface method. */ + @Override + public String function() { + return function; + } + + /** OverloadID implements the InterpretableCall interface method. */ + @Override + public String overloadID() { + return overload; + } + + /** Args returns the argument to the unary function. */ + @Override + public Interpretable[] args() { + return args; + } + + @Override + public String toString() { + return "EvalVarArgs{" + + "id=" + + id + + ", function='" + + function + + '\'' + + ", overload='" + + overload + + '\'' + + ", args=" + + Arrays.toString(args) + + ", trait=" + + trait + + ", impl=" + + impl + + '}'; + } +} + +final class EvalReceiverVarArgs extends AbstractEval implements Coster, InterpretableCall { + private final String function; + private final String overload; + private final Interpretable[] args; + + public EvalReceiverVarArgs(long id, String function, String overload, Interpretable[] args) { + super(id); + this.function = Objects.requireNonNull(function); + this.overload = Objects.requireNonNull(overload); + this.args = Objects.requireNonNull(args); + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val arg0 = args[0].eval(ctx); + if (isUnknownOrError(arg0)) { + return arg0; + } + + return switch (args.length) { + case 3 -> evalReceiverTail2(ctx, arg0); + case 4 -> evalReceiverTail3(ctx, arg0); + default -> evalReceiverTail(ctx, arg0); + }; + } + + private Val evalReceiverTail2(org.projectnessie.cel.interpreter.Activation ctx, Val arg0) { + Val arg1 = args[1].eval(ctx); + if (isUnknownOrError(arg1)) { + return arg1; + } + Val arg2 = args[2].eval(ctx); + if (isUnknownOrError(arg2)) { + return arg2; + } + return receiveOrNoSuchOverload(arg0, arg1, arg2); + } + + private Val evalReceiverTail3(org.projectnessie.cel.interpreter.Activation ctx, Val arg0) { + Val arg1 = args[1].eval(ctx); + if (isUnknownOrError(arg1)) { + return arg1; + } + Val arg2 = args[2].eval(ctx); + if (isUnknownOrError(arg2)) { + return arg2; + } + Val arg3 = args[3].eval(ctx); + if (isUnknownOrError(arg3)) { + return arg3; + } + return receiveOrNoSuchOverload(arg0, arg1, arg2, arg3); + } + + private Val evalReceiverTail(org.projectnessie.cel.interpreter.Activation ctx, Val arg0) { + Val[] tailArgs = new Val[args.length - 1]; + for (int i = 1; i < args.length; i++) { + Val argVal = args[i].eval(ctx); + if (isUnknownOrError(argVal)) { + return argVal; + } + tailArgs[i - 1] = argVal; + } + return receiveOrNoSuchOverload(arg0, tailArgs); + } + + private Val receiveOrNoSuchOverload(Val arg0, Val... tailArgs) { + if (arg0.type().hasTrait(Trait.ReceiverType)) { + return ((Receiver) arg0).receive(function, overload, tailArgs); + } + return noSuchOverload(arg0, function, overload, tailArgs); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + Cost c = sumOfCost(args); + return c.add(OneOne); // add cost for function + } + + /** Function implements the InterpretableCall interface method. */ + @Override + public String function() { + return function; + } + + /** OverloadID implements the InterpretableCall interface method. */ + @Override + public String overloadID() { + return overload; + } + + /** Args returns the argument to the unary function. */ + @Override + public Interpretable[] args() { + return args; + } + + @Override + public String toString() { + return "EvalReceiverVarArgs{" + + "id=" + + id + + ", function='" + + function + + '\'' + + ", overload='" + + overload + + '\'' + + ", args=" + + Arrays.toString(args) + + '}'; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedConstructionNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedConstructionNodes.java new file mode 100644 index 00000000..55bff9d1 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedConstructionNodes.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.Util.isUnknownOrError; +import static org.projectnessie.cel.interpreter.Interpretable.sumOfCost; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.projectnessie.cel.common.types.MapT; +import org.projectnessie.cel.common.types.OptionalT; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeProvider; +import org.projectnessie.cel.common.types.ref.Val; + +/** Package-private established construction nodes. */ +final class EvalMap extends AbstractEval implements Coster { + final Interpretable[] keys; + final Interpretable[] vals; + final boolean[] optionalEntries; + private final TypeAdapter adapter; + + EvalMap(long id, Interpretable[] keys, Interpretable[] vals, TypeAdapter adapter) { + this(id, keys, vals, new boolean[keys.length], adapter); + } + + EvalMap( + long id, + Interpretable[] keys, + Interpretable[] vals, + boolean[] optionalEntries, + TypeAdapter adapter) { + super(id); + this.keys = keys; + this.vals = vals; + this.optionalEntries = optionalEntries; + this.adapter = adapter; + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Map entries = new HashMap<>(keys.length * 4 / 3 + 1); + // If any argument is unknown or error early terminate. + for (int i = 0; i < keys.length; i++) { + Interpretable key = keys[i]; + Val keyVal = key.eval(ctx); + if (isUnknownOrError(keyVal)) { + return keyVal; + } + if (!MapT.isSupportedLiteralKeyType(keyVal)) { + return newErr("unsupported key type"); + } + Val valVal = vals[i].eval(ctx); + if (isUnknownOrError(valVal)) { + return valVal; + } + if (optionalEntries[i]) { + if (!(valVal instanceof OptionalT optional)) { + return newErr("optional map entry is not optional"); + } + if (!optional.hasValue()) { + continue; + } + valVal = optional.getValue(); + } + if (entries.putIfAbsent(keyVal, valVal) != null) { + // Prevent duplicate keys, error out. + return newErr("Failed with repeated key"); + } + } + return MapT.newWrappedMap(adapter, entries); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + Cost k = sumOfCost(keys); + Cost v = sumOfCost(vals); + return k.add(v); + } + + @Override + public String toString() { + return "EvalMap{" + + "id=" + + id + + ", keys=" + + Arrays.toString(keys) + + ", vals=" + + Arrays.toString(vals) + + '}'; + } +} + +final class EvalObj extends AbstractEval implements Coster { + private final String typeName; + private final String[] fields; + private final Interpretable[] vals; + private final boolean[] optionalEntries; + private final TypeProvider provider; + + EvalObj( + long id, + String typeName, + String[] fields, + Interpretable[] vals, + boolean[] optionalEntries, + TypeProvider provider) { + super(id); + this.typeName = Objects.requireNonNull(typeName); + this.fields = Objects.requireNonNull(fields); + this.vals = Objects.requireNonNull(vals); + this.optionalEntries = optionalEntries; + this.provider = Objects.requireNonNull(provider); + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Map fieldVals = new HashMap<>(); + // If any argument is unknown or error early terminate. + for (int i = 0; i < fields.length; i++) { + String field = fields[i]; + Val val = vals[i].eval(ctx); + if (isUnknownOrError(val)) { + return val; + } + if (optionalEntries[i]) { + if (!(val instanceof OptionalT optional)) { + return newErr("optional message field is not optional"); + } + if (!optional.hasValue()) { + continue; + } + val = optional.getValue(); + } + fieldVals.put(field, val); + } + return provider.newValue(typeName, fieldVals); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return sumOfCost(vals); + } + + @Override + public String toString() { + return "EvalObj{" + + "id=" + + id + + ", typeName='" + + typeName + + '\'' + + ", fields=" + + Arrays.toString(fields) + + ", vals=" + + Arrays.toString(vals) + + ", provider=" + + provider + + '}'; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedControlNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedControlNodes.java new file mode 100644 index 00000000..3b8caf92 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedControlNodes.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.Err.isError; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.UnknownT.isUnknown; + +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.ref.Val; + +class EvalOr extends AbstractEvalLhsRhs { + // TODO combine with EvalExhaustiveOr + EvalOr(long id, Interpretable lhs, Interpretable rhs) { + super(id, lhs, rhs); + } + + @SuppressWarnings("DuplicatedCode") + @Override + public Val eval(Activation ctx) { + // short-circuit lhs. + Val lVal = lhs.eval(ctx); + if (lVal == True) { + return True; + } + // short-circuit on rhs. + Val rVal = rhs.eval(ctx); + if (rVal == True) { + return True; + } + // return if both sides are bool false. + if (lVal == False && rVal == False) { + return False; + } + // TODO: return both values as a set if both are unknown or error. + // prefer left unknown to right unknown. + if (isUnknown(lVal)) { + return lVal; + } + if (isUnknown(rVal)) { + return rVal; + } + // If the left-hand side is non-boolean return it as the error. + if (isError(lVal)) { + return lVal; + } + return noSuchOverload(lVal, Operator.LogicalOr.id, rVal); + } + + @Override + public Cost cost() { + return Interpretable.calShortCircuitBinaryOpsCost(lhs, rhs); + } + + @Override + public String toString() { + return "EvalOr{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; + } +} + +class EvalAnd extends AbstractEvalLhsRhs { + // TODO combine with EvalExhaustiveAnd + EvalAnd(long id, Interpretable lhs, Interpretable rhs) { + super(id, lhs, rhs); + } + + @Override + public Val eval(Activation ctx) { + // short-circuit lhs. + Val lVal = lhs.eval(ctx); + if (lVal == False) { + return False; + } + // short-circuit on rhs. + Val rVal = rhs.eval(ctx); + if (rVal == False) { + return False; + } + // return if both sides are bool true. + if (lVal == True && rVal == True) { + return True; + } + // TODO: return both values as a set if both are unknown or error. + // prefer left unknown to right unknown. + if (isUnknown(lVal)) { + return lVal; + } + if (isUnknown(rVal)) { + return rVal; + } + // If the left-hand side is non-boolean return it as the error. + if (isError(lVal)) { + return lVal; + } + return noSuchOverload(lVal, Operator.LogicalAnd.id, rVal); + } + + @Override + public Cost cost() { + return Interpretable.calShortCircuitBinaryOpsCost(lhs, rhs); + } + + @Override + public String toString() { + return "EvalAnd{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedDecoratorNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedDecoratorNodes.java new file mode 100644 index 00000000..f2a79161 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedDecoratorNodes.java @@ -0,0 +1,464 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.Err.isError; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.UnknownT.isUnknown; +import static org.projectnessie.cel.common.types.Util.isUnknownOrError; +import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; +import static org.projectnessie.cel.interpreter.Interpretable.calExhaustiveBinaryOpsCost; + +import java.util.Objects; +import java.util.Set; +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; +import org.projectnessie.cel.interpreter.AttributeFactory.ConstantQualifier; +import org.projectnessie.cel.interpreter.AttributeFactory.ConstantQualifierEquator; +import org.projectnessie.cel.interpreter.AttributeFactory.Qualifier; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableAttribute; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableConst; +import org.projectnessie.cel.interpreter.InterpretableDecorator.EvalObserver; + +/** Package-private established decorator nodes. */ +final class EvalSetMembership extends AbstractEval implements Coster { + private final Interpretable inst; + private final Interpretable arg; + private final String argTypeName; + private final Set valueSet; + + EvalSetMembership(Interpretable inst, Interpretable arg, String argTypeName, Set valueSet) { + super(inst.id()); + this.inst = inst; + this.arg = arg; + this.argTypeName = argTypeName; + this.valueSet = valueSet; + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val val = arg.eval(ctx); + if (isUnknownOrError(val)) { + return val; + } + if (valueSet.isEmpty()) { + return False; + } + if (!val.type().typeName().equals(argTypeName)) { + return noSuchOverload(null, Operator.In.id, val); + } + return valueSet.contains(val) ? True : False; + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return estimateCost(arg); + } + + @Override + public String toString() { + return "EvalSetMembership{" + + "id=" + + id + + ", inst=" + + inst + + ", arg=" + + arg + + ", argTypeName='" + + argTypeName + + '\'' + + ", valueSet=" + + valueSet + + '}'; + } +} + +final class EvalWatch implements Interpretable, Coster { + private final Interpretable i; + private final EvalObserver observer; + + public EvalWatch(Interpretable i, EvalObserver observer) { + this.i = Objects.requireNonNull(i); + this.observer = Objects.requireNonNull(observer); + } + + @Override + public long id() { + return i.id(); + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val val = i.eval(ctx); + observer.observe(id(), val); + return val; + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return estimateCost(i); + } + + @Override + public String toString() { + return "EvalWatch{" + i + '}'; + } +} + +final class EvalWatchAttr implements Coster, InterpretableAttribute, Attribute { + private final InterpretableAttribute attr; + private final EvalObserver observer; + + public EvalWatchAttr(InterpretableAttribute attr, EvalObserver observer) { + this.attr = Objects.requireNonNull(attr); + this.observer = Objects.requireNonNull(observer); + } + + @Override + public long id() { + return attr.id(); + } + + /** + * AddQualifier creates a wrapper over the incoming qualifier which observes the qualification + * result. + */ + @Override + public Attribute addQualifier(AttributeFactory.Qualifier q) { + if (q instanceof ConstantQualifierEquator cq) { + q = new EvalWatchConstQualEquat(cq, observer, attr.adapter()); + } else if (q instanceof ConstantQualifier cq) { + q = new EvalWatchConstQual(cq, observer, attr.adapter()); + } else { + q = new EvalWatchQual(q, observer, attr.adapter()); + } + attr.addQualifier(q); + return this; + } + + @Override + public Attribute attr() { + return attr.attr(); + } + + @Override + public TypeAdapter adapter() { + return attr.adapter(); + } + + @Override + public Object qualify(Activation vars, Object obj) { + return attr.qualify(vars, obj); + } + + @Override + public Object resolve(Activation act) { + return attr.resolve(act); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return estimateCost(attr); + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val val = attr.eval(ctx); + observer.observe(id(), val); + return val; + } + + @Override + public String toString() { + return "EvalWatchAttr{" + attr + '}'; + } +} + +abstract class AbstractEvalWatch extends AbstractEval + implements Coster, Qualifier { + protected final T delegate; + protected final EvalObserver observer; + protected final TypeAdapter adapter; + + AbstractEvalWatch(T delegate, EvalObserver observer, TypeAdapter adapter) { + super(delegate.id()); + this.delegate = delegate; + this.observer = Objects.requireNonNull(observer); + this.adapter = Objects.requireNonNull(adapter); + } + + /** Qualify observes the qualification of a object via a value computed at runtime. */ + @Override + public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { + Object out = delegate.qualify(vars, obj); + Val val; + if (out != null) { + val = adapter.nativeToValue(out); + } else { + val = newErr(String.format("qualify failed, vars=%s, obj=%s", vars, obj)); + } + observer.observe(id(), val); + return out; + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return estimateCost(delegate); + } +} + +final class EvalWatchConstQualEquat extends AbstractEvalWatch + implements ConstantQualifierEquator { + EvalWatchConstQualEquat( + ConstantQualifierEquator delegate, EvalObserver observer, TypeAdapter adapter) { + super(delegate, observer, adapter); + } + + @Override + public Val eval(Activation activation) { + throw new UnsupportedOperationException("WTF?"); + } + + @Override + public Val value() { + return delegate.value(); + } + + /** + * QualifierValueEquals tests whether the incoming value is equal to the qualificying constant. + */ + @Override + public boolean qualifierValueEquals(Object value) { + return delegate.qualifierValueEquals(value); + } + + @Override + public String toString() { + return "EvalWatchConstQualEquat{" + delegate + '}'; + } +} + +final class EvalWatchConstQual extends AbstractEvalWatch + implements ConstantQualifier, Coster { + EvalWatchConstQual(ConstantQualifier delegate, EvalObserver observer, TypeAdapter adapter) { + super(delegate, observer, adapter); + } + + @Override + public Val eval(Activation activation) { + throw new UnsupportedOperationException("WTF?"); + } + + @Override + public Val value() { + return delegate.value(); + } + + @Override + public String toString() { + return "EvalWatchConstQual{" + delegate + '}'; + } +} + +final class EvalWatchQual extends AbstractEvalWatch { + public EvalWatchQual(Qualifier delegate, EvalObserver observer, TypeAdapter adapter) { + super(delegate, observer, adapter); + } + + @Override + public Val eval(Activation activation) { + throw new UnsupportedOperationException("WTF?"); + } + + @Override + public String toString() { + return "EvalWatchQual{" + delegate + '}'; + } +} + +final class EvalWatchConst implements InterpretableConst, Coster { + private final InterpretableConst c; + private final EvalObserver observer; + + EvalWatchConst(InterpretableConst c, EvalObserver observer) { + this.c = Objects.requireNonNull(c); + this.observer = Objects.requireNonNull(observer); + } + + @Override + public long id() { + return c.id(); + } + + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation activation) { + Val val = value(); + observer.observe(id(), val); + return val; + } + + @Override + public Val value() { + return c.value(); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return estimateCost(c); + } + + @Override + public String toString() { + return "EvalWatchConst{" + c + '}'; + } +} + +final class EvalExhaustiveOr extends AbstractEvalLhsRhs { + // TODO combine with EvalOr + EvalExhaustiveOr(long id, Interpretable lhs, Interpretable rhs) { + super(id, lhs, rhs); + } + + /** Eval implements the Interpretable interface method. */ + @SuppressWarnings("DuplicatedCode") + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val lVal = lhs.eval(ctx); + Val rVal = rhs.eval(ctx); + if (lVal == True || rVal == True) { + return True; + } + if (lVal == False && rVal == False) { + return False; + } + if (isUnknown(lVal)) { + return lVal; + } + if (isUnknown(rVal)) { + return rVal; + } + // TODO: Combine the errors into a set in the future. + // If the left-hand side is non-boolean return it as the error. + if (isError(lVal)) { + return lVal; + } + return noSuchOverload(lVal, Operator.LogicalOr.id, rVal); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return calExhaustiveBinaryOpsCost(lhs, rhs); + } + + @Override + public String toString() { + return "EvalExhaustiveOr{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; + } +} + +final class EvalExhaustiveAnd extends AbstractEvalLhsRhs { + // TODO combine with EvalAnd + EvalExhaustiveAnd(long id, Interpretable lhs, Interpretable rhs) { + super(id, lhs, rhs); + } + + /** Eval implements the Interpretable interface method. */ + @SuppressWarnings("DuplicatedCode") + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val lVal = lhs.eval(ctx); + Val rVal = rhs.eval(ctx); + if (lVal == False || rVal == False) { + return False; + } + if (lVal == True && rVal == True) { + return True; + } + if (isUnknown(lVal)) { + return lVal; + } + if (isUnknown(rVal)) { + return rVal; + } + if (isError(lVal)) { + return lVal; + } + return noSuchOverload(lVal, Operator.LogicalAnd.id, rVal); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return calExhaustiveBinaryOpsCost(lhs, rhs); + } + + @Override + public String toString() { + return "EvalExhaustiveAnd{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; + } +} + +final class EvalExhaustiveConditional extends AbstractEval implements Coster { + // TODO combine with EvalConditional + private final TypeAdapter adapter; + private final ConditionalAttribute attr; + + EvalExhaustiveConditional(long id, TypeAdapter adapter, ConditionalAttribute attr) { + super(id); + this.adapter = Objects.requireNonNull(adapter); + this.attr = Objects.requireNonNull(attr); + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val cVal = attr.expr().eval(ctx); + Object tVal = attr.truthy().resolve(ctx); + Object fVal = attr.falsy().resolve(ctx); + if (cVal == True) { + return adapter.nativeToValue(tVal); + } else if (cVal == False) { + return adapter.nativeToValue(fVal); + } else { + return noSuchOverload(null, Operator.Conditional.id, cVal); + } + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return attr.cost(); + } + + @Override + public String toString() { + return "EvalExhaustiveConditional{" + "id=" + id + ", attr=" + attr + '}'; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedFoldNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedFoldNodes.java new file mode 100644 index 00000000..ce6394d8 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedFoldNodes.java @@ -0,0 +1,512 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.Err.valOrErr; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.Util.isUnknownOrError; +import static org.projectnessie.cel.interpreter.Activation.emptyActivation; +import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.IterableT; +import org.projectnessie.cel.common.types.IteratorT; +import org.projectnessie.cel.common.types.ListT; +import org.projectnessie.cel.common.types.MapT; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; +import org.projectnessie.cel.common.types.traits.Mapper; +import org.projectnessie.cel.common.types.traits.Sizer; +import org.projectnessie.cel.common.types.traits.Trait; + +/** Package-private established fold nodes. */ +final class EvalMapFold extends AbstractEval implements Coster { + final String iterVar; + final String iterVar2; + final Interpretable iterRange; + final Interpretable filter; + final Interpretable transform; + final TypeAdapter adapter; + + EvalMapFold( + long id, + String iterVar, + String iterVar2, + Interpretable iterRange, + Interpretable filter, + Interpretable transform, + TypeAdapter adapter) { + super(id); + this.iterVar = iterVar; + this.iterVar2 = iterVar2; + this.iterRange = iterRange; + this.filter = filter; + this.transform = transform; + this.adapter = adapter; + } + + @SuppressWarnings("DuplicatedCode") + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val foldRange = iterRange.eval(ctx); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return valOrErr( + foldRange, "got '%s', expected iterable type", foldRange.getClass().getName()); + } + + VarActivation iterCtx = new VarActivation(); + iterCtx.parent = ctx; + iterCtx.name = iterVar; + VarActivation iterCtx2 = null; + if (!iterVar2.isEmpty()) { + iterCtx2 = new VarActivation(); + iterCtx2.parent = iterCtx; + iterCtx2.name = iterVar2; + } + Map values = new HashMap<>(mapCapacity(foldRange)); + IteratorT it = ((IterableT) foldRange).iterator(); + long index = 0L; + var isLister = foldRange instanceof Lister; + var mapper = (foldRange instanceof Mapper m) ? m : null; + while (it.hasNext() == True) { + Val next = it.next(); + Val key; + Activation loopCtx = iterCtx; + if (iterCtx2 != null) { + if (isLister) { + key = intOf(index); + iterCtx.val = key; + iterCtx2.val = next; + } else if (mapper != null) { + key = next; + iterCtx.val = key; + iterCtx2.val = mapper.get(next); + } else { + return valOrErr( + foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); + } + loopCtx = iterCtx2; + } else { + key = next; + iterCtx.val = next; + } + index++; + + if (filter != null) { + Val include = filter.eval(loopCtx); + if (include == False) { + continue; + } + if (include != True) { + return noSuchOverload(null, Operator.Conditional.id, include); + } + } + + Val value = transform.eval(loopCtx); + if (isUnknownOrError(value)) { + return value; + } + values.put(key, value); + } + return MapT.newWrappedMap(adapter, values); + } + + int mapCapacity(Val foldRange) { + if (foldRange.type().hasTrait(Trait.SizerType)) { + int size = ((Sizer) foldRange).nativeSize(); + if (size > 0) { + long capacity = size * 4L / 3 + 1; + return capacity <= Integer.MAX_VALUE ? (int) capacity : Integer.MAX_VALUE; + } + } + return 0; + } + + @SuppressWarnings("DuplicatedCode") + @Override + public Cost cost() { + Cost range = estimateCost(iterRange); + Cost result = estimateCost(transform); + if (filter != null) { + result = result.add(estimateCost(filter)); + } + Val foldRange = iterRange.eval(emptyActivation()); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return Cost.Unknown; + } + long rangeCnt; + if (foldRange instanceof Sizer sizer) { + rangeCnt = sizer.nativeSize(); + } else { + rangeCnt = 0L; + IteratorT it = ((IterableT) foldRange).iterator(); + while (it.hasNext() == True) { + it.next(); + rangeCnt++; + } + } + return range.add(result.multiply(rangeCnt)); + } + + @Override + public String toString() { + return "EvalMapFold{" + + "id=" + + id + + ", iterVar='" + + iterVar + + '\'' + + ", iterVar2='" + + iterVar2 + + '\'' + + ", iterRange=" + + iterRange + + ", filter=" + + filter + + ", transform=" + + transform + + '}'; + } +} + +final class EvalExhaustiveFold extends AbstractEval implements Coster { + // TODO combine with EvalFold + private final String accuVar; + private final String iterVar; + private final String iterVar2; + private final Interpretable iterRange; + private final Interpretable accu; + private final Interpretable cond; + private final Interpretable step; + private final Interpretable result; + + EvalExhaustiveFold( + long id, + Interpretable accu, + String accuVar, + Interpretable iterRange, + String iterVar, + String iterVar2, + Interpretable cond, + Interpretable step, + Interpretable result) { + super(id); + this.accuVar = accuVar; + this.iterVar = iterVar; + this.iterVar2 = iterVar2; + this.iterRange = iterRange; + this.accu = accu; + this.cond = cond; + this.step = step; + this.result = result; + } + + /** Eval implements the Interpretable interface method. */ + @SuppressWarnings("DuplicatedCode") + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val foldRange = iterRange.eval(ctx); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return valOrErr( + foldRange, "got '%s', expected iterable type", foldRange.getClass().getName()); + } + // Configure the fold activation with the accumulator initial value. + VarActivation accuCtx = new VarActivation(); + accuCtx.parent = ctx; + accuCtx.name = accuVar; + accuCtx.val = accu.eval(ctx); + VarActivation iterCtx = new VarActivation(); + iterCtx.parent = accuCtx; + iterCtx.name = iterVar; + VarActivation iterCtx2 = null; + if (!iterVar2.isEmpty()) { + iterCtx2 = new VarActivation(); + iterCtx2.parent = iterCtx; + iterCtx2.name = iterVar2; + } + IteratorT it = ((IterableT) foldRange).iterator(); + var isLister = foldRange instanceof Lister; + var mapper = (foldRange instanceof Mapper m) ? m : null; + long index = 0L; + while (it.hasNext() == True) { + // Modify the iter var in the fold activation. + Val next = it.next(); + Activation loopCtx = iterCtx; + if (iterCtx2 != null) { + if (isLister) { + iterCtx.val = intOf(index); + iterCtx2.val = next; + } else if (mapper != null) { + iterCtx.val = next; + iterCtx2.val = mapper.get(next); + } else { + return valOrErr( + foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); + } + loopCtx = iterCtx2; + } else { + iterCtx.val = next; + } + index++; + + // Evaluate the condition, but don't terminate the loop as this is exhaustive eval! + cond.eval(loopCtx); + + // Evalute the evaluation step into accu var. + accuCtx.val = step.eval(loopCtx); + } + // Compute the result. + return result.eval(accuCtx); + } + + /** Cost implements the Coster interface method. */ + @SuppressWarnings("DuplicatedCode") + @Override + public Cost cost() { + // Compute the cost for evaluating iterRange. + Cost i = estimateCost(iterRange); + + // Compute the size of iterRange. If the size depends on the input, return the maximum + // possible + // cost range. + Val foldRange = iterRange.eval(emptyActivation()); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return Cost.Unknown; + } + long rangeCnt; + if (foldRange instanceof Sizer sizer) { + rangeCnt = sizer.nativeSize(); + } else { + rangeCnt = 0L; + IteratorT it = ((IterableT) foldRange).iterator(); + while (it.hasNext() == True) { + it.next(); + rangeCnt++; + } + } + + Cost a = estimateCost(accu); + Cost c = estimateCost(cond); + Cost s = estimateCost(step); + Cost r = estimateCost(result); + + // The cond and step costs are multiplied by size(iterRange). + return i.add(a).add(c.multiply(rangeCnt)).add(s.multiply(rangeCnt)).add(r); + } + + @Override + public String toString() { + return "EvalExhaustiveFold{" + + "id=" + + id + + ", accuVar='" + + accuVar + + '\'' + + ", iterVar='" + + iterVar + + '\'' + + ", iterVar2='" + + iterVar2 + + '\'' + + ", iterRange=" + + iterRange + + ", accu=" + + accu + + ", cond=" + + cond + + ", step=" + + step + + ", result=" + + result + + '}'; + } +} + +final class EvalExhaustiveListFold extends AbstractEval implements Coster { + private final EvalListFold fold; + + EvalExhaustiveListFold(EvalListFold fold) { + super(fold.id); + this.fold = fold; + } + + @SuppressWarnings("DuplicatedCode") + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val foldRange = fold.iterRange.eval(ctx); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return valOrErr( + foldRange, "got '%s', expected iterable type", foldRange.getClass().getName()); + } + + VarActivation iterCtx = new VarActivation(); + iterCtx.parent = ctx; + iterCtx.name = fold.iterVar; + VarActivation iterCtx2 = null; + if (!fold.iterVar2.isEmpty()) { + iterCtx2 = new VarActivation(); + iterCtx2.parent = iterCtx; + iterCtx2.name = fold.iterVar2; + } + List values = new ArrayList<>(fold.listCapacity(foldRange)); + Val result = null; + IteratorT it = ((IterableT) foldRange).iterator(); + long index = 0L; + while (it.hasNext() == True) { + Val next = it.next(); + Activation loopCtx = iterCtx; + if (iterCtx2 != null) { + if (foldRange instanceof Lister) { + iterCtx.val = intOf(index); + iterCtx2.val = next; + } else if (foldRange instanceof Mapper) { + iterCtx.val = next; + iterCtx2.val = ((Mapper) foldRange).get(next); + } else { + return valOrErr( + foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); + } + loopCtx = iterCtx2; + } else { + iterCtx.val = next; + } + index++; + + Val include = fold.filter != null ? fold.filter.eval(loopCtx) : True; + Val value = fold.transform.eval(loopCtx); + if (include == False) { + continue; + } + if (include != True) { + result = noSuchOverload(null, Operator.Conditional.id, include); + continue; + } + if (result == null) { + if (isUnknownOrError(value)) { + result = value; + } else { + values.add(value); + } + } + } + return result != null + ? result + : ListT.newValArrayList(fold.adapter, values.toArray(new Val[0])); + } + + @Override + public Cost cost() { + return fold.cost(); + } + + @Override + public String toString() { + return "EvalExhaustiveListFold{" + fold + '}'; + } +} + +final class EvalExhaustiveMapFold extends AbstractEval implements Coster { + private final EvalMapFold fold; + + EvalExhaustiveMapFold(EvalMapFold fold) { + super(fold.id); + this.fold = fold; + } + + @SuppressWarnings("DuplicatedCode") + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val foldRange = fold.iterRange.eval(ctx); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return valOrErr( + foldRange, "got '%s', expected iterable type", foldRange.getClass().getName()); + } + + VarActivation iterCtx = new VarActivation(); + iterCtx.parent = ctx; + iterCtx.name = fold.iterVar; + VarActivation iterCtx2 = null; + if (!fold.iterVar2.isEmpty()) { + iterCtx2 = new VarActivation(); + iterCtx2.parent = iterCtx; + iterCtx2.name = fold.iterVar2; + } + Map values = new HashMap<>(fold.mapCapacity(foldRange)); + Val result = null; + IteratorT it = ((IterableT) foldRange).iterator(); + long index = 0L; + while (it.hasNext() == True) { + Val next = it.next(); + Val key; + Activation loopCtx = iterCtx; + if (iterCtx2 != null) { + if (foldRange instanceof Lister) { + key = intOf(index); + iterCtx.val = key; + iterCtx2.val = next; + } else if (foldRange instanceof Mapper) { + key = next; + iterCtx.val = key; + iterCtx2.val = ((Mapper) foldRange).get(next); + } else { + return valOrErr( + foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); + } + loopCtx = iterCtx2; + } else { + key = next; + iterCtx.val = next; + } + index++; + + Val include = fold.filter != null ? fold.filter.eval(loopCtx) : True; + Val value = fold.transform.eval(loopCtx); + if (include == False) { + continue; + } + if (include != True) { + result = noSuchOverload(null, Operator.Conditional.id, include); + continue; + } + if (result == null) { + if (isUnknownOrError(value)) { + result = value; + } else { + values.put(key, value); + } + } + } + return result != null ? result : MapT.newWrappedMap(fold.adapter, values); + } + + @Override + public Cost cost() { + return fold.cost(); + } + + @Override + public String toString() { + return "EvalExhaustiveMapFold{" + fold + '}'; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedScalarNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedScalarNodes.java new file mode 100644 index 00000000..d4f863c3 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedScalarNodes.java @@ -0,0 +1,497 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.Err.noSuchAttributeException; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.UnknownT.unknownOf; +import static org.projectnessie.cel.common.types.Util.isUnknownOrError; +import static org.projectnessie.cel.interpreter.Coster.Cost.OneOne; +import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; + +import java.util.Objects; +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.Overloads; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Negater; +import org.projectnessie.cel.common.types.traits.Receiver; +import org.projectnessie.cel.common.types.traits.Trait; +import org.projectnessie.cel.interpreter.Activation.PartialActivation; +import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableAttribute; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableCall; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableConst; +import org.projectnessie.cel.interpreter.functions.BinaryOp; +import org.projectnessie.cel.interpreter.functions.UnaryOp; + +abstract class AbstractEval implements Interpretable { + protected final long id; + + AbstractEval(long id) { + this.id = id; + } + + @Override + public long id() { + return id; + } + + @Override + public String toString() { + return "id=" + id; + } +} + +abstract class AbstractEvalLhsRhs extends AbstractEval implements Coster { + protected final Interpretable lhs; + protected final Interpretable rhs; + + AbstractEvalLhsRhs(long id, Interpretable lhs, Interpretable rhs) { + super(id); + this.lhs = Objects.requireNonNull(lhs); + this.rhs = Objects.requireNonNull(rhs); + } + + @Override + public String toString() { + return "AbstractEvalLhsRhs{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; + } +} + +/** Evaluates a checked top-level variable directly from the activation. */ +class EvalIdent extends AbstractEval implements Coster { + protected final String name; + protected final TypeAdapter adapter; + + EvalIdent(long id, String name, TypeAdapter adapter) { + super(id); + this.name = Objects.requireNonNull(name); + this.adapter = Objects.requireNonNull(adapter); + } + + @SuppressWarnings("DuplicatedCode") + @Override + public Val eval(Activation ctx) { + if (ctx instanceof PartialActivation partialActivation) { + for (AttributePattern pattern : partialActivation.unknownAttributePatterns()) { + if (pattern.variableMatches(name)) { + return unknownOf(id); + } + } + } + + Object value = ctx.resolve(name); + if (Activation.ABSENT == value) { + RuntimeException err = noSuchAttributeException("id: " + id + ", names: [" + name + "]"); + return newErr(err, err.toString()); + } + return adapter.nativeToValue(value); + } + + @SuppressWarnings("DuplicatedCode") + protected final Object resolveRaw(Activation ctx) { + if (ctx instanceof PartialActivation partialActivation) { + for (AttributePattern pattern : partialActivation.unknownAttributePatterns()) { + if (pattern.variableMatches(name)) { + return unknownOf(id); + } + } + } + + Object value = ctx.resolve(name); + if (Activation.ABSENT == value) { + RuntimeException err = noSuchAttributeException("id: " + id + ", names: [" + name + "]"); + return newErr(err, err.toString()); + } + return value; + } + + @Override + public Cost cost() { + return OneOne; + } + + @Override + public String toString() { + return "EvalIdent{" + "id=" + id + ", name='" + name + '\'' + '}'; + } +} + +/** Checked aggregate identifier whose ordinary evaluation uses exact checked materialization. */ +class EvalExactAggregateIdent extends EvalIdent { + final CheckedAggregateMaterializer materializer; + + EvalExactAggregateIdent( + long id, String name, TypeAdapter adapter, CheckedAggregateMaterializer materializer) { + super(id, name, adapter); + this.materializer = materializer; + } + + @Override + public Val eval(Activation ctx) { + Object value = resolveRaw(ctx); + return value instanceof Val val && isUnknownOrError(val) + ? val + : materializer.materialize(value); + } +} + +class EvalConst extends AbstractEval implements InterpretableConst, Coster { + private final Val val; + + EvalConst(long id, Val val) { + super(id); + this.val = val; + } + + @Override + public Val eval(Activation activation) { + return val; + } + + @Override + public Cost cost() { + return Cost.None; + } + + @Override + public Val value() { + return val; + } + + @Override + public String toString() { + return "EvalConst{" + "id=" + id + ", val=" + val + '}'; + } +} + +class EvalUnary extends AbstractEval implements InterpretableCall, Coster { + private final String function; + private final String overload; + protected final Interpretable arg; + private final Trait trait; + private final UnaryOp impl; + + EvalUnary( + long id, String function, String overload, Interpretable arg, Trait trait, UnaryOp impl) { + super(id); + this.function = Objects.requireNonNull(function); + this.overload = Objects.requireNonNull(overload); + this.arg = Objects.requireNonNull(arg); + this.trait = trait; + this.impl = impl; + } + + @Override + public Val eval(Activation ctx) { + return evalPrepared(arg.eval(ctx)); + } + + protected final Val evalPrepared(Val argValue) { + if (isUnknownOrError(argValue)) { + return argValue; + } + if (impl != null && (trait == null || argValue.type().hasTrait(trait))) { + return impl.invoke(argValue); + } + if (argValue.type().hasTrait(Trait.ReceiverType)) { + return ((Receiver) argValue).receive(function, overload); + } + return noSuchOverload(argValue, function, overload, new Val[0]); + } + + @Override + public Cost cost() { + return Cost.OneOne.add(estimateCost(arg)); + } + + @Override + public String function() { + return function; + } + + @Override + public String overloadID() { + return overload; + } + + @Override + public Interpretable[] args() { + return new Interpretable[] {arg}; + } + + @Override + public String toString() { + return "EvalUnary{" + + "id=" + + id + + ", function='" + + function + + '\'' + + ", overload='" + + overload + + '\'' + + ", arg=" + + arg + + ", trait=" + + trait + + ", impl=" + + impl + + '}'; + } +} + +class EvalEq extends AbstractEvalLhsRhs implements InterpretableCall { + EvalEq(long id, Interpretable lhs, Interpretable rhs) { + super(id, lhs, rhs); + } + + @Override + public Val eval(Activation ctx) { + return evalPrepared(lhs.eval(ctx), rhs.eval(ctx)); + } + + protected final Val evalPrepared(Val left, Val right) { + if (isUnknownOrError(left)) { + return left; + } + if (isUnknownOrError(right)) { + return right; + } + return left.equal(right); + } + + @Override + public Cost cost() { + return Interpretable.calExhaustiveBinaryOpsCost(lhs, rhs); + } + + @Override + public String function() { + return Operator.Equals.id; + } + + @Override + public String overloadID() { + return Overloads.Equals; + } + + @Override + public Interpretable[] args() { + return new Interpretable[] {lhs, rhs}; + } + + @Override + public String toString() { + return "EvalEq{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; + } +} + +class EvalNe extends AbstractEvalLhsRhs implements InterpretableCall { + EvalNe(long id, Interpretable lhs, Interpretable rhs) { + super(id, lhs, rhs); + } + + @Override + public Val eval(Activation ctx) { + return evalPrepared(lhs.eval(ctx), rhs.eval(ctx)); + } + + protected final Val evalPrepared(Val left, Val right) { + if (isUnknownOrError(left)) { + return left; + } + if (isUnknownOrError(right)) { + return right; + } + Val equal = left.equal(right); + return switch (equal.type().typeEnum()) { + case Err -> equal; + case Bool -> ((Negater) equal).negate(); + default -> noSuchOverload(left, Operator.NotEquals.id, right); + }; + } + + @Override + public Cost cost() { + return Interpretable.calExhaustiveBinaryOpsCost(lhs, rhs); + } + + @Override + public String function() { + return Operator.NotEquals.id; + } + + @Override + public String overloadID() { + return Overloads.NotEquals; + } + + @Override + public Interpretable[] args() { + return new Interpretable[] {lhs, rhs}; + } + + @Override + public String toString() { + return "EvalNe{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; + } +} + +class EvalBinary extends AbstractEvalLhsRhs implements InterpretableCall { + protected final String function; + protected final String overload; + protected final Trait trait; + protected final BinaryOp impl; + + EvalBinary( + long id, + String function, + String overload, + Interpretable lhs, + Interpretable rhs, + Trait trait, + BinaryOp impl) { + super(id, lhs, rhs); + this.function = Objects.requireNonNull(function); + this.overload = Objects.requireNonNull(overload); + this.trait = trait; + this.impl = impl; + } + + @Override + public Val eval(Activation ctx) { + return evalPrepared(lhs.eval(ctx), rhs.eval(ctx)); + } + + protected final Val evalPrepared(Val left, Val right) { + if (isUnknownOrError(left)) { + return left; + } + if (isUnknownOrError(right)) { + return right; + } + if (impl != null && (trait == null || left.type().hasTrait(trait))) { + return impl.invoke(left, right); + } + if (left.type().hasTrait(Trait.ReceiverType)) { + return ((Receiver) left).receive(function, overload, right); + } + return noSuchOverload(left, function, overload, new Val[] {right}); + } + + @Override + public Cost cost() { + return Interpretable.calExhaustiveBinaryOpsCost(lhs, rhs); + } + + @Override + public String function() { + return function; + } + + @Override + public String overloadID() { + return overload; + } + + @Override + public Interpretable[] args() { + return new Interpretable[] {lhs, rhs}; + } + + @Override + public String toString() { + return "EvalBinary{" + + "id=" + + id + + ", lhs=" + + lhs + + ", rhs=" + + rhs + + ", function='" + + function + + '\'' + + ", overload='" + + overload + + '\'' + + ", trait=" + + trait + + ", impl=" + + impl + + '}'; + } +} + +/** Evaluates an attribute value. */ +class EvalAttr extends AbstractEval + implements InterpretableAttribute, Coster, AttributeFactory.Qualifier, Attribute { + protected final TypeAdapter adapter; + protected Attribute attr; + + EvalAttr(TypeAdapter adapter, Attribute attr) { + super(attr.id()); + this.adapter = Objects.requireNonNull(adapter); + this.attr = Objects.requireNonNull(attr); + } + + @Override + public Attribute addQualifier(AttributeFactory.Qualifier qualifier) { + attr = attr.addQualifier(qualifier); + return attr; + } + + @Override + public Attribute attr() { + return attr; + } + + @Override + public TypeAdapter adapter() { + return adapter; + } + + @Override + public Cost cost() { + return estimateCost(attr); + } + + @Override + public Val eval(Activation ctx) { + try { + return adapter.nativeToValue(attr.resolve(ctx)); + } catch (Exception e) { + return newErr(e, e.toString()); + } + } + + @Override + public Object qualify(Activation ctx, Object obj) { + return attr.qualify(ctx, obj); + } + + @Override + public Object resolve(Activation ctx) { + return attr.resolve(ctx); + } + + @Override + public String toString() { + return "EvalAttr{" + "id=" + id + ", attr=" + attr + '}'; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/ExprInterpreter.java b/core/src/main/java/org/projectnessie/cel/interpreter/ExprInterpreter.java new file mode 100644 index 00000000..2fe1428f --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/ExprInterpreter.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import com.google.api.expr.v1alpha1.CheckedExpr; +import com.google.api.expr.v1alpha1.Expr; +import com.google.api.expr.v1alpha1.Reference; +import com.google.api.expr.v1alpha1.Type; +import java.util.HashMap; +import java.util.Map; +import org.projectnessie.cel.common.containers.Container; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeProvider; + +/** Default {@link Interpreter} implementation. */ +final class ExprInterpreter implements Interpreter { + private final Dispatcher dispatcher; + private final Container container; + private final TypeProvider provider; + private final TypeAdapter adapter; + private final AttributeFactory attrFactory; + private final PlanningPolicy planningPolicy; + + ExprInterpreter( + Dispatcher dispatcher, + Container container, + TypeProvider provider, + TypeAdapter adapter, + AttributeFactory attrFactory, + PlanningPolicy planningPolicy) { + this.dispatcher = dispatcher; + this.container = container; + this.provider = provider; + this.adapter = adapter; + this.attrFactory = attrFactory; + this.planningPolicy = planningPolicy; + } + + @Override + public Interpretable newInterpretable(CheckedExpr checked, InterpretableDecorator... decorators) { + return newInterpretable( + checked.getExpr(), checked.getReferenceMapMap(), checked.getTypeMapMap(), decorators); + } + + @Override + public Interpretable newInterpretable( + Expr expr, + Map refMap, + Map typeMap, + InterpretableDecorator... decorators) { + return checkedPlanner(refMap, typeMap, decorators).plan(expr); + } + + @Override + public Interpretable newUncheckedInterpretable(Expr expr, InterpretableDecorator... decorators) { + return uncheckedPlanner(decorators).plan(expr); + } + + Planner checkedPlanner(CheckedExpr checked, InterpretableDecorator... decorators) { + return checkedPlanner(checked.getReferenceMapMap(), checked.getTypeMapMap(), decorators); + } + + Planner checkedPlanner( + Map refMap, Map typeMap, InterpretableDecorator... decorators) { + return new Planner( + dispatcher, + provider, + adapter, + attrFactory, + container, + refMap, + typeMap, + effectiveCheckedPolicy(decorators), + decorators); + } + + Planner uncheckedPlanner(InterpretableDecorator... decorators) { + return new Planner( + dispatcher, + provider, + adapter, + attrFactory, + container, + new HashMap<>(), + new HashMap<>(), + PlanningPolicy.ESTABLISHED_ONLY, + decorators); + } + + private PlanningPolicy effectiveCheckedPolicy(InterpretableDecorator[] decorators) { + return decorators.length == 0 ? planningPolicy : PlanningPolicy.ESTABLISHED_ONLY; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java b/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java index 1c8ee0c6..92e20cf3 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java @@ -15,59 +15,16 @@ */ package org.projectnessie.cel.interpreter; -import static org.projectnessie.cel.common.types.BoolT.False; -import static org.projectnessie.cel.common.types.BoolT.True; -import static org.projectnessie.cel.common.types.Err.isError; -import static org.projectnessie.cel.common.types.Err.newErr; -import static org.projectnessie.cel.common.types.Err.noSuchAttributeException; -import static org.projectnessie.cel.common.types.Err.noSuchOverload; -import static org.projectnessie.cel.common.types.Err.valOrErr; -import static org.projectnessie.cel.common.types.UnknownT.isUnknown; -import static org.projectnessie.cel.common.types.UnknownT.unknownOf; -import static org.projectnessie.cel.common.types.Util.isUnknownOrError; -import static org.projectnessie.cel.interpreter.Activation.emptyActivation; -import static org.projectnessie.cel.interpreter.Coster.Cost.OneOne; import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; import static org.projectnessie.cel.interpreter.Coster.costOf; -import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import org.projectnessie.cel.common.operators.Operator; -import org.projectnessie.cel.common.types.Err; -import org.projectnessie.cel.common.types.IterableT; -import org.projectnessie.cel.common.types.IteratorT; -import org.projectnessie.cel.common.types.ListT; -import org.projectnessie.cel.common.types.MapT; -import org.projectnessie.cel.common.types.Overloads; -import org.projectnessie.cel.common.types.StringT; -import org.projectnessie.cel.common.types.ref.FieldType; import org.projectnessie.cel.common.types.ref.TypeAdapter; -import org.projectnessie.cel.common.types.ref.TypeEnum; -import org.projectnessie.cel.common.types.ref.TypeProvider; import org.projectnessie.cel.common.types.ref.Val; -import org.projectnessie.cel.common.types.traits.Container; -import org.projectnessie.cel.common.types.traits.FieldTester; -import org.projectnessie.cel.common.types.traits.Negater; import org.projectnessie.cel.common.types.traits.Receiver; -import org.projectnessie.cel.common.types.traits.Sizer; -import org.projectnessie.cel.common.types.traits.Trait; -import org.projectnessie.cel.interpreter.Activation.PartialActivation; -import org.projectnessie.cel.interpreter.Activation.VarActivation; import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; -import org.projectnessie.cel.interpreter.AttributeFactory.ConditionalAttribute; -import org.projectnessie.cel.interpreter.AttributeFactory.ConstantQualifier; -import org.projectnessie.cel.interpreter.AttributeFactory.ConstantQualifierEquator; import org.projectnessie.cel.interpreter.AttributeFactory.Qualifier; import org.projectnessie.cel.interpreter.Coster.Cost; -import org.projectnessie.cel.interpreter.InterpretableDecorator.EvalObserver; -import org.projectnessie.cel.interpreter.functions.BinaryOp; -import org.projectnessie.cel.interpreter.functions.FunctionOp; -import org.projectnessie.cel.interpreter.functions.UnaryOp; /** * Interpretable can accept a given Activation and produce a value along with an accompanying @@ -144,1661 +101,37 @@ interface InterpretableCall extends Interpretable { Interpretable[] args(); } - // Core Interpretable implementations used during the program planning phase. - - /** evalIdent evaluates a checked top-level variable directly from the activation. */ - final class EvalIdent extends AbstractEval implements Coster { - private final String name; - private final TypeAdapter adapter; - - EvalIdent(long id, String name, TypeAdapter adapter) { - super(id); - this.name = Objects.requireNonNull(name); - this.adapter = Objects.requireNonNull(adapter); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(Activation ctx) { - if (ctx instanceof PartialActivation) { - for (AttributePattern pattern : ((PartialActivation) ctx).unknownAttributePatterns()) { - if (pattern.variableMatches(name)) { - return unknownOf(id); - } - } - } - - ResolvedValue value = ctx.resolveName(name); - if (!value.present()) { - RuntimeException err = noSuchAttributeException("id: " + id + ", names: [" + name + "]"); - return newErr(err, err.toString()); - } - return adapter.nativeToValue(value.value()); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return OneOne; - } - - @Override - public String toString() { - return "EvalIdent{" + "id=" + id + ", name='" + name + '\'' + '}'; - } - } - - final class EvalTestOnly implements Interpretable, Coster { - private final long id; - private final Interpretable op; - private final StringT field; - private final FieldType fieldType; - - EvalTestOnly(long id, Interpretable op, StringT field, FieldType fieldType) { - this.id = id; - this.op = Objects.requireNonNull(op); - this.field = Objects.requireNonNull(field); - this.fieldType = fieldType; - } - - /** ID implements the Interpretable interface method. */ - @Override - public long id() { - return id; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - // Handle field selection on a proto in the most efficient way possible. - if (fieldType != null) { - if (op instanceof InterpretableAttribute) { - InterpretableAttribute opAttr = (InterpretableAttribute) op; - Object opVal = opAttr.resolve(ctx); - if (opVal instanceof Val) { - Val refVal = (Val) opVal; - opVal = refVal.value(); - } - if (fieldType.isSet.isSet(opVal)) { - return True; - } - return False; - } - } - - Val obj = op.eval(ctx); - if (obj instanceof FieldTester) { - return ((FieldTester) obj).isSet(field); - } - if (obj instanceof Container) { - return ((Container) obj).contains(field); - } - return valOrErr(obj, "invalid type for field selection."); - } - - /** - * Cost provides the heuristic cost of a `has(field)` macro. The cost has at least 1 for - * determining if the field exists, apart from the cost of accessing the field. - */ - @Override - public Cost cost() { - Cost c = estimateCost(op); - return c.add(OneOne); - } - - @Override - public String toString() { - return "EvalTestOnly{" + "id=" + id + ", field=" + field + '}'; - } - } - /** NewConstValue creates a new constant valued Interpretable. */ static InterpretableConst newConstValue(long id, Val val) { return new EvalConst(id, val); } - abstract class AbstractEval implements Interpretable { - protected final long id; - - AbstractEval(long id) { - this.id = id; - } - - /** ID implements the Interpretable interface method. */ - @Override - public long id() { - return id; - } - - @Override - public String toString() { - return "id=" + id; - } - } - - abstract class AbstractEvalLhsRhs extends AbstractEval implements Coster { - protected final Interpretable lhs; - protected final Interpretable rhs; - - AbstractEvalLhsRhs(long id, Interpretable lhs, Interpretable rhs) { - super(id); - this.lhs = Objects.requireNonNull(lhs); - this.rhs = Objects.requireNonNull(rhs); - } - - @Override - public String toString() { - return "AbstractEvalLhsRhs{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; - } - } - - final class EvalConst extends AbstractEval implements InterpretableConst, Coster { - private final Val val; - - EvalConst(long id, Val val) { - super(id); - this.val = val; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation activation) { - return val; - } - - /** Cost returns zero for a constant valued Interpretable. */ - @Override - public Cost cost() { - return Cost.None; - } - - /** Value implements the InterpretableConst interface method. */ - @Override - public Val value() { - return val; - } - - @Override - public String toString() { - return "EvalConst{" + "id=" + id + ", val=" + val + '}'; - } - } - - final class EvalOr extends AbstractEvalLhsRhs { - // TODO combine with EvalExhaustiveOr - EvalOr(long id, Interpretable lhs, Interpretable rhs) { - super(id, lhs, rhs); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - // short-circuit lhs. - Val lVal = lhs.eval(ctx); - if (lVal == True) { - return True; - } - // short-circuit on rhs. - Val rVal = rhs.eval(ctx); - if (rVal == True) { - return True; - } - // return if both sides are bool false. - if (lVal == False && rVal == False) { - return False; - } - // TODO: return both values as a set if both are unknown or error. - // prefer left unknown to right unknown. - if (isUnknown(lVal)) { - return lVal; - } - if (isUnknown(rVal)) { - return rVal; - } - // If the left-hand side is non-boolean return it as the error. - if (isError(lVal)) { - return lVal; - } - return noSuchOverload(lVal, Operator.LogicalOr.id, rVal); - } - - /** - * Cost implements the Coster interface method. The minimum possible cost incurs when the - * left-hand side expr is sufficient in determining the evaluation result. - */ - @Override - public Cost cost() { - return calShortCircuitBinaryOpsCost(lhs, rhs); - } - - @Override - public String toString() { - return "EvalOr{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; - } - } - - final class EvalAnd extends AbstractEvalLhsRhs { - // TODO combine with EvalExhaustiveAnd - EvalAnd(long id, Interpretable lhs, Interpretable rhs) { - super(id, lhs, rhs); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - // short-circuit lhs. - Val lVal = lhs.eval(ctx); - if (lVal == False) { - return False; - } - // short-circuit on rhs. - Val rVal = rhs.eval(ctx); - if (rVal == False) { - return False; - } - // return if both sides are bool true. - if (lVal == True && rVal == True) { - return True; - } - // TODO: return both values as a set if both are unknown or error. - // prefer left unknown to right unknown. - if (isUnknown(lVal)) { - return lVal; - } - if (isUnknown(rVal)) { - return rVal; - } - // If the left-hand side is non-boolean return it as the error. - if (isError(lVal)) { - return lVal; - } - return noSuchOverload(lVal, Operator.LogicalAnd.id, rVal); - } - - /** - * Cost implements the Coster interface method. The minimum possible cost incurs when the - * left-hand side expr is sufficient in determining the evaluation result. - */ - @Override - public Cost cost() { - return calShortCircuitBinaryOpsCost(lhs, rhs); - } - - @Override - public String toString() { - return "EvalAnd{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; - } - } - static Cost calShortCircuitBinaryOpsCost(Interpretable lhs, Interpretable rhs) { Cost l = estimateCost(lhs); Cost r = estimateCost(rhs); return costOf(l.min, l.max + r.max + 1); } - final class EvalEq extends AbstractEvalLhsRhs implements InterpretableCall { - EvalEq(long id, Interpretable lhs, Interpretable rhs) { - super(id, lhs, rhs); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val lVal = lhs.eval(ctx); - Val rVal = rhs.eval(ctx); - // Early return if any argument to the function is unknown or error. - if (isUnknownOrError(lVal)) { - return lVal; - } - if (isUnknownOrError(rVal)) { - return rVal; - } - return lVal.equal(rVal); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return calExhaustiveBinaryOpsCost(lhs, rhs); - } - - /** Function implements the InterpretableCall interface method. */ - @Override - public String function() { - return Operator.Equals.id; - } - - /** OverloadID implements the InterpretableCall interface method. */ - @Override - public String overloadID() { - return Overloads.Equals; - } - - /** Args implements the InterpretableCall interface method. */ - @Override - public Interpretable[] args() { - return new Interpretable[] {lhs, rhs}; - } - - @Override - public String toString() { - return "EvalEq{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; - } - } - - final class EvalNe extends AbstractEvalLhsRhs implements InterpretableCall { - EvalNe(long id, Interpretable lhs, Interpretable rhs) { - super(id, lhs, rhs); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val lVal = lhs.eval(ctx); - Val rVal = rhs.eval(ctx); - // Early return if any argument to the function is unknown or error. - if (isUnknownOrError(lVal)) { - return lVal; - } - if (isUnknownOrError(rVal)) { - return rVal; - } - Val eqVal = lVal.equal(rVal); - switch (eqVal.type().typeEnum()) { - case Err: - return eqVal; - case Bool: - return ((Negater) eqVal).negate(); - } - return noSuchOverload(lVal, Operator.NotEquals.id, rVal); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return calExhaustiveBinaryOpsCost(lhs, rhs); - } - - /** Function implements the InterpretableCall interface method. */ - @Override - public String function() { - return Operator.NotEquals.id; - } - - /** OverloadID implements the InterpretableCall interface method. */ - @Override - public String overloadID() { - return Overloads.NotEquals; - } - - /** Args implements the InterpretableCall interface method. */ - @Override - public Interpretable[] args() { - return new Interpretable[] {lhs, rhs}; - } - - @Override - public String toString() { - return "EvalNe{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; - } - } - - final class EvalZeroArity extends AbstractEval implements InterpretableCall, Coster { - private final String function; - private final String overload; - private final FunctionOp impl; - - EvalZeroArity(long id, String function, String overload, FunctionOp impl) { - super(id); - this.function = Objects.requireNonNull(function); - this.overload = Objects.requireNonNull(overload); - this.impl = impl; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation activation) { - return impl.invoke(); - } - - /** Cost returns 1 representing the heuristic cost of the function. */ - @Override - public Cost cost() { - return Cost.OneOne; - } - - /** Function implements the InterpretableCall interface method. */ - @Override - public String function() { - return function; - } - - /** OverloadID implements the InterpretableCall interface method. */ - @Override - public String overloadID() { - return overload; - } - - /** Args returns the argument to the unary function. */ - @Override - public Interpretable[] args() { - return new Interpretable[0]; - } - - @Override - public String toString() { - return "EvalZeroArity{" - + "id=" - + id - + ", function='" - + function - + '\'' - + ", overload='" - + overload - + '\'' - + ", impl=" - + impl - + '}'; - } - } - - final class EvalUnary extends AbstractEval implements InterpretableCall, Coster { - private final String function; - private final String overload; - private final Interpretable arg; - private final Trait trait; - private final UnaryOp impl; - - EvalUnary( - long id, String function, String overload, Interpretable arg, Trait trait, UnaryOp impl) { - super(id); - this.function = Objects.requireNonNull(function); - this.overload = Objects.requireNonNull(overload); - this.arg = Objects.requireNonNull(arg); - this.trait = trait; - this.impl = impl; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val argVal = arg.eval(ctx); - // Early return if the argument to the function is unknown or error. - if (isUnknownOrError(argVal)) { - return argVal; - } - // If the implementation is bound and the argument value has the right traits required to - // invoke it, then call the implementation. - if (impl != null && (trait == null || argVal.type().hasTrait(trait))) { - return impl.invoke(argVal); - } - // Otherwise, if the argument is a ReceiverType attempt to invoke the receiver method on the - // operand (arg0). - if (argVal.type().hasTrait(Trait.ReceiverType)) { - return ((Receiver) argVal).receive(function, overload); - } - return noSuchOverload(argVal, function, overload, new Val[] {}); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - Cost c = estimateCost(arg); - return Cost.OneOne.add(c); // add cost for function - } - - /** Function implements the InterpretableCall interface method. */ - @Override - public String function() { - return function; - } - - /** OverloadID implements the InterpretableCall interface method. */ - @Override - public String overloadID() { - return overload; - } - - /** Args returns the argument to the unary function. */ - @Override - public Interpretable[] args() { - return new Interpretable[] {arg}; - } - - @Override - public String toString() { - return "EvalUnary{" - + "id=" - + id - + ", function='" - + function - + '\'' - + ", overload='" - + overload - + '\'' - + ", arg=" - + arg - + ", trait=" - + trait - + ", impl=" - + impl - + '}'; - } - } - - final class EvalBinary extends AbstractEvalLhsRhs implements InterpretableCall { - private final String function; - private final String overload; - private final Trait trait; - private final BinaryOp impl; - - EvalBinary( - long id, - String function, - String overload, - Interpretable lhs, - Interpretable rhs, - Trait trait, - BinaryOp impl) { - super(id, lhs, rhs); - this.function = Objects.requireNonNull(function); - this.overload = Objects.requireNonNull(overload); - this.trait = trait; - this.impl = impl; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val lVal = lhs.eval(ctx); - Val rVal = rhs.eval(ctx); - // Early return if any argument to the function is unknown or error. - if (isUnknownOrError(lVal)) { - return lVal; - } - if (isUnknownOrError(rVal)) { - return rVal; - } - // If the implementation is bound and the argument value has the right traits required to - // invoke it, then call the implementation. - if (impl != null && (trait == null || lVal.type().hasTrait(trait))) { - return impl.invoke(lVal, rVal); - } - // Otherwise, if the argument is a ReceiverType attempt to invoke the receiver method on the - // operand (arg0). - if (lVal.type().hasTrait(Trait.ReceiverType)) { - return ((Receiver) lVal).receive(function, overload, rVal); - } - return noSuchOverload(lVal, function, overload, new Val[] {rVal}); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return calExhaustiveBinaryOpsCost(lhs, rhs); - } - - /** Function implements the InterpretableCall interface method. */ - @Override - public String function() { - return function; - } - - /** OverloadID implements the InterpretableCall interface method. */ - @Override - public String overloadID() { - return overload; - } - - /** Args returns the argument to the unary function. */ - @Override - public Interpretable[] args() { - return new Interpretable[] {lhs, rhs}; - } - - @Override - public String toString() { - return "EvalBinary{" - + "id=" - + id - + ", lhs=" - + lhs - + ", rhs=" - + rhs - + ", function='" - + function - + '\'' - + ", overload='" - + overload - + '\'' - + ", trait=" - + trait - + ", impl=" - + impl - + '}'; - } - } - - final class EvalVarArgs extends AbstractEval implements Coster, InterpretableCall { - private final String function; - private final String overload; - private final Interpretable[] args; - private final Trait trait; - private final FunctionOp impl; - - public EvalVarArgs( - long id, - String function, - String overload, - Interpretable[] args, - Trait trait, - FunctionOp impl) { - super(id); - this.function = Objects.requireNonNull(function); - this.overload = Objects.requireNonNull(overload); - this.args = Objects.requireNonNull(args); - this.trait = trait; - this.impl = impl; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val[] argVals = new Val[args.length]; - // Early return if any argument to the function is unknown or error. - for (int i = 0; i < args.length; i++) { - Interpretable arg = args[i]; - argVals[i] = arg.eval(ctx); - if (isUnknownOrError(argVals[i])) { - return argVals[i]; - } - } - // If the implementation is bound and the argument value has the right traits required to - // invoke it, then call the implementation. - Val arg0 = argVals[0]; - if (impl != null && (trait == null || arg0.type().hasTrait(trait))) { - return impl.invoke(argVals); - } - // Otherwise, if the argument is a ReceiverType attempt to invoke the receiver method on the - // operand (arg0). - if (arg0.type().hasTrait(Trait.ReceiverType)) { - return receiveVarArgs((Receiver) arg0, function, overload, argVals); - } - return noSuchOverload(arg0, function, overload, argVals); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - Cost c = sumOfCost(args); - return c.add(OneOne); // add cost for function - } - - /** Function implements the InterpretableCall interface method. */ - @Override - public String function() { - return function; - } - - /** OverloadID implements the InterpretableCall interface method. */ - @Override - public String overloadID() { - return overload; - } - - /** Args returns the argument to the unary function. */ - @Override - public Interpretable[] args() { - return args; - } - - @Override - public String toString() { - return "EvalVarArgs{" - + "id=" - + id - + ", function='" - + function - + '\'' - + ", overload='" - + overload - + '\'' - + ", args=" - + Arrays.toString(args) - + ", trait=" - + trait - + ", impl=" - + impl - + '}'; - } - } - - final class EvalList extends AbstractEval implements Coster { - final Interpretable[] elems; - private final TypeAdapter adapter; - - EvalList(long id, Interpretable[] elems, TypeAdapter adapter) { - super(id); - this.elems = elems; - this.adapter = adapter; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val[] elemVals = new Val[elems.length]; - // If any argument is unknown or error early terminate. - for (int i = 0; i < elems.length; i++) { - Interpretable elem = elems[i]; - Val elemVal = elem.eval(ctx); - if (isUnknownOrError(elemVal)) { - return elemVal; - } - elemVals[i] = elemVal; - } - return adapter.nativeToValue(elemVals); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return sumOfCost(elems); - } - - @Override - public String toString() { - return "EvalList{" + "id=" + id + ", elems=" + Arrays.toString(elems) + '}'; + static Cost sumOfCost(Interpretable[] interps) { + long min = 0L; + long max = 0L; + for (Interpretable in : interps) { + Cost t = estimateCost(in); + min += t.min; + max += t.max; } + return costOf(min, max); } - final class EvalMap extends AbstractEval implements Coster { - final Interpretable[] keys; - final Interpretable[] vals; - private final TypeAdapter adapter; - - EvalMap(long id, Interpretable[] keys, Interpretable[] vals, TypeAdapter adapter) { - super(id); - this.keys = keys; - this.vals = vals; - this.adapter = adapter; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Map entries = new HashMap<>(keys.length * 4 / 3 + 1); - // If any argument is unknown or error early terminate. - for (int i = 0; i < keys.length; i++) { - Interpretable key = keys[i]; - Val keyVal = key.eval(ctx); - if (isUnknownOrError(keyVal)) { - return keyVal; - } - if (keyVal.type().typeEnum() == TypeEnum.Null) { - return newErr("unsupported key type"); - } - Val valVal = vals[i].eval(ctx); - if (isUnknownOrError(valVal)) { - return valVal; - } - if (entries.putIfAbsent(keyVal, valVal) != null) { - // Prevent duplicate keys, error out. - return newErr("Failed with repeated key"); - } - } - return MapT.newWrappedMap(adapter, entries); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - Cost k = sumOfCost(keys); - Cost v = sumOfCost(vals); - return k.add(v); - } - - @Override - public String toString() { - return "EvalMap{" - + "id=" - + id - + ", keys=" - + Arrays.toString(keys) - + ", vals=" - + Arrays.toString(vals) - + '}'; - } - } - - final class EvalObj extends AbstractEval implements Coster { - private final String typeName; - private final String[] fields; - private final Interpretable[] vals; - private final TypeProvider provider; - - EvalObj( - long id, String typeName, String[] fields, Interpretable[] vals, TypeProvider provider) { - super(id); - this.typeName = Objects.requireNonNull(typeName); - this.fields = Objects.requireNonNull(fields); - this.vals = Objects.requireNonNull(vals); - this.provider = Objects.requireNonNull(provider); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Map fieldVals = new HashMap<>(); - // If any argument is unknown or error early terminate. - for (int i = 0; i < fields.length; i++) { - String field = fields[i]; - Val val = vals[i].eval(ctx); - if (isUnknownOrError(val)) { - return val; - } - fieldVals.put(field, val); - } - return provider.newValue(typeName, fieldVals); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return sumOfCost(vals); - } - - @Override - public String toString() { - return "EvalObj{" - + "id=" - + id - + ", typeName='" - + typeName - + '\'' - + ", fields=" - + Arrays.toString(fields) - + ", vals=" - + Arrays.toString(vals) - + ", provider=" - + provider - + '}'; - } - } - - static Cost sumOfCost(Interpretable[] interps) { - long min = 0L; - long max = 0L; - for (Interpretable in : interps) { - Cost t = estimateCost(in); - min += t.min; - max += t.max; - } - return costOf(min, max); - } - - final class EvalFold extends AbstractEval implements Coster { - // TODO combine with EvalExhaustiveFold - final String accuVar; - final String iterVar; - final Interpretable iterRange; - final Interpretable accu; - final Interpretable cond; - final Interpretable step; - final Interpretable result; - - EvalFold( - long id, - String accuVar, - Interpretable accu, - String iterVar, - Interpretable iterRange, - Interpretable cond, - Interpretable step, - Interpretable result) { - super(id); - this.accuVar = accuVar; - this.iterVar = iterVar; - this.iterRange = iterRange; - this.accu = accu; - this.cond = cond; - this.step = step; - this.result = result; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val foldRange = iterRange.eval(ctx); - if (!foldRange.type().hasTrait(Trait.IterableType)) { - return valOrErr( - foldRange, "got '%s', expected iterable type", foldRange.getClass().getName()); - } - // Configure the fold activation with the accumulator initial value. - VarActivation accuCtx = new VarActivation(); - accuCtx.parent = ctx; - accuCtx.name = accuVar; - accuCtx.val = accu.eval(ctx); - VarActivation iterCtx = new VarActivation(); - iterCtx.parent = accuCtx; - iterCtx.name = iterVar; - IteratorT it = ((IterableT) foldRange).iterator(); - while (it.hasNext() == True) { - // Modify the iter var in the fold activation. - iterCtx.val = it.next(); - - // Evaluate the condition, terminate the loop if false. - Val c = cond.eval(iterCtx); - if (c == False) { - break; - } - - // Evalute the evaluation step into accu var. - accuCtx.val = step.eval(iterCtx); - } - // Compute the result. - return result.eval(accuCtx); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - // Compute the cost for evaluating iterRange. - Cost i = estimateCost(iterRange); - - // Compute the size of iterRange. If the size depends on the input, return the maximum - // possible - // cost range. - Val foldRange = iterRange.eval(emptyActivation()); - if (!foldRange.type().hasTrait(Trait.IterableType)) { - return Cost.Unknown; - } - long rangeCnt = 0L; - IteratorT it = ((IterableT) foldRange).iterator(); - while (it.hasNext() == True) { - it.next(); - rangeCnt++; - } - Cost a = estimateCost(accu); - Cost c = estimateCost(cond); - Cost s = estimateCost(step); - Cost r = estimateCost(result); - - // The cond and step costs are multiplied by size(iterRange). The minimum possible cost incurs - // when the evaluation result can be determined by the first iteration. - return i.add(a) - .add(r) - .add(costOf(c.min, c.max * rangeCnt)) - .add(costOf(s.min, s.max * rangeCnt)); - } - - @Override - public String toString() { - return "EvalFold{" - + "id=" - + id - + ", accuVar='" - + accuVar - + '\'' - + ", iterVar='" - + iterVar - + '\'' - + ", iterRange=" - + iterRange - + ", accu=" - + accu - + ", cond=" - + cond - + ", step=" - + step - + ", result=" - + result - + '}'; - } - } - - final class EvalListFold extends AbstractEval implements Coster { - final String iterVar; - final Interpretable iterRange; - final Interpretable filter; - final Interpretable transform; - private final TypeAdapter adapter; - - EvalListFold( - long id, - String iterVar, - Interpretable iterRange, - Interpretable filter, - Interpretable transform, - TypeAdapter adapter) { - super(id); - this.iterVar = iterVar; - this.iterRange = iterRange; - this.filter = filter; - this.transform = transform; - this.adapter = adapter; - } - - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val foldRange = iterRange.eval(ctx); - if (!foldRange.type().hasTrait(Trait.IterableType)) { - return valOrErr( - foldRange, "got '%s', expected iterable type", foldRange.getClass().getName()); - } - - VarActivation iterCtx = new VarActivation(); - iterCtx.parent = ctx; - iterCtx.name = iterVar; - List values = new ArrayList<>(listCapacity(foldRange)); - IteratorT it = ((IterableT) foldRange).iterator(); - while (it.hasNext() == True) { - iterCtx.val = it.next(); - - if (filter != null) { - Val include = filter.eval(iterCtx); - if (include == False) { - continue; - } - if (include != True) { - return noSuchOverload(null, Operator.Conditional.id, include); - } - } - - Val value = transform.eval(iterCtx); - if (isUnknownOrError(value)) { - return value; - } - values.add(value); - } - return ListT.newValArrayList(adapter, values.toArray(new Val[0])); - } - - private int listCapacity(Val foldRange) { - if (foldRange.type().hasTrait(Trait.SizerType)) { - long size = ((Sizer) foldRange).size().intValue(); - if (size > 0 && size <= Integer.MAX_VALUE) { - return (int) size; - } - } - return 0; - } - - @Override - public Cost cost() { - Cost range = estimateCost(iterRange); - Cost result = estimateCost(transform); - if (filter != null) { - result = result.add(estimateCost(filter)); - } - Val foldRange = iterRange.eval(emptyActivation()); - if (!foldRange.type().hasTrait(Trait.IterableType)) { - return Cost.Unknown; - } - long rangeCnt = 0L; - IteratorT it = ((IterableT) foldRange).iterator(); - while (it.hasNext() == True) { - it.next(); - rangeCnt++; - } - return range.add(result.multiply(rangeCnt)); - } - - @Override - public String toString() { - return "EvalListFold{" - + "id=" - + id - + ", iterVar='" - + iterVar - + '\'' - + ", iterRange=" - + iterRange - + ", filter=" - + filter - + ", transform=" - + transform - + '}'; - } - } - - // Optional Intepretable implementations that specialize, subsume, or extend the core evaluation - // plan via decorators. - - /** - * evalSetMembership is an Interpretable implementation which tests whether an input value exists - * within the set of map keys used to model a set. - */ - final class EvalSetMembership extends AbstractEval implements Coster { - private final Interpretable inst; - private final Interpretable arg; - private final String argTypeName; - private final Set valueSet; - - EvalSetMembership( - Interpretable inst, Interpretable arg, String argTypeName, Set valueSet) { - super(inst.id()); - this.inst = inst; - this.arg = arg; - this.argTypeName = argTypeName; - this.valueSet = valueSet; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val val = arg.eval(ctx); - if (!val.type().typeName().equals(argTypeName)) { - return noSuchOverload(null, Operator.In.id, val); - } - return valueSet.contains(val) ? True : False; - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return estimateCost(arg); - } - - @Override - public String toString() { - return "EvalSetMembership{" - + "id=" - + id - + ", inst=" - + inst - + ", arg=" - + arg - + ", argTypeName='" - + argTypeName - + '\'' - + ", valueSet=" - + valueSet - + '}'; - } - } - - final class EvalReceiverVarArgs extends AbstractEval implements Coster, InterpretableCall { - private final String function; - private final String overload; - private final Interpretable[] args; - - public EvalReceiverVarArgs(long id, String function, String overload, Interpretable[] args) { - super(id); - this.function = Objects.requireNonNull(function); - this.overload = Objects.requireNonNull(overload); - this.args = Objects.requireNonNull(args); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val arg0 = args[0].eval(ctx); - if (isUnknownOrError(arg0)) { - return arg0; - } - - switch (args.length) { - case 3: - return evalReceiverTail2(ctx, arg0); - case 4: - return evalReceiverTail3(ctx, arg0); - default: - return evalReceiverTail(ctx, arg0); - } - } - - private Val evalReceiverTail2(org.projectnessie.cel.interpreter.Activation ctx, Val arg0) { - Val arg1 = args[1].eval(ctx); - if (isUnknownOrError(arg1)) { - return arg1; - } - Val arg2 = args[2].eval(ctx); - if (isUnknownOrError(arg2)) { - return arg2; - } - return receiveOrNoSuchOverload(arg0, arg1, arg2); - } - - private Val evalReceiverTail3(org.projectnessie.cel.interpreter.Activation ctx, Val arg0) { - Val arg1 = args[1].eval(ctx); - if (isUnknownOrError(arg1)) { - return arg1; - } - Val arg2 = args[2].eval(ctx); - if (isUnknownOrError(arg2)) { - return arg2; - } - Val arg3 = args[3].eval(ctx); - if (isUnknownOrError(arg3)) { - return arg3; - } - return receiveOrNoSuchOverload(arg0, arg1, arg2, arg3); - } - - private Val evalReceiverTail(org.projectnessie.cel.interpreter.Activation ctx, Val arg0) { - Val[] tailArgs = new Val[args.length - 1]; - for (int i = 1; i < args.length; i++) { - Val argVal = args[i].eval(ctx); - if (isUnknownOrError(argVal)) { - return argVal; - } - tailArgs[i - 1] = argVal; - } - return receiveOrNoSuchOverload(arg0, tailArgs); - } - - private Val receiveOrNoSuchOverload(Val arg0, Val... tailArgs) { - if (arg0.type().hasTrait(Trait.ReceiverType)) { - return ((Receiver) arg0).receive(function, overload, tailArgs); - } - return noSuchOverload(arg0, function, overload, tailArgs); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - Cost c = sumOfCost(args); - return c.add(OneOne); // add cost for function - } - - /** Function implements the InterpretableCall interface method. */ - @Override - public String function() { - return function; - } - - /** OverloadID implements the InterpretableCall interface method. */ - @Override - public String overloadID() { - return overload; - } - - /** Args returns the argument to the unary function. */ - @Override - public Interpretable[] args() { - return args; - } - - @Override - public String toString() { - return "EvalReceiverVarArgs{" - + "id=" - + id - + ", function='" - + function - + '\'' - + ", overload='" - + overload - + '\'' - + ", args=" - + Arrays.toString(args) - + '}'; - } - } - - static Val receiveVarArgs(Receiver receiver, String function, String overload, Val[] argVals) { - switch (argVals.length) { - case 1: - return receiver.receive(function, overload); - case 2: - return receiver.receive(function, overload, argVals[1]); - case 3: - return receiver.receive(function, overload, argVals[1], argVals[2]); - case 4: - return receiver.receive(function, overload, argVals[1], argVals[2], argVals[3]); - default: - return receiver.receive(function, overload, Arrays.copyOfRange(argVals, 1, argVals.length)); - } - } - - /** - * evalWatch is an Interpretable implementation that wraps the execution of a given expression so - * that it may observe the computed value and send it to an observer. - */ - final class EvalWatch implements Interpretable, Coster { - private final Interpretable i; - private final EvalObserver observer; - - public EvalWatch(Interpretable i, EvalObserver observer) { - this.i = Objects.requireNonNull(i); - this.observer = Objects.requireNonNull(observer); - } - - @Override - public long id() { - return i.id(); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val val = i.eval(ctx); - observer.observe(id(), val); - return val; - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return estimateCost(i); - } - - @Override - public String toString() { - return "EvalWatch{" + i + '}'; - } - } - - /** - * evalWatchAttr describes a watcher of an instAttr Interpretable. - * - *

Since the watcher may be selected against at a later stage in program planning, the watcher - * must implement the instAttr interface by proxy. - */ - final class EvalWatchAttr implements Coster, InterpretableAttribute, Attribute { - private final InterpretableAttribute attr; - private final EvalObserver observer; - - public EvalWatchAttr(InterpretableAttribute attr, EvalObserver observer) { - this.attr = Objects.requireNonNull(attr); - this.observer = Objects.requireNonNull(observer); - } - - @Override - public long id() { - return attr.id(); - } - - /** - * AddQualifier creates a wrapper over the incoming qualifier which observes the qualification - * result. - */ - @Override - public Attribute addQualifier(AttributeFactory.Qualifier q) { - if (q instanceof ConstantQualifierEquator) { - ConstantQualifierEquator cq = (ConstantQualifierEquator) q; - q = new EvalWatchConstQualEquat(cq, observer, attr.adapter()); - } else if (q instanceof ConstantQualifier) { - ConstantQualifier cq = (ConstantQualifier) q; - q = new EvalWatchConstQual(cq, observer, attr.adapter()); - } else { - q = new EvalWatchQual(q, observer, attr.adapter()); - } - attr.addQualifier(q); - return this; - } - - @Override - public Attribute attr() { - return attr.attr(); - } - - @Override - public TypeAdapter adapter() { - return attr.adapter(); - } - - @Override - public Object qualify(Activation vars, Object obj) { - return attr.qualify(vars, obj); - } - - @Override - public Object resolve(Activation act) { - return attr.resolve(act); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return estimateCost(attr); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val val = attr.eval(ctx); - observer.observe(id(), val); - return val; - } - - @Override - public String toString() { - return "EvalWatchAttr{" + attr + '}'; - } - } - - abstract class AbstractEvalWatch extends AbstractEval - implements Coster, Qualifier { - protected final T delegate; - protected final EvalObserver observer; - protected final TypeAdapter adapter; - - AbstractEvalWatch(T delegate, EvalObserver observer, TypeAdapter adapter) { - super(delegate.id()); - this.delegate = delegate; - this.observer = Objects.requireNonNull(observer); - this.adapter = Objects.requireNonNull(adapter); - } - - /** Qualify observes the qualification of a object via a value computed at runtime. */ - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - Object out = delegate.qualify(vars, obj); - Val val; - if (out != null) { - val = adapter.nativeToValue(out); - } else { - val = newErr(String.format("qualify failed, vars=%s, obj=%s", vars, obj)); - } - observer.observe(id(), val); - return out; - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return estimateCost(delegate); - } - } - - final class EvalWatchConstQualEquat extends AbstractEvalWatch - implements ConstantQualifierEquator { - EvalWatchConstQualEquat( - ConstantQualifierEquator delegate, EvalObserver observer, TypeAdapter adapter) { - super(delegate, observer, adapter); - } - - @Override - public Val eval(Activation activation) { - throw new UnsupportedOperationException("WTF?"); - } - - @Override - public Val value() { - return delegate.value(); - } - - /** - * QualifierValueEquals tests whether the incoming value is equal to the qualificying constant. - */ - @Override - public boolean qualifierValueEquals(Object value) { - return delegate.qualifierValueEquals(value); - } - - @Override - public String toString() { - return "EvalWatchConstQualEquat{" + delegate + '}'; - } - } - - /** - * evalWatchConstQual observes the qualification of an object using a constant boolean, int, - * string, or uint. - */ - final class EvalWatchConstQual extends AbstractEvalWatch - implements ConstantQualifier, Coster { - EvalWatchConstQual(ConstantQualifier delegate, EvalObserver observer, TypeAdapter adapter) { - super(delegate, observer, adapter); - } - - @Override - public Val eval(Activation activation) { - throw new UnsupportedOperationException("WTF?"); - } - - @Override - public Val value() { - return delegate.value(); - } - - @Override - public String toString() { - return "EvalWatchConstQual{" + delegate + '}'; - } - } - - /** evalWatchQual observes the qualification of an object by a value computed at runtime. */ - final class EvalWatchQual extends AbstractEvalWatch { - public EvalWatchQual(Qualifier delegate, EvalObserver observer, TypeAdapter adapter) { - super(delegate, observer, adapter); - } - - @Override - public Val eval(Activation activation) { - throw new UnsupportedOperationException("WTF?"); - } - - @Override - public String toString() { - return "EvalWatchQual{" + delegate + '}'; - } - } - - /** evalWatchConst describes a watcher of an instConst Interpretable. */ - final class EvalWatchConst implements InterpretableConst, Coster { - private final InterpretableConst c; - private final EvalObserver observer; - - EvalWatchConst(InterpretableConst c, EvalObserver observer) { - this.c = Objects.requireNonNull(c); - this.observer = Objects.requireNonNull(observer); - } - - @Override - public long id() { - return c.id(); - } - - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation activation) { - Val val = value(); - observer.observe(id(), val); - return val; - } - - @Override - public Val value() { - return c.value(); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return estimateCost(c); - } - - @Override - public String toString() { - return "EvalWatchConst{" + c + '}'; - } - } - - /** evalExhaustiveOr is just like evalOr, but does not short-circuit argument evaluation. */ - final class EvalExhaustiveOr extends AbstractEvalLhsRhs { - // TODO combine with EvalOr - EvalExhaustiveOr(long id, Interpretable lhs, Interpretable rhs) { - super(id, lhs, rhs); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val lVal = lhs.eval(ctx); - Val rVal = rhs.eval(ctx); - if (lVal == True || rVal == True) { - return True; - } - if (lVal == False && rVal == False) { - return False; - } - if (isUnknown(lVal)) { - return lVal; - } - if (isUnknown(rVal)) { - return rVal; - } - // TODO: Combine the errors into a set in the future. - // If the left-hand side is non-boolean return it as the error. - if (isError(lVal)) { - return lVal; - } - return noSuchOverload(lVal, Operator.LogicalOr.id, rVal); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return calExhaustiveBinaryOpsCost(lhs, rhs); - } - - @Override - public String toString() { - return "EvalExhaustiveOr{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; - } - } - - /** evalExhaustiveAnd is just like evalAnd, but does not short-circuit argument evaluation. */ - final class EvalExhaustiveAnd extends AbstractEvalLhsRhs { - // TODO combine with EvalAnd - EvalExhaustiveAnd(long id, Interpretable lhs, Interpretable rhs) { - super(id, lhs, rhs); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val lVal = lhs.eval(ctx); - Val rVal = rhs.eval(ctx); - if (lVal == False || rVal == False) { - return False; - } - if (lVal == True && rVal == True) { - return True; - } - if (isUnknown(lVal)) { - return lVal; - } - if (isUnknown(rVal)) { - return rVal; - } - if (isError(lVal)) { - return lVal; - } - return noSuchOverload(lVal, Operator.LogicalAnd.id, rVal); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return calExhaustiveBinaryOpsCost(lhs, rhs); - } - - @Override - public String toString() { - return "EvalExhaustiveAnd{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; - } + static Val receiveVarArgs(Receiver receiver, String function, String overload, Val[] argVals) { + return switch (argVals.length) { + case 1 -> receiver.receive(function, overload); + case 2 -> receiver.receive(function, overload, argVals[1]); + case 3 -> receiver.receive(function, overload, argVals[1], argVals[2]); + case 4 -> receiver.receive(function, overload, argVals[1], argVals[2], argVals[3]); + default -> + receiver.receive(function, overload, Arrays.copyOfRange(argVals, 1, argVals.length)); + }; } static Cost calExhaustiveBinaryOpsCost(Interpretable lhs, Interpretable rhs) { @@ -1806,286 +139,4 @@ static Cost calExhaustiveBinaryOpsCost(Interpretable lhs, Interpretable rhs) { Cost r = estimateCost(rhs); return Cost.OneOne.add(l).add(r); } - - /** - * evalExhaustiveConditional is like evalConditional, but does not short-circuit argument - * evaluation. - */ - final class EvalExhaustiveConditional extends AbstractEval implements Coster { - // TODO combine with EvalConditional - private final TypeAdapter adapter; - private final ConditionalAttribute attr; - - EvalExhaustiveConditional(long id, TypeAdapter adapter, ConditionalAttribute attr) { - super(id); - this.adapter = Objects.requireNonNull(adapter); - this.attr = Objects.requireNonNull(attr); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val cVal = attr.expr.eval(ctx); - Object tVal = attr.truthy.resolve(ctx); - Object fVal = attr.falsy.resolve(ctx); - if (cVal == True) { - return adapter.nativeToValue(tVal); - } else if (cVal == False) { - return adapter.nativeToValue(fVal); - } else { - return noSuchOverload(null, Operator.Conditional.id, cVal); - } - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return attr.cost(); - } - - @Override - public String toString() { - return "EvalExhaustiveConditional{" + "id=" + id + ", attr=" + attr + '}'; - } - } - - /** evalExhaustiveFold is like evalFold, but does not short-circuit argument evaluation. */ - final class EvalExhaustiveFold extends AbstractEval implements Coster { - // TODO combine with EvalFold - private final String accuVar; - private final String iterVar; - private final Interpretable iterRange; - private final Interpretable accu; - private final Interpretable cond; - private final Interpretable step; - private final Interpretable result; - - EvalExhaustiveFold( - long id, - Interpretable accu, - String accuVar, - Interpretable iterRange, - String iterVar, - Interpretable cond, - Interpretable step, - Interpretable result) { - super(id); - this.accuVar = accuVar; - this.iterVar = iterVar; - this.iterRange = iterRange; - this.accu = accu; - this.cond = cond; - this.step = step; - this.result = result; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val foldRange = iterRange.eval(ctx); - if (!foldRange.type().hasTrait(Trait.IterableType)) { - return valOrErr( - foldRange, "got '%s', expected iterable type", foldRange.getClass().getName()); - } - // Configure the fold activation with the accumulator initial value. - VarActivation accuCtx = new VarActivation(); - accuCtx.parent = ctx; - accuCtx.name = accuVar; - accuCtx.val = accu.eval(ctx); - VarActivation iterCtx = new VarActivation(); - iterCtx.parent = accuCtx; - iterCtx.name = iterVar; - IteratorT it = ((IterableT) foldRange).iterator(); - while (it.hasNext() == True) { - // Modify the iter var in the fold activation. - iterCtx.val = it.next(); - - // Evaluate the condition, but don't terminate the loop as this is exhaustive eval! - cond.eval(iterCtx); - - // Evalute the evaluation step into accu var. - accuCtx.val = step.eval(iterCtx); - } - // Compute the result. - return result.eval(accuCtx); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - // Compute the cost for evaluating iterRange. - Cost i = estimateCost(iterRange); - - // Compute the size of iterRange. If the size depends on the input, return the maximum - // possible - // cost range. - Val foldRange = iterRange.eval(emptyActivation()); - if (!foldRange.type().hasTrait(Trait.IterableType)) { - return Cost.Unknown; - } - long rangeCnt = 0L; - IteratorT it = ((IterableT) foldRange).iterator(); - while (it.hasNext() == True) { - it.next(); - rangeCnt++; - } - - Cost a = estimateCost(accu); - Cost c = estimateCost(cond); - Cost s = estimateCost(step); - Cost r = estimateCost(result); - - // The cond and step costs are multiplied by size(iterRange). - return i.add(a).add(c.multiply(rangeCnt)).add(s.multiply(rangeCnt)).add(r); - } - - @Override - public String toString() { - return "EvalExhaustiveFold{" - + "id=" - + id - + ", accuVar='" - + accuVar - + '\'' - + ", iterVar='" - + iterVar - + '\'' - + ", iterRange=" - + iterRange - + ", accu=" - + accu - + ", cond=" - + cond - + ", step=" - + step - + ", result=" - + result - + '}'; - } - } - - /** EvalExhaustiveListFold evaluates every filter and transform without short-circuiting. */ - final class EvalExhaustiveListFold extends AbstractEval implements Coster { - private final EvalListFold fold; - - EvalExhaustiveListFold(EvalListFold fold) { - super(fold.id); - this.fold = fold; - } - - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val foldRange = fold.iterRange.eval(ctx); - if (!foldRange.type().hasTrait(Trait.IterableType)) { - return valOrErr( - foldRange, "got '%s', expected iterable type", foldRange.getClass().getName()); - } - - VarActivation iterCtx = new VarActivation(); - iterCtx.parent = ctx; - iterCtx.name = fold.iterVar; - List values = new ArrayList<>(fold.listCapacity(foldRange)); - Val result = null; - IteratorT it = ((IterableT) foldRange).iterator(); - while (it.hasNext() == True) { - iterCtx.val = it.next(); - - Val include = fold.filter != null ? fold.filter.eval(iterCtx) : True; - Val value = fold.transform.eval(iterCtx); - if (include == False) { - continue; - } - if (include != True) { - result = noSuchOverload(null, Operator.Conditional.id, include); - continue; - } - if (result == null) { - if (isUnknownOrError(value)) { - result = value; - } else { - values.add(value); - } - } - } - return result != null - ? result - : ListT.newValArrayList(fold.adapter, values.toArray(new Val[0])); - } - - @Override - public Cost cost() { - return fold.cost(); - } - - @Override - public String toString() { - return "EvalExhaustiveListFold{" + fold + '}'; - } - } - - /** evalAttr evaluates an Attribute value. */ - final class EvalAttr extends AbstractEval - implements InterpretableAttribute, Coster, Qualifier, Attribute { - private final TypeAdapter adapter; - private Attribute attr; - - EvalAttr(TypeAdapter adapter, Attribute attr) { - super(attr.id()); - this.adapter = Objects.requireNonNull(adapter); - this.attr = Objects.requireNonNull(attr); - } - - /** AddQualifier implements the instAttr interface method. */ - @Override - public Attribute addQualifier(AttributeFactory.Qualifier qualifier) { - attr = attr.addQualifier(qualifier); - return attr; - } - - /** Attr implements the instAttr interface method. */ - @Override - public Attribute attr() { - return attr; - } - - /** Adapter implements the instAttr interface method. */ - @Override - public TypeAdapter adapter() { - return adapter; - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return estimateCost(attr); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - try { - Object v = attr.resolve(ctx); - return adapter.nativeToValue(v); - } catch (Exception e) { - return newErr(e, e.toString()); - } - } - - /** Qualify proxies to the Attribute's Qualify method. */ - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation ctx, Object obj) { - return attr.qualify(ctx, obj); - } - - /** Resolve proxies to the Attribute's Resolve method. */ - @Override - public Object resolve(org.projectnessie.cel.interpreter.Activation ctx) { - return attr.resolve(ctx); - } - - @Override - public String toString() { - return "EvalAttr{" + "id=" + id + ", attr=" + attr + '}'; - } - } } diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretableDecorator.java b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretableDecorator.java index eb4f9978..ea92b841 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretableDecorator.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretableDecorator.java @@ -15,7 +15,6 @@ */ package org.projectnessie.cel.interpreter; -import static org.projectnessie.cel.common.types.BoolT.False; import static org.projectnessie.cel.common.types.BoolT.True; import static org.projectnessie.cel.common.types.Err.throwErrorAsIllegalStateException; import static org.projectnessie.cel.common.types.IntT.IntZero; @@ -30,22 +29,6 @@ import org.projectnessie.cel.common.types.ref.Type; import org.projectnessie.cel.common.types.ref.Val; import org.projectnessie.cel.common.types.traits.Lister; -import org.projectnessie.cel.interpreter.AttributeFactory.ConditionalAttribute; -import org.projectnessie.cel.interpreter.Interpretable.EvalAnd; -import org.projectnessie.cel.interpreter.Interpretable.EvalExhaustiveAnd; -import org.projectnessie.cel.interpreter.Interpretable.EvalExhaustiveConditional; -import org.projectnessie.cel.interpreter.Interpretable.EvalExhaustiveFold; -import org.projectnessie.cel.interpreter.Interpretable.EvalExhaustiveListFold; -import org.projectnessie.cel.interpreter.Interpretable.EvalExhaustiveOr; -import org.projectnessie.cel.interpreter.Interpretable.EvalFold; -import org.projectnessie.cel.interpreter.Interpretable.EvalList; -import org.projectnessie.cel.interpreter.Interpretable.EvalListFold; -import org.projectnessie.cel.interpreter.Interpretable.EvalMap; -import org.projectnessie.cel.interpreter.Interpretable.EvalOr; -import org.projectnessie.cel.interpreter.Interpretable.EvalSetMembership; -import org.projectnessie.cel.interpreter.Interpretable.EvalWatch; -import org.projectnessie.cel.interpreter.Interpretable.EvalWatchAttr; -import org.projectnessie.cel.interpreter.Interpretable.EvalWatchConst; import org.projectnessie.cel.interpreter.Interpretable.InterpretableAttribute; import org.projectnessie.cel.interpreter.Interpretable.InterpretableCall; import org.projectnessie.cel.interpreter.Interpretable.InterpretableConst; @@ -89,31 +72,31 @@ static InterpretableDecorator decObserveEval(EvalObserver observer) { */ static InterpretableDecorator decDisableShortcircuits() { return i -> { - if (i instanceof EvalOr) { - EvalOr expr = (EvalOr) i; + if (i instanceof EvalOr expr) { return new EvalExhaustiveOr(expr.id, expr.lhs, expr.rhs); } - if (i instanceof EvalAnd) { - EvalAnd expr = (EvalAnd) i; + if (i instanceof EvalAnd expr) { return new EvalExhaustiveAnd(expr.id, expr.lhs, expr.rhs); } - if (i instanceof EvalFold) { - EvalFold expr = (EvalFold) i; + if (i instanceof EvalFold expr) { return new EvalExhaustiveFold( expr.id, expr.accu, expr.accuVar, expr.iterRange, expr.iterVar, + expr.iterVar2, expr.cond, expr.step, expr.result); } - if (i instanceof EvalListFold) { - return new EvalExhaustiveListFold((EvalListFold) i); + if (i instanceof EvalListFold fold) { + return new EvalExhaustiveListFold(fold); } - if (i instanceof InterpretableAttribute) { - InterpretableAttribute expr = (InterpretableAttribute) i; + if (i instanceof EvalMapFold fold) { + return new EvalExhaustiveMapFold(fold); + } + if (i instanceof InterpretableAttribute expr) { if (expr.attr() instanceof ConditionalAttribute) { return new EvalExhaustiveConditional( i.id(), expr.adapter(), (ConditionalAttribute) expr.attr()); @@ -140,8 +123,7 @@ static InterpretableDecorator decOptimize() { if (i instanceof EvalMap) { return maybeBuildMapLiteral(i, (EvalMap) i); } - if (i instanceof InterpretableCall) { - InterpretableCall inst = (InterpretableCall) i; + if (i instanceof InterpretableCall inst) { if (inst.overloadID().equals(Overloads.InList)) { return maybeOptimizeSetMembership(i, inst); } @@ -200,15 +182,14 @@ static Interpretable maybeOptimizeSetMembership(Interpretable i, InterpretableCa Interpretable[] args = inlist.args(); Interpretable lhs = args[0]; Interpretable rhs = args[1]; - if (!(rhs instanceof InterpretableConst)) { + if (!(rhs instanceof InterpretableConst l)) { return i; } - InterpretableConst l = (InterpretableConst) rhs; // When the incoming binary call is flagged with as the InList overload, the value will // always be convertible to a `traits.Lister` type. Lister list = (Lister) l.value(); if (list.size() == IntZero) { - return newConstValue(inlist.id(), False); + return new EvalSetMembership(inlist, lhs, null, Set.of()); } IteratorT it = list.iterator(); Type typ = null; @@ -226,6 +207,11 @@ static Interpretable maybeOptimizeSetMembership(Interpretable i, InterpretableCa } valueSet.add(elem); } + if (typ == null) { + // A custom Lister can report a non-zero size but still yield no elements. In that case the + // optimizer cannot determine the element type and must preserve the original evaluation. + return i; + } return new EvalSetMembership(inlist, lhs, typ.typeName(), valueSet); } } diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java index 7d9c7416..3121bab1 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java @@ -15,64 +15,15 @@ */ package org.projectnessie.cel.interpreter; -import static org.projectnessie.cel.common.types.BytesT.bytesOf; -import static org.projectnessie.cel.common.types.DoubleT.doubleOf; -import static org.projectnessie.cel.common.types.DurationT.durationOf; -import static org.projectnessie.cel.common.types.IntT.intOf; -import static org.projectnessie.cel.common.types.StringT.stringOf; -import static org.projectnessie.cel.common.types.TimestampT.timestampOf; -import static org.projectnessie.cel.common.types.Types.boolOf; -import static org.projectnessie.cel.common.types.UintT.uintOf; -import static org.projectnessie.cel.interpreter.Interpretable.newConstValue; - import com.google.api.expr.v1alpha1.CheckedExpr; -import com.google.api.expr.v1alpha1.Constant; import com.google.api.expr.v1alpha1.Expr; -import com.google.api.expr.v1alpha1.Expr.Call; -import com.google.api.expr.v1alpha1.Expr.Comprehension; -import com.google.api.expr.v1alpha1.Expr.CreateList; -import com.google.api.expr.v1alpha1.Expr.CreateStruct; -import com.google.api.expr.v1alpha1.Expr.CreateStruct.Entry; -import com.google.api.expr.v1alpha1.Expr.Ident; -import com.google.api.expr.v1alpha1.Expr.Select; import com.google.api.expr.v1alpha1.Reference; import com.google.api.expr.v1alpha1.Type; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.projectnessie.cel.common.containers.Container; -import org.projectnessie.cel.common.operators.Operator; -import org.projectnessie.cel.common.types.NullT; -import org.projectnessie.cel.common.types.ref.FieldType; import org.projectnessie.cel.common.types.ref.TypeAdapter; import org.projectnessie.cel.common.types.ref.TypeProvider; -import org.projectnessie.cel.common.types.ref.Val; -import org.projectnessie.cel.common.types.traits.Trait; -import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; -import org.projectnessie.cel.interpreter.AttributeFactory.Qualifier; -import org.projectnessie.cel.interpreter.Interpretable.EvalAnd; -import org.projectnessie.cel.interpreter.Interpretable.EvalAttr; -import org.projectnessie.cel.interpreter.Interpretable.EvalBinary; -import org.projectnessie.cel.interpreter.Interpretable.EvalEq; -import org.projectnessie.cel.interpreter.Interpretable.EvalFold; -import org.projectnessie.cel.interpreter.Interpretable.EvalIdent; -import org.projectnessie.cel.interpreter.Interpretable.EvalList; -import org.projectnessie.cel.interpreter.Interpretable.EvalListFold; -import org.projectnessie.cel.interpreter.Interpretable.EvalMap; -import org.projectnessie.cel.interpreter.Interpretable.EvalNe; -import org.projectnessie.cel.interpreter.Interpretable.EvalObj; -import org.projectnessie.cel.interpreter.Interpretable.EvalOr; -import org.projectnessie.cel.interpreter.Interpretable.EvalReceiverVarArgs; -import org.projectnessie.cel.interpreter.Interpretable.EvalTestOnly; -import org.projectnessie.cel.interpreter.Interpretable.EvalUnary; -import org.projectnessie.cel.interpreter.Interpretable.EvalVarArgs; -import org.projectnessie.cel.interpreter.Interpretable.EvalZeroArity; -import org.projectnessie.cel.interpreter.Interpretable.InterpretableAttribute; -import org.projectnessie.cel.interpreter.Interpretable.InterpretableConst; -import org.projectnessie.cel.interpreter.functions.BinaryOp; -import org.projectnessie.cel.interpreter.functions.FunctionOp; -import org.projectnessie.cel.interpreter.functions.Overload; -import org.projectnessie.cel.interpreter.functions.UnaryOp; /** interpretablePlanner creates an Interpretable evaluation plan from a proto Expr value. */ public interface InterpretablePlanner { @@ -101,6 +52,7 @@ static InterpretablePlanner newPlanner( cont, checked.getReferenceMapMap(), checked.getTypeMapMap(), + PlanningPolicy.ESTABLISHED_ONLY, decorators); } @@ -117,7 +69,16 @@ static InterpretablePlanner newPlanner( Map refMap, Map typeMap, InterpretableDecorator... decorators) { - return new Planner(disp, provider, adapter, attrFactory, cont, refMap, typeMap, decorators); + return new Planner( + disp, + provider, + adapter, + attrFactory, + cont, + refMap, + typeMap, + PlanningPolicy.ESTABLISHED_ONLY, + decorators); } /** @@ -133,832 +94,14 @@ static InterpretablePlanner newUncheckedPlanner( Container cont, InterpretableDecorator... decorators) { return new Planner( - disp, provider, adapter, attrFactory, cont, new HashMap<>(), new HashMap<>(), decorators); - } - - /** planner is an implementatio of the interpretablePlanner interface. */ - static final class Planner implements InterpretablePlanner { - private final Dispatcher disp; - private final TypeProvider provider; - private final TypeAdapter adapter; - private final AttributeFactory attrFactory; - private final Container container; - private final Map refMap; - private final Map typeMap; - private final Map fieldTypes = new HashMap<>(); - private final InterpretableDecorator[] decorators; - - Planner( - Dispatcher disp, - TypeProvider provider, - TypeAdapter adapter, - AttributeFactory attrFactory, - Container container, - Map refMap, - Map typeMap, - InterpretableDecorator[] decorators) { - this.disp = disp; - this.provider = provider; - this.adapter = adapter; - this.attrFactory = attrFactory; - this.container = container; - this.refMap = refMap; - this.typeMap = typeMap; - this.decorators = decorators; - } - - /** - * Plan implements the interpretablePlanner interface. This implementation of the Plan method - * also applies decorators to each Interpretable generated as part of the overall plan. - * Decorators are useful for layering functionality into the evaluation that is not natively - * understood by CEL, such as state-tracking, expression re-write, and possibly efficient - * thread-safe memoization of repeated expressions. - */ - @Override - public Interpretable plan(Expr expr) { - switch (expr.getExprKindCase()) { - case CALL_EXPR: - return decorate(planCall(expr)); - case IDENT_EXPR: - return decorate(planIdent(expr)); - case SELECT_EXPR: - return decorate(planSelect(expr)); - case LIST_EXPR: - return decorate(planCreateList(expr)); - case STRUCT_EXPR: - return decorate(planCreateStruct(expr)); - case COMPREHENSION_EXPR: - return decorate(planComprehension(expr)); - case CONST_EXPR: - return decorate(planConst(expr)); - } - throw new IllegalArgumentException( - String.format("unsupported expr of kind %s: '%s'", expr.getExprKindCase(), expr)); - } - - /** - * decorate applies the InterpretableDecorator functions to the given Interpretable. Both the - * Interpretable and error generated by a Plan step are accepted as arguments for convenience. - */ - Interpretable decorate(Interpretable i) { - for (InterpretableDecorator dec : decorators) { - i = dec.decorate(i); - if (i == null) { - return null; - } - } - return i; - } - - /** planIdent creates an Interpretable that resolves an identifier from an Activation. */ - Interpretable planIdent(Expr expr) { - // Establish whether the identifier is in the reference map. - Reference identRef = refMap.get(expr.getId()); - if (identRef != null) { - return planCheckedIdent(expr.getId(), identRef); - } - // Create the possible attribute list for the unresolved reference. - Ident ident = expr.getIdentExpr(); - return new EvalAttr(adapter, attrFactory.maybeAttribute(expr.getId(), ident.getName())); - } - - Interpretable planCheckedIdent(long id, Reference identRef) { - // Plan a constant reference if this is the case for this simple identifier. - if (identRef.getValue() != Reference.getDefaultInstance().getValue()) { - return plan(Expr.newBuilder().setId(id).setConstExpr(identRef.getValue()).build()); - } - - // Check to see whether the type map indicates this is a type name. All types should be - // registered with the provider. - Type cType = typeMap.get(id); - if (cType != null && cType.getType() != Type.getDefaultInstance()) { - Val cVal = provider.findIdent(identRef.getName()); - if (cVal == null) { - throw new IllegalStateException( - String.format("reference to undefined type: %s", identRef.getName())); - } - return newConstValue(id, cVal); - } - - // Otherwise, evaluate the checked top-level variable directly for ordinary plans. Decorated - // programs keep the attribute shape because custom decorators may inspect attributes. - if (decorators.length == 0) { - return new EvalIdent(id, identRef.getName(), adapter); - } - return new EvalAttr(adapter, attrFactory.absoluteAttribute(id, identRef.getName())); - } - - /** - * planSelect creates an Interpretable with either: - * - *

    - *
  1. selects a field from a map or proto. - *
  2. creates a field presence test for a select within a has() macro. - *
  3. resolves the select expression to a namespaced identifier. - *
- */ - Interpretable planSelect(Expr expr) { - // If the Select id appears in the reference map from the CheckedExpr proto then it is either - // a namespaced identifier or enum value. - Reference identRef = refMap.get(expr.getId()); - if (identRef != null) { - return planCheckedIdent(expr.getId(), identRef); - } - - Select sel = expr.getSelectExpr(); - // Plan the operand evaluation. - Interpretable op = - decorators.length == 0 ? planSelectOperand(sel.getOperand()) : plan(sel.getOperand()); - - // Determine the field type if this is a proto message type. - FieldType fieldType = null; - Type opType = typeMap.get(sel.getOperand().getId()); - if (opType != null && !opType.getMessageType().isEmpty()) { - FieldType ft = findFieldType(opType.getMessageType(), sel.getField()); - if (ft != null && ft.isSet != null && ft.getFrom != null) { - fieldType = ft; - } - } - - // If the Select was marked TestOnly, this is a presence test. - // - // Note: presence tests are defined for structured (e.g. proto) and dynamic values (map, json) - // as follows: - // - True if the object field has a non-default value, e.g. obj.str != "" - // - True if the dynamic value has the field defined, e.g. key in map - // - // However, presence tests are not defined for qualified identifier names with primitive - // types. - // If a string named 'a.b.c' is declared in the environment and referenced within - // `has(a.b.c)`, - // it is not clear whether has should error or follow the convention defined for structured - // values. - if (sel.getTestOnly()) { - // Return the test only eval expression. - return new EvalTestOnly(expr.getId(), op, stringOf(sel.getField()), fieldType); - } - // Build a qualifier. - Qualifier qual = - fieldType != null - ? new AttributeFactory.FieldQualifier( - expr.getId(), sel.getField(), fieldType, adapter) - : attrFactory.newQualifier(opType, expr.getId(), sel.getField()); - if (qual == null) { - return null; - } - // Lastly, create a field selection Interpretable. - if (op instanceof InterpretableAttribute) { - InterpretableAttribute attr = (InterpretableAttribute) op; - attr.addQualifier(qual); - return attr; - } - - InterpretableAttribute relAttr = relativeAttr(op.id(), op); - if (relAttr == null) { - return null; - } - relAttr.addQualifier(qual); - return relAttr; - } - - private Interpretable planSelectOperand(Expr operand) { - if (operand.getExprKindCase() != Expr.ExprKindCase.IDENT_EXPR) { - return plan(operand); - } - - Reference identRef = refMap.get(operand.getId()); - if (identRef == null || identRef.getValue() != Reference.getDefaultInstance().getValue()) { - return plan(operand); - } - - Type cType = typeMap.get(operand.getId()); - if (cType != null && cType.getType() != Type.getDefaultInstance()) { - return plan(operand); - } - - return new EvalAttr( - adapter, attrFactory.absoluteAttribute(operand.getId(), identRef.getName())); - } - - private FieldType findFieldType(String messageType, String fieldName) { - String key = messageType + '\n' + fieldName; - FieldType ft = fieldTypes.get(key); - if (ft != null) { - return ft; - } - ft = provider.findFieldType(messageType, fieldName); - if (ft != null) { - fieldTypes.put(key, ft); - } - return ft; - } - - /** - * planCall creates a callable Interpretable while specializing for common functions and - * invocation patterns. Specifically, conditional operators &&, ||, ?:, and (in)equality - * functions result in optimized Interpretable values. - */ - Interpretable planCall(Expr expr) { - Call call = expr.getCallExpr(); - ResolvedFunction resolvedFunc = resolveFunction(expr); - // target, fnName, oName := p.resolveFunction(expr) - int argCount = call.getArgsCount(); - int offset = 0; - if (resolvedFunc.target != null) { - argCount++; - offset++; - } - - Interpretable[] args = new Interpretable[argCount]; - if (resolvedFunc.target != null) { - Interpretable arg = plan(resolvedFunc.target); - if (arg == null) { - return null; - } - args[0] = arg; - } - for (int i = 0; i < call.getArgsCount(); i++) { - Expr argExpr = call.getArgs(i); - Interpretable arg = plan(argExpr); - args[i + offset] = arg; - } - - // Generate specialized Interpretable operators by function name if possible. - if (resolvedFunc.fnName.equals(Operator.LogicalAnd.id)) return planCallLogicalAnd(expr, args); - if (resolvedFunc.fnName.equals(Operator.LogicalOr.id)) return planCallLogicalOr(expr, args); - if (resolvedFunc.fnName.equals(Operator.Conditional.id)) - return planCallConditional(expr, args); - if (resolvedFunc.fnName.equals(Operator.Equals.id)) return planCallEqual(expr, args); - if (resolvedFunc.fnName.equals(Operator.NotEquals.id)) return planCallNotEqual(expr, args); - if (resolvedFunc.fnName.equals(Operator.Index.id)) return planCallIndex(expr, args); - - // Otherwise, generate Interpretable calls specialized by argument count. - // Try to find the specific function by overload id. - Overload fnDef = null; - if (resolvedFunc.overloadId != null && !resolvedFunc.overloadId.isEmpty()) { - fnDef = disp.findOverload(resolvedFunc.overloadId); - } - // If the overload id couldn't resolve the function, try the simple function name. - if (fnDef == null) { - fnDef = disp.findOverload(resolvedFunc.fnName); - } - switch (argCount) { - case 0: - return planCallZero(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef); - case 1: - return planCallUnary(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, args); - case 2: - return planCallBinary(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, args); - default: - return planCallVarArgs(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, args); - } - } - - /** planCallZero generates a zero-arity callable Interpretable. */ - static Interpretable planCallZero(Expr expr, String function, String overload, Overload impl) { - if (impl == null || impl.function == null) { - throw new IllegalArgumentException(String.format("no such overload: %s()", function)); - } - return new EvalZeroArity(expr.getId(), function, overload, impl.function); - } - - /** planCallUnary generates a unary callable Interpretable. */ - static Interpretable planCallUnary( - Expr expr, String function, String overload, Overload impl, Interpretable[] args) { - UnaryOp fn = null; - Trait trait = null; - if (impl != null) { - if (impl.unary == null) { - throw new IllegalStateException(String.format("no such overload: %s(arg)", function)); - } - fn = impl.unary; - trait = impl.operandTrait; - } - return new EvalUnary(expr.getId(), function, overload, args[0], trait, fn); - } - - /** planCallBinary generates a binary callable Interpretable. */ - static Interpretable planCallBinary( - Expr expr, String function, String overload, Overload impl, Interpretable... args) { - BinaryOp fn = null; - Trait trait = null; - if (impl != null) { - if (impl.binary == null) { - throw new IllegalStateException( - String.format("no such overload: %s(lhs, rhs)", function)); - } - fn = impl.binary; - trait = impl.operandTrait; - } - return new EvalBinary(expr.getId(), function, overload, args[0], args[1], trait, fn); - } - - /** planCallVarArgs generates a variable argument callable Interpretable. */ - static Interpretable planCallVarArgs( - Expr expr, String function, String overload, Overload impl, Interpretable... args) { - if (impl == null) { - return new EvalReceiverVarArgs(expr.getId(), function, overload, args); - } - FunctionOp fn = null; - Trait trait = null; - if (impl.function == null) { - throw new IllegalStateException(String.format("no such overload: %s(...)", function)); - } - fn = impl.function; - trait = impl.operandTrait; - return new EvalVarArgs(expr.getId(), function, overload, args, trait, fn); - } - - /** planCallEqual generates an equals (==) Interpretable. */ - static Interpretable planCallEqual(Expr expr, Interpretable... args) { - return new EvalEq(expr.getId(), args[0], args[1]); - } - - /** planCallNotEqual generates a not equals (!=) Interpretable. */ - static Interpretable planCallNotEqual(Expr expr, Interpretable... args) { - return new EvalNe(expr.getId(), args[0], args[1]); - } - - /** planCallLogicalAnd generates a logical and (&&) Interpretable. */ - static Interpretable planCallLogicalAnd(Expr expr, Interpretable... args) { - return new EvalAnd(expr.getId(), args[0], args[1]); - } - - /** planCallLogicalOr generates a logical or (||) Interpretable. */ - static Interpretable planCallLogicalOr(Expr expr, Interpretable... args) { - return new EvalOr(expr.getId(), args[0], args[1]); - } - - /** planCallConditional generates a conditional / ternary (c ? t : f) Interpretable. */ - Interpretable planCallConditional(Expr expr, Interpretable... args) { - Interpretable cond = args[0]; - - Interpretable t = args[1]; - Attribute tAttr; - if (t instanceof InterpretableAttribute) { - InterpretableAttribute truthyAttr = (InterpretableAttribute) t; - tAttr = truthyAttr.attr(); - } else { - tAttr = attrFactory.relativeAttribute(t.id(), t); - } - - Interpretable f = args[2]; - Attribute fAttr; - if (f instanceof InterpretableAttribute) { - InterpretableAttribute falsyAttr = (InterpretableAttribute) f; - fAttr = falsyAttr.attr(); - } else { - fAttr = attrFactory.relativeAttribute(f.id(), f); - } - - return new EvalAttr( - adapter, attrFactory.conditionalAttribute(expr.getId(), cond, tAttr, fAttr)); - } - - /** - * planCallIndex either extends an attribute with the argument to the index operation, or - * creates a relative attribute based on the return of a function call or operation. - */ - Interpretable planCallIndex(Expr expr, Interpretable... args) { - Interpretable op = args[0]; - Interpretable ind = args[1]; - InterpretableAttribute opAttr = relativeAttr(op.id(), op); - if (opAttr == null) { - return null; - } - Type opType = typeMap.get(expr.getCallExpr().getTarget().getId()); - if (ind instanceof InterpretableConst) { - InterpretableConst indConst = (InterpretableConst) ind; - Qualifier qual = attrFactory.newQualifier(opType, expr.getId(), indConst.value()); - if (qual == null) { - return null; - } - opAttr.addQualifier(qual); - return opAttr; - } - if (ind instanceof InterpretableAttribute) { - InterpretableAttribute indAttr = (InterpretableAttribute) ind; - Qualifier qual = attrFactory.newQualifier(opType, expr.getId(), indAttr); - if (qual == null) { - return null; - } - opAttr.addQualifier(qual); - return opAttr; - } - InterpretableAttribute indQual = relativeAttr(expr.getId(), ind); - if (indQual == null) { - return null; - } - opAttr.addQualifier(indQual); - return opAttr; - } - - /** planCreateList generates a list construction Interpretable. */ - Interpretable planCreateList(Expr expr) { - CreateList list = expr.getListExpr(); - Interpretable[] elems = new Interpretable[list.getElementsCount()]; - for (int i = 0; i < list.getElementsCount(); i++) { - Expr elem = list.getElements(i); - Interpretable elemVal = plan(elem); - if (elemVal == null) { - return null; - } - elems[i] = elemVal; - } - return new EvalList(expr.getId(), elems, adapter); - } - - /** planCreateStruct generates a map or object construction Interpretable. */ - Interpretable planCreateStruct(Expr expr) { - CreateStruct str = expr.getStructExpr(); - if (!str.getMessageName().isEmpty()) { - return planCreateObj(expr); - } - List entries = str.getEntriesList(); - Interpretable[] keys = new Interpretable[entries.size()]; - Interpretable[] vals = new Interpretable[entries.size()]; - for (int i = 0; i < entries.size(); i++) { - Entry entry = entries.get(i); - Interpretable keyVal = plan(entry.getMapKey()); - if (keyVal == null) { - return null; - } - keys[i] = keyVal; - - Interpretable valVal = plan(entry.getValue()); - if (valVal == null) { - return null; - } - vals[i] = valVal; - } - return new EvalMap(expr.getId(), keys, vals, adapter); - } - - /** planCreateObj generates an object construction Interpretable. */ - Interpretable planCreateObj(Expr expr) { - CreateStruct obj = expr.getStructExpr(); - String typeName = resolveTypeName(obj.getMessageName()); - if (typeName == null) { - throw new IllegalStateException(String.format("unknown type: %s", obj.getMessageName())); - } - List entries = obj.getEntriesList(); - String[] fields = new String[entries.size()]; - Interpretable[] vals = new Interpretable[entries.size()]; - for (int i = 0; i < entries.size(); i++) { - Entry entry = entries.get(i); - fields[i] = entry.getFieldKey(); - Interpretable val = plan(entry.getValue()); - if (val == null) { - return null; - } - vals[i] = val; - } - return new EvalObj(expr.getId(), typeName, fields, vals, provider); - } - - /** planComprehension generates an Interpretable fold operation. */ - Interpretable planComprehension(Expr expr) { - Comprehension fold = expr.getComprehensionExpr(); - MacroListFold macroListFold = macroListFold(fold); - if (macroListFold != null) { - Interpretable iterRange = plan(fold.getIterRange()); - if (iterRange == null) { - return null; - } - Interpretable filter = null; - if (macroListFold.filter != null) { - filter = plan(macroListFold.filter); - if (filter == null) { - return null; - } - } - Interpretable transform = plan(macroListFold.transform); - if (transform == null) { - return null; - } - return new EvalListFold( - expr.getId(), fold.getIterVar(), iterRange, filter, transform, adapter); - } - - Interpretable accu = plan(fold.getAccuInit()); - if (accu == null) { - return null; - } - Interpretable iterRange = plan(fold.getIterRange()); - if (iterRange == null) { - return null; - } - Interpretable cond = plan(fold.getLoopCondition()); - if (cond == null) { - return null; - } - Interpretable step = plan(fold.getLoopStep()); - if (step == null) { - return null; - } - Interpretable result = plan(fold.getResult()); - if (result == null) { - return null; - } - return new EvalFold( - expr.getId(), fold.getAccuVar(), accu, fold.getIterVar(), iterRange, cond, step, result); - } - - private static MacroListFold macroListFold(Comprehension fold) { - if (!isEmptyList(fold.getAccuInit()) - || !isBoolConst(fold.getLoopCondition(), true) - || !isIdent(fold.getResult(), fold.getAccuVar())) { - return null; - } - - Expr step = fold.getLoopStep(); - Expr filter = null; - if (isCall(step, Operator.Conditional.id, 3)) { - Call conditional = step.getCallExpr(); - if (!isIdent(conditional.getArgs(2), fold.getAccuVar())) { - return null; - } - filter = conditional.getArgs(0); - step = conditional.getArgs(1); - } - - Expr transform = appendedValue(fold.getAccuVar(), step); - if (transform == null - || referencesIdent(transform, fold.getAccuVar()) - || (filter != null && referencesIdent(filter, fold.getAccuVar()))) { - return null; - } - return new MacroListFold(filter, transform); - } - - private static Expr appendedValue(String accuVar, Expr step) { - if (!isCall(step, Operator.Add.id, 2)) { - return null; - } - Call add = step.getCallExpr(); - if (!isIdent(add.getArgs(0), accuVar)) { - return null; - } - Expr list = add.getArgs(1); - if (list.getExprKindCase() != Expr.ExprKindCase.LIST_EXPR - || list.getListExpr().getElementsCount() != 1) { - return null; - } - return list.getListExpr().getElements(0); - } - - private static boolean isCall(Expr expr, String function, int argCount) { - return expr.getExprKindCase() == Expr.ExprKindCase.CALL_EXPR - && expr.getCallExpr().getFunction().equals(function) - && expr.getCallExpr().getArgsCount() == argCount - && !expr.getCallExpr().hasTarget(); - } - - private static boolean isIdent(Expr expr, String name) { - return expr.getExprKindCase() == Expr.ExprKindCase.IDENT_EXPR - && expr.getIdentExpr().getName().equals(name); - } - - private static boolean isEmptyList(Expr expr) { - return expr.getExprKindCase() == Expr.ExprKindCase.LIST_EXPR - && expr.getListExpr().getElementsCount() == 0; - } - - private static boolean isBoolConst(Expr expr, boolean value) { - return expr.getExprKindCase() == Expr.ExprKindCase.CONST_EXPR - && expr.getConstExpr().getConstantKindCase() == Constant.ConstantKindCase.BOOL_VALUE - && expr.getConstExpr().getBoolValue() == value; - } - - private static boolean referencesIdent(Expr expr, String name) { - switch (expr.getExprKindCase()) { - case IDENT_EXPR: - return expr.getIdentExpr().getName().equals(name); - case SELECT_EXPR: - return referencesIdent(expr.getSelectExpr().getOperand(), name); - case CALL_EXPR: - Call call = expr.getCallExpr(); - if (call.hasTarget() && referencesIdent(call.getTarget(), name)) { - return true; - } - for (Expr arg : call.getArgsList()) { - if (referencesIdent(arg, name)) { - return true; - } - } - return false; - case LIST_EXPR: - for (Expr elem : expr.getListExpr().getElementsList()) { - if (referencesIdent(elem, name)) { - return true; - } - } - return false; - case STRUCT_EXPR: - for (Entry entry : expr.getStructExpr().getEntriesList()) { - if (referencesIdent(entry.getValue(), name)) { - return true; - } - } - return false; - case COMPREHENSION_EXPR: - Comprehension comprehension = expr.getComprehensionExpr(); - return referencesIdent(comprehension.getIterRange(), name) - || referencesIdent(comprehension.getAccuInit(), name) - || (!comprehension.getIterVar().equals(name) - && !comprehension.getAccuVar().equals(name) - && referencesIdent(comprehension.getLoopCondition(), name)) - || (!comprehension.getIterVar().equals(name) - && !comprehension.getAccuVar().equals(name) - && referencesIdent(comprehension.getLoopStep(), name)) - || (!comprehension.getAccuVar().equals(name) - && referencesIdent(comprehension.getResult(), name)); - default: - return false; - } - } - - private static final class MacroListFold { - final Expr filter; - final Expr transform; - - private MacroListFold(Expr filter, Expr transform) { - this.filter = filter; - this.transform = transform; - } - } - - /** planConst generates a constant valued Interpretable. */ - static Interpretable planConst(Expr expr) { - Val val = constValue(expr.getConstExpr()); - if (val == null) { - return null; - } - return newConstValue(expr.getId(), val); - } - - /** constValue converts a proto Constant value to a ref.Val. */ - @SuppressWarnings("deprecation") - static Val constValue(Constant c) { - switch (c.getConstantKindCase()) { - case BOOL_VALUE: - return boolOf(c.getBoolValue()); - case BYTES_VALUE: - return bytesOf(c.getBytesValue()); - case DOUBLE_VALUE: - return doubleOf(c.getDoubleValue()); - case DURATION_VALUE: - return durationOf(c.getDurationValue()); - case INT64_VALUE: - return intOf(c.getInt64Value()); - case NULL_VALUE: - return NullT.NullValue; - case STRING_VALUE: - return stringOf(c.getStringValue()); - case TIMESTAMP_VALUE: - return timestampOf(c.getTimestampValue()); - case UINT64_VALUE: - return uintOf(c.getUint64Value()); - } - throw new IllegalArgumentException( - String.format("unknown constant type: '%s' of kind '%s'", c, c.getConstantKindCase())); - } - - /** - * resolveTypeName takes a qualified string constructed at parse time, applies the proto - * namespace resolution rules to it in a scan over possible matching types in the TypeProvider. - */ - String resolveTypeName(String typeName) { - for (String qualifiedTypeName : container.resolveCandidateNames(typeName)) { - if (provider.findType(qualifiedTypeName) != null) { - return qualifiedTypeName; - } - } - return null; - } - - static final class ResolvedFunction { - final Expr target; - final String fnName; - final String overloadId; - - ResolvedFunction(Expr target, String fnName, String overloadId) { - this.target = target; - this.fnName = fnName; - this.overloadId = overloadId; - } - } - - /** - * resolveFunction determines the call target, function name, and overload name from a given - * Expr value. - * - *

The resolveFunction resolves ambiguities where a function may either be a receiver-style - * invocation or a qualified global function name. - * - *

    - *
  • The target expression may only consist of ident and select expressions. - *
  • The function is declared in the environment using its fully-qualified name. - *
  • The fully-qualified function name matches the string serialized target value. - *
- */ - ResolvedFunction resolveFunction(Expr expr) { - // Note: similar logic exists within the `checker/checker.go`. If making changes here - // please consider the impact on checker.go and consolidate implementations or mirror code - // as appropriate. - Call call = expr.getCallExpr(); - Expr target = call.hasTarget() ? call.getTarget() : null; - String fnName = call.getFunction(); - - // Checked expressions always have a reference map entry, and _should_ have the fully - // qualified - // function name as the fnName value. - Reference oRef = refMap.get(expr.getId()); - if (oRef != null) { - if (oRef.getOverloadIdCount() == 1) { - return new ResolvedFunction(target, fnName, oRef.getOverloadId(0)); - } - // Note, this namespaced function name will not appear as a fully qualified name in ASTs - // built and stored before cel-go v0.5.0; however, this functionality did not work at all - // before the v0.5.0 release. - return new ResolvedFunction(target, fnName, ""); - } - - // Parse-only expressions need to handle the same logic as is normally performed at check - // time, - // but with potentially much less information. The only reliable source of information about - // which functions are configured is the dispatcher. - if (target == null) { - // If the user has a parse-only expression, then it should have been configured as such in - // the interpreter dispatcher as it may have been omitted from the checker environment. - for (String qualifiedName : container.resolveCandidateNames(fnName)) { - if (disp.findOverload(qualifiedName) != null) { - return new ResolvedFunction(target, qualifiedName, ""); - } - } - // It's possible that the overload was not found, but this situation is accounted for in - // the planCall phase; however, the leading dot used for denoting fully-qualified - // namespaced identifiers must be stripped, as all declarations already use fully-qualified - // names. This stripping behavior is handled automatically by the ResolveCandidateNames - // call. - return new ResolvedFunction(target, stripLeadingDot(fnName), ""); - } - - // Handle the situation where the function target actually indicates a qualified function - // name. - String qualifiedPrefix = toQualifiedName(target); - if (qualifiedPrefix != null) { - String maybeQualifiedName = qualifiedPrefix + "." + fnName; - for (String qualifiedName : container.resolveCandidateNames(maybeQualifiedName)) { - if (disp.findOverload(qualifiedName) != null) { - // Clear the target to ensure the proper arity is used for finding the - // implementation. - return new ResolvedFunction(null, qualifiedName, ""); - } - } - } - // In the default case, the function is exactly as it was advertised: a receiver call on with - // an expression-based target with the given simple function name. - return new ResolvedFunction(target, fnName, ""); - } - - InterpretableAttribute relativeAttr(long id, Interpretable eval) { - InterpretableAttribute eAttr; - if (eval instanceof InterpretableAttribute) { - eAttr = (InterpretableAttribute) eval; - } else { - eAttr = new EvalAttr(adapter, attrFactory.relativeAttribute(id, eval)); - } - Interpretable decAttr = decorate(eAttr); - if (decAttr == null) { - return null; - } - if (!(decAttr instanceof InterpretableAttribute)) { - throw new IllegalStateException( - String.format( - "invalid attribute decoration: %s(%s)", decAttr, decAttr.getClass().getName())); - } - eAttr = (InterpretableAttribute) decAttr; - return eAttr; - } - - /** - * toQualifiedName converts an expression AST into a qualified name if possible, with a boolean - * 'found' value that indicates if the conversion is successful. - */ - String toQualifiedName(Expr operand) { - // If the checker identified the expression as an attribute by the type-checker, then it can't - // possibly be part of qualified name in a namespace. - if (refMap.containsKey(operand.getId())) { - return ""; - } - // Since functions cannot be both namespaced and receiver functions, if the operand is not an - // qualified variable name, return the (possibly) qualified name given the expressions. - return Container.toQualifiedName(operand); - } - - String stripLeadingDot(String name) { - return name.startsWith(".") ? name.substring(1) : name; - } + disp, + provider, + adapter, + attrFactory, + cont, + new HashMap<>(), + new HashMap<>(), + PlanningPolicy.ESTABLISHED_ONLY, + decorators); } } diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/Interpreter.java b/core/src/main/java/org/projectnessie/cel/interpreter/Interpreter.java index dfac180d..b1c6e45b 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/Interpreter.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Interpreter.java @@ -19,8 +19,6 @@ import static org.projectnessie.cel.interpreter.InterpretableDecorator.decDisableShortcircuits; import static org.projectnessie.cel.interpreter.InterpretableDecorator.decObserveEval; import static org.projectnessie.cel.interpreter.InterpretableDecorator.decOptimize; -import static org.projectnessie.cel.interpreter.InterpretablePlanner.newPlanner; -import static org.projectnessie.cel.interpreter.InterpretablePlanner.newUncheckedPlanner; import com.google.api.expr.v1alpha1.CheckedExpr; import com.google.api.expr.v1alpha1.Expr; @@ -100,7 +98,31 @@ static Interpreter newInterpreter( TypeProvider provider, TypeAdapter adapter, AttributeFactory attrFactory) { - return new ExprInterpreter(dispatcher, container, provider, adapter, attrFactory); + return new ExprInterpreter( + dispatcher, container, provider, adapter, attrFactory, PlanningPolicy.ESTABLISHED_ONLY); + } + + /** + * NewInterpreter builds an Interpreter with planning-time permission to use native + * specializations for eligible checked expressions. + * + *

Native planning remains disabled for unchecked expressions and whenever decorators are + * supplied directly to {@link #newInterpretable}. + */ + static Interpreter newInterpreter( + Dispatcher dispatcher, + Container container, + TypeProvider provider, + TypeAdapter adapter, + AttributeFactory attrFactory, + boolean allowNativePlanning) { + return new ExprInterpreter( + dispatcher, + container, + provider, + adapter, + attrFactory, + PlanningPolicy.nativeSpecialization(allowNativePlanning)); } /** @@ -113,53 +135,4 @@ static Interpreter newStandardInterpreter( dispatcher.add(Overload.standardOverloads()); return newInterpreter(dispatcher, container, provider, adapter, resolver); } - - final class ExprInterpreter implements Interpreter { - private final Dispatcher dispatcher; - private final Container container; - private final TypeProvider provider; - private final TypeAdapter adapter; - private final AttributeFactory attrFactory; - - ExprInterpreter( - Dispatcher dispatcher, - Container container, - TypeProvider provider, - TypeAdapter adapter, - AttributeFactory attrFactory) { - this.dispatcher = dispatcher; - this.container = container; - this.provider = provider; - this.adapter = adapter; - this.attrFactory = attrFactory; - } - - @Override - public Interpretable newInterpretable( - CheckedExpr checked, InterpretableDecorator... decorators) { - InterpretablePlanner p = - newPlanner(dispatcher, provider, adapter, attrFactory, container, checked, decorators); - return p.plan(checked.getExpr()); - } - - @Override - public Interpretable newInterpretable( - Expr expr, - Map refMap, - Map typeMap, - InterpretableDecorator... decorators) { - InterpretablePlanner p = - newPlanner( - dispatcher, provider, adapter, attrFactory, container, refMap, typeMap, decorators); - return p.plan(expr); - } - - @Override - public Interpretable newUncheckedInterpretable( - Expr expr, InterpretableDecorator... decorators) { - InterpretablePlanner p = - newUncheckedPlanner(dispatcher, provider, adapter, attrFactory, container, decorators); - return p.plan(expr); - } - } } diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/NativeAttributeNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/NativeAttributeNodes.java new file mode 100644 index 00000000..00736e1a --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/NativeAttributeNodes.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.Err.isError; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.UnknownT.isUnknown; +import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; +import static org.projectnessie.cel.interpreter.ValueSignal.signal; + +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; + +abstract class NativeScalarAttr extends EvalAttr { + private final long semanticId; + private final Attribute partialAttribute; + + NativeScalarAttr(long id, TypeAdapter adapter, Attribute attribute, Attribute partialAttribute) { + super(adapter, attribute); + this.semanticId = id; + this.partialAttribute = partialAttribute; + } + + @Override + public final long id() { + return semanticId; + } + + final Object resolveNative(Activation activation) { + try { + return activation instanceof Activation.PartialActivation && partialAttribute != null + ? partialAttribute.resolve(activation) + : attr.resolve(activation); + } catch (ValueSignal valueSignal) { + throw valueSignal; + } catch (Exception e) { + throw signal(newErr(e, e.toString())); + } + } + + final boolean usesPartialAttribute(Activation activation) { + return activation instanceof Activation.PartialActivation && partialAttribute != null; + } +} + +class NativeIntAttr extends NativeScalarAttr implements NativeIntCapability { + NativeIntAttr(long id, TypeAdapter adapter, Attribute attribute, Attribute partialAttribute) { + super(id, adapter, attribute, partialAttribute); + } + + @Override + public long evalInt(Activation activation) { + return NativeSupport.intValue(adapter, resolveNative(activation)); + } +} + +final class NativeUintAttr extends NativeScalarAttr implements NativeUintCapability { + NativeUintAttr(long id, TypeAdapter adapter, Attribute attribute, Attribute partialAttribute) { + super(id, adapter, attribute, partialAttribute); + } + + @Override + public long evalUint(Activation activation) { + return NativeSupport.uintValue(adapter, resolveNative(activation)); + } +} + +final class NativeBooleanAttr extends NativeScalarAttr implements NativeBooleanCapability { + NativeBooleanAttr(long id, TypeAdapter adapter, Attribute attribute, Attribute partialAttribute) { + super(id, adapter, attribute, partialAttribute); + } + + @Override + public boolean evalBoolean(Activation activation) { + return NativeSupport.booleanValue(adapter, resolveNative(activation)); + } +} + +final class NativeDoubleAttr extends NativeScalarAttr implements NativeDoubleCapability { + NativeDoubleAttr(long id, TypeAdapter adapter, Attribute attribute, Attribute partialAttribute) { + super(id, adapter, attribute, partialAttribute); + } + + @Override + public double evalDouble(Activation activation) { + return NativeSupport.doubleValue(adapter, resolveNative(activation)); + } +} + +final class NativeStringAttr extends NativeScalarAttr implements NativeStringCapability { + NativeStringAttr(long id, TypeAdapter adapter, Attribute attribute, Attribute partialAttribute) { + super(id, adapter, attribute, partialAttribute); + } + + @Override + public String evalString(Activation activation) { + return NativeSupport.stringValue(adapter, resolveNative(activation)); + } +} + +final class NativeNullAttr extends NativeScalarAttr implements NativeNullCapability { + NativeNullAttr(long id, TypeAdapter adapter, Attribute attribute, Attribute partialAttribute) { + super(id, adapter, attribute, partialAttribute); + } + + @Override + public void evalNull(Activation activation) { + NativeSupport.nullValue(adapter, resolveNative(activation)); + } +} + +/** Checked exact aggregate field whose getter result remains raw until its consumer chooses. */ +class EvalExactAggregateFieldAttr extends AbstractEval implements Coster { + private final Attribute attribute; + private final Attribute partialAttribute; + final CheckedAggregateMaterializer materializer; + + EvalExactAggregateFieldAttr( + long id, + Attribute attribute, + Attribute partialAttribute, + CheckedAggregateMaterializer materializer) { + super(id); + this.attribute = attribute; + this.partialAttribute = partialAttribute; + this.materializer = materializer; + } + + final Object resolveRaw(Activation activation) { + try { + return activation instanceof Activation.PartialActivation && partialAttribute != null + ? partialAttribute.resolve(activation) + : attribute.resolve(activation); + } catch (ValueSignal failure) { + throw failure; + } catch (Exception failure) { + throw signal(newErr(failure, failure.toString())); + } + } + + @Override + public Val eval(Activation activation) { + try { + Object value = resolveRaw(activation); + return value instanceof Val val && (isError(val) || isUnknown(val)) + ? val + : materializer.materialize(value); + } catch (ValueSignal failure) { + return failure.value; + } + } + + @Override + public Cost cost() { + return estimateCost(attribute); + } +} + +final class NativeExactListFieldAttr extends EvalExactAggregateFieldAttr + implements NativeListSourceCapability { + NativeExactListFieldAttr( + long id, + Attribute attribute, + Attribute partialAttribute, + CheckedAggregateMaterializer materializer) { + super(id, attribute, partialAttribute, materializer); + } + + @Override + public Object evalRaw(Activation activation) { + return resolveRaw(activation); + } + + @Override + public Val materializeResolvedList(Object value) { + return value instanceof Val val && (isError(val) || isUnknown(val)) + ? val + : materializer.materialize(value); + } + + @Override + public Val materializeResolvedElement(Object value) { + return materializer.materializeListElement(value); + } + + @Override + public boolean exactListSource() { + return true; + } +} + +final class NativeExactMapFieldAttr extends EvalExactAggregateFieldAttr + implements NativeMapSourceCapability { + NativeExactMapFieldAttr( + long id, + Attribute attribute, + Attribute partialAttribute, + CheckedAggregateMaterializer materializer) { + super(id, attribute, partialAttribute, materializer); + } + + @Override + public Object evalRaw(Activation activation) { + return resolveRaw(activation); + } + + @Override + public Val materializeResolvedMap(Object value) { + return value instanceof Val val && (isError(val) || isUnknown(val)) + ? val + : materializer.materialize(value); + } + + @Override + public boolean exactMapSource() { + return true; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/NativeCapabilities.java b/core/src/main/java/org/projectnessie/cel/interpreter/NativeCapabilities.java new file mode 100644 index 00000000..95057533 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/NativeCapabilities.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import org.projectnessie.cel.common.types.ref.Val; + +interface NativeIntCapability extends Interpretable { + long evalInt(Activation activation); +} + +interface NativeUintCapability extends Interpretable { + long evalUint(Activation activation); +} + +interface NativeDoubleCapability extends Interpretable { + double evalDouble(Activation activation); +} + +interface NativeBooleanCapability extends Interpretable { + boolean evalBoolean(Activation activation); +} + +interface NativeStringCapability extends Interpretable { + String evalString(Activation activation); +} + +interface NativeNullCapability extends Interpretable { + void evalNull(Activation activation); +} + +interface NativeRawCapability extends Interpretable { + Object evalRaw(Activation activation); +} + +/** + * A list-valued expression that can resolve its host representation once and materialize that + * already-resolved representation without replaying the expression. + */ +interface NativeListSourceCapability extends NativeRawCapability { + Val materializeResolvedList(Object value); + + Val materializeResolvedElement(Object value); + + boolean exactListSource(); +} + +/** + * A map-valued expression that can resolve its host representation once and materialize that + * already-resolved representation without replaying the expression. + */ +interface NativeMapSourceCapability extends NativeRawCapability { + Val materializeResolvedMap(Object value); + + boolean exactMapSource(); +} + +interface NativeScalarListLiteralCapability extends Interpretable { + int evalSize(Activation activation); +} + +interface NativeBooleanListLiteralCapability extends NativeScalarListLiteralCapability { + boolean evalBooleanAt(Activation activation, int index); +} + +interface NativeIntListLiteralCapability extends NativeScalarListLiteralCapability { + long evalIntAt(Activation activation, int index); +} + +interface NativeUintListLiteralCapability extends NativeScalarListLiteralCapability { + long evalUintAt(Activation activation, int index); +} + +interface NativeDoubleListLiteralCapability extends NativeScalarListLiteralCapability { + double evalDoubleAt(Activation activation, int index); +} + +interface NativeStringListLiteralCapability extends NativeScalarListLiteralCapability { + String evalStringAt(Activation activation, int index); + + boolean evalContains(Activation activation, NativeStringCapability needle); +} + +interface NativeScalarListFoldCapability extends Interpretable { + NativeScalarKind elementKind(); + + long evalSize(Activation activation); + + boolean evalBooleanAt(Activation activation, int index); + + long evalIntAt(Activation activation, int index); + + long evalUintAt(Activation activation, int index); + + double evalDoubleAt(Activation activation, int index); + + String evalStringAt(Activation activation, int index); + + void evalNullAt(Activation activation, int index); + + boolean evalStringContains(Activation activation, NativeStringCapability needle); + + NativeIntAggregateValues evalIntValues(Activation activation); +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/NativeIsland.java b/core/src/main/java/org/projectnessie/cel/interpreter/NativeIsland.java new file mode 100644 index 00000000..32a542a1 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/NativeIsland.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static java.util.Objects.requireNonNull; +import static org.projectnessie.cel.common.types.UintT.uintOf; + +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; + +/** One-way adaptation boundary around a maximal typed evaluation island. */ +final class NativeIsland implements Interpretable, Coster { + private final Interpretable root; + private final TypeAdapter adapter; + + NativeIsland(Interpretable root, TypeAdapter adapter) { + if (!(root instanceof NativeIntCapability) + && !(root instanceof NativeUintCapability) + && !(root instanceof NativeDoubleCapability) + && !(root instanceof NativeBooleanCapability) + && !(root instanceof NativeStringCapability) + && !(root instanceof NativeNullCapability)) { + throw new IllegalArgumentException("native island root has no terminal scalar capability"); + } + this.root = requireNonNull(root, "root"); + this.adapter = requireNonNull(adapter, "adapter"); + } + + static boolean supports(Interpretable interpretable) { + return interpretable instanceof NativeIntCapability + || interpretable instanceof NativeUintCapability + || interpretable instanceof NativeDoubleCapability + || interpretable instanceof NativeBooleanCapability + || interpretable instanceof NativeStringCapability + || interpretable instanceof NativeNullCapability; + } + + Interpretable root() { + return root; + } + + @Override + public long id() { + return root.id(); + } + + @Override + public Val eval(Activation activation) { + try { + if (root instanceof NativeIntCapability intCapability) { + return adapter.nativeToValue(intCapability.evalInt(activation)); + } + if (root instanceof NativeUintCapability uintCapability) { + return uintOf(uintCapability.evalUint(activation)); + } + if (root instanceof NativeBooleanCapability booleanCapability) { + return adapter.nativeToValue(booleanCapability.evalBoolean(activation)); + } + if (root instanceof NativeDoubleCapability doubleCapability) { + return adapter.nativeToValue(doubleCapability.evalDouble(activation)); + } + if (root instanceof NativeStringCapability stringCapability) { + return adapter.nativeToValue(stringCapability.evalString(activation)); + } + ((NativeNullCapability) root).evalNull(activation); + return org.projectnessie.cel.common.types.NullT.NullValue; + } catch (ValueSignal signal) { + return signal.value; + } + } + + @Override + public Cost cost() { + return Cost.estimateCost(root); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/NativeListConcatNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/NativeListConcatNodes.java new file mode 100644 index 00000000..e4eee91a --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/NativeListConcatNodes.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.interpreter.ValueSignal.signal; + +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.Overloads; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; +import org.projectnessie.cel.interpreter.functions.Overload; + +/** + * Checked list concatenation retained as an ordinary established node except when an immediate + * structural consumer can use its exact sources directly. + */ +final class NativeListConcat extends EvalBinary { + final NativeListSourceCapability leftSource; + final NativeListSourceCapability rightSource; + + NativeListConcat(long id, Interpretable left, Interpretable right, Overload implementation) { + super( + id, + Operator.Add.id, + Overloads.AddList, + left, + right, + implementation.operandTrait, + implementation.binary); + this.leftSource = (NativeListSourceCapability) left; + this.rightSource = (NativeListSourceCapability) right; + } +} + +/** Resolves both concat operands before applying structural list operations. */ +final class NativeListConcatKernel { + private NativeListConcatKernel() {} + + static ResolvedConcat resolve(NativeListConcat concat, Activation activation) { + Object leftRaw = null; + Object rightRaw = null; + ValueSignal leftFailure = null; + ValueSignal rightFailure = null; + + try { + leftRaw = concat.leftSource.evalRaw(activation); + } catch (ValueSignal failure) { + leftFailure = failure; + } catch (Exception failure) { + leftFailure = signal(newErr(failure, failure.toString())); + } + try { + rightRaw = concat.rightSource.evalRaw(activation); + } catch (ValueSignal failure) { + rightFailure = failure; + } catch (Exception failure) { + rightFailure = signal(newErr(failure, failure.toString())); + } + + int leftSize = 0; + int rightSize = 0; + if (leftFailure == null) { + try { + leftSize = NativeListSources.size(concat.leftSource, leftRaw, true); + } catch (ValueSignal failure) { + leftFailure = failure; + } catch (Exception failure) { + leftFailure = signal(newErr(failure, failure.toString())); + } + } + if (rightFailure == null) { + try { + rightSize = NativeListSources.size(concat.rightSource, rightRaw, true); + } catch (ValueSignal failure) { + rightFailure = failure; + } catch (Exception failure) { + rightFailure = signal(newErr(failure, failure.toString())); + } + } + + if (leftFailure != null) { + throw leftFailure; + } + if (rightFailure != null) { + throw rightFailure; + } + return new ResolvedConcat(leftRaw, rightRaw, leftSize, rightSize); + } + + static Selection select(NativeListConcat concat, Activation activation, int index) { + ResolvedConcat resolved = resolve(concat, activation); + long size = (long) resolved.leftSize + resolved.rightSize; + if (index < 0 || index >= size) { + throw signal( + newErr("invalid_argument: index '%d' out of range in list of size '%d'", index, size)); + } + return index < resolved.leftSize + ? new Selection(concat.leftSource, resolved.leftRaw, index) + : new Selection(concat.rightSource, resolved.rightRaw, index - resolved.leftSize); + } + + record ResolvedConcat(Object leftRaw, Object rightRaw, int leftSize, int rightSize) {} + + record Selection(NativeListSourceCapability source, Object raw, int index) {} +} + +final class NativeListConcatSize extends EvalUnary implements NativeIntCapability { + private final NativeListConcat concat; + + NativeListConcatSize( + long id, String function, String overload, NativeListConcat concat, Overload implementation) { + super(id, function, overload, concat, implementation.operandTrait, implementation.unary); + this.concat = concat; + } + + @Override + public long evalInt(Activation activation) { + NativeListConcatKernel.ResolvedConcat resolved = + NativeListConcatKernel.resolve(concat, activation); + return (long) resolved.leftSize() + resolved.rightSize(); + } +} + +abstract class NativeListConcatIndex extends NativeScalarAttr { + final NativeListConcat concat; + final int index; + + NativeListConcatIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeListConcat concat, + int index) { + super(id, adapter, establishedAttribute, null); + this.concat = concat; + this.index = index; + } + + final NativeListConcatKernel.Selection select(Activation activation) { + return NativeListConcatKernel.select(concat, activation, index); + } +} + +final class NativeBooleanListConcatIndex extends NativeListConcatIndex + implements NativeBooleanCapability { + NativeBooleanListConcatIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeListConcat concat, + int index) { + super(id, adapter, establishedAttribute, concat, index); + } + + @Override + public boolean evalBoolean(Activation activation) { + NativeListConcatKernel.Selection selected = select(activation); + return NativeListSources.booleanAt( + selected.source(), selected.raw(), selected.index(), true, adapter); + } +} + +final class NativeIntListConcatIndex extends NativeListConcatIndex implements NativeIntCapability { + NativeIntListConcatIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeListConcat concat, + int index) { + super(id, adapter, establishedAttribute, concat, index); + } + + @Override + public long evalInt(Activation activation) { + NativeListConcatKernel.Selection selected = select(activation); + return NativeListSources.intAt( + selected.source(), selected.raw(), selected.index(), true, adapter); + } +} + +final class NativeUintListConcatIndex extends NativeListConcatIndex + implements NativeUintCapability { + NativeUintListConcatIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeListConcat concat, + int index) { + super(id, adapter, establishedAttribute, concat, index); + } + + @Override + public long evalUint(Activation activation) { + NativeListConcatKernel.Selection selected = select(activation); + return NativeListSources.uintAt( + selected.source(), selected.raw(), selected.index(), true, adapter); + } +} + +final class NativeDoubleListConcatIndex extends NativeListConcatIndex + implements NativeDoubleCapability { + NativeDoubleListConcatIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeListConcat concat, + int index) { + super(id, adapter, establishedAttribute, concat, index); + } + + @Override + public double evalDouble(Activation activation) { + NativeListConcatKernel.Selection selected = select(activation); + return NativeListSources.doubleAt( + selected.source(), selected.raw(), selected.index(), true, adapter); + } +} + +final class NativeStringListConcatIndex extends NativeListConcatIndex + implements NativeStringCapability { + NativeStringListConcatIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeListConcat concat, + int index) { + super(id, adapter, establishedAttribute, concat, index); + } + + @Override + public String evalString(Activation activation) { + NativeListConcatKernel.Selection selected = select(activation); + return NativeListSources.stringAt( + selected.source(), selected.raw(), selected.index(), true, adapter); + } +} + +final class NativeNullListConcatIndex extends NativeListConcatIndex + implements NativeNullCapability { + NativeNullListConcatIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeListConcat concat, + int index) { + super(id, adapter, establishedAttribute, concat, index); + } + + @Override + public void evalNull(Activation activation) { + NativeListConcatKernel.Selection selected = select(activation); + NativeListSources.nullAt(selected.source(), selected.raw(), selected.index(), true, adapter); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/NativeListFoldNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/NativeListFoldNodes.java new file mode 100644 index 00000000..7adb6f9c --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/NativeListFoldNodes.java @@ -0,0 +1,610 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static java.util.Objects.requireNonNull; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.Err.isError; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.UnknownT.isUnknown; +import static org.projectnessie.cel.interpreter.ValueSignal.signal; + +import java.util.ArrayList; +import java.util.Arrays; +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.Overloads; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; +import org.projectnessie.cel.interpreter.functions.Overload; + +final class NativeScalarListFold extends EvalListFold implements NativeScalarListFoldCapability { + final NativeListSourceCapability range; + private final NativeScalarKind inputKind; + final NativeBooleanCapability predicate; + final Interpretable nativeTransform; + private final NativeScalarKind outputKind; + private final TypeAdapter adapter; + private final String variable; + + NativeScalarListFold( + long id, + String variable, + Interpretable range, + Interpretable establishedFilter, + Interpretable establishedTransform, + NativeScalarKind inputKind, + NativeBooleanCapability predicate, + Interpretable nativeTransform, + NativeScalarKind outputKind, + TypeAdapter adapter) { + super(id, variable, "", range, establishedFilter, establishedTransform, adapter); + this.range = (NativeListSourceCapability) range; + this.inputKind = requireNonNull(inputKind, "inputKind"); + this.predicate = predicate; + this.nativeTransform = requireNonNull(nativeTransform, "nativeTransform"); + this.outputKind = requireNonNull(outputKind, "outputKind"); + this.adapter = requireNonNull(adapter, "adapter"); + this.variable = requireNonNull(variable, "variable"); + } + + @Override + public NativeScalarKind elementKind() { + return outputKind; + } + + @Override + public long evalSize(Activation activation) { + NativeListFoldEvaluation evaluation = + new NativeListFoldEvaluation(activation, variable, this, -1); + NativeScalarLoopKernel.evaluate(range, inputKind, activation, evaluation, evaluation); + return evaluation.position; + } + + @Override + public boolean evalBooleanAt(Activation activation, int index) { + return evaluateAt(activation, index).booleanValue; + } + + @Override + public long evalIntAt(Activation activation, int index) { + return evaluateAt(activation, index).intValue; + } + + @Override + public long evalUintAt(Activation activation, int index) { + return evaluateAt(activation, index).uintValue; + } + + @Override + public double evalDoubleAt(Activation activation, int index) { + return evaluateAt(activation, index).doubleValue; + } + + @Override + public String evalStringAt(Activation activation, int index) { + return requireNonNull( + evaluateAt(activation, index).stringValue, "native string expression returned null"); + } + + @Override + public void evalNullAt(Activation activation, int index) { + evaluateAt(activation, index); + } + + @Override + public boolean evalStringContains(Activation activation, NativeStringCapability needle) { + String needleValue = null; + Val slowNeedle = null; + try { + needleValue = needle.evalString(activation); + } catch (ValueSignal valueSignal) { + slowNeedle = valueSignal.value; + } + + NativeStringMembershipEvaluation evaluation = + new NativeStringMembershipEvaluation(activation, variable, this, needleValue, slowNeedle); + try { + NativeScalarLoopKernel.evaluate(range, inputKind, activation, evaluation, evaluation); + } catch (ValueSignal valueSignal) { + if (evaluation.exceptionalNeedle()) { + throw signal(evaluation.slowNeedle()); + } + throw valueSignal; + } + return evaluation.finish(); + } + + @Override + public NativeIntAggregateValues evalIntValues(Activation activation) { + if (outputKind != NativeScalarKind.INT) { + throw new IllegalStateException("integer values requested from " + outputKind + " list fold"); + } + NativeIntListFoldEvaluation evaluation = + new NativeIntListFoldEvaluation(activation, variable, this); + NativeScalarLoopKernel.evaluate(range, inputKind, activation, evaluation, evaluation); + return evaluation.values(); + } + + private NativeListFoldEvaluation evaluateAt(Activation activation, int index) { + NativeListFoldEvaluation evaluation = + new NativeListFoldEvaluation(activation, variable, this, index); + try { + NativeScalarLoopKernel.evaluate(range, inputKind, activation, evaluation, evaluation); + } catch (ValueSignal valueSignal) { + throw isError(valueSignal.value) ? propagatedError(valueSignal.value) : valueSignal; + } + evaluation.finishIndex(); + return evaluation; + } + + boolean include(NativeLoopBinding binding) { + if (predicate == null) { + return true; + } + try { + return predicate.evalBoolean(binding); + } catch (ValueSignal valueSignal) { + throw signal(noSuchOverload(null, Operator.Conditional.id, valueSignal.value)); + } + } + + void evaluateTransform(NativeListFoldEvaluation evaluation, boolean selected) { + try { + switch (outputKind) { + case BOOLEAN -> { + boolean value = ((NativeBooleanCapability) nativeTransform).evalBoolean(evaluation); + if (selected) { + evaluation.booleanValue = value; + } + } + case INT -> { + long value = ((NativeIntCapability) nativeTransform).evalInt(evaluation); + if (selected) { + evaluation.intValue = value; + } + } + case UINT -> { + long value = ((NativeUintCapability) nativeTransform).evalUint(evaluation); + if (selected) { + evaluation.uintValue = value; + } + } + case DOUBLE -> { + double value = ((NativeDoubleCapability) nativeTransform).evalDouble(evaluation); + if (selected) { + evaluation.doubleValue = value; + } + } + case STRING -> { + String value = ((NativeStringCapability) nativeTransform).evalString(evaluation); + if (selected) { + evaluation.stringValue = value; + } + } + case NULL -> ((NativeNullCapability) nativeTransform).evalNull(evaluation); + } + } catch (ValueSignal valueSignal) { + if (isError(valueSignal.value) || isUnknown(valueSignal.value)) { + throw valueSignal; + } + if (selected) { + evaluation.slowValue = valueSignal.value; + } + } + } + + private static ValueSignal propagatedError(Val value) { + return NativeSupport.propagatedError(value); + } +} + +final class NativeStringMembershipEvaluation extends NativeLoopBinding + implements NativeScalarLoopConsumer { + private final NativeScalarListFold source; + private final String needle; + private final Val slowNeedle; + private final boolean exceptionalNeedle; + private long position; + private long firstMatch = -1L; + private ArrayList slowValues; + private ArrayList constructedValues; + + NativeStringMembershipEvaluation( + Activation parent, + String variable, + NativeScalarListFold source, + String needle, + Val slowNeedle) { + super(parent, variable); + this.source = requireNonNull(source, "source"); + this.needle = needle; + this.slowNeedle = slowNeedle; + this.exceptionalNeedle = isError(slowNeedle) || isUnknown(slowNeedle); + if (slowNeedle != null && !exceptionalNeedle) { + constructedValues = new ArrayList<>(); + } + } + + @Override + public boolean test(NativeLoopBinding ignored) { + if (!source.include(this)) { + return false; + } + try { + recordOutput(((NativeStringCapability) source.nativeTransform).evalString(this)); + } catch (ValueSignal valueSignal) { + if (isError(valueSignal.value) || isUnknown(valueSignal.value)) { + throw valueSignal; + } + recordOutput(valueSignal.value); + } + return false; + } + + boolean exceptionalNeedle() { + return exceptionalNeedle; + } + + Val slowNeedle() { + return slowNeedle; + } + + private void recordOutput(String value) { + if (exceptionalNeedle) { + return; + } + if (slowNeedle != null) { + constructedValues.add(stringOf(value)); + } else if (firstMatch == -1L && needle.equals(value)) { + firstMatch = position; + } + position++; + } + + private void recordOutput(Val value) { + if (exceptionalNeedle) { + return; + } + if (slowNeedle != null) { + constructedValues.add(value); + } else { + if (slowValues == null) { + slowValues = new ArrayList<>(); + } + slowValues.add(new NativePositionedValue(position, value)); + } + position++; + } + + boolean finish() { + if (exceptionalNeedle) { + throw signal(slowNeedle); + } + if (slowNeedle != null) { + for (Val value : constructedValues) { + if (slowNeedle.equal(value) == True) { + return true; + } + } + return false; + } + if (slowValues != null) { + Val needleValue = stringOf(needle); + for (NativePositionedValue slowValue : slowValues) { + if (firstMatch != -1L && slowValue.position() >= firstMatch) { + break; + } + if (needleValue.equal(slowValue.value()) == True) { + return true; + } + } + } + return firstMatch != -1L; + } +} + +record NativePositionedValue(long position, Val value) { + NativePositionedValue { + requireNonNull(value, "value"); + } +} + +final class NativeIntListFoldEvaluation extends NativeLoopBinding + implements NativeScalarLoopConsumer { + private final NativeScalarListFold source; + private long[] values = new long[0]; + private Val[] slowValues; + private int size; + + NativeIntListFoldEvaluation(Activation parent, String variable, NativeScalarListFold source) { + super(parent, variable); + this.source = requireNonNull(source, "source"); + } + + @Override + public void prepareCapacity(int capacity) { + if (source.predicate == null && capacity > 0) { + values = new long[capacity]; + } + } + + @Override + public boolean test(NativeLoopBinding ignored) { + if (!source.include(this)) { + return false; + } + ensureCapacity(size + 1); + try { + values[size] = ((NativeIntCapability) source.nativeTransform).evalInt(this); + } catch (ValueSignal valueSignal) { + if (isError(valueSignal.value) || isUnknown(valueSignal.value)) { + throw valueSignal; + } + if (slowValues == null) { + slowValues = new Val[values.length]; + } + slowValues[size] = valueSignal.value; + } + size++; + return false; + } + + private void ensureCapacity(int required) { + if (required <= values.length) { + return; + } + int capacity = + values.length == 0 + ? 10 + : source.predicate != null + ? Math.multiplyExact(values.length, 2) + : Math.addExact(values.length, Math.max(1, values.length >> 1)); + capacity = Math.max(required, capacity); + values = Arrays.copyOf(values, capacity); + if (slowValues != null) { + slowValues = Arrays.copyOf(slowValues, capacity); + } + } + + NativeIntAggregateValues values() { + return new NativeIntAggregateValues(values, slowValues, size); + } +} + +record NativeIntAggregateValues(long[] values, Val[] slowValues, int size) { + NativeIntAggregateValues { + requireNonNull(values, "values"); + } + + void set(int index, NativeLoopBinding binding) { + Val slowValue = slowValues != null ? slowValues[index] : null; + if (slowValue != null) { + binding.setObject(slowValue); + } else { + binding.setInt(values[index]); + } + } +} + +final class NativeListFoldEvaluation extends NativeLoopBinding implements NativeScalarLoopConsumer { + private final NativeScalarListFold source; + private final int selectedIndex; + long position; + boolean booleanValue; + long intValue; + long uintValue; + double doubleValue; + String stringValue; + Val slowValue; + + NativeListFoldEvaluation( + Activation parent, String variable, NativeScalarListFold source, int selectedIndex) { + super(parent, variable); + this.source = requireNonNull(source, "source"); + this.selectedIndex = selectedIndex; + } + + @Override + public boolean test(NativeLoopBinding ignored) { + if (!source.include(this)) { + return false; + } + boolean selected = position == selectedIndex; + source.evaluateTransform(this, selected); + position++; + return false; + } + + void finishIndex() { + if (selectedIndex < 0 || selectedIndex >= position) { + throw signal( + newErr( + "invalid_argument: index '%d' out of range in list of size '%d'", + selectedIndex, position)); + } + if (slowValue != null) { + throw signal(slowValue); + } + } +} + +final class NativeStringListFoldMembership extends EvalBinary implements NativeBooleanCapability { + final NativeStringCapability needle; + final NativeScalarListFoldCapability source; + + NativeStringListFoldMembership( + long id, Interpretable needle, Interpretable source, Overload implementation) { + super( + id, + Operator.In.id, + Overloads.InList, + needle, + source, + implementation.operandTrait, + implementation.binary); + this.needle = (NativeStringCapability) needle; + this.source = (NativeScalarListFoldCapability) source; + } + + @Override + public boolean evalBoolean(Activation activation) { + return source.evalStringContains(activation, needle); + } +} + +final class NativeListFoldSize extends EvalUnary implements NativeIntCapability { + final NativeScalarListFoldCapability source; + + NativeListFoldSize( + long id, String function, String overload, Interpretable operand, Overload implementation) { + super(id, function, overload, operand, implementation.operandTrait, implementation.unary); + this.source = (NativeScalarListFoldCapability) operand; + } + + @Override + public long evalInt(Activation activation) { + return source.evalSize(activation); + } +} + +final class NativeBooleanListFoldIndex extends NativeScalarAttr implements NativeBooleanCapability { + final NativeScalarListFoldCapability source; + private final int index; + + NativeBooleanListFoldIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeScalarListFoldCapability source, + int index) { + super(id, adapter, establishedAttribute, null); + this.source = source; + this.index = index; + } + + @Override + public boolean evalBoolean(Activation activation) { + return source.evalBooleanAt(activation, index); + } +} + +final class NativeIntListFoldIndex extends NativeScalarAttr implements NativeIntCapability { + final NativeScalarListFoldCapability source; + private final int index; + + NativeIntListFoldIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeScalarListFoldCapability source, + int index) { + super(id, adapter, establishedAttribute, null); + this.source = source; + this.index = index; + } + + @Override + public long evalInt(Activation activation) { + return source.evalIntAt(activation, index); + } +} + +final class NativeUintListFoldIndex extends NativeScalarAttr implements NativeUintCapability { + final NativeScalarListFoldCapability source; + private final int index; + + NativeUintListFoldIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeScalarListFoldCapability source, + int index) { + super(id, adapter, establishedAttribute, null); + this.source = source; + this.index = index; + } + + @Override + public long evalUint(Activation activation) { + return source.evalUintAt(activation, index); + } +} + +final class NativeDoubleListFoldIndex extends NativeScalarAttr implements NativeDoubleCapability { + final NativeScalarListFoldCapability source; + private final int index; + + NativeDoubleListFoldIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeScalarListFoldCapability source, + int index) { + super(id, adapter, establishedAttribute, null); + this.source = source; + this.index = index; + } + + @Override + public double evalDouble(Activation activation) { + return source.evalDoubleAt(activation, index); + } +} + +final class NativeStringListFoldIndex extends NativeScalarAttr implements NativeStringCapability { + final NativeScalarListFoldCapability source; + private final int index; + + NativeStringListFoldIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeScalarListFoldCapability source, + int index) { + super(id, adapter, establishedAttribute, null); + this.source = source; + this.index = index; + } + + @Override + public String evalString(Activation activation) { + return source.evalStringAt(activation, index); + } +} + +final class NativeNullListFoldIndex extends NativeScalarAttr implements NativeNullCapability { + final NativeScalarListFoldCapability source; + private final int index; + + NativeNullListFoldIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeScalarListFoldCapability source, + int index) { + super(id, adapter, establishedAttribute, null); + this.source = source; + this.index = index; + } + + @Override + public void evalNull(Activation activation) { + source.evalNullAt(activation, index); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/NativeListNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/NativeListNodes.java new file mode 100644 index 00000000..d155ed0a --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/NativeListNodes.java @@ -0,0 +1,1118 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static java.util.Objects.requireNonNull; +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.Err.isError; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.Types.boolOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; +import static org.projectnessie.cel.common.types.UnknownT.isUnknown; +import static org.projectnessie.cel.common.types.Util.isUnknownOrError; +import static org.projectnessie.cel.interpreter.ValueSignal.signal; + +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.BoolT; +import org.projectnessie.cel.common.types.Overloads; +import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; +import org.projectnessie.cel.interpreter.functions.Overload; + +abstract class NativeListIndex extends NativeScalarAttr { + final NativeListSourceCapability source; + final int index; + private final NativeIntCapability dynamicIndex; + final boolean directArrayAccess; + + NativeListIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + Attribute partialAttribute, + NativeListSourceCapability source, + long index, + boolean directArrayAccess) { + super(id, adapter, establishedAttribute, partialAttribute); + this.source = source; + this.index = Math.toIntExact(index); + this.dynamicIndex = null; + this.directArrayAccess = directArrayAccess; + } + + NativeListIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeListSourceCapability source, + NativeIntCapability dynamicIndex, + boolean directArrayAccess) { + super(id, adapter, establishedAttribute, null); + this.source = source; + this.index = 0; + this.dynamicIndex = dynamicIndex; + this.directArrayAccess = directArrayAccess; + } + + final Object resolveTarget(Activation activation) { + Object target; + try { + target = source.evalRaw(activation); + } catch (ValueSignal valueSignal) { + target = valueSignal.value; + } catch (Exception e) { + throw signal(newErr(e, e.toString())); + } + if (target instanceof Val value && isError(value)) { + throw NativeSupport.propagatedError(value); + } + return target; + } + + final int selectedIndex(Activation activation, Object target) { + if (dynamicIndex == null) { + return index; + } + if (target instanceof Val sourceValue && isUnknownOrError(sourceValue)) { + throw signal(sourceValue); + } + try { + long evaluatedIndex = dynamicIndex.evalInt(activation); + if (evaluatedIndex < Integer.MIN_VALUE || evaluatedIndex > Integer.MAX_VALUE) { + int size = NativeListSources.size(source, target, directArrayAccess); + throw signal( + newErr( + "invalid_argument: index '%d' out of range in list of size '%d'", + evaluatedIndex, size)); + } + return (int) evaluatedIndex; + } catch (ValueSignal valueSignal) { + throw valueSignal; + } catch (Exception failure) { + throw signal(newErr(failure, failure.toString())); + } + } +} + +final class NativeBooleanListIndex extends NativeListIndex implements NativeBooleanCapability { + NativeBooleanListIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + Attribute partialAttribute, + NativeListSourceCapability source, + long index, + boolean directArrayAccess) { + super(id, adapter, establishedAttribute, partialAttribute, source, index, directArrayAccess); + } + + NativeBooleanListIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeListSourceCapability source, + NativeIntCapability dynamicIndex, + boolean directArrayAccess) { + super(id, adapter, establishedAttribute, source, dynamicIndex, directArrayAccess); + } + + @Override + public boolean evalBoolean(Activation activation) { + Object target = resolveTarget(activation); + return NativeListSources.booleanAt( + source, target, selectedIndex(activation, target), directArrayAccess, adapter); + } +} + +final class NativeIntListIndex extends NativeListIndex implements NativeIntCapability { + NativeIntListIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + Attribute partialAttribute, + NativeListSourceCapability source, + long index, + boolean directArrayAccess) { + super(id, adapter, establishedAttribute, partialAttribute, source, index, directArrayAccess); + } + + NativeIntListIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeListSourceCapability source, + NativeIntCapability dynamicIndex, + boolean directArrayAccess) { + super(id, adapter, establishedAttribute, source, dynamicIndex, directArrayAccess); + } + + @Override + public long evalInt(Activation activation) { + if (usesPartialAttribute(activation)) { + return NativeSupport.intValue(adapter, resolveNative(activation)); + } + Object target = resolveTarget(activation); + return NativeListSources.intAt( + source, target, selectedIndex(activation, target), directArrayAccess, adapter); + } +} + +final class NativeUintListIndex extends NativeListIndex implements NativeUintCapability { + NativeUintListIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + Attribute partialAttribute, + NativeListSourceCapability source, + long index, + boolean directArrayAccess) { + super(id, adapter, establishedAttribute, partialAttribute, source, index, directArrayAccess); + } + + NativeUintListIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeListSourceCapability source, + NativeIntCapability dynamicIndex, + boolean directArrayAccess) { + super(id, adapter, establishedAttribute, source, dynamicIndex, directArrayAccess); + } + + @Override + public long evalUint(Activation activation) { + if (usesPartialAttribute(activation)) { + return NativeSupport.uintValue(adapter, resolveNative(activation)); + } + Object target = resolveTarget(activation); + return NativeListSources.uintAt( + source, target, selectedIndex(activation, target), directArrayAccess, adapter); + } +} + +final class NativeDoubleListIndex extends NativeListIndex implements NativeDoubleCapability { + NativeDoubleListIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + Attribute partialAttribute, + NativeListSourceCapability source, + long index, + boolean directArrayAccess) { + super(id, adapter, establishedAttribute, partialAttribute, source, index, directArrayAccess); + } + + NativeDoubleListIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeListSourceCapability source, + NativeIntCapability dynamicIndex, + boolean directArrayAccess) { + super(id, adapter, establishedAttribute, source, dynamicIndex, directArrayAccess); + } + + @Override + public double evalDouble(Activation activation) { + if (usesPartialAttribute(activation)) { + return NativeSupport.doubleValue(adapter, resolveNative(activation)); + } + Object target = resolveTarget(activation); + return NativeListSources.doubleAt( + source, target, selectedIndex(activation, target), directArrayAccess, adapter); + } +} + +final class NativeStringListIndex extends NativeListIndex implements NativeStringCapability { + NativeStringListIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + Attribute partialAttribute, + NativeListSourceCapability source, + long index, + boolean directArrayAccess) { + super(id, adapter, establishedAttribute, partialAttribute, source, index, directArrayAccess); + } + + NativeStringListIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeListSourceCapability source, + NativeIntCapability dynamicIndex, + boolean directArrayAccess) { + super(id, adapter, establishedAttribute, source, dynamicIndex, directArrayAccess); + } + + @Override + public String evalString(Activation activation) { + if (usesPartialAttribute(activation)) { + return NativeSupport.stringValue(adapter, resolveNative(activation)); + } + Object target = resolveTarget(activation); + return NativeListSources.stringAt( + source, target, selectedIndex(activation, target), directArrayAccess, adapter); + } +} + +final class NativeNullListIndex extends NativeListIndex implements NativeNullCapability { + NativeNullListIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + Attribute partialAttribute, + NativeListSourceCapability source, + long index, + boolean directArrayAccess) { + super(id, adapter, establishedAttribute, partialAttribute, source, index, directArrayAccess); + } + + NativeNullListIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeListSourceCapability source, + NativeIntCapability dynamicIndex, + boolean directArrayAccess) { + super(id, adapter, establishedAttribute, source, dynamicIndex, directArrayAccess); + } + + @Override + public void evalNull(Activation activation) { + Object target = resolveTarget(activation); + NativeListSources.nullAt( + source, target, selectedIndex(activation, target), directArrayAccess, adapter); + } +} + +final class NativeStringListMembership extends EvalBinary implements NativeBooleanCapability { + private final NativeStringCapability nativeNeedle; + private final NativeListSourceCapability nativeList; + private final boolean directArrayAccess; + + NativeStringListMembership( + long id, + Interpretable needle, + Interpretable list, + Overload implementation, + boolean directArrayAccess) { + super( + id, + Operator.In.id, + Overloads.InList, + needle, + list, + implementation.operandTrait, + implementation.binary); + this.nativeNeedle = (NativeStringCapability) needle; + this.nativeList = (NativeListSourceCapability) list; + this.directArrayAccess = directArrayAccess; + } + + @Override + public boolean evalBoolean(Activation activation) { + String needle = null; + Val slowNeedle = null; + try { + needle = nativeNeedle.evalString(activation); + } catch (ValueSignal valueSignal) { + slowNeedle = valueSignal.value; + } + + Object list; + try { + list = nativeList.evalRaw(activation); + } catch (ValueSignal valueSignal) { + list = valueSignal.value; + } + + if (slowNeedle == null && directArrayAccess && list instanceof String[] values) { + needle = requireNonNull(needle, "native string capability returned null"); + for (String element : values) { + if (needle.equals(element)) { + return true; + } + } + return false; + } + + Val needleValue = slowNeedle != null ? slowNeedle : stringOf(needle); + Val listValue = nativeList.materializeResolvedList(list); + return NativeScalarContinuations.booleanResult(evalPrepared(needleValue, listValue)); + } +} + +/** + * Exact host-set membership for the scalar kinds whose Java representation has a CEL-compatible + * {@link java.util.Set#contains(Object)} operation. + */ +final class NativeExactSetMembership extends EvalBinary implements NativeBooleanCapability { + private final NativeScalarKind kind; + private final NativeListSourceCapability source; + + NativeExactSetMembership( + long id, + Interpretable needle, + Interpretable list, + Overload implementation, + NativeScalarKind kind) { + super( + id, + Operator.In.id, + Overloads.InList, + needle, + list, + implementation.operandTrait, + implementation.binary); + this.kind = kind; + this.source = (NativeListSourceCapability) list; + } + + @Override + public boolean evalBoolean(Activation activation) { + boolean booleanNeedle = false; + long integerNeedle = 0L; + double doubleNeedle = 0.0d; + String stringNeedle = null; + Val slowNeedle = null; + try { + switch (kind) { + case BOOLEAN -> booleanNeedle = ((NativeBooleanCapability) lhs).evalBoolean(activation); + case INT -> integerNeedle = ((NativeIntCapability) lhs).evalInt(activation); + case UINT -> integerNeedle = ((NativeUintCapability) lhs).evalUint(activation); + case DOUBLE -> doubleNeedle = ((NativeDoubleCapability) lhs).evalDouble(activation); + case STRING -> stringNeedle = ((NativeStringCapability) lhs).evalString(activation); + case NULL -> throw new IllegalStateException("null Set membership is not specialized"); + } + } catch (ValueSignal valueSignal) { + slowNeedle = valueSignal.value; + } + + Object raw; + try { + raw = source.evalRaw(activation); + } catch (ValueSignal valueSignal) { + raw = valueSignal.value; + } + + if (slowNeedle == null) { + try { + int direct = + NativeListSources.exactSetContains( + raw, kind, booleanNeedle, integerNeedle, doubleNeedle, stringNeedle); + if (direct != NativeListSources.UNSUPPORTED) { + return direct != 0; + } + } catch (ValueSignal valueSignal) { + throw valueSignal; + } catch (Exception failure) { + throw signal(newErr(failure, failure.toString())); + } + } + + Val needleValue = + slowNeedle != null + ? slowNeedle + : switch (kind) { + case BOOLEAN -> boolOf(booleanNeedle); + case INT -> intOf(integerNeedle); + case UINT -> uintOf(integerNeedle); + case DOUBLE -> doubleOf(doubleNeedle); + case STRING -> stringOf(stringNeedle); + case NULL -> + throw new IllegalStateException("null Set membership is not specialized"); + }; + Val listValue; + try { + listValue = source.materializeResolvedList(raw); + } catch (ValueSignal valueSignal) { + listValue = valueSignal.value; + } + return NativeScalarContinuations.booleanResult(evalPrepared(needleValue, listValue)); + } +} + +/** Encounter-order CEL list equality over two exact host aggregate sources. */ +final class NativeExactListEquality extends EvalEq implements NativeBooleanCapability { + private final NativeListSourceCapability leftSource; + private final NativeListSourceCapability rightSource; + private final NativeScalarKind kind; + private final TypeAdapter adapter; + + NativeExactListEquality( + long id, + Interpretable left, + Interpretable right, + NativeScalarKind kind, + TypeAdapter adapter) { + super(id, left, right); + this.leftSource = (NativeListSourceCapability) left; + this.rightSource = (NativeListSourceCapability) right; + this.kind = kind; + this.adapter = adapter; + } + + @SuppressWarnings("DuplicatedCode") + @Override + public boolean evalBoolean(Activation activation) { + Object leftRaw; + Object rightRaw; + Val leftSlow = null; + Val rightSlow = null; + try { + leftRaw = leftSource.evalRaw(activation); + } catch (ValueSignal valueSignal) { + leftRaw = null; + leftSlow = valueSignal.value; + } + try { + rightRaw = rightSource.evalRaw(activation); + } catch (ValueSignal valueSignal) { + rightRaw = null; + rightSlow = valueSignal.value; + } + + if (leftSlow == null && rightSlow == null) { + try { + int direct = + NativeListSources.exactListEquals( + leftSource, leftRaw, rightSource, rightRaw, kind, adapter); + if (direct != NativeListSources.UNSUPPORTED) { + return direct != 0; + } + } catch (ValueSignal valueSignal) { + throw valueSignal; + } catch (Exception failure) { + throw signal(newErr(failure, failure.toString())); + } + } + + Val leftValue = leftSlow != null ? leftSlow : leftSource.materializeResolvedList(leftRaw); + Val rightValue = rightSlow != null ? rightSlow : rightSource.materializeResolvedList(rightRaw); + return NativeScalarContinuations.booleanResult(evalPrepared(leftValue, rightValue)); + } +} + +/** Encounter-order CEL list inequality over two exact host aggregate sources. */ +final class NativeExactListInequality extends EvalNe implements NativeBooleanCapability { + private final NativeListSourceCapability leftSource; + private final NativeListSourceCapability rightSource; + private final NativeScalarKind kind; + private final TypeAdapter adapter; + + NativeExactListInequality( + long id, + Interpretable left, + Interpretable right, + NativeScalarKind kind, + TypeAdapter adapter) { + super(id, left, right); + this.leftSource = (NativeListSourceCapability) left; + this.rightSource = (NativeListSourceCapability) right; + this.kind = kind; + this.adapter = adapter; + } + + @SuppressWarnings("DuplicatedCode") + @Override + public boolean evalBoolean(Activation activation) { + Object leftRaw; + Object rightRaw; + Val leftSlow = null; + Val rightSlow = null; + try { + leftRaw = leftSource.evalRaw(activation); + } catch (ValueSignal valueSignal) { + leftRaw = null; + leftSlow = valueSignal.value; + } + try { + rightRaw = rightSource.evalRaw(activation); + } catch (ValueSignal valueSignal) { + rightRaw = null; + rightSlow = valueSignal.value; + } + + if (leftSlow == null && rightSlow == null) { + try { + int direct = + NativeListSources.exactListEquals( + leftSource, leftRaw, rightSource, rightRaw, kind, adapter); + if (direct != NativeListSources.UNSUPPORTED) { + return direct == 0; + } + } catch (ValueSignal valueSignal) { + throw valueSignal; + } catch (Exception failure) { + throw signal(newErr(failure, failure.toString())); + } + } + + Val leftValue = leftSlow != null ? leftSlow : leftSource.materializeResolvedList(leftRaw); + Val rightValue = rightSlow != null ? rightSlow : rightSource.materializeResolvedList(rightRaw); + return NativeScalarContinuations.booleanResult(evalPrepared(leftValue, rightValue)); + } +} + +abstract class NativeScalarListLiteral extends EvalList + implements NativeScalarListLiteralCapability { + final Interpretable[] nativeElements; + private final boolean constantElements; + + NativeScalarListLiteral( + long id, Interpretable[] elements, boolean constantElements, TypeAdapter adapter) { + super(id, establishedElements(elements, adapter), adapter); + this.nativeElements = elements.clone(); + this.constantElements = constantElements; + } + + @Override + public final int evalSize(Activation activation) { + if (!constantElements) { + for (int i = 0; i < nativeElements.length; i++) { + evalDiscarded(activation, i); + } + } + return nativeElements.length; + } + + abstract void evalDiscarded(Activation activation, int index); + + final void checkLiteralIndex(int index) { + if (index < 0 || index >= nativeElements.length) { + throw signal( + newErr( + "invalid_argument: index '%d' out of range in list of size '%d'", + index, nativeElements.length)); + } + } + + final ValueSignal propagatedError(Val value) { + return NativeSupport.propagatedError(value); + } + + private static Interpretable[] establishedElements( + Interpretable[] elements, TypeAdapter adapter) { + Interpretable[] established = elements.clone(); + for (int i = 0; i < established.length; i++) { + if (NativeIsland.supports(established[i])) { + established[i] = new NativeIsland(established[i], adapter); + } + } + return established; + } +} + +final class NativeBooleanListLiteral extends NativeScalarListLiteral + implements NativeBooleanListLiteralCapability { + NativeBooleanListLiteral( + long id, Interpretable[] elements, boolean constantElements, TypeAdapter adapter) { + super(id, elements, constantElements, adapter); + } + + @Override + public boolean evalBooleanAt(Activation activation, int index) { + boolean selected = false; + Val selectedSlow = null; + for (int i = 0; i < nativeElements.length; i++) { + try { + boolean value = ((NativeBooleanCapability) nativeElements[i]).evalBoolean(activation); + if (i == index) { + selected = value; + } + } catch (ValueSignal valueSignal) { + if (isError(valueSignal.value)) { + throw propagatedError(valueSignal.value); + } + if (isUnknown(valueSignal.value)) { + throw valueSignal; + } + if (i == index) { + selectedSlow = valueSignal.value; + } + } + } + checkLiteralIndex(index); + if (selectedSlow != null) { + throw signal(selectedSlow); + } + return selected; + } + + @Override + void evalDiscarded(Activation activation, int index) { + try { + ((NativeBooleanCapability) nativeElements[index]).evalBoolean(activation); + } catch (ValueSignal valueSignal) { + if (isUnknownOrError(valueSignal.value)) { + throw valueSignal; + } + } + } +} + +final class NativeIntListLiteral extends NativeScalarListLiteral + implements NativeIntListLiteralCapability { + NativeIntListLiteral( + long id, Interpretable[] elements, boolean constantElements, TypeAdapter adapter) { + super(id, elements, constantElements, adapter); + } + + @Override + public long evalIntAt(Activation activation, int index) { + long selected = 0L; + Val selectedSlow = null; + for (int i = 0; i < nativeElements.length; i++) { + try { + long value = ((NativeIntCapability) nativeElements[i]).evalInt(activation); + if (i == index) { + selected = value; + } + } catch (ValueSignal valueSignal) { + if (isError(valueSignal.value)) { + throw propagatedError(valueSignal.value); + } + if (isUnknown(valueSignal.value)) { + throw valueSignal; + } + if (i == index) { + selectedSlow = valueSignal.value; + } + } + } + checkLiteralIndex(index); + if (selectedSlow != null) { + throw signal(selectedSlow); + } + return selected; + } + + @Override + void evalDiscarded(Activation activation, int index) { + try { + ((NativeIntCapability) nativeElements[index]).evalInt(activation); + } catch (ValueSignal valueSignal) { + if (isUnknownOrError(valueSignal.value)) { + throw valueSignal; + } + } + } +} + +final class NativeUintListLiteral extends NativeScalarListLiteral + implements NativeUintListLiteralCapability { + NativeUintListLiteral( + long id, Interpretable[] elements, boolean constantElements, TypeAdapter adapter) { + super(id, elements, constantElements, adapter); + } + + @Override + public long evalUintAt(Activation activation, int index) { + long selected = 0L; + Val selectedSlow = null; + for (int i = 0; i < nativeElements.length; i++) { + try { + long value = ((NativeUintCapability) nativeElements[i]).evalUint(activation); + if (i == index) { + selected = value; + } + } catch (ValueSignal valueSignal) { + if (isError(valueSignal.value)) { + throw propagatedError(valueSignal.value); + } + if (isUnknown(valueSignal.value)) { + throw valueSignal; + } + if (i == index) { + selectedSlow = valueSignal.value; + } + } + } + checkLiteralIndex(index); + if (selectedSlow != null) { + throw signal(selectedSlow); + } + return selected; + } + + @Override + void evalDiscarded(Activation activation, int index) { + try { + ((NativeUintCapability) nativeElements[index]).evalUint(activation); + } catch (ValueSignal valueSignal) { + if (isUnknownOrError(valueSignal.value)) { + throw valueSignal; + } + } + } +} + +final class NativeDoubleListLiteral extends NativeScalarListLiteral + implements NativeDoubleListLiteralCapability { + NativeDoubleListLiteral( + long id, Interpretable[] elements, boolean constantElements, TypeAdapter adapter) { + super(id, elements, constantElements, adapter); + } + + @Override + public double evalDoubleAt(Activation activation, int index) { + double selected = 0.0d; + Val selectedSlow = null; + for (int i = 0; i < nativeElements.length; i++) { + try { + double value = ((NativeDoubleCapability) nativeElements[i]).evalDouble(activation); + if (i == index) { + selected = value; + } + } catch (ValueSignal valueSignal) { + if (isError(valueSignal.value)) { + throw propagatedError(valueSignal.value); + } + if (isUnknown(valueSignal.value)) { + throw valueSignal; + } + if (i == index) { + selectedSlow = valueSignal.value; + } + } + } + checkLiteralIndex(index); + if (selectedSlow != null) { + throw signal(selectedSlow); + } + return selected; + } + + @Override + void evalDiscarded(Activation activation, int index) { + try { + ((NativeDoubleCapability) nativeElements[index]).evalDouble(activation); + } catch (ValueSignal valueSignal) { + if (isUnknownOrError(valueSignal.value)) { + throw valueSignal; + } + } + } +} + +final class NativeStringListLiteral extends NativeScalarListLiteral + implements NativeStringListLiteralCapability { + NativeStringListLiteral( + long id, Interpretable[] elements, boolean constantElements, TypeAdapter adapter) { + super(id, elements, constantElements, adapter); + } + + @Override + public String evalStringAt(Activation activation, int index) { + String selected = null; + Val selectedSlow = null; + for (int i = 0; i < nativeElements.length; i++) { + try { + String value = ((NativeStringCapability) nativeElements[i]).evalString(activation); + if (i == index) { + selected = value; + } + } catch (ValueSignal valueSignal) { + if (isError(valueSignal.value)) { + throw propagatedError(valueSignal.value); + } + if (isUnknown(valueSignal.value)) { + throw valueSignal; + } + if (i == index) { + selectedSlow = valueSignal.value; + } + } + } + checkLiteralIndex(index); + if (selectedSlow != null) { + throw signal(selectedSlow); + } + return requireNonNull(selected); + } + + @Override + public boolean evalContains(Activation activation, NativeStringCapability needle) { + String needleValue = null; + Val slowNeedle = null; + try { + needleValue = needle.evalString(activation); + } catch (ValueSignal valueSignal) { + slowNeedle = valueSignal.value; + } + return slowNeedle != null + ? evalContainsSlowNeedle(activation, slowNeedle) + : evalContainsString(activation, requireNonNull(needleValue)); + } + + private boolean evalContainsSlowNeedle(Activation activation, Val needle) { + if (isError(needle) || isUnknown(needle)) { + for (Interpretable element : nativeElements) { + try { + ((NativeStringCapability) element).evalString(activation); + } catch (ValueSignal valueSignal) { + if (isUnknownOrError(valueSignal.value)) { + break; + } + } + } + throw signal(needle); + } + + Val[] values = new Val[nativeElements.length]; + for (int i = 0; i < nativeElements.length; i++) { + try { + values[i] = stringOf(((NativeStringCapability) nativeElements[i]).evalString(activation)); + } catch (ValueSignal valueSignal) { + if (isUnknownOrError(valueSignal.value)) { + throw valueSignal; + } + values[i] = valueSignal.value; + } + } + for (Val value : values) { + if (needle.equal(value) == BoolT.True) { + return true; + } + } + return false; + } + + private boolean evalContainsString(Activation activation, String needle) { + int firstMatch = -1; + Val[] slowValues = null; + for (int i = 0; i < nativeElements.length; i++) { + try { + String value = ((NativeStringCapability) nativeElements[i]).evalString(activation); + if (firstMatch == -1 && needle.equals(value)) { + firstMatch = i; + } + } catch (ValueSignal valueSignal) { + if (isUnknownOrError(valueSignal.value)) { + throw valueSignal; + } + if (slowValues == null) { + slowValues = new Val[nativeElements.length]; + } + slowValues[i] = valueSignal.value; + } + } + + if (slowValues != null) { + StringT needleValue = stringOf(needle); + int limit = firstMatch == -1 ? nativeElements.length : firstMatch; + for (int i = 0; i < limit; i++) { + Val slowValue = slowValues[i]; + if (slowValue != null && needleValue.equal(slowValue) == BoolT.True) { + return true; + } + } + } + return firstMatch != -1; + } + + @Override + void evalDiscarded(Activation activation, int index) { + try { + ((NativeStringCapability) nativeElements[index]).evalString(activation); + } catch (ValueSignal valueSignal) { + if (isUnknownOrError(valueSignal.value)) { + throw valueSignal; + } + } + } +} + +final class NativeBooleanListLiteralIndex extends NativeScalarAttr + implements NativeBooleanCapability { + private final NativeBooleanListLiteralCapability list; + private final int index; + + NativeBooleanListLiteralIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeBooleanListLiteralCapability list, + int index) { + super(id, adapter, establishedAttribute, null); + this.list = list; + this.index = index; + } + + @Override + public boolean evalBoolean(Activation activation) { + return list.evalBooleanAt(activation, index); + } +} + +final class NativeIntListLiteralIndex extends NativeScalarAttr implements NativeIntCapability { + private final NativeIntListLiteralCapability list; + private final int index; + + NativeIntListLiteralIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeIntListLiteralCapability list, + int index) { + super(id, adapter, establishedAttribute, null); + this.list = list; + this.index = index; + } + + @Override + public long evalInt(Activation activation) { + return list.evalIntAt(activation, index); + } +} + +final class NativeUintListLiteralIndex extends NativeScalarAttr implements NativeUintCapability { + private final NativeUintListLiteralCapability list; + private final int index; + + NativeUintListLiteralIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeUintListLiteralCapability list, + int index) { + super(id, adapter, establishedAttribute, null); + this.list = list; + this.index = index; + } + + @Override + public long evalUint(Activation activation) { + return list.evalUintAt(activation, index); + } +} + +final class NativeDoubleListLiteralIndex extends NativeScalarAttr + implements NativeDoubleCapability { + private final NativeDoubleListLiteralCapability list; + private final int index; + + NativeDoubleListLiteralIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeDoubleListLiteralCapability list, + int index) { + super(id, adapter, establishedAttribute, null); + this.list = list; + this.index = index; + } + + @Override + public double evalDouble(Activation activation) { + return list.evalDoubleAt(activation, index); + } +} + +final class NativeStringListLiteralIndex extends NativeScalarAttr + implements NativeStringCapability { + private final NativeStringListLiteralCapability list; + private final int index; + + NativeStringListLiteralIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeStringListLiteralCapability list, + int index) { + super(id, adapter, establishedAttribute, null); + this.list = list; + this.index = index; + } + + @Override + public String evalString(Activation activation) { + return list.evalStringAt(activation, index); + } +} + +final class NativeListLiteralSize extends EvalUnary implements NativeIntCapability { + private final NativeScalarListLiteralCapability list; + + NativeListLiteralSize( + long id, String function, String overload, Interpretable operand, Overload implementation) { + super(id, function, overload, operand, implementation.operandTrait, implementation.unary); + this.list = (NativeScalarListLiteralCapability) operand; + } + + @Override + public long evalInt(Activation activation) { + return list.evalSize(activation); + } +} + +final class NativeListSourceSize extends EvalUnary implements NativeIntCapability { + private final NativeListSourceCapability source; + private final boolean directArrayAccess; + + NativeListSourceSize( + long id, + String function, + String overload, + Interpretable operand, + Overload implementation, + boolean directArrayAccess) { + super(id, function, overload, operand, implementation.operandTrait, implementation.unary); + this.source = (NativeListSourceCapability) operand; + this.directArrayAccess = directArrayAccess; + } + + @Override + public long evalInt(Activation activation) { + Object value; + try { + value = source.evalRaw(activation); + } catch (ValueSignal valueSignal) { + value = valueSignal.value; + } + return NativeListSources.size(source, value, directArrayAccess); + } +} + +final class NativeStringListLiteralMembership extends EvalBinary + implements NativeBooleanCapability { + private final NativeStringCapability needle; + private final NativeStringListLiteralCapability list; + + NativeStringListLiteralMembership( + long id, Interpretable needle, Interpretable list, Overload implementation) { + super( + id, + Operator.In.id, + Overloads.InList, + needle, + list, + implementation.operandTrait, + implementation.binary); + this.needle = (NativeStringCapability) needle; + this.list = (NativeStringListLiteralCapability) list; + } + + @Override + public boolean evalBoolean(Activation activation) { + return list.evalContains(activation, needle); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/NativeListSources.java b/core/src/main/java/org/projectnessie/cel/interpreter/NativeListSources.java new file mode 100644 index 00000000..d533ac52 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/NativeListSources.java @@ -0,0 +1,585 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.Err.isError; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.UnknownT.isUnknown; +import static org.projectnessie.cel.interpreter.AttributeFactory.refResolve; +import static org.projectnessie.cel.interpreter.ValueSignal.signal; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; +import org.projectnessie.cel.common.types.traits.Sizer; + +/** + * Shared representation dispatch for a resolved list source. + * + *

The kernel never resolves a source expression. Unsupported or general representations are + * materialized through the source capability using the already-resolved value. + */ +final class NativeListSources { + static final int UNSUPPORTED = -1; + + private NativeListSources() {} + + /** + * Tests membership against an exact host {@link Set} without changing CEL's scalar semantics. + * + *

The return value is {@link #UNSUPPORTED} when the resolved representation does not match the + * exact representation for {@code kind}. The caller must then materialize this already-resolved + * value and continue through the established implementation. + */ + static int exactSetContains( + Object raw, + NativeScalarKind kind, + boolean booleanNeedle, + long integerNeedle, + double doubleNeedle, + String stringNeedle) { + if (!(raw instanceof Set values)) { + return UNSUPPORTED; + } + if (values.isEmpty()) { + return 0; + } + boolean contains; + switch (kind) { + case BOOLEAN -> contains = values.contains(booleanNeedle); + case INT -> contains = values.contains(integerNeedle); + case UINT -> + contains = + values.contains(integerNeedle) || values.contains(ULong.valueOf(integerNeedle)); + case DOUBLE -> { + if (Double.isNaN(doubleNeedle)) { + return 0; + } + contains = + doubleNeedle == 0.0d + ? values.contains(0.0d) || values.contains(-0.0d) + : values.contains(doubleNeedle); + } + case STRING -> contains = values.contains(stringNeedle); + case NULL -> { + return UNSUPPORTED; + } + default -> throw new IllegalStateException("Unhandled scalar kind " + kind); + } + return contains ? 1 : 0; + } + + static int exactListEquals( + NativeListSourceCapability leftSource, + Object leftRaw, + NativeListSourceCapability rightSource, + Object rightRaw, + NativeScalarKind kind, + TypeAdapter adapter) { + ExactCursor left = ExactCursor.create(leftRaw); + ExactCursor right = ExactCursor.create(rightRaw); + if (left == null || right == null) { + return UNSUPPORTED; + } + if (left.size != right.size) { + return 0; + } + for (int i = 0; i < left.size; i++) { + boolean equal = + switch (kind) { + case BOOLEAN -> + left.nextBoolean(leftSource, adapter) == right.nextBoolean(rightSource, adapter); + case INT -> + left.nextInteger(leftSource, adapter, false) + == right.nextInteger(rightSource, adapter, false); + case UINT -> + left.nextInteger(leftSource, adapter, true) + == right.nextInteger(rightSource, adapter, true); + case DOUBLE -> + left.nextDouble(leftSource, adapter) == right.nextDouble(rightSource, adapter); + case STRING -> + left.nextString(leftSource, adapter).equals(right.nextString(rightSource, adapter)); + case NULL -> throw new IllegalStateException("null list equality is not specialized"); + }; + if (!equal) { + return 0; + } + } + return 1; + } + + static int size(NativeListSourceCapability source, Object raw, boolean directArrayAccess) { + if (source.exactListSource()) { + int size = directSize(raw); + if (size >= 0) { + return size; + } + } else if (directArrayAccess) { + int size = directArraySize(raw); + if (size >= 0) { + return size; + } + } + Val materialized = materializedList(source, raw); + if (materialized instanceof Sizer sizer) { + return sizer.nativeSize(); + } + throw signal(newErr("got '%s', expected list type", materialized.getClass().getName())); + } + + static boolean booleanAt( + NativeListSourceCapability source, + Object raw, + int index, + boolean directArrayAccess, + TypeAdapter adapter) { + Object selected = selected(source, raw, index, directArrayAccess); + if (source.exactListSource() && !(selected instanceof Boolean)) { + selected = source.materializeResolvedElement(selected); + } + return NativeSupport.booleanValue(adapter, selected); + } + + static long intAt( + NativeListSourceCapability source, + Object raw, + int index, + boolean directArrayAccess, + TypeAdapter adapter) { + if (directArrayAccess) { + if (raw instanceof long[] values) { + checkIndex(index, values.length); + return values[index]; + } + if (raw instanceof int[] values) { + checkIndex(index, values.length); + return values[index]; + } + } + Object selected = selected(source, raw, index, directArrayAccess); + if (source.exactListSource() + && (selected instanceof Byte + || selected instanceof Short + || selected instanceof Integer + || selected instanceof Long)) { + return ((Number) selected).longValue(); + } + if (source.exactListSource()) { + selected = source.materializeResolvedElement(selected); + } + return NativeSupport.intValue(adapter, selected); + } + + static long uintAt( + NativeListSourceCapability source, + Object raw, + int index, + boolean directArrayAccess, + TypeAdapter adapter) { + if (source.exactListSource() && directArrayAccess && raw instanceof long[] values) { + checkIndex(index, values.length); + return values[index]; + } + Object selected = selected(source, raw, index, directArrayAccess); + if (source.exactListSource()) { + if (selected instanceof Long bits) { + return bits; + } + if (selected instanceof ULong unsigned) { + return unsigned.longValue(); + } + selected = source.materializeResolvedElement(selected); + } + return NativeSupport.uintValue(adapter, selected); + } + + static double doubleAt( + NativeListSourceCapability source, + Object raw, + int index, + boolean directArrayAccess, + TypeAdapter adapter) { + if (directArrayAccess && raw instanceof double[] values) { + checkIndex(index, values.length); + return values[index]; + } + Object selected = selected(source, raw, index, directArrayAccess); + if (source.exactListSource() && (selected instanceof Float || selected instanceof Double)) { + return ((Number) selected).doubleValue(); + } + if (source.exactListSource()) { + selected = source.materializeResolvedElement(selected); + } + return NativeSupport.doubleValue(adapter, selected); + } + + static String stringAt( + NativeListSourceCapability source, + Object raw, + int index, + boolean directArrayAccess, + TypeAdapter adapter) { + Object selected = selected(source, raw, index, directArrayAccess); + if (source.exactListSource() && !(selected instanceof String)) { + selected = source.materializeResolvedElement(selected); + } + return NativeSupport.stringValue(adapter, selected); + } + + static void nullAt( + NativeListSourceCapability source, + Object raw, + int index, + boolean directArrayAccess, + TypeAdapter adapter) { + Object selected = selected(source, raw, index, directArrayAccess); + if (source.exactListSource() && selected != null) { + selected = source.materializeResolvedElement(selected); + } + NativeSupport.nullValue(adapter, selected); + } + + static boolean traverseResolved( + NativeListSourceCapability source, + Object raw, + NativeScalarKind elementKind, + NativeLoopBinding binding, + NativeScalarLoopConsumer consumer) { + if (elementKind == NativeScalarKind.INT && raw instanceof int[] values) { + consumer.prepareCapacity(values.length); + for (int value : values) { + binding.setInt(value); + if (consumer.test(binding)) { + return true; + } + } + return false; + } + if (elementKind == NativeScalarKind.INT && raw instanceof long[] values) { + consumer.prepareCapacity(values.length); + for (long value : values) { + binding.setInt(value); + if (consumer.test(binding)) { + return true; + } + } + return false; + } + if (source.exactListSource() + && elementKind == NativeScalarKind.UINT + && raw instanceof long[] values) { + consumer.prepareCapacity(values.length); + for (long value : values) { + binding.setUint(value); + if (consumer.test(binding)) { + return true; + } + } + return false; + } + if (elementKind == NativeScalarKind.DOUBLE && raw instanceof double[] values) { + consumer.prepareCapacity(values.length); + for (double value : values) { + binding.setDouble(value); + if (consumer.test(binding)) { + return true; + } + } + return false; + } + if (elementKind == NativeScalarKind.STRING && raw instanceof String[] values) { + consumer.prepareCapacity(values.length); + for (String value : values) { + if (value != null || !source.exactListSource()) { + binding.setString(value); + } else { + binding.setObject(source.materializeResolvedElement(null)); + } + if (consumer.test(binding)) { + return true; + } + } + return false; + } + if (raw instanceof Object[] values) { + consumer.prepareCapacity(values.length); + for (Object value : values) { + setBindingValue(binding, elementKind, value, source); + if (consumer.test(binding)) { + return true; + } + } + return false; + } + if (source.exactListSource() && raw instanceof Collection values) { + consumer.prepareCapacity(values.size()); + for (Object value : values) { + setBindingValue(binding, elementKind, value, source); + if (consumer.test(binding)) { + return true; + } + } + return false; + } + return NativeScalarLoopKernel.evaluateMaterialized( + materializedList(source, raw), binding, consumer); + } + + private static Object selected( + NativeListSourceCapability source, Object raw, int index, boolean directArrayAccess) { + try { + return selectedUnchecked(source, raw, index, directArrayAccess); + } catch (ValueSignal valueSignal) { + throw valueSignal; + } catch (Exception failure) { + throw signal(newErr(failure, failure.toString())); + } + } + + private static Object selectedUnchecked( + NativeListSourceCapability source, Object raw, int index, boolean directArrayAccess) { + if (source.exactListSource()) { + if (raw instanceof Object[] values) { + checkIndex(index, values.length); + return values[index]; + } + if (raw instanceof List values) { + checkIndex(index, values.size()); + return values.get(index); + } + if (raw instanceof Collection values) { + checkIndex(index, values.size()); + Iterator iterator = values.iterator(); + for (int current = 0; iterator.hasNext(); current++) { + Object value = iterator.next(); + if (current == index) { + return value; + } + } + throw signal(newErr("collection size changed during exact aggregate evaluation")); + } + } else if (directArrayAccess && raw instanceof Object[] values) { + checkIndex(index, values.length); + Object value = values[index]; + if (value != null) { + return value; + } + } + if (source instanceof NativeRawIdent identifier) { + return refResolve(identifier.adapter, intOf(index), raw); + } + Val materialized = materializedList(source, raw); + if (materialized instanceof Lister list) { + return list.nativeGetAt(index); + } + throw signal(newErr("got '%s', expected list type", materialized.getClass().getName())); + } + + private static Val materializedList(NativeListSourceCapability source, Object raw) { + Val materialized = source.materializeResolvedList(raw); + if (isError(materialized) || isUnknown(materialized)) { + throw signal(materialized); + } + return materialized; + } + + private static int directSize(Object raw) { + int arraySize = directArraySize(raw); + if (arraySize >= 0) { + return arraySize; + } + return raw instanceof Collection collection ? collection.size() : -1; + } + + private static int directArraySize(Object raw) { + if (raw instanceof int[] values) { + return values.length; + } + if (raw instanceof long[] values) { + return values.length; + } + if (raw instanceof double[] values) { + return values.length; + } + return raw instanceof Object[] values ? values.length : -1; + } + + private static void setBindingValue( + NativeLoopBinding binding, + NativeScalarKind kind, + Object value, + NativeListSourceCapability source) { + if (source.exactListSource()) { + switch (kind) { + case INT -> { + if (value instanceof Byte + || value instanceof Short + || value instanceof Integer + || value instanceof Long) { + binding.setInt(((Number) value).longValue()); + return; + } + } + case UINT -> { + if (value instanceof Long bits) { + binding.setUint(bits); + return; + } + if (value instanceof ULong unsigned) { + binding.setUint(unsigned.longValue()); + return; + } + } + case DOUBLE -> { + if (value instanceof Float || value instanceof Double) { + binding.setDouble(((Number) value).doubleValue()); + return; + } + } + case STRING -> { + if (value instanceof String string) { + binding.setString(string); + return; + } + } + case BOOLEAN -> { + if (value instanceof Boolean) { + binding.setObject(value); + return; + } + } + case NULL -> { + if (value == null) { + binding.setObject(null); + return; + } + } + } + value = source.materializeResolvedElement(value); + } + binding.setObject(value); + } + + private static void checkIndex(int index, int size) { + if (index < 0 || index >= size) { + throw signal( + newErr("invalid_argument: index '%d' out of range in list of size '%d'", index, size)); + } + } + + private static final class ExactCursor { + private final Object values; + private final int size; + private Iterator iterator; + private int index; + + private ExactCursor(Object values, int size) { + this.values = values; + this.size = size; + } + + static ExactCursor create(Object values) { + int size = directSize(values); + if (size < 0) { + return null; + } + return new ExactCursor(values, size); + } + + boolean nextBoolean(NativeListSourceCapability source, TypeAdapter adapter) { + Object value = nextObject(); + if (value instanceof Boolean bool) { + return bool; + } + return NativeSupport.booleanValue(adapter, source.materializeResolvedElement(value)); + } + + long nextInteger(NativeListSourceCapability source, TypeAdapter adapter, boolean unsigned) { + if (!unsigned && values instanceof int[] ints) { + return ints[index++]; + } + if (values instanceof long[] longs) { + return longs[index++]; + } + Object value = nextObject(); + if (unsigned) { + if (value instanceof Long bits) { + return bits; + } + if (value instanceof ULong uint) { + return uint.longValue(); + } + return NativeSupport.uintValue(adapter, source.materializeResolvedElement(value)); + } + if (value instanceof Byte + || value instanceof Short + || value instanceof Integer + || value instanceof Long) { + return ((Number) value).longValue(); + } + return NativeSupport.intValue(adapter, source.materializeResolvedElement(value)); + } + + double nextDouble(NativeListSourceCapability source, TypeAdapter adapter) { + if (values instanceof double[] doubles) { + return doubles[index++]; + } + Object value = nextObject(); + if (value instanceof Float || value instanceof Double) { + return ((Number) value).doubleValue(); + } + return NativeSupport.doubleValue(adapter, source.materializeResolvedElement(value)); + } + + String nextString(NativeListSourceCapability source, TypeAdapter adapter) { + Object value = nextObject(); + if (value instanceof String string) { + return string; + } + return NativeSupport.stringValue(adapter, source.materializeResolvedElement(value)); + } + + private Object nextObject() { + if (values instanceof Collection collection) { + if (iterator == null) { + iterator = collection.iterator(); + } + index++; + return iterator.next(); + } + int current = index++; + if (values instanceof Object[] objects) { + return objects[current]; + } + if (values instanceof int[] ints) { + return ints[current]; + } + if (values instanceof long[] longs) { + return longs[current]; + } + if (values instanceof double[] doubles) { + return doubles[current]; + } + throw new IllegalStateException("unsupported exact list source " + values.getClass()); + } + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/NativeLoopNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/NativeLoopNodes.java new file mode 100644 index 00000000..0864c3e4 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/NativeLoopNodes.java @@ -0,0 +1,565 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static java.util.Objects.requireNonNull; +import static org.projectnessie.cel.common.operators.Operator.LogicalAnd; +import static org.projectnessie.cel.common.operators.Operator.LogicalOr; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.Err.isError; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.Types.boolOf; +import static org.projectnessie.cel.common.types.UnknownT.isUnknown; +import static org.projectnessie.cel.interpreter.ValueSignal.signal; + +import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.types.IterableT; +import org.projectnessie.cel.common.types.IteratorT; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Sizer; +import org.projectnessie.cel.common.types.traits.Trait; + +enum NativeQuantifier { + ALL(true, false), + EXISTS(false, true); + + final boolean initialValue; + final boolean shortCircuitValue; + + NativeQuantifier(boolean initialValue, boolean shortCircuitValue) { + this.initialValue = initialValue; + this.shortCircuitValue = shortCircuitValue; + } +} + +interface NativeScalarLoopConsumer { + default void prepareCapacity(int capacity) {} + + boolean test(NativeLoopBinding binding); +} + +final class NativeScalarLoopKernel { + private NativeScalarLoopKernel() {} + + static boolean evaluate( + NativeListSourceCapability range, + NativeScalarKind elementKind, + Activation activation, + NativeLoopBinding binding, + NativeScalarLoopConsumer consumer) { + Object target; + try { + target = range.evalRaw(activation); + } catch (ValueSignal valueSignal) { + target = valueSignal.value; + } + + return NativeListSources.traverseResolved(range, target, elementKind, binding, consumer); + } + + static boolean evaluateMaterialized( + Val foldRange, NativeLoopBinding binding, NativeScalarLoopConsumer consumer) { + if (isError(foldRange) || isUnknown(foldRange)) { + throw signal(foldRange); + } + if (!foldRange.type().hasTrait(Trait.IterableType)) { + throw signal(newErr("got '%s', expected iterable type", foldRange.getClass().getName())); + } + if (foldRange instanceof Sizer sizer) { + consumer.prepareCapacity(sizer.nativeSize()); + } + IteratorT iterator = ((IterableT) foldRange).iterator(); + while (iterator.hasNext() == True) { + binding.setObject(iterator.next()); + if (consumer.test(binding)) { + return true; + } + } + return false; + } +} + +abstract class NativeScalarLoopFold extends EvalFold + implements NativeBooleanCapability, NativeScalarLoopConsumer { + private final NativeListSourceCapability range; + private final NativeScalarKind elementKind; + final NativeBooleanCapability predicate; + final TypeAdapter adapter; + final String variable; + + NativeScalarLoopFold( + long id, + String accumulator, + Interpretable accumulatorInitial, + String variable, + Interpretable range, + Interpretable condition, + Interpretable step, + Interpretable result, + NativeScalarKind elementKind, + NativeBooleanCapability predicate, + TypeAdapter adapter) { + super(id, accumulator, accumulatorInitial, variable, "", range, condition, step, result); + this.range = (NativeListSourceCapability) range; + this.elementKind = requireNonNull(elementKind, "elementKind"); + this.predicate = requireNonNull(predicate, "predicate"); + this.adapter = requireNonNull(adapter, "adapter"); + this.variable = requireNonNull(variable, "variable"); + } + + final boolean evaluate(Activation activation, NativeLoopBinding binding) { + return NativeScalarLoopKernel.evaluate(range, elementKind, activation, binding, this); + } + + @Override + public abstract boolean test(NativeLoopBinding binding); +} + +final class NativeQuantifierFold extends NativeScalarLoopFold { + private final NativeQuantifier quantifier; + + NativeQuantifierFold( + long id, + String accumulator, + Interpretable accumulatorInitial, + String variable, + Interpretable range, + Interpretable condition, + Interpretable step, + Interpretable result, + NativeScalarKind elementKind, + NativeBooleanCapability predicate, + NativeQuantifier quantifier, + TypeAdapter adapter) { + super( + id, + accumulator, + accumulatorInitial, + variable, + range, + condition, + step, + result, + elementKind, + predicate, + adapter); + this.quantifier = requireNonNull(quantifier, "quantifier"); + } + + @Override + public boolean test(NativeLoopBinding binding) { + try { + return predicate.evalBoolean(binding) == quantifier.shortCircuitValue; + } catch (ValueSignal valueSignal) { + binding.record(valueSignal.value, quantifier); + return false; + } + } + + @Override + public boolean evalBoolean(Activation activation) { + NativeLoopBinding binding = new NativeLoopBinding(activation, variable); + if (evaluate(activation, binding)) { + return quantifier.shortCircuitValue; + } + return binding.finish(quantifier); + } +} + +final class NativeExistsOneFold extends NativeScalarLoopFold { + NativeExistsOneFold( + long id, + String accumulator, + Interpretable accumulatorInitial, + String variable, + Interpretable range, + Interpretable condition, + Interpretable step, + Interpretable result, + NativeScalarKind elementKind, + NativeBooleanCapability predicate, + TypeAdapter adapter) { + super( + id, + accumulator, + accumulatorInitial, + variable, + range, + condition, + step, + result, + elementKind, + predicate, + adapter); + } + + @Override + public boolean test(NativeLoopBinding binding) { + try { + binding.record(predicate.evalBoolean(binding)); + } catch (ValueSignal valueSignal) { + binding.record(valueSignal.value); + } catch (RuntimeException failure) { + binding.record(failure); + } + return false; + } + + @Override + public boolean evalBoolean(Activation activation) { + NativeLoopBinding binding = new NativeLoopBinding(activation, variable); + evaluate(activation, binding); + return binding.finishExistsOne(); + } +} + +abstract class NativeIntMappedLoopFold extends EvalFold implements NativeBooleanCapability { + final NativeScalarListFoldCapability source; + final NativeBooleanCapability predicate; + final String variable; + + NativeIntMappedLoopFold( + long id, + String accumulator, + Interpretable accumulatorInitial, + String variable, + Interpretable range, + Interpretable condition, + Interpretable step, + Interpretable result, + NativeBooleanCapability predicate) { + super(id, accumulator, accumulatorInitial, variable, "", range, condition, step, result); + this.source = (NativeScalarListFoldCapability) range; + this.predicate = requireNonNull(predicate, "predicate"); + this.variable = requireNonNull(variable, "variable"); + } + + final NativeIntAggregateValues evaluateSource(Activation activation) { + return source.evalIntValues(activation); + } +} + +final class NativeIntMappedQuantifierFold extends NativeIntMappedLoopFold { + private final NativeQuantifier quantifier; + + NativeIntMappedQuantifierFold( + long id, + String accumulator, + Interpretable accumulatorInitial, + String variable, + Interpretable range, + Interpretable condition, + Interpretable step, + Interpretable result, + NativeBooleanCapability predicate, + NativeQuantifier quantifier) { + super(id, accumulator, accumulatorInitial, variable, range, condition, step, result, predicate); + this.quantifier = requireNonNull(quantifier, "quantifier"); + } + + @Override + public boolean evalBoolean(Activation activation) { + NativeIntAggregateValues values = evaluateSource(activation); + NativeLoopBinding binding = new NativeLoopBinding(activation, variable); + for (int i = 0; i < values.size(); i++) { + values.set(i, binding); + try { + if (predicate.evalBoolean(binding) == quantifier.shortCircuitValue) { + return quantifier.shortCircuitValue; + } + } catch (ValueSignal valueSignal) { + binding.record(valueSignal.value, quantifier); + } + } + return binding.finish(quantifier); + } +} + +final class NativeIntMappedExistsOneFold extends NativeIntMappedLoopFold { + NativeIntMappedExistsOneFold( + long id, + String accumulator, + Interpretable accumulatorInitial, + String variable, + Interpretable range, + Interpretable condition, + Interpretable step, + Interpretable result, + NativeBooleanCapability predicate) { + super(id, accumulator, accumulatorInitial, variable, range, condition, step, result, predicate); + } + + @Override + public boolean evalBoolean(Activation activation) { + NativeIntAggregateValues values = evaluateSource(activation); + NativeLoopBinding binding = new NativeLoopBinding(activation, variable); + for (int i = 0; i < values.size(); i++) { + values.set(i, binding); + try { + binding.record(predicate.evalBoolean(binding)); + } catch (ValueSignal valueSignal) { + binding.record(valueSignal.value); + } catch (RuntimeException failure) { + binding.record(failure); + } + } + return binding.finishExistsOne(); + } +} + +class NativeLoopBinding implements Activation { + private final Activation parent; + private final String variable; + private NativeScalarKind valueKind; + private Object objectValue; + private long intValue; + private double doubleValue; + private String stringValue; + private Val pending; + private long matches; + + NativeLoopBinding(Activation parent, String variable) { + this.parent = requireNonNull(parent, "parent"); + this.variable = requireNonNull(variable, "variable"); + } + + boolean matches(String name) { + return !name.startsWith(".") && variable.equals(name); + } + + void setInt(long value) { + valueKind = NativeScalarKind.INT; + intValue = value; + objectValue = null; + } + + void setUint(long value) { + valueKind = NativeScalarKind.UINT; + intValue = value; + objectValue = null; + } + + void setDouble(double value) { + valueKind = NativeScalarKind.DOUBLE; + doubleValue = value; + objectValue = null; + } + + void setString(String value) { + valueKind = NativeScalarKind.STRING; + stringValue = value; + objectValue = value; + } + + void setObject(Object value) { + valueKind = null; + objectValue = value; + } + + boolean booleanValue(TypeAdapter adapter) { + return NativeSupport.booleanValue(adapter, objectValue); + } + + long intValue(TypeAdapter adapter) { + return valueKind == NativeScalarKind.INT + ? intValue + : NativeSupport.intValue(adapter, objectValue); + } + + long uintValue(TypeAdapter adapter) { + return valueKind == NativeScalarKind.UINT + ? intValue + : NativeSupport.uintValue(adapter, objectValue); + } + + double doubleValue(TypeAdapter adapter) { + return valueKind == NativeScalarKind.DOUBLE + ? doubleValue + : NativeSupport.doubleValue(adapter, objectValue); + } + + String stringValue(TypeAdapter adapter) { + if (valueKind != NativeScalarKind.STRING) { + return NativeSupport.stringValue(adapter, objectValue); + } + if (stringValue == null) { + throw signal(stringOf(null)); + } + return stringValue; + } + + void record(Val value, NativeQuantifier quantifier) { + pending = + logicalSlow( + pending != null ? pending : boolOf(quantifier.initialValue), + value, + quantifier == NativeQuantifier.ALL); + } + + boolean finish(NativeQuantifier quantifier) { + if (pending != null) { + throw signal(pending); + } + return quantifier.initialValue; + } + + void record(boolean value) { + if (value) { + if (pending == null) { + matches++; + } else if (isError(pending)) { + pending = wrapPropagatedError(pending).value; + } + } + } + + void record(Val value) { + pending = isError(value) ? wrapPropagatedError(value).value : value; + } + + void record(RuntimeException failure) { + pending = newErr(failure, failure.toString()); + } + + boolean finishExistsOne() { + if (pending != null) { + throw signal(pending); + } + return matches == 1; + } + + @Override + public Activation parent() { + return parent; + } + + @Override + public Object resolve(String name) { + if (matches(name)) { + if (valueKind == null) { + return objectValue; + } + return switch (valueKind) { + case INT -> intValue; + case UINT -> ULong.valueOf(intValue); + case DOUBLE -> doubleValue; + case STRING -> stringValue != null ? stringValue : stringOf(null); + case BOOLEAN, NULL -> objectValue; + }; + } + return parent.resolve(name.startsWith(".") ? name.substring(1) : name); + } + + @SuppressWarnings("removal") + @Override + public ResolvedValue resolveName(String name) { + return ResolvedValue.mapTo(resolve(name)); + } + + @SuppressWarnings("DuplicatedCode") + private static Val logicalSlow(Val left, Val right, boolean and) { + Val shortCircuit = boolOf(!and); + Val identity = boolOf(and); + if (left == shortCircuit || right == shortCircuit) { + return shortCircuit; + } + if (left == identity && right == identity) { + return identity; + } + if (isUnknown(left)) { + return left; + } + if (isUnknown(right)) { + return right; + } + if (isError(left)) { + return left; + } + return noSuchOverload(left, and ? LogicalAnd.id : LogicalOr.id, right); + } + + private static ValueSignal wrapPropagatedError(Val value) { + return NativeSupport.propagatedError(value); + } +} + +final class NativeBooleanLocalIdent extends EvalIdent implements NativeBooleanCapability { + NativeBooleanLocalIdent(long id, String name, TypeAdapter adapter) { + super(id, name, adapter); + } + + @Override + public boolean evalBoolean(Activation activation) { + return activation instanceof NativeLoopBinding binding + ? binding.booleanValue(adapter) + : NativeSupport.booleanValue(adapter, resolveRaw(activation)); + } +} + +final class NativeIntLocalIdent extends EvalIdent implements NativeIntCapability { + NativeIntLocalIdent(long id, String name, TypeAdapter adapter) { + super(id, name, adapter); + } + + @Override + public long evalInt(Activation activation) { + return activation instanceof NativeLoopBinding binding + ? binding.intValue(adapter) + : NativeSupport.intValue(adapter, resolveRaw(activation)); + } +} + +final class NativeUintLocalIdent extends EvalIdent implements NativeUintCapability { + NativeUintLocalIdent(long id, String name, TypeAdapter adapter) { + super(id, name, adapter); + } + + @Override + public long evalUint(Activation activation) { + return activation instanceof NativeLoopBinding binding + ? binding.uintValue(adapter) + : NativeSupport.uintValue(adapter, resolveRaw(activation)); + } +} + +final class NativeDoubleLocalIdent extends EvalIdent implements NativeDoubleCapability { + NativeDoubleLocalIdent(long id, String name, TypeAdapter adapter) { + super(id, name, adapter); + } + + @Override + public double evalDouble(Activation activation) { + return activation instanceof NativeLoopBinding binding + ? binding.doubleValue(adapter) + : NativeSupport.doubleValue(adapter, resolveRaw(activation)); + } +} + +final class NativeStringLocalIdent extends EvalIdent implements NativeStringCapability { + NativeStringLocalIdent(long id, String name, TypeAdapter adapter) { + super(id, name, adapter); + } + + @Override + public String evalString(Activation activation) { + return activation instanceof NativeLoopBinding binding + ? binding.stringValue(adapter) + : NativeSupport.stringValue(adapter, resolveRaw(activation)); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/NativeMapNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/NativeMapNodes.java new file mode 100644 index 00000000..febdf01c --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/NativeMapNodes.java @@ -0,0 +1,443 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.Err.isError; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.Err.noSuchKey; +import static org.projectnessie.cel.common.types.UnknownT.isUnknown; +import static org.projectnessie.cel.interpreter.ValueSignal.signal; + +import java.util.Map; +import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.Overloads; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Mapper; +import org.projectnessie.cel.common.types.traits.Sizer; +import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; +import org.projectnessie.cel.interpreter.functions.Overload; + +/** Shared exact-map operations over an already-resolved host value. */ +final class NativeMapSources { + static final Object ABSENT = new Object(); + + private NativeMapSources() {} + + static int size(NativeMapSourceCapability source, Object raw) { + try { + if (source.exactMapSource() && raw instanceof Map map) { + return map.size(); + } + Val materialized = materializedMap(source, raw); + if (materialized instanceof Sizer sizer) { + return sizer.nativeSize(); + } + throw signal(newErr("got '%s', expected map type", materialized.getClass().getName())); + } catch (ValueSignal failure) { + throw failure; + } catch (Exception failure) { + throw signal(newErr(failure, failure.toString())); + } + } + + /** + * Returns the raw value, including Java null for present null, or {@link #ABSENT} for a missing + * key. + */ + static Object lookup(NativeMapSourceCapability source, Object raw, Object hostKey, Val celKey) { + try { + if (source.exactMapSource() && raw instanceof Map map) { + Object value = map.get(hostKey); + return value != null || map.containsKey(hostKey) ? value : ABSENT; + } + Mapper materialized = materializedMapper(source, raw); + Val value = materialized.find(celKey); + return value != null ? value : ABSENT; + } catch (ValueSignal failure) { + throw failure; + } catch (Exception failure) { + throw signal(newErr(failure, failure.toString())); + } + } + + static Val checkedValue(NativeMapSourceCapability source, Object raw, Val celKey) { + Mapper materialized = materializedMapper(source, raw); + Val value = materialized.find(celKey); + if (value == null) { + throw signal(noSuchKey(celKey)); + } + return value; + } + + private static Mapper materializedMapper(NativeMapSourceCapability source, Object raw) { + Val materialized = materializedMap(source, raw); + if (materialized instanceof Mapper mapper) { + return mapper; + } + throw signal(newErr("got '%s', expected map type", materialized.getClass().getName())); + } + + private static Val materializedMap(NativeMapSourceCapability source, Object raw) { + Val materialized = source.materializeResolvedMap(raw); + if (isError(materialized) || isUnknown(materialized)) { + throw signal(materialized); + } + return materialized; + } +} + +final class NativeMapSize extends EvalUnary implements NativeIntCapability { + private final NativeMapSourceCapability source; + + NativeMapSize( + long id, String function, String overload, Interpretable operand, Overload implementation) { + super(id, function, overload, operand, implementation.operandTrait, implementation.unary); + this.source = (NativeMapSourceCapability) operand; + } + + @Override + public long evalInt(Activation activation) { + Object raw; + try { + raw = source.evalRaw(activation); + } catch (ValueSignal failure) { + raw = failure.value; + } + return NativeMapSources.size(source, raw); + } +} + +abstract class NativeMapIndex extends NativeScalarAttr { + final NativeMapSourceCapability source; + final Object hostKey; + final Val celKey; + + NativeMapIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeMapSourceCapability source, + Object hostKey, + Val celKey) { + super(id, adapter, establishedAttribute, null); + this.source = source; + this.hostKey = hostKey; + this.celKey = celKey; + } + + final Object resolveMap(Activation activation) { + Object raw; + try { + raw = source.evalRaw(activation); + } catch (ValueSignal failure) { + raw = failure.value; + } + return raw; + } + + final Object selectValue(Object raw) { + Object value = NativeMapSources.lookup(source, raw, hostKey, celKey); + if (value == NativeMapSources.ABSENT) { + throw signal(noSuchKey(celKey)); + } + return value; + } + + final Val checkedValue(Object raw) { + return NativeMapSources.checkedValue(source, raw, celKey); + } +} + +final class NativeBooleanMapIndex extends NativeMapIndex implements NativeBooleanCapability { + NativeBooleanMapIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeMapSourceCapability source, + Object hostKey, + Val celKey) { + super(id, adapter, establishedAttribute, source, hostKey, celKey); + } + + @Override + public boolean evalBoolean(Activation activation) { + Object raw = resolveMap(activation); + Object value = selectValue(raw); + return value instanceof Boolean bool + ? bool + : NativeSupport.booleanValue(adapter, checkedValue(raw)); + } +} + +final class NativeIntMapIndex extends NativeMapIndex implements NativeIntCapability { + NativeIntMapIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeMapSourceCapability source, + Object hostKey, + Val celKey) { + super(id, adapter, establishedAttribute, source, hostKey, celKey); + } + + @Override + public long evalInt(Activation activation) { + Object raw = resolveMap(activation); + Object value = selectValue(raw); + if (value instanceof Byte + || value instanceof Short + || value instanceof Integer + || value instanceof Long) { + return ((Number) value).longValue(); + } + return NativeSupport.intValue(adapter, checkedValue(raw)); + } +} + +final class NativeUintMapIndex extends NativeMapIndex implements NativeUintCapability { + NativeUintMapIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeMapSourceCapability source, + Object hostKey, + Val celKey) { + super(id, adapter, establishedAttribute, source, hostKey, celKey); + } + + @Override + public long evalUint(Activation activation) { + Object raw = resolveMap(activation); + Object value = selectValue(raw); + if (value instanceof Long bits) { + return bits; + } + if (value instanceof ULong unsigned) { + return unsigned.longValue(); + } + return NativeSupport.uintValue(adapter, checkedValue(raw)); + } +} + +final class NativeDoubleMapIndex extends NativeMapIndex implements NativeDoubleCapability { + NativeDoubleMapIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeMapSourceCapability source, + Object hostKey, + Val celKey) { + super(id, adapter, establishedAttribute, source, hostKey, celKey); + } + + @Override + public double evalDouble(Activation activation) { + Object raw = resolveMap(activation); + Object value = selectValue(raw); + if (value instanceof Float || value instanceof Double) { + return ((Number) value).doubleValue(); + } + return NativeSupport.doubleValue(adapter, checkedValue(raw)); + } +} + +final class NativeStringMapIndex extends NativeMapIndex implements NativeStringCapability { + NativeStringMapIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeMapSourceCapability source, + Object hostKey, + Val celKey) { + super(id, adapter, establishedAttribute, source, hostKey, celKey); + } + + @Override + public String evalString(Activation activation) { + Object raw = resolveMap(activation); + Object value = selectValue(raw); + return value instanceof String string + ? string + : NativeSupport.stringValue(adapter, checkedValue(raw)); + } +} + +final class NativeNullMapIndex extends NativeMapIndex implements NativeNullCapability { + NativeNullMapIndex( + long id, + TypeAdapter adapter, + Attribute establishedAttribute, + NativeMapSourceCapability source, + Object hostKey, + Val celKey) { + super(id, adapter, establishedAttribute, source, hostKey, celKey); + } + + @Override + public void evalNull(Activation activation) { + Object raw = resolveMap(activation); + if (selectValue(raw) != null) { + NativeSupport.nullValue(adapter, checkedValue(raw)); + } + } +} + +final class NativeMapMembership extends EvalBinary implements NativeBooleanCapability { + private final NativeMapSourceCapability source; + private final Object hostKey; + private final Val celKey; + + NativeMapMembership( + long id, + Interpretable key, + Interpretable map, + Object hostKey, + Val celKey, + Overload implementation) { + super( + id, + Operator.In.id, + Overloads.InMap, + key, + map, + implementation.operandTrait, + implementation.binary); + this.source = (NativeMapSourceCapability) map; + this.hostKey = hostKey; + this.celKey = celKey; + } + + @Override + public boolean evalBoolean(Activation activation) { + Object raw; + try { + raw = source.evalRaw(activation); + } catch (ValueSignal failure) { + raw = failure.value; + } + return NativeMapSources.lookup(source, raw, hostKey, celKey) != NativeMapSources.ABSENT; + } +} + +abstract class NativeMapAggregateIndex extends AbstractEval { + final NativeMapSourceCapability source; + final Object hostKey; + final Val celKey; + final CheckedAggregateMaterializer materializer; + + NativeMapAggregateIndex( + long id, + NativeMapSourceCapability source, + Object hostKey, + Val celKey, + CheckedAggregateMaterializer materializer) { + super(id); + this.source = source; + this.hostKey = hostKey; + this.celKey = celKey; + this.materializer = materializer; + } + + final Object selectRaw(Activation activation) { + Object raw; + try { + raw = source.evalRaw(activation); + } catch (ValueSignal failure) { + raw = failure.value; + } + Object value = NativeMapSources.lookup(source, raw, hostKey, celKey); + if (value == NativeMapSources.ABSENT) { + throw signal(noSuchKey(celKey)); + } + return value; + } + + @Override + public Val eval(Activation activation) { + try { + Object value = selectRaw(activation); + return value instanceof Val val && (isError(val) || isUnknown(val)) + ? val + : materializer.materialize(value); + } catch (ValueSignal failure) { + return failure.value; + } + } +} + +final class NativeMapListIndex extends NativeMapAggregateIndex + implements NativeListSourceCapability { + NativeMapListIndex( + long id, + NativeMapSourceCapability source, + Object hostKey, + Val celKey, + CheckedAggregateMaterializer materializer) { + super(id, source, hostKey, celKey, materializer); + } + + @Override + public Object evalRaw(Activation activation) { + return selectRaw(activation); + } + + @Override + public Val materializeResolvedList(Object value) { + return value instanceof Val val && (isError(val) || isUnknown(val)) + ? val + : materializer.materialize(value); + } + + @Override + public Val materializeResolvedElement(Object value) { + return materializer.materializeListElement(value); + } + + @Override + public boolean exactListSource() { + return true; + } +} + +final class NativeMapMapIndex extends NativeMapAggregateIndex implements NativeMapSourceCapability { + NativeMapMapIndex( + long id, + NativeMapSourceCapability source, + Object hostKey, + Val celKey, + CheckedAggregateMaterializer materializer) { + super(id, source, hostKey, celKey, materializer); + } + + @Override + public Object evalRaw(Activation activation) { + return selectRaw(activation); + } + + @Override + public Val materializeResolvedMap(Object value) { + return value instanceof Val val && (isError(val) || isUnknown(val)) + ? val + : materializer.materialize(value); + } + + @Override + public boolean exactMapSource() { + return true; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/NativeOverloadDescriptor.java b/core/src/main/java/org/projectnessie/cel/interpreter/NativeOverloadDescriptor.java new file mode 100644 index 00000000..c98e7a50 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/NativeOverloadDescriptor.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static java.util.Objects.requireNonNull; + +/** + * Interpreter-owned metadata for one checked overload that may gain a typed implementation. + * + *

The exact resolved {@code Overload} identity remains the primary provenance key. Function and + * overload names are additional checked-expression guards, not substitutes for that identity. + */ +record NativeOverloadDescriptor(String function, String overloadId) { + NativeOverloadDescriptor { + requireNonNull(function, "function"); + requireNonNull(overloadId, "overloadId"); + } + + boolean matches(String checkedFunction, String checkedOverloadId) { + return function.equals(checkedFunction) && overloadId.equals(checkedOverloadId); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/NativeScalarNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/NativeScalarNodes.java new file mode 100644 index 00000000..f100b66c --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/NativeScalarNodes.java @@ -0,0 +1,1233 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static java.util.Objects.requireNonNull; +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.Err.divideByZero; +import static org.projectnessie.cel.common.types.Err.errIntOverflow; +import static org.projectnessie.cel.common.types.Err.isError; +import static org.projectnessie.cel.common.types.Err.maybeNoSuchOverloadErr; +import static org.projectnessie.cel.common.types.Err.modulusByZero; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.Overflow.addInt64Checked; +import static org.projectnessie.cel.common.types.Overflow.divideInt64Checked; +import static org.projectnessie.cel.common.types.Overflow.moduloInt64Checked; +import static org.projectnessie.cel.common.types.Overflow.multiplyInt64Checked; +import static org.projectnessie.cel.common.types.Overflow.negateInt64Checked; +import static org.projectnessie.cel.common.types.Overflow.subtractInt64Checked; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.Types.boolOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; +import static org.projectnessie.cel.common.types.UnknownT.isUnknown; +import static org.projectnessie.cel.interpreter.ValueSignal.signal; + +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.BoolT; +import org.projectnessie.cel.common.types.DoubleT; +import org.projectnessie.cel.common.types.IntT; +import org.projectnessie.cel.common.types.NullT; +import org.projectnessie.cel.common.types.Overflow.OverflowException; +import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.UintT; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; +import org.projectnessie.cel.interpreter.functions.Overload; + +final class NativeIntConst extends EvalConst implements NativeIntCapability { + private final long value; + + NativeIntConst(long id, long value) { + super(id, intOf(value)); + this.value = value; + } + + @Override + public long evalInt(Activation activation) { + return value; + } +} + +final class NativeUintConst extends EvalConst implements NativeUintCapability { + private final long value; + + NativeUintConst(long id, long value) { + super(id, uintOf(value)); + this.value = value; + } + + @Override + public long evalUint(Activation activation) { + return value; + } +} + +final class NativeDoubleConst extends EvalConst implements NativeDoubleCapability { + private final double value; + + NativeDoubleConst(long id, double value) { + super(id, doubleOf(value)); + this.value = value; + } + + @Override + public double evalDouble(Activation activation) { + return value; + } +} + +final class NativeBooleanConst extends EvalConst implements NativeBooleanCapability { + private final boolean value; + + NativeBooleanConst(long id, boolean value) { + super(id, boolOf(value)); + this.value = value; + } + + @Override + public boolean evalBoolean(Activation activation) { + return value; + } +} + +final class NativeStringConst extends EvalConst implements NativeStringCapability { + private final String value; + + NativeStringConst(long id, String value) { + super(id, stringOf(value)); + this.value = value; + } + + @Override + public String evalString(Activation activation) { + return value; + } +} + +final class NativeNullConst extends EvalConst implements NativeNullCapability { + NativeNullConst(long id) { + super(id, NullT.NullValue); + } + + @Override + public void evalNull(Activation activation) {} +} + +final class NativeIntIdent extends EvalIdent implements NativeIntCapability { + NativeIntIdent(long id, String name, TypeAdapter adapter) { + super(id, name, adapter); + } + + @Override + public long evalInt(Activation activation) { + return NativeSupport.intValue(adapter, resolveRaw(activation)); + } +} + +final class NativeUintIdent extends EvalIdent implements NativeUintCapability { + NativeUintIdent(long id, String name, TypeAdapter adapter) { + super(id, name, adapter); + } + + @Override + public long evalUint(Activation activation) { + return NativeSupport.uintValue(adapter, resolveRaw(activation)); + } +} + +final class NativeBooleanIdent extends EvalIdent implements NativeBooleanCapability { + NativeBooleanIdent(long id, String name, TypeAdapter adapter) { + super(id, name, adapter); + } + + @Override + public boolean evalBoolean(Activation activation) { + return NativeSupport.booleanValue(adapter, resolveRaw(activation)); + } +} + +final class NativeDoubleIdent extends EvalIdent implements NativeDoubleCapability { + NativeDoubleIdent(long id, String name, TypeAdapter adapter) { + super(id, name, adapter); + } + + @Override + public double evalDouble(Activation activation) { + return NativeSupport.doubleValue(adapter, resolveRaw(activation)); + } +} + +final class NativeStringIdent extends EvalIdent implements NativeStringCapability { + NativeStringIdent(long id, String name, TypeAdapter adapter) { + super(id, name, adapter); + } + + @Override + public String evalString(Activation activation) { + return NativeSupport.stringValue(adapter, resolveRaw(activation)); + } +} + +final class NativeNullIdent extends EvalIdent implements NativeNullCapability { + NativeNullIdent(long id, String name, TypeAdapter adapter) { + super(id, name, adapter); + } + + @Override + public void evalNull(Activation activation) { + NativeSupport.nullValue(adapter, resolveRaw(activation)); + } +} + +final class NativeRawIdent extends EvalIdent implements NativeListSourceCapability { + NativeRawIdent(long id, String name, TypeAdapter adapter) { + super(id, name, adapter); + } + + @Override + public Object evalRaw(Activation activation) { + return resolveRaw(activation); + } + + @Override + public Val materializeResolvedList(Object value) { + return adapter.nativeToValue(value); + } + + @Override + public Val materializeResolvedElement(Object value) { + return adapter.nativeToValue(value); + } + + @Override + public boolean exactListSource() { + return false; + } +} + +final class NativeExactAggregateIdent extends EvalExactAggregateIdent + implements NativeListSourceCapability { + NativeExactAggregateIdent( + long id, String name, TypeAdapter adapter, CheckedAggregateMaterializer materializer) { + super(id, name, adapter, materializer); + } + + @Override + public Object evalRaw(Activation activation) { + return resolveRaw(activation); + } + + @Override + public Val materializeResolvedList(Object value) { + return value instanceof Val val && (isError(val) || isUnknown(val)) + ? val + : materializer.materialize(value); + } + + @Override + public Val materializeResolvedElement(Object value) { + return materializer.materializeListElement(value); + } + + @Override + public boolean exactListSource() { + return true; + } +} + +final class NativeExactMapIdent extends EvalExactAggregateIdent + implements NativeMapSourceCapability { + NativeExactMapIdent( + long id, String name, TypeAdapter adapter, CheckedAggregateMaterializer materializer) { + super(id, name, adapter, materializer); + } + + @Override + public Object evalRaw(Activation activation) { + return resolveRaw(activation); + } + + @Override + public Val materializeResolvedMap(Object value) { + return value instanceof Val val && (isError(val) || isUnknown(val)) + ? val + : materializer.materialize(value); + } + + @Override + public boolean exactMapSource() { + return true; + } +} + +class NativeIntBinary extends EvalBinary implements NativeIntCapability { + private final NativeArithmetic operation; + + NativeIntBinary( + long id, + String function, + String overload, + Interpretable left, + Interpretable right, + Overload implementation, + NativeArithmetic operation) { + super(id, function, overload, left, right, implementation.operandTrait, implementation.binary); + this.operation = operation; + } + + @SuppressWarnings({"DuplicatedCode", "ReassignedVariable", "SuspiciousNameCombination"}) + @Override + public final long evalInt(Activation activation) { + long leftValue = 0L; + long rightValue = 0L; + Val leftSlow = null; + Val rightSlow = null; + try { + leftValue = ((NativeIntCapability) lhs).evalInt(activation); + } catch (ValueSignal signal) { + leftSlow = signal.value; + } + try { + rightValue = ((NativeIntCapability) rhs).evalInt(activation); + } catch (ValueSignal signal) { + rightSlow = signal.value; + } + if (leftSlow != null || rightSlow != null) { + return NativeScalarContinuations.intResult( + evalPrepared( + leftSlow != null ? leftSlow : intOf(leftValue), + rightSlow != null ? rightSlow : intOf(rightValue))); + } + try { + return switch (operation) { + case ADD -> addInt64Checked(leftValue, rightValue); + case SUBTRACT -> subtractInt64Checked(leftValue, rightValue); + case MULTIPLY -> multiplyInt64Checked(leftValue, rightValue); + case DIVIDE -> { + if (rightValue == 0L) { + throw signal(divideByZero()); + } + yield divideInt64Checked(leftValue, rightValue); + } + case MODULO -> { + if (rightValue == 0L) { + throw signal(modulusByZero()); + } + yield moduloInt64Checked(leftValue, rightValue); + } + }; + } catch (OverflowException e) { + throw signal(errIntOverflow); + } + } +} + +final class NativeIntAdd extends NativeIntBinary { + NativeIntAdd( + long id, + String function, + String overload, + Interpretable left, + Interpretable right, + Overload implementation) { + super(id, function, overload, left, right, implementation, NativeArithmetic.ADD); + } +} + +final class NativeBooleanNot extends EvalUnary implements NativeBooleanCapability { + NativeBooleanNot( + long id, String function, String overload, Interpretable argument, Overload implementation) { + super(id, function, overload, argument, implementation.operandTrait, implementation.unary); + } + + @Override + public boolean evalBoolean(Activation activation) { + try { + return !((NativeBooleanCapability) arg).evalBoolean(activation); + } catch (ValueSignal signal) { + return NativeScalarContinuations.booleanResult(evalPrepared(signal.value)); + } + } +} + +final class NativeIntNegate extends EvalUnary implements NativeIntCapability { + NativeIntNegate( + long id, String function, String overload, Interpretable argument, Overload implementation) { + super(id, function, overload, argument, implementation.operandTrait, implementation.unary); + } + + @Override + public long evalInt(Activation activation) { + try { + return negateInt64Checked(((NativeIntCapability) arg).evalInt(activation)); + } catch (ValueSignal signal) { + return NativeScalarContinuations.intResult(evalPrepared(signal.value)); + } catch (OverflowException e) { + throw signal(errIntOverflow); + } + } +} + +final class NativeDoubleNegate extends EvalUnary implements NativeDoubleCapability { + NativeDoubleNegate( + long id, String function, String overload, Interpretable argument, Overload implementation) { + super(id, function, overload, argument, implementation.operandTrait, implementation.unary); + } + + @Override + public double evalDouble(Activation activation) { + try { + return -((NativeDoubleCapability) arg).evalDouble(activation); + } catch (ValueSignal signal) { + return NativeScalarContinuations.doubleResult(evalPrepared(signal.value)); + } + } +} + +final class NativeDoubleBinary extends EvalBinary implements NativeDoubleCapability { + private final NativeArithmetic operation; + + NativeDoubleBinary( + long id, + String function, + String overload, + Interpretable left, + Interpretable right, + Overload implementation, + NativeArithmetic operation) { + super(id, function, overload, left, right, implementation.operandTrait, implementation.binary); + this.operation = operation; + } + + @SuppressWarnings("DuplicatedCode") + @Override + public double evalDouble(Activation activation) { + double leftValue = 0.0d; + double rightValue = 0.0d; + Val leftSlow = null; + Val rightSlow = null; + try { + leftValue = ((NativeDoubleCapability) lhs).evalDouble(activation); + } catch (ValueSignal signal) { + leftSlow = signal.value; + } + try { + rightValue = ((NativeDoubleCapability) rhs).evalDouble(activation); + } catch (ValueSignal signal) { + rightSlow = signal.value; + } + if (leftSlow != null || rightSlow != null) { + return NativeScalarContinuations.doubleResult( + evalPrepared( + leftSlow != null ? leftSlow : doubleOf(leftValue), + rightSlow != null ? rightSlow : doubleOf(rightValue))); + } + return switch (operation) { + case ADD -> leftValue + rightValue; + case SUBTRACT -> leftValue - rightValue; + case MULTIPLY -> leftValue * rightValue; + case DIVIDE -> leftValue / rightValue; + case MODULO -> throw new IllegalStateException("double modulo is not supported"); + }; + } +} + +final class NativeStringConcat extends EvalBinary implements NativeStringCapability { + NativeStringConcat( + long id, + String function, + String overload, + Interpretable left, + Interpretable right, + Overload implementation) { + super(id, function, overload, left, right, implementation.operandTrait, implementation.binary); + } + + @SuppressWarnings("DuplicatedCode") + @Override + public String evalString(Activation activation) { + String leftValue = null; + String rightValue = null; + Val leftSlow = null; + Val rightSlow = null; + try { + leftValue = ((NativeStringCapability) lhs).evalString(activation); + } catch (ValueSignal signal) { + leftSlow = signal.value; + } + try { + rightValue = ((NativeStringCapability) rhs).evalString(activation); + } catch (ValueSignal signal) { + rightSlow = signal.value; + } + if (leftSlow != null || rightSlow != null) { + return NativeScalarContinuations.stringResult( + evalPrepared( + leftSlow != null ? leftSlow : stringOf(leftValue), + rightSlow != null ? rightSlow : stringOf(rightValue))); + } + return leftValue + rightValue; + } +} + +class NativeScalarEq extends EvalEq implements NativeBooleanCapability { + private final NativeScalarKind kind; + + NativeScalarEq(long id, Interpretable left, Interpretable right, NativeScalarKind kind) { + super(id, left, right); + this.kind = kind; + } + + @Override + public final boolean evalBoolean(Activation activation) { + return NativeScalarEquality.evaluate(kind, lhs, rhs, activation, this, null); + } +} + +final class NativeScalarNe extends EvalNe implements NativeBooleanCapability { + private final NativeScalarKind kind; + + NativeScalarNe(long id, Interpretable left, Interpretable right, NativeScalarKind kind) { + super(id, left, right); + this.kind = kind; + } + + @Override + public boolean evalBoolean(Activation activation) { + return NativeScalarEquality.evaluate(kind, lhs, rhs, activation, null, this); + } +} + +final class NativeBooleanEq extends NativeScalarEq { + NativeBooleanEq(long id, Interpretable left, Interpretable right) { + super(id, left, right, NativeScalarKind.BOOLEAN); + } +} + +final class NativeIntEq extends NativeScalarEq { + NativeIntEq(long id, Interpretable left, Interpretable right) { + super(id, left, right, NativeScalarKind.INT); + } +} + +final class NativeScalarEquality { + private NativeScalarEquality() {} + + static boolean evaluate( + NativeScalarKind kind, + Interpretable left, + Interpretable right, + Activation activation, + EvalEq equality, + EvalNe inequality) { + return switch (kind) { + case BOOLEAN -> equalBoolean(left, right, activation, equality, inequality); + case INT -> equalInt(left, right, activation, equality, inequality); + case UINT -> equalUint(left, right, activation, equality, inequality); + case DOUBLE -> equalDouble(left, right, activation, equality, inequality); + case STRING -> equalString(left, right, activation, equality, inequality); + case NULL -> equalNull(left, right, activation, equality, inequality); + }; + } + + @SuppressWarnings("DuplicatedCode") + private static boolean equalBoolean( + Interpretable left, + Interpretable right, + Activation activation, + EvalEq equality, + EvalNe inequality) { + boolean leftValue = false; + boolean rightValue = false; + Val leftSlow = null; + Val rightSlow = null; + try { + leftValue = ((NativeBooleanCapability) left).evalBoolean(activation); + } catch (ValueSignal valueSignal) { + leftSlow = valueSignal.value; + } + try { + rightValue = ((NativeBooleanCapability) right).evalBoolean(activation); + } catch (ValueSignal valueSignal) { + rightSlow = valueSignal.value; + } + if (leftSlow != null || rightSlow != null) { + return slow( + leftSlow != null ? leftSlow : boolOf(leftValue), + rightSlow != null ? rightSlow : boolOf(rightValue), + equality, + inequality); + } + return result(leftValue == rightValue, inequality); + } + + @SuppressWarnings("DuplicatedCode") + private static boolean equalInt( + Interpretable left, + Interpretable right, + Activation activation, + EvalEq equality, + EvalNe inequality) { + long leftValue = 0L; + long rightValue = 0L; + Val leftSlow = null; + Val rightSlow = null; + try { + leftValue = ((NativeIntCapability) left).evalInt(activation); + } catch (ValueSignal valueSignal) { + leftSlow = valueSignal.value; + } + try { + rightValue = ((NativeIntCapability) right).evalInt(activation); + } catch (ValueSignal valueSignal) { + rightSlow = valueSignal.value; + } + if (leftSlow != null || rightSlow != null) { + return slow( + leftSlow != null ? leftSlow : intOf(leftValue), + rightSlow != null ? rightSlow : intOf(rightValue), + equality, + inequality); + } + return result(leftValue == rightValue, inequality); + } + + @SuppressWarnings("DuplicatedCode") + private static boolean equalUint( + Interpretable left, + Interpretable right, + Activation activation, + EvalEq equality, + EvalNe inequality) { + long leftValue = 0L; + long rightValue = 0L; + Val leftSlow = null; + Val rightSlow = null; + try { + leftValue = ((NativeUintCapability) left).evalUint(activation); + } catch (ValueSignal valueSignal) { + leftSlow = valueSignal.value; + } + try { + rightValue = ((NativeUintCapability) right).evalUint(activation); + } catch (ValueSignal valueSignal) { + rightSlow = valueSignal.value; + } + if (leftSlow != null || rightSlow != null) { + return slow( + leftSlow != null ? leftSlow : uintOf(leftValue), + rightSlow != null ? rightSlow : uintOf(rightValue), + equality, + inequality); + } + return result(leftValue == rightValue, inequality); + } + + @SuppressWarnings("DuplicatedCode") + private static boolean equalDouble( + Interpretable left, + Interpretable right, + Activation activation, + EvalEq equality, + EvalNe inequality) { + double leftValue = 0.0d; + double rightValue = 0.0d; + Val leftSlow = null; + Val rightSlow = null; + try { + leftValue = ((NativeDoubleCapability) left).evalDouble(activation); + } catch (ValueSignal valueSignal) { + leftSlow = valueSignal.value; + } + try { + rightValue = ((NativeDoubleCapability) right).evalDouble(activation); + } catch (ValueSignal valueSignal) { + rightSlow = valueSignal.value; + } + if (leftSlow != null || rightSlow != null) { + return slow( + leftSlow != null ? leftSlow : doubleOf(leftValue), + rightSlow != null ? rightSlow : doubleOf(rightValue), + equality, + inequality); + } + return result(leftValue == rightValue, inequality); + } + + @SuppressWarnings("DuplicatedCode") + private static boolean equalString( + Interpretable left, + Interpretable right, + Activation activation, + EvalEq equality, + EvalNe inequality) { + String leftValue = null; + String rightValue = null; + Val leftSlow = null; + Val rightSlow = null; + try { + leftValue = ((NativeStringCapability) left).evalString(activation); + } catch (ValueSignal valueSignal) { + leftSlow = valueSignal.value; + } + try { + rightValue = ((NativeStringCapability) right).evalString(activation); + } catch (ValueSignal valueSignal) { + rightSlow = valueSignal.value; + } + if (leftSlow != null || rightSlow != null) { + return slow( + leftSlow != null ? leftSlow : stringOf(leftValue), + rightSlow != null ? rightSlow : stringOf(rightValue), + equality, + inequality); + } + String fastLeft = requireNonNull(leftValue, "native string capability returned null"); + String fastRight = requireNonNull(rightValue, "native string capability returned null"); + return result(fastLeft.equals(fastRight), inequality); + } + + private static boolean equalNull( + Interpretable left, + Interpretable right, + Activation activation, + EvalEq equality, + EvalNe inequality) { + Val leftSlow = null; + Val rightSlow = null; + try { + ((NativeNullCapability) left).evalNull(activation); + } catch (ValueSignal valueSignal) { + leftSlow = valueSignal.value; + } + try { + ((NativeNullCapability) right).evalNull(activation); + } catch (ValueSignal valueSignal) { + rightSlow = valueSignal.value; + } + if (leftSlow == null && rightSlow == null) { + return result(true, inequality); + } + return slow( + leftSlow != null ? leftSlow : NullT.NullValue, + rightSlow != null ? rightSlow : NullT.NullValue, + equality, + inequality); + } + + private static boolean slow(Val left, Val right, EvalEq equality, EvalNe inequality) { + return NativeScalarContinuations.booleanResult( + equality != null + ? equality.evalPrepared(left, right) + : inequality.evalPrepared(left, right)); + } + + private static boolean result(boolean equal, EvalNe inequality) { + return (inequality == null) == equal; + } +} + +final class NativeLogicalAnd extends EvalAnd implements NativeBooleanCapability { + NativeLogicalAnd(long id, Interpretable left, Interpretable right) { + super(id, left, right); + } + + @Override + public boolean evalBoolean(Activation activation) { + return NativeLogical.evaluate(lhs, rhs, activation, true); + } +} + +final class NativeLogicalOr extends EvalOr implements NativeBooleanCapability { + NativeLogicalOr(long id, Interpretable left, Interpretable right) { + super(id, left, right); + } + + @Override + public boolean evalBoolean(Activation activation) { + return NativeLogical.evaluate(lhs, rhs, activation, false); + } +} + +final class NativeLogical { + private NativeLogical() {} + + @SuppressWarnings("DuplicatedCode") + static boolean evaluate( + Interpretable left, Interpretable right, Activation activation, boolean and) { + boolean leftValue = false; + Val leftSlow = null; + try { + leftValue = ((NativeBooleanCapability) left).evalBoolean(activation); + } catch (ValueSignal valueSignal) { + leftSlow = valueSignal.value; + } + if (leftSlow == null && leftValue != and) { + return !and; + } + + boolean rightValue = false; + Val rightSlow = null; + try { + rightValue = ((NativeBooleanCapability) right).evalBoolean(activation); + } catch (ValueSignal valueSignal) { + rightSlow = valueSignal.value; + } + if (rightSlow == null && rightValue != and) { + return !and; + } + if (leftSlow == null && rightSlow == null) { + return and; + } + + Val leftResult = leftSlow != null ? leftSlow : boolOf(leftValue); + Val rightResult = rightSlow != null ? rightSlow : boolOf(rightValue); + if (isUnknown(leftResult)) { + throw signal(leftResult); + } + if (isUnknown(rightResult)) { + throw signal(rightResult); + } + if (isError(leftResult)) { + throw signal(leftResult); + } + throw signal( + noSuchOverload( + leftResult, and ? Operator.LogicalAnd.id : Operator.LogicalOr.id, rightResult)); + } +} + +abstract class NativeConditional extends EvalAttr { + final NativeBooleanCapability condition; + + NativeConditional(TypeAdapter adapter, Attribute attribute, NativeBooleanCapability condition) { + super(adapter, attribute); + this.condition = condition; + } + + final boolean selectTruthy(Activation activation) { + try { + return condition.evalBoolean(activation); + } catch (ValueSignal valueSignal) { + if (isUnknown(valueSignal.value)) { + throw valueSignal; + } + if (isError(valueSignal.value)) { + throw NativeSupport.propagatedError(valueSignal.value); + } + throw signal(maybeNoSuchOverloadErr(valueSignal.value)); + } + } + + final ValueSignal selectedBranch(ValueSignal valueSignal) { + if (!isError(valueSignal.value)) { + return valueSignal; + } + return NativeSupport.propagatedError(valueSignal.value); + } +} + +final class NativeBooleanConditional extends NativeConditional implements NativeBooleanCapability { + private final NativeBooleanCapability truthy; + private final NativeBooleanCapability falsy; + + NativeBooleanConditional( + TypeAdapter adapter, + Attribute attribute, + NativeBooleanCapability condition, + NativeBooleanCapability truthy, + NativeBooleanCapability falsy) { + super(adapter, attribute, condition); + this.truthy = truthy; + this.falsy = falsy; + } + + @Override + public boolean evalBoolean(Activation activation) { + boolean selectTruthy = selectTruthy(activation); + try { + return selectTruthy ? truthy.evalBoolean(activation) : falsy.evalBoolean(activation); + } catch (ValueSignal valueSignal) { + throw selectedBranch(valueSignal); + } + } +} + +final class NativeIntConditional extends NativeConditional implements NativeIntCapability { + private final NativeIntCapability truthy; + private final NativeIntCapability falsy; + + NativeIntConditional( + TypeAdapter adapter, + Attribute attribute, + NativeBooleanCapability condition, + NativeIntCapability truthy, + NativeIntCapability falsy) { + super(adapter, attribute, condition); + this.truthy = truthy; + this.falsy = falsy; + } + + @Override + public long evalInt(Activation activation) { + boolean selectTruthy = selectTruthy(activation); + try { + return selectTruthy ? truthy.evalInt(activation) : falsy.evalInt(activation); + } catch (ValueSignal valueSignal) { + throw selectedBranch(valueSignal); + } + } +} + +final class NativeUintConditional extends NativeConditional implements NativeUintCapability { + private final NativeUintCapability truthy; + private final NativeUintCapability falsy; + + NativeUintConditional( + TypeAdapter adapter, + Attribute attribute, + NativeBooleanCapability condition, + NativeUintCapability truthy, + NativeUintCapability falsy) { + super(adapter, attribute, condition); + this.truthy = truthy; + this.falsy = falsy; + } + + @Override + public long evalUint(Activation activation) { + boolean selectTruthy = selectTruthy(activation); + try { + return selectTruthy ? truthy.evalUint(activation) : falsy.evalUint(activation); + } catch (ValueSignal valueSignal) { + throw selectedBranch(valueSignal); + } + } +} + +final class NativeDoubleConditional extends NativeConditional implements NativeDoubleCapability { + private final NativeDoubleCapability truthy; + private final NativeDoubleCapability falsy; + + NativeDoubleConditional( + TypeAdapter adapter, + Attribute attribute, + NativeBooleanCapability condition, + NativeDoubleCapability truthy, + NativeDoubleCapability falsy) { + super(adapter, attribute, condition); + this.truthy = truthy; + this.falsy = falsy; + } + + @Override + public double evalDouble(Activation activation) { + boolean selectTruthy = selectTruthy(activation); + try { + return selectTruthy ? truthy.evalDouble(activation) : falsy.evalDouble(activation); + } catch (ValueSignal valueSignal) { + throw selectedBranch(valueSignal); + } + } +} + +final class NativeStringConditional extends NativeConditional implements NativeStringCapability { + private final NativeStringCapability truthy; + private final NativeStringCapability falsy; + + NativeStringConditional( + TypeAdapter adapter, + Attribute attribute, + NativeBooleanCapability condition, + NativeStringCapability truthy, + NativeStringCapability falsy) { + super(adapter, attribute, condition); + this.truthy = truthy; + this.falsy = falsy; + } + + @Override + public String evalString(Activation activation) { + boolean selectTruthy = selectTruthy(activation); + try { + return selectTruthy ? truthy.evalString(activation) : falsy.evalString(activation); + } catch (ValueSignal valueSignal) { + throw selectedBranch(valueSignal); + } + } +} + +final class NativeNullConditional extends NativeConditional implements NativeNullCapability { + private final NativeNullCapability truthy; + private final NativeNullCapability falsy; + + NativeNullConditional( + TypeAdapter adapter, + Attribute attribute, + NativeBooleanCapability condition, + NativeNullCapability truthy, + NativeNullCapability falsy) { + super(adapter, attribute, condition); + this.truthy = truthy; + this.falsy = falsy; + } + + @Override + public void evalNull(Activation activation) { + boolean selectTruthy = selectTruthy(activation); + try { + if (selectTruthy) { + truthy.evalNull(activation); + } else { + falsy.evalNull(activation); + } + } catch (ValueSignal valueSignal) { + throw selectedBranch(valueSignal); + } + } +} + +final class NativeScalarComparison extends EvalBinary implements NativeBooleanCapability { + private final NativeScalarKind kind; + private final NativeComparison comparison; + + NativeScalarComparison( + long id, + String function, + String overload, + Interpretable left, + Interpretable right, + Overload implementation, + NativeScalarKind kind, + NativeComparison comparison) { + super(id, function, overload, left, right, implementation.operandTrait, implementation.binary); + this.kind = kind; + this.comparison = comparison; + } + + @Override + public boolean evalBoolean(Activation activation) { + return switch (kind) { + case BOOLEAN -> compareBoolean(activation); + case INT -> compareInt(activation); + case UINT -> compareUint(activation); + case DOUBLE -> compareDouble(activation); + case STRING -> compareString(activation); + case NULL -> throw new IllegalStateException("null values are not ordered"); + }; + } + + @SuppressWarnings("DuplicatedCode") + private boolean compareBoolean(Activation activation) { + boolean leftValue = false; + boolean rightValue = false; + Val leftSlow = null; + Val rightSlow = null; + try { + leftValue = ((NativeBooleanCapability) lhs).evalBoolean(activation); + } catch (ValueSignal signal) { + leftSlow = signal.value; + } + try { + rightValue = ((NativeBooleanCapability) rhs).evalBoolean(activation); + } catch (ValueSignal signal) { + rightSlow = signal.value; + } + if (leftSlow != null || rightSlow != null) { + return slowComparison( + leftSlow != null ? leftSlow : boolOf(leftValue), + rightSlow != null ? rightSlow : boolOf(rightValue)); + } + return comparison.test(Boolean.compare(leftValue, rightValue)); + } + + @SuppressWarnings("DuplicatedCode") + private boolean compareInt(Activation activation) { + long leftValue = 0L; + long rightValue = 0L; + Val leftSlow = null; + Val rightSlow = null; + try { + leftValue = ((NativeIntCapability) lhs).evalInt(activation); + } catch (ValueSignal signal) { + leftSlow = signal.value; + } + try { + rightValue = ((NativeIntCapability) rhs).evalInt(activation); + } catch (ValueSignal signal) { + rightSlow = signal.value; + } + if (leftSlow != null || rightSlow != null) { + return slowComparison( + leftSlow != null ? leftSlow : intOf(leftValue), + rightSlow != null ? rightSlow : intOf(rightValue)); + } + return comparison.test(Long.compare(leftValue, rightValue)); + } + + @SuppressWarnings("DuplicatedCode") + private boolean compareUint(Activation activation) { + long leftValue = 0L; + long rightValue = 0L; + Val leftSlow = null; + Val rightSlow = null; + try { + leftValue = ((NativeUintCapability) lhs).evalUint(activation); + } catch (ValueSignal signal) { + leftSlow = signal.value; + } + try { + rightValue = ((NativeUintCapability) rhs).evalUint(activation); + } catch (ValueSignal signal) { + rightSlow = signal.value; + } + if (leftSlow != null || rightSlow != null) { + return slowComparison( + leftSlow != null ? leftSlow : uintOf(leftValue), + rightSlow != null ? rightSlow : uintOf(rightValue)); + } + return comparison.test(Long.compareUnsigned(leftValue, rightValue)); + } + + @SuppressWarnings("DuplicatedCode") + private boolean compareDouble(Activation activation) { + double leftValue = 0.0d; + double rightValue = 0.0d; + Val leftSlow = null; + Val rightSlow = null; + try { + leftValue = ((NativeDoubleCapability) lhs).evalDouble(activation); + } catch (ValueSignal signal) { + leftSlow = signal.value; + } + try { + rightValue = ((NativeDoubleCapability) rhs).evalDouble(activation); + } catch (ValueSignal signal) { + rightSlow = signal.value; + } + if (leftSlow != null || rightSlow != null) { + return slowComparison( + leftSlow != null ? leftSlow : doubleOf(leftValue), + rightSlow != null ? rightSlow : doubleOf(rightValue)); + } + int result = leftValue == rightValue ? 0 : Double.compare(leftValue, rightValue); + return comparison.test(result); + } + + @SuppressWarnings("DuplicatedCode") + private boolean compareString(Activation activation) { + String leftValue = null; + String rightValue = null; + Val leftSlow = null; + Val rightSlow = null; + try { + leftValue = ((NativeStringCapability) lhs).evalString(activation); + } catch (ValueSignal signal) { + leftSlow = signal.value; + } + try { + rightValue = ((NativeStringCapability) rhs).evalString(activation); + } catch (ValueSignal signal) { + rightSlow = signal.value; + } + if (leftSlow != null || rightSlow != null) { + return slowComparison( + leftSlow != null ? leftSlow : stringOf(leftValue), + rightSlow != null ? rightSlow : stringOf(rightValue)); + } + String fastLeft = requireNonNull(leftValue, "native string capability returned null"); + String fastRight = requireNonNull(rightValue, "native string capability returned null"); + return comparison.test(fastLeft.compareTo(fastRight)); + } + + private boolean slowComparison(Val leftValue, Val rightValue) { + return NativeScalarContinuations.booleanResult(evalPrepared(leftValue, rightValue)); + } +} + +enum NativeArithmetic { + ADD, + SUBTRACT, + MULTIPLY, + DIVIDE, + MODULO +} + +enum NativeComparison { + LESS, + LESS_EQUALS, + GREATER, + GREATER_EQUALS; + + boolean test(int result) { + return switch (this) { + case LESS -> result < 0; + case LESS_EQUALS -> result <= 0; + case GREATER -> result > 0; + case GREATER_EQUALS -> result >= 0; + }; + } +} + +enum NativeScalarKind { + BOOLEAN, + INT, + UINT, + DOUBLE, + STRING, + NULL +} + +final class NativeScalarContinuations { + private NativeScalarContinuations() {} + + static boolean booleanResult(Val result) { + if (result instanceof BoolT) { + return result.booleanValue(); + } + throw signal(result); + } + + static long intResult(Val result) { + if (result instanceof IntT) { + return result.intValue(); + } + throw signal(result); + } + + static long uintResult(Val result) { + if (result instanceof UintT) { + return result.intValue(); + } + throw signal(result); + } + + static double doubleResult(Val result) { + if (result instanceof DoubleT) { + return result.doubleValue(); + } + throw signal(result); + } + + static String stringResult(Val result) { + if (result instanceof StringT && result.value() != null) { + return (String) result.value(); + } + throw signal(result); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/NativeSupport.java b/core/src/main/java/org/projectnessie/cel/interpreter/NativeSupport.java new file mode 100644 index 00000000..dfd70188 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/NativeSupport.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.interpreter.ValueSignal.signal; + +import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.types.Err.ErrException; +import org.projectnessie.cel.common.types.NullT; +import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; + +final class NativeSupport { + private NativeSupport() {} + + static Val adapt(TypeAdapter adapter, Object value) { + return adapter.nativeToValue(value); + } + + static long intValue(TypeAdapter adapter, Object value) { + if (value instanceof Long longValue) { + return longValue; + } + Val val = adapt(adapter, value); + return NativeScalarContinuations.intResult(val); + } + + static long uintValue(TypeAdapter adapter, Object value) { + if (value instanceof ULong unsigned) { + return unsigned.longValue(); + } + Val val = adapt(adapter, value); + return NativeScalarContinuations.uintResult(val); + } + + static boolean booleanValue(TypeAdapter adapter, Object value) { + if (value instanceof Boolean booleanValue) { + return booleanValue; + } + Val val = adapt(adapter, value); + return NativeScalarContinuations.booleanResult(val); + } + + static double doubleValue(TypeAdapter adapter, Object value) { + if (value instanceof Double doubleValue) { + return doubleValue; + } + Val val = adapt(adapter, value); + return NativeScalarContinuations.doubleResult(val); + } + + static String stringValue(TypeAdapter adapter, Object value) { + if (value instanceof String stringValue) { + return stringValue; + } + Val val = adapt(adapter, value); + if (val instanceof StringT) { + String stringValue = (String) val.value(); + if (stringValue != null) { + return stringValue; + } + } + throw signal(val); + } + + static void nullValue(TypeAdapter adapter, Object value) { + if (value == null) { + return; + } + Val val = adapt(adapter, value); + if (val == NullT.NullValue) { + return; + } + throw signal(val); + } + + static ValueSignal propagatedError(Val value) { + ErrException error = new ErrException("message: %s", value); + return signal(newErr(error, error.toString())); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/Planner.java b/core/src/main/java/org/projectnessie/cel/interpreter/Planner.java new file mode 100644 index 00000000..27b8f6b0 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Planner.java @@ -0,0 +1,2919 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.BytesT.bytesOf; +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.DurationT.durationOf; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.TimestampT.timestampOf; +import static org.projectnessie.cel.common.types.Types.boolOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; +import static org.projectnessie.cel.interpreter.AttributePattern.newPartialAttributeFactory; +import static org.projectnessie.cel.interpreter.Interpretable.newConstValue; + +import com.google.api.expr.v1alpha1.Constant; +import com.google.api.expr.v1alpha1.Expr; +import com.google.api.expr.v1alpha1.Expr.Call; +import com.google.api.expr.v1alpha1.Expr.Comprehension; +import com.google.api.expr.v1alpha1.Expr.CreateList; +import com.google.api.expr.v1alpha1.Expr.CreateStruct; +import com.google.api.expr.v1alpha1.Expr.CreateStruct.Entry; +import com.google.api.expr.v1alpha1.Expr.Ident; +import com.google.api.expr.v1alpha1.Expr.Select; +import com.google.api.expr.v1alpha1.Reference; +import com.google.api.expr.v1alpha1.Type; +import com.google.api.expr.v1alpha1.Type.PrimitiveType; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.containers.Container; +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.NullT; +import org.projectnessie.cel.common.types.Overloads; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.pb.ProtoTypeRegistry; +import org.projectnessie.cel.common.types.ref.ExactAggregateFieldProvider; +import org.projectnessie.cel.common.types.ref.ExactAggregateTypeAdapter; +import org.projectnessie.cel.common.types.ref.FieldType; +import org.projectnessie.cel.common.types.ref.StandardScalarFieldProvider; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeProvider; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Trait; +import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; +import org.projectnessie.cel.interpreter.AttributeFactory.Qualifier; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableAttribute; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableConst; +import org.projectnessie.cel.interpreter.functions.BinaryOp; +import org.projectnessie.cel.interpreter.functions.FunctionOp; +import org.projectnessie.cel.interpreter.functions.Overload; +import org.projectnessie.cel.interpreter.functions.QuaternaryOp; +import org.projectnessie.cel.interpreter.functions.QuinaryOp; +import org.projectnessie.cel.interpreter.functions.TernaryOp; +import org.projectnessie.cel.interpreter.functions.UnaryOp; + +/** Plans an {@link Interpretable} evaluation tree from a proto expression. */ +final class Planner implements InterpretablePlanner { + private final Dispatcher disp; + private final TypeProvider provider; + private final TypeAdapter adapter; + private final AttributeFactory attrFactory; + private final Container container; + private final Map refMap; + private final Map typeMap; + private final Map fieldTypes = new HashMap<>(); + private final PlanningPolicy policy; + private final InterpretableDecorator[] decorators; + private AttributeFactory partialAttrFactory; + private int planDepth; + private NativeLocalVariable nativeLocalVariable; + + Planner( + Dispatcher disp, + TypeProvider provider, + TypeAdapter adapter, + AttributeFactory attrFactory, + Container container, + Map refMap, + Map typeMap, + PlanningPolicy policy, + InterpretableDecorator[] decorators) { + this.disp = disp; + this.provider = provider; + this.adapter = adapter; + this.attrFactory = attrFactory; + this.container = container; + this.refMap = refMap; + this.typeMap = typeMap; + this.policy = policy; + this.decorators = decorators; + } + + PlanningPolicy policy() { + return policy; + } + + /** + * Plan implements the interpretablePlanner interface. This implementation of the Plan method also + * applies decorators to each Interpretable generated as part of the overall plan. Decorators are + * useful for layering functionality into the evaluation that is not natively understood by CEL, + * such as state-tracking, expression re-write, and possibly efficient thread-safe memoization of + * repeated expressions. + */ + @Override + public Interpretable plan(Expr expr) { + boolean root = planDepth == 0; + planDepth++; + try { + Interpretable result = + switch (expr.getExprKindCase()) { + case CALL_EXPR -> planCall(expr); + case IDENT_EXPR -> planIdent(expr); + case SELECT_EXPR -> planSelect(expr); + case LIST_EXPR -> planCreateList(expr); + case STRUCT_EXPR -> planCreateStruct(expr); + case COMPREHENSION_EXPR -> planComprehension(expr); + case CONST_EXPR -> planConst(expr); + default -> + throw new IllegalArgumentException( + String.format( + "unsupported expr of kind %s: '%s'", expr.getExprKindCase(), expr)); + }; + if (root && policy.nativeSpecializationPermitted() && NativeIsland.supports(result)) { + result = new NativeIsland(result, adapter); + } + return decorate(result); + } finally { + planDepth--; + } + } + + /** + * decorate applies the InterpretableDecorator functions to the given Interpretable. Both the + * Interpretable and error generated by a Plan step are accepted as arguments for convenience. + */ + Interpretable decorate(Interpretable i) { + for (InterpretableDecorator dec : decorators) { + i = dec.decorate(i); + if (i == null) { + return null; + } + } + return i; + } + + /** planIdent creates an Interpretable that resolves an identifier from an Activation. */ + Interpretable planIdent(Expr expr) { + // Establish whether the identifier is in the reference map. + Reference identRef = refMap.get(expr.getId()); + if (identRef != null) { + return planCheckedIdent(expr.getId(), identRef); + } + // Create the possible attribute list for the unresolved reference. + Ident ident = expr.getIdentExpr(); + return new EvalAttr(adapter, attrFactory.maybeAttribute(expr.getId(), ident.getName())); + } + + Interpretable planCheckedIdent(long id, Reference identRef) { + String identName = identRef.getName(); + String providerName = identName.startsWith(".") ? identName.substring(1) : identName; + // Plan a constant reference if this is the case for this simple identifier. + if (identRef.getValue() != Reference.getDefaultInstance().getValue()) { + return plan(Expr.newBuilder().setId(id).setConstExpr(identRef.getValue()).build()); + } + + // Check to see whether the type map indicates this is a type name. All types should be + // registered with the provider. + Type cType = typeMap.get(id); + if (cType != null && cType.getType() != Type.getDefaultInstance()) { + Val cVal = provider.findIdent(providerName); + if (cVal == null) { + throw new IllegalStateException( + String.format("reference to undefined type: %s", providerName)); + } + return newConstValue(id, cVal); + } + + // Otherwise, evaluate the checked top-level variable directly for ordinary plans. Decorated + // programs keep the attribute shape because custom decorators may inspect attributes. + if (decorators.length == 0) { + if (adapter instanceof ExactAggregateTypeAdapter exactAdapter && isAggregateType(cType)) { + CheckedAggregateMaterializer materializer = + new CheckedAggregateMaterializer(exactAdapter, cType); + if (nativeCertifiedHostAggregatePlanning()) { + if (cType.getTypeKindCase() == Type.TypeKindCase.LIST_TYPE) { + return new NativeExactAggregateIdent(id, identName, adapter, materializer); + } + if (cType.getTypeKindCase() == Type.TypeKindCase.MAP_TYPE) { + return new NativeExactMapIdent(id, identName, adapter, materializer); + } + } + return new EvalExactAggregateIdent(id, identName, adapter, materializer); + } + if (nativeScalarPlanning()) { + if (nativeLocalVariable != null + && nativeLocalVariable.name.equals(identName) + && nativeLocalVariable.kind == nativeKind(id)) { + return switch (nativeLocalVariable.kind) { + case BOOLEAN -> new NativeBooleanLocalIdent(id, identName, adapter); + case INT -> new NativeIntLocalIdent(id, identName, adapter); + case UINT -> new NativeUintLocalIdent(id, identName, adapter); + case DOUBLE -> new NativeDoubleLocalIdent(id, identName, adapter); + case STRING -> new NativeStringLocalIdent(id, identName, adapter); + case NULL -> new EvalIdent(id, identName, adapter); + }; + } + if (hasPrimitiveType(id, PrimitiveType.INT64)) { + return new NativeIntIdent(id, identName, adapter); + } + if (hasPrimitiveType(id, PrimitiveType.UINT64)) { + return new NativeUintIdent(id, identName, adapter); + } + if (hasPrimitiveType(id, PrimitiveType.BOOL)) { + return new NativeBooleanIdent(id, identName, adapter); + } + if (hasPrimitiveType(id, PrimitiveType.DOUBLE)) { + return new NativeDoubleIdent(id, identName, adapter); + } + if (hasPrimitiveType(id, PrimitiveType.STRING)) { + return new NativeStringIdent(id, identName, adapter); + } + if (hasNullType(id)) { + return new NativeNullIdent(id, identName, adapter); + } + if (hasListType(id)) { + return new NativeRawIdent(id, identName, adapter); + } + } + return new EvalIdent(id, identName, adapter); + } + return new EvalAttr(adapter, attrFactory.absoluteAttribute(id, identName)); + } + + /** + * planSelect creates an Interpretable with either: + * + *

    + *
  1. selects a field from a map or proto. + *
  2. creates a field presence test for a select within a has() macro. + *
  3. resolves the select expression to a namespaced identifier. + *
+ */ + Interpretable planSelect(Expr expr) { + // If the Select id appears in the reference map from the CheckedExpr proto then it is either + // a namespaced identifier or enum value. + Reference identRef = refMap.get(expr.getId()); + if (identRef != null) { + return planCheckedIdent(expr.getId(), identRef); + } + + Select sel = expr.getSelectExpr(); + // Plan the operand evaluation. + Interpretable op = + decorators.length == 0 ? planSelectOperand(sel.getOperand()) : plan(sel.getOperand()); + + // Determine the field type if this is a proto message type. + FieldType fieldType = null; + Type opType = typeMap.get(sel.getOperand().getId()); + if (opType != null && !opType.getMessageType().isEmpty()) { + FieldType ft = findFieldType(opType.getMessageType(), sel.getField()); + if (ft != null && ft.isSet != null && ft.getFrom != null) { + fieldType = ft; + } + } + + // If the Select was marked TestOnly, this is a presence test. + // + // Note: presence tests are defined for structured (e.g. proto) and dynamic values (map, json) + // as follows: + // - True if the object field has a non-default value, e.g. obj.str != "" + // - True if the dynamic value has the field defined, e.g. key in map + // + // However, presence tests are not defined for qualified identifier names with primitive + // types. + // If a string named 'a.b.c' is declared in the environment and referenced within + // `has(a.b.c)`, + // it is not clear whether has should error or follow the convention defined for structured + // values. + if (sel.getTestOnly()) { + // Return the test only eval expression. + return new EvalTestOnly(expr.getId(), asEstablished(op), stringOf(sel.getField()), fieldType); + } + // Build a qualifier. + boolean rawExactAggregateField = exactAggregateField(expr, fieldType); + Qualifier qual = + fieldType == null + ? attrFactory.newQualifier(opType, expr.getId(), sel.getField()) + : rawExactAggregateField + ? new RawExactAggregateFieldQualifier( + expr.getId(), sel.getField(), fieldType, adapter) + : fieldQualifier(expr, sel, fieldType); + if (qual == null) { + return null; + } + // Lastly, create a field selection Interpretable. + if (op instanceof InterpretableAttribute attr) { + attr.addQualifier(qual); + return specializeSelector( + expr, + opType, + fieldType, + attr, + rawExactAggregateField + ? partialRawExactSelectAttribute(sel, qual) + : partialSelectAttribute(expr, sel, opType)); + } + + InterpretableAttribute relAttr = relativeAttr(op.id(), asEstablished(op)); + if (relAttr == null) { + return null; + } + relAttr.addQualifier(qual); + return specializeSelector( + expr, + opType, + fieldType, + relAttr, + rawExactAggregateField + ? partialRawExactSelectAttribute(sel, qual) + : partialSelectAttribute(expr, sel, opType)); + } + + private boolean exactAggregateField(Expr expr, FieldType fieldType) { + return decorators.length == 0 + && fieldType != null + && isAggregateType(typeMap.get(expr.getId())) + && provider == adapter + && provider instanceof ExactAggregateFieldProvider + && adapter instanceof ExactAggregateTypeAdapter; + } + + private FieldQualifier fieldQualifier(Expr expr, Select select, FieldType fieldType) { + Type resultType = typeMap.get(expr.getId()); + CheckedAggregateMaterializer materializer = null; + if (isAggregateType(resultType) + && provider == adapter + && provider instanceof ExactAggregateFieldProvider + && adapter instanceof ExactAggregateTypeAdapter exactAdapter) { + materializer = new CheckedAggregateMaterializer(exactAdapter, resultType); + } + return new FieldQualifier(expr.getId(), select.getField(), fieldType, adapter, materializer); + } + + private Interpretable specializeScalarSelector( + Expr expr, + Type operandType, + FieldType fieldType, + InterpretableAttribute attribute, + Attribute partialAttribute) { + if (!nativeScalarPlanning() || operandType == null || partialAttribute == null) { + return attribute; + } + boolean exactMap = exactStringMapResult(expr, operandType); + boolean exactField = + fieldType != null + && provider instanceof StandardScalarFieldProvider + && !operandType.getMessageType().startsWith("google.protobuf.") + && (provider instanceof ProtoTypeRegistry + || hasPrimitiveType(expr.getId(), PrimitiveType.STRING)); + return exactMap || exactField + ? specializeScalarAttribute(expr, attribute, partialAttribute) + : attribute; + } + + private Interpretable specializeSelector( + Expr expr, + Type operandType, + FieldType fieldType, + InterpretableAttribute attribute, + Attribute partialAttribute) { + Type resultType = typeMap.get(expr.getId()); + if (exactAggregateField(expr, fieldType) + && resultType != null + && adapter instanceof ExactAggregateTypeAdapter exactAdapter) { + CheckedAggregateMaterializer materializer = + new CheckedAggregateMaterializer(exactAdapter, resultType); + if (nativeCertifiedHostAggregatePlanning() && partialAttribute != null) { + if (resultType.getTypeKindCase() == Type.TypeKindCase.LIST_TYPE) { + return new NativeExactListFieldAttr( + expr.getId(), attribute.attr(), partialAttribute, materializer); + } + if (resultType.getTypeKindCase() == Type.TypeKindCase.MAP_TYPE) { + return new NativeExactMapFieldAttr( + expr.getId(), attribute.attr(), partialAttribute, materializer); + } + } + return new EvalExactAggregateFieldAttr( + expr.getId(), attribute.attr(), partialAttribute, materializer); + } + return specializeScalarSelector(expr, operandType, fieldType, attribute, partialAttribute); + } + + private Interpretable planSelectOperand(Expr operand) { + if (operand.getExprKindCase() != Expr.ExprKindCase.IDENT_EXPR) { + return plan(operand); + } + + Reference identRef = refMap.get(operand.getId()); + if (identRef == null || identRef.getValue() != Reference.getDefaultInstance().getValue()) { + return plan(operand); + } + + Type cType = typeMap.get(operand.getId()); + if (cType != null && cType.getType() != Type.getDefaultInstance()) { + return plan(operand); + } + + return new EvalAttr( + adapter, attrFactory.absoluteAttribute(operand.getId(), identRef.getName())); + } + + private FieldType findFieldType(String messageType, String fieldName) { + String key = messageType + '\n' + fieldName; + FieldType ft = fieldTypes.get(key); + if (ft != null) { + return ft; + } + ft = provider.findFieldType(messageType, fieldName); + if (ft != null) { + fieldTypes.put(key, ft); + } + return ft; + } + + /** + * planCall creates a callable Interpretable while specializing for common functions and + * invocation patterns. Specifically, conditional operators &&, ||, ?:, and (in)equality functions + * result in optimized Interpretable values. + */ + Interpretable planCall(Expr expr) { + Call call = expr.getCallExpr(); + ResolvedFunction resolvedFunc = resolveFunction(expr); + // target, fnName, oName := p.resolveFunction(expr) + int argCount = call.getArgsCount(); + int offset = 0; + if (resolvedFunc.target != null) { + argCount++; + offset++; + } + + Interpretable[] args = new Interpretable[argCount]; + if (resolvedFunc.target != null) { + Interpretable arg = plan(resolvedFunc.target); + if (arg == null) { + return null; + } + args[0] = arg; + } + for (int i = 0; i < call.getArgsCount(); i++) { + Expr argExpr = call.getArgs(i); + Interpretable arg = plan(argExpr); + args[i + offset] = arg; + } + + // Generate specialized Interpretable operators by function name if possible. + if (resolvedFunc.fnName.equals(Operator.LogicalAnd.id)) + return planCallLogicalAnd(expr, resolvedFunc, args); + if (resolvedFunc.fnName.equals(Operator.LogicalOr.id)) + return planCallLogicalOr(expr, resolvedFunc, args); + if (resolvedFunc.fnName.equals(Operator.Conditional.id)) + return planCallConditional(expr, resolvedFunc, args); + if (resolvedFunc.fnName.equals(Operator.Equals.id)) + return planCallEqual(expr, resolvedFunc, args); + if (resolvedFunc.fnName.equals(Operator.NotEquals.id)) + return planCallNotEqual(expr, resolvedFunc, args); + if (resolvedFunc.fnName.equals(Operator.Add.id)) { + Interpretable concat = specializeListConcat(expr, resolvedFunc, args); + if (concat != null) { + return concat; + } + } + if (resolvedFunc.fnName.equals(Operator.Index.id)) + return planCallIndex(expr, resolvedFunc, args); + if (resolvedFunc.fnName.equals(Operator.In.id)) { + Interpretable mapMembership = specializeExactMapMembership(expr, resolvedFunc, args); + if (mapMembership != null) { + return mapMembership; + } + Interpretable exactSetMembership = + specializeExactScalarSetMembership(expr, resolvedFunc, args); + if (exactSetMembership != null) { + return exactSetMembership; + } + Interpretable literalMembership = + specializeStringListLiteralMembership(expr, resolvedFunc, args); + if (literalMembership != null) { + return literalMembership; + } + Interpretable foldMembership = specializeStringListFoldMembership(expr, resolvedFunc, args); + if (foldMembership != null) { + return foldMembership; + } + Interpretable membership = specializeTopLevelStringListMembership(expr, resolvedFunc, args); + if (membership != null) { + return membership; + } + } + if (resolvedFunc.fnName.equals(Overloads.Size)) { + Interpretable concatSize = specializeListConcatSize(expr, resolvedFunc, args); + if (concatSize != null) { + return concatSize; + } + Interpretable literalSize = specializeScalarListLiteralSize(expr, resolvedFunc, args); + if (literalSize != null) { + return literalSize; + } + Interpretable foldSize = specializeScalarListFoldSize(expr, resolvedFunc, args); + if (foldSize != null) { + return foldSize; + } + Interpretable sourceSize = specializeTopLevelListSize(expr, resolvedFunc, args); + if (sourceSize != null) { + return sourceSize; + } + Interpretable mapSize = specializeExactMapSize(expr, resolvedFunc, args); + if (mapSize != null) { + return mapSize; + } + } + + // Otherwise, generate Interpretable calls specialized by argument count. + Overload fnDef = resolvedFunc.implementation; + Interpretable nativeScalar = specializeStrictScalarCall(expr, resolvedFunc, args); + if (nativeScalar != null) { + return nativeScalar; + } + Interpretable[] established = asEstablished(args); + return switch (argCount) { + case 0 -> planCallZero(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef); + case 1 -> + planCallUnary(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, established); + case 2 -> + planCallBinary(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, established); + case 3 -> + planCallTernary(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, established); + case 4 -> + planCallQuaternary( + expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, established); + case 5 -> + planCallQuinary(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, established); + default -> + planCallVarArgs(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, established); + }; + } + + private Interpretable specializeListConcat( + Expr expr, ResolvedFunction resolvedFunction, Interpretable[] arguments) { + if (!nativeCertifiedHostAggregatePlanning() + || resolvedFunction.nativeDescriptor() == null + || !resolvedFunction.overloadId.equals(Overloads.AddList) + || arguments.length != 2 + || !(arguments[0] instanceof NativeListSourceCapability left) + || !(arguments[1] instanceof NativeListSourceCapability right) + || !left.exactListSource() + || !right.exactListSource()) { + return null; + } + Call call = expr.getCallExpr(); + Type resultType = typeMap.get(expr.getId()); + Type leftType = typeMap.get(call.getArgs(0).getId()); + Type rightType = typeMap.get(call.getArgs(1).getId()); + if (resultType == null + || resultType.getTypeKindCase() != Type.TypeKindCase.LIST_TYPE + || !resultType.equals(leftType) + || !resultType.equals(rightType)) { + return null; + } + return new NativeListConcat( + expr.getId(), arguments[0], arguments[1], resolvedFunction.implementation); + } + + private Interpretable specializeListConcatSize( + Expr expr, ResolvedFunction resolvedFunction, Interpretable[] arguments) { + if (!nativeCertifiedHostAggregatePlanning() + || resolvedFunction.nativeDescriptor() == null + || (!resolvedFunction.overloadId.equals(Overloads.SizeList) + && !resolvedFunction.overloadId.equals(Overloads.SizeListInst)) + || arguments.length != 1 + || !(arguments[0] instanceof NativeListConcat concat) + || !hasPrimitiveType(expr.getId(), PrimitiveType.INT64)) { + return null; + } + return new NativeListConcatSize( + expr.getId(), + resolvedFunction.fnName, + resolvedFunction.overloadId, + concat, + resolvedFunction.implementation); + } + + private Interpretable specializeExactMapSize( + Expr expr, ResolvedFunction resolvedFunction, Interpretable[] arguments) { + if (!nativeCertifiedHostAggregatePlanning() + || resolvedFunction.nativeDescriptor() == null + || (!resolvedFunction.overloadId.equals(Overloads.SizeMap) + && !resolvedFunction.overloadId.equals(Overloads.SizeMapInst)) + || arguments.length != 1 + || !(arguments[0] instanceof NativeMapSourceCapability source) + || !source.exactMapSource() + || !hasPrimitiveType(expr.getId(), PrimitiveType.INT64)) { + return null; + } + return new NativeMapSize( + expr.getId(), + resolvedFunction.fnName, + resolvedFunction.overloadId, + arguments[0], + resolvedFunction.implementation); + } + + private Interpretable specializeExactMapMembership( + Expr expr, ResolvedFunction resolvedFunction, Interpretable[] arguments) { + Call call = expr.getCallExpr(); + if (!nativeCertifiedHostAggregatePlanning() + || resolvedFunction.nativeDescriptor() == null + || !resolvedFunction.overloadId.equals(Overloads.InMap) + || call.hasTarget() + || call.getArgsCount() != 2 + || arguments.length != 2 + || !(arguments[1] instanceof NativeMapSourceCapability source) + || !source.exactMapSource() + || !hasPrimitiveType(expr.getId(), PrimitiveType.BOOL)) { + return null; + } + ExactMapKey key = exactMapKey(call.getArgs(0), arguments[0]); + return key != null + ? new NativeMapMembership( + expr.getId(), + arguments[0], + arguments[1], + key.hostValue, + key.celValue, + resolvedFunction.implementation) + : null; + } + + private Interpretable specializeStrictScalarCall( + Expr expr, ResolvedFunction resolved, Interpretable[] args) { + if (!nativeScalarPlanning() || resolved.nativeDescriptor() == null) { + return null; + } + String overload = resolved.overloadId; + Overload implementation = resolved.implementation; + if (args.length == 1) { + Interpretable argument = args[0]; + if (overload.equals(Overloads.LogicalNot) + && argument instanceof NativeBooleanCapability + && hasPrimitiveType(expr.getId(), PrimitiveType.BOOL)) { + return new NativeBooleanNot( + expr.getId(), resolved.fnName, overload, argument, implementation); + } + if (overload.equals(Overloads.NegateInt64) + && argument instanceof NativeIntCapability + && hasPrimitiveType(expr.getId(), PrimitiveType.INT64)) { + return new NativeIntNegate( + expr.getId(), resolved.fnName, overload, argument, implementation); + } + if (overload.equals(Overloads.NegateDouble) + && argument instanceof NativeDoubleCapability + && hasPrimitiveType(expr.getId(), PrimitiveType.DOUBLE)) { + return new NativeDoubleNegate( + expr.getId(), resolved.fnName, overload, argument, implementation); + } + return null; + } + if (args.length != 2) { + return null; + } + + NativeArithmetic arithmetic = arithmetic(overload); + if (arithmetic != null) { + if (args[0] instanceof NativeIntCapability + && args[1] instanceof NativeIntCapability + && hasPrimitiveType(expr.getId(), PrimitiveType.INT64)) { + if (arithmetic == NativeArithmetic.ADD) { + return new NativeIntAdd( + expr.getId(), resolved.fnName, overload, args[0], args[1], implementation); + } + return new NativeIntBinary( + expr.getId(), resolved.fnName, overload, args[0], args[1], implementation, arithmetic); + } + if (args[0] instanceof NativeDoubleCapability + && args[1] instanceof NativeDoubleCapability + && hasPrimitiveType(expr.getId(), PrimitiveType.DOUBLE)) { + return new NativeDoubleBinary( + expr.getId(), resolved.fnName, overload, args[0], args[1], implementation, arithmetic); + } + return null; + } + + if (overload.equals(Overloads.AddString) + && args[0] instanceof NativeStringCapability + && args[1] instanceof NativeStringCapability + && hasPrimitiveType(expr.getId(), PrimitiveType.STRING)) { + return new NativeStringConcat( + expr.getId(), resolved.fnName, overload, args[0], args[1], implementation); + } + + NativeComparison comparison = comparison(resolved.fnName); + NativeScalarKind kind = comparisonKind(overload); + if (comparison != null + && kind != null + && supportsComparison(kind, args[0]) + && supportsComparison(kind, args[1]) + && hasPrimitiveType(expr.getId(), PrimitiveType.BOOL)) { + return new NativeScalarComparison( + expr.getId(), + resolved.fnName, + overload, + args[0], + args[1], + implementation, + kind, + comparison); + } + return null; + } + + private static NativeArithmetic arithmetic(String overload) { + return switch (overload) { + case Overloads.AddInt64, Overloads.AddDouble -> NativeArithmetic.ADD; + case Overloads.SubtractInt64, Overloads.SubtractDouble -> NativeArithmetic.SUBTRACT; + case Overloads.MultiplyInt64, Overloads.MultiplyDouble -> NativeArithmetic.MULTIPLY; + case Overloads.DivideInt64, Overloads.DivideDouble -> NativeArithmetic.DIVIDE; + default -> overload.equals(Overloads.ModuloInt64) ? NativeArithmetic.MODULO : null; + }; + } + + private static NativeComparison comparison(String function) { + if (function.equals(Operator.Less.id)) { + return NativeComparison.LESS; + } + if (function.equals(Operator.LessEquals.id)) { + return NativeComparison.LESS_EQUALS; + } + if (function.equals(Operator.Greater.id)) { + return NativeComparison.GREATER; + } + return function.equals(Operator.GreaterEquals.id) ? NativeComparison.GREATER_EQUALS : null; + } + + private static NativeScalarKind comparisonKind(String overload) { + return switch (overload) { + case Overloads.LessBool, + Overloads.LessEqualsBool, + Overloads.GreaterBool, + Overloads.GreaterEqualsBool -> + NativeScalarKind.BOOLEAN; + case Overloads.LessInt64, + Overloads.LessEqualsInt64, + Overloads.GreaterInt64, + Overloads.GreaterEqualsInt64 -> + NativeScalarKind.INT; + case Overloads.LessUint64, + Overloads.LessEqualsUint64, + Overloads.GreaterUint64, + Overloads.GreaterEqualsUint64 -> + NativeScalarKind.UINT; + case Overloads.LessDouble, + Overloads.LessEqualsDouble, + Overloads.GreaterDouble, + Overloads.GreaterEqualsDouble -> + NativeScalarKind.DOUBLE; + case Overloads.LessString, + Overloads.LessEqualsString, + Overloads.GreaterString, + Overloads.GreaterEqualsString -> + NativeScalarKind.STRING; + default -> null; + }; + } + + private static boolean supportsComparison(NativeScalarKind kind, Interpretable argument) { + return switch (kind) { + case BOOLEAN -> argument instanceof NativeBooleanCapability; + case INT -> argument instanceof NativeIntCapability; + case UINT -> argument instanceof NativeUintCapability; + case DOUBLE -> argument instanceof NativeDoubleCapability; + case STRING -> argument instanceof NativeStringCapability; + case NULL -> false; + }; + } + + /** planCallZero generates a zero-arity callable Interpretable. */ + static Interpretable planCallZero(Expr expr, String function, String overload, Overload impl) { + if (impl == null || impl.function == null) { + throw new IllegalArgumentException(String.format("no such overload: %s()", function)); + } + return new EvalZeroArity(expr.getId(), function, overload, impl.function); + } + + /** planCallUnary generates a unary callable Interpretable. */ + static Interpretable planCallUnary( + Expr expr, String function, String overload, Overload impl, Interpretable[] args) { + UnaryOp fn = null; + Trait trait = null; + if (impl != null) { + if (impl.unary == null) { + throw new IllegalStateException(String.format("no such overload: %s(arg)", function)); + } + fn = impl.unary; + trait = impl.operandTrait; + } + return new EvalUnary(expr.getId(), function, overload, args[0], trait, fn); + } + + /** planCallBinary generates a binary callable Interpretable. */ + static Interpretable planCallBinary( + Expr expr, String function, String overload, Overload impl, Interpretable... args) { + BinaryOp fn = null; + Trait trait = null; + if (impl != null) { + if (impl.binary == null) { + throw new IllegalStateException(String.format("no such overload: %s(lhs, rhs)", function)); + } + fn = impl.binary; + trait = impl.operandTrait; + } + return new EvalBinary(expr.getId(), function, overload, args[0], args[1], trait, fn); + } + + /** planCallTernary generates a ternary or variable argument callable Interpretable. */ + Interpretable planCallTernary( + Expr expr, String function, String overload, Overload impl, Interpretable... args) { + if (impl == null) { + return new EvalReceiverVarArgs(expr.getId(), function, overload, args); + } + if (impl.ternary != null) { + TernaryOp fn = impl.ternary; + return new EvalTernary( + expr.getId(), function, overload, args[0], args[1], args[2], impl.operandTrait, fn); + } + if (impl.function != null) { + return new EvalVarArgs( + expr.getId(), function, overload, args, impl.operandTrait, impl.function); + } + throw new IllegalStateException(String.format("no such overload: %s(...)", function)); + } + + /** planCallQuaternary generates a quaternary or variable argument callable Interpretable. */ + Interpretable planCallQuaternary( + Expr expr, String function, String overload, Overload impl, Interpretable... args) { + if (impl == null) { + return new EvalReceiverVarArgs(expr.getId(), function, overload, args); + } + if (impl.quaternary != null) { + QuaternaryOp fn = impl.quaternary; + return new EvalQuaternary( + expr.getId(), + function, + overload, + args[0], + args[1], + args[2], + args[3], + impl.operandTrait, + fn); + } + if (impl.function != null) { + return new EvalVarArgs( + expr.getId(), function, overload, args, impl.operandTrait, impl.function); + } + throw new IllegalStateException(String.format("no such overload: %s(...)", function)); + } + + /** planCallQuinary generates a quinary or variable argument callable Interpretable. */ + Interpretable planCallQuinary( + Expr expr, String function, String overload, Overload impl, Interpretable... args) { + if (impl == null) { + return new EvalReceiverVarArgs(expr.getId(), function, overload, args); + } + if (impl.quinary != null) { + QuinaryOp fn = impl.quinary; + return new EvalQuinary( + expr.getId(), + function, + overload, + args[0], + args[1], + args[2], + args[3], + args[4], + impl.operandTrait, + fn); + } + if (impl.function != null) { + return new EvalVarArgs( + expr.getId(), function, overload, args, impl.operandTrait, impl.function); + } + throw new IllegalStateException(String.format("no such overload: %s(...)", function)); + } + + /** planCallVarArgs generates a variable argument callable Interpretable. */ + static Interpretable planCallVarArgs( + Expr expr, String function, String overload, Overload impl, Interpretable... args) { + if (impl == null) { + return new EvalReceiverVarArgs(expr.getId(), function, overload, args); + } + FunctionOp fn; + Trait trait; + if (impl.function == null) { + throw new IllegalStateException(String.format("no such overload: %s(...)", function)); + } + fn = impl.function; + trait = impl.operandTrait; + return new EvalVarArgs(expr.getId(), function, overload, args, trait, fn); + } + + /** planCallEqual generates an equals (==) Interpretable. */ + Interpretable planCallEqual(Expr expr, ResolvedFunction resolvedFunction, Interpretable... args) { + Interpretable exactListEquality = + specializeExactScalarListEquality(expr, resolvedFunction, args); + if (exactListEquality != null) { + return exactListEquality; + } + if (nativeScalarPlanning() + && resolvedFunction.overloadId.equals(Overloads.Equals) + && args.length == 2 + && hasPrimitiveType(expr.getId(), PrimitiveType.BOOL)) { + NativeScalarKind kind = equalityKind(args[0], args[1]); + if (kind != null) { + if (kind == NativeScalarKind.BOOLEAN) { + return new NativeBooleanEq(expr.getId(), args[0], args[1]); + } + if (kind == NativeScalarKind.INT) { + return new NativeIntEq(expr.getId(), args[0], args[1]); + } + return new NativeScalarEq(expr.getId(), args[0], args[1], kind); + } + } + Interpretable[] established = asEstablished(args); + return new EvalEq(expr.getId(), established[0], established[1]); + } + + private Interpretable specializeExactScalarListEquality( + Expr expr, ResolvedFunction resolvedFunction, Interpretable[] arguments) { + if (!resolvedFunction.overloadId.equals(Overloads.Equals)) { + return null; + } + NativeScalarKind kind = exactScalarListEqualityKind(expr, arguments); + return kind != null + ? new NativeExactListEquality(expr.getId(), arguments[0], arguments[1], kind, adapter) + : null; + } + + private Interpretable specializeExactScalarListInequality( + Expr expr, ResolvedFunction resolvedFunction, Interpretable[] arguments) { + if (!resolvedFunction.overloadId.equals(Overloads.NotEquals)) { + return null; + } + NativeScalarKind kind = exactScalarListEqualityKind(expr, arguments); + return kind != null + ? new NativeExactListInequality(expr.getId(), arguments[0], arguments[1], kind, adapter) + : null; + } + + private NativeScalarKind exactScalarListEqualityKind(Expr expr, Interpretable[] arguments) { + if (!nativeCertifiedHostAggregatePlanning() + || arguments.length != 2 + || !(arguments[0] instanceof NativeListSourceCapability left) + || !(arguments[1] instanceof NativeListSourceCapability right) + || !left.exactListSource() + || !right.exactListSource() + || !hasPrimitiveType(expr.getId(), PrimitiveType.BOOL)) { + return null; + } + Type leftType = typeMap.get(expr.getCallExpr().getArgs(0).getId()); + Type rightType = typeMap.get(expr.getCallExpr().getArgs(1).getId()); + if (leftType == null + || leftType.getTypeKindCase() != Type.TypeKindCase.LIST_TYPE + || !leftType.equals(rightType)) { + return null; + } + NativeScalarKind kind = nativeKind(leftType.getListType().getElemType()); + return kind != null && kind != NativeScalarKind.NULL ? kind : null; + } + + /** planCallNotEqual generates a not equals (!=) Interpretable. */ + Interpretable planCallNotEqual( + Expr expr, ResolvedFunction resolvedFunction, Interpretable... args) { + Interpretable exactListInequality = + specializeExactScalarListInequality(expr, resolvedFunction, args); + if (exactListInequality != null) { + return exactListInequality; + } + if (nativeScalarPlanning() + && resolvedFunction.overloadId.equals(Overloads.NotEquals) + && args.length == 2 + && hasPrimitiveType(expr.getId(), PrimitiveType.BOOL)) { + NativeScalarKind kind = equalityKind(args[0], args[1]); + if (kind != null) { + return new NativeScalarNe(expr.getId(), args[0], args[1], kind); + } + } + Interpretable[] established = asEstablished(args); + return new EvalNe(expr.getId(), established[0], established[1]); + } + + /** planCallLogicalAnd generates a logical and (&&) Interpretable. */ + Interpretable planCallLogicalAnd( + Expr expr, ResolvedFunction resolvedFunction, Interpretable... args) { + if (nativeScalarPlanning() + && resolvedFunction.overloadId.equals(Overloads.LogicalAnd) + && args.length == 2 + && args[0] instanceof NativeBooleanCapability + && args[1] instanceof NativeBooleanCapability + && hasPrimitiveType(expr.getId(), PrimitiveType.BOOL)) { + return new NativeLogicalAnd(expr.getId(), args[0], args[1]); + } + Interpretable[] established = asEstablished(args); + return new EvalAnd(expr.getId(), established[0], established[1]); + } + + /** planCallLogicalOr generates a logical or (||) Interpretable. */ + Interpretable planCallLogicalOr( + Expr expr, ResolvedFunction resolvedFunction, Interpretable... args) { + if (nativeScalarPlanning() + && resolvedFunction.overloadId.equals(Overloads.LogicalOr) + && args.length == 2 + && args[0] instanceof NativeBooleanCapability + && args[1] instanceof NativeBooleanCapability + && hasPrimitiveType(expr.getId(), PrimitiveType.BOOL)) { + return new NativeLogicalOr(expr.getId(), args[0], args[1]); + } + Interpretable[] established = asEstablished(args); + return new EvalOr(expr.getId(), established[0], established[1]); + } + + /** planCallConditional generates a conditional / ternary (c ? t : f) Interpretable. */ + Interpretable planCallConditional( + Expr expr, ResolvedFunction resolvedFunction, Interpretable... args) { + Interpretable cond = args[0]; + Interpretable t = args[1]; + Interpretable f = args[2]; + + Interpretable establishedCond = asEstablished(cond); + Interpretable establishedTruthy = asEstablished(t); + Interpretable establishedFalsy = asEstablished(f); + + Attribute tAttr; + if (establishedTruthy instanceof InterpretableAttribute truthyAttr) { + tAttr = truthyAttr.attr(); + } else { + tAttr = attrFactory.relativeAttribute(establishedTruthy.id(), establishedTruthy); + } + + Attribute fAttr; + if (establishedFalsy instanceof InterpretableAttribute falsyAttr) { + fAttr = falsyAttr.attr(); + } else { + fAttr = attrFactory.relativeAttribute(establishedFalsy.id(), establishedFalsy); + } + + Attribute attribute = + attrFactory.conditionalAttribute(expr.getId(), establishedCond, tAttr, fAttr); + if (nativeScalarPlanning() + && resolvedFunction.overloadId.equals(Overloads.Conditional) + && cond instanceof NativeBooleanCapability condition) { + if (t instanceof NativeBooleanCapability truthy + && f instanceof NativeBooleanCapability falsy + && hasPrimitiveType(expr.getId(), PrimitiveType.BOOL)) { + return new NativeBooleanConditional(adapter, attribute, condition, truthy, falsy); + } + if (t instanceof NativeIntCapability truthy + && f instanceof NativeIntCapability falsy + && hasPrimitiveType(expr.getId(), PrimitiveType.INT64)) { + return new NativeIntConditional(adapter, attribute, condition, truthy, falsy); + } + if (t instanceof NativeUintCapability truthy + && f instanceof NativeUintCapability falsy + && hasPrimitiveType(expr.getId(), PrimitiveType.UINT64)) { + return new NativeUintConditional(adapter, attribute, condition, truthy, falsy); + } + if (t instanceof NativeDoubleCapability truthy + && f instanceof NativeDoubleCapability falsy + && hasPrimitiveType(expr.getId(), PrimitiveType.DOUBLE)) { + return new NativeDoubleConditional(adapter, attribute, condition, truthy, falsy); + } + if (t instanceof NativeStringCapability truthy + && f instanceof NativeStringCapability falsy + && hasPrimitiveType(expr.getId(), PrimitiveType.STRING)) { + return new NativeStringConditional(adapter, attribute, condition, truthy, falsy); + } + if (t instanceof NativeNullCapability truthy + && f instanceof NativeNullCapability falsy + && hasNullType(expr.getId())) { + return new NativeNullConditional(adapter, attribute, condition, truthy, falsy); + } + } + return new EvalAttr(adapter, attribute); + } + + private static NativeScalarKind equalityKind(Interpretable left, Interpretable right) { + if (left instanceof NativeBooleanCapability && right instanceof NativeBooleanCapability) { + return NativeScalarKind.BOOLEAN; + } + if (left instanceof NativeIntCapability && right instanceof NativeIntCapability) { + return NativeScalarKind.INT; + } + if (left instanceof NativeUintCapability && right instanceof NativeUintCapability) { + return NativeScalarKind.UINT; + } + if (left instanceof NativeDoubleCapability && right instanceof NativeDoubleCapability) { + return NativeScalarKind.DOUBLE; + } + if (left instanceof NativeStringCapability && right instanceof NativeStringCapability) { + return NativeScalarKind.STRING; + } + return left instanceof NativeNullCapability && right instanceof NativeNullCapability + ? NativeScalarKind.NULL + : null; + } + + /** + * planCallIndex either extends an attribute with the argument to the index operation, or creates + * a relative attribute based on the return of a function call or operation. + */ + Interpretable planCallIndex(Expr expr, ResolvedFunction resolvedFunction, Interpretable... args) { + Interpretable op = args[0]; + Interpretable ind = args[1]; + InterpretableAttribute opAttr = relativeAttr(op.id(), op); + if (opAttr == null) { + return null; + } + Call call = expr.getCallExpr(); + Expr operandExpr = call.hasTarget() ? call.getTarget() : call.getArgs(0); + Type opType = typeMap.get(operandExpr.getId()); + if (ind instanceof InterpretableConst indConst) { + Qualifier qual = attrFactory.newQualifier(opType, expr.getId(), indConst.value()); + if (qual == null) { + return null; + } + opAttr.addQualifier(qual); + Interpretable mapIndex = + specializeExactMapIndex(expr, resolvedFunction, opType, op, indConst, opAttr); + if (mapIndex != opAttr) { + return mapIndex; + } + Interpretable concatIndex = + specializeListConcatIndex(expr, resolvedFunction, op, indConst, opAttr); + if (concatIndex != opAttr) { + return concatIndex; + } + if (resolvedFunction.nativeDescriptor() != null + && resolvedFunction.overloadId.equals(Overloads.IndexMap) + && exactStringMapResult(expr, opType)) { + Interpretable specializedMap = + specializeScalarAttribute( + expr, opAttr, partialIndexAttribute(expr, operandExpr, opType, indConst.value())); + if (specializedMap != opAttr) { + return specializedMap; + } + } + Interpretable specialized = + specializeScalarListLiteralIndex(expr, resolvedFunction, op, indConst, opAttr); + if (specialized != opAttr) { + return specialized; + } + specialized = specializeScalarListFoldIndex(expr, resolvedFunction, op, indConst, opAttr); + if (specialized != opAttr) { + return specialized; + } + specialized = + specializeTopLevelListIndex( + expr, resolvedFunction, operandExpr, opType, op, indConst, opAttr); + if (specialized != opAttr || !NativeIsland.supports(op)) { + return specialized; + } + InterpretableAttribute establishedOp = relativeAttr(op.id(), asEstablished(op)); + if (establishedOp == null) { + return null; + } + establishedOp.addQualifier(qual); + return establishedOp; + } + if (ind instanceof InterpretableAttribute indAttr) { + Qualifier qual = attrFactory.newQualifier(opType, expr.getId(), indAttr); + if (qual == null) { + return null; + } + InterpretableAttribute establishedOp = relativeAttr(op.id(), asEstablished(op)); + if (establishedOp == null) { + return null; + } + establishedOp.addQualifier(qual); + return establishedOp; + } + InterpretableAttribute indQual = relativeAttr(expr.getId(), asEstablished(ind)); + if (indQual == null) { + return null; + } + InterpretableAttribute establishedOp = relativeAttr(op.id(), asEstablished(op)); + if (establishedOp == null) { + return null; + } + establishedOp.addQualifier(indQual); + return specializeDynamicTopLevelListIndex( + expr, resolvedFunction, operandExpr, opType, op, ind, establishedOp); + } + + private Interpretable specializeListConcatIndex( + Expr expr, + ResolvedFunction resolvedFunction, + Interpretable operand, + InterpretableConst index, + InterpretableAttribute established) { + Call call = expr.getCallExpr(); + Expr indexExpression = call.hasTarget() ? call.getArgs(0) : call.getArgs(1); + if (!nativeCertifiedHostAggregatePlanning() + || resolvedFunction.nativeDescriptor() == null + || !resolvedFunction.overloadId.equals(Overloads.IndexList) + || !(operand instanceof NativeListConcat concat) + || indexExpression.getExprKindCase() != Expr.ExprKindCase.CONST_EXPR + || indexExpression.getConstExpr().getConstantKindCase() + != Constant.ConstantKindCase.INT64_VALUE + || !hasPrimitiveType(indexExpression.getId(), PrimitiveType.INT64)) { + return established; + } + long indexValue = index.value().intValue(); + if (indexValue < Integer.MIN_VALUE || indexValue > Integer.MAX_VALUE) { + return established; + } + int effectiveIndex = Math.toIntExact(indexValue); + if (hasPrimitiveType(expr.getId(), PrimitiveType.BOOL)) { + return new NativeBooleanListConcatIndex( + expr.getId(), adapter, established.attr(), concat, effectiveIndex); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.INT64)) { + return new NativeIntListConcatIndex( + expr.getId(), adapter, established.attr(), concat, effectiveIndex); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.UINT64)) { + return new NativeUintListConcatIndex( + expr.getId(), adapter, established.attr(), concat, effectiveIndex); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.DOUBLE)) { + return new NativeDoubleListConcatIndex( + expr.getId(), adapter, established.attr(), concat, effectiveIndex); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.STRING)) { + return new NativeStringListConcatIndex( + expr.getId(), adapter, established.attr(), concat, effectiveIndex); + } + return hasNullType(expr.getId()) + ? new NativeNullListConcatIndex( + expr.getId(), adapter, established.attr(), concat, effectiveIndex) + : established; + } + + private Interpretable specializeExactMapIndex( + Expr expr, + ResolvedFunction resolvedFunction, + Type operandType, + Interpretable operand, + InterpretableConst index, + InterpretableAttribute established) { + if (!nativeCertifiedHostAggregatePlanning() + || resolvedFunction.nativeDescriptor() == null + || !resolvedFunction.overloadId.equals(Overloads.IndexMap) + || !(operand instanceof NativeMapSourceCapability source) + || !source.exactMapSource() + || operandType == null + || operandType.getTypeKindCase() != Type.TypeKindCase.MAP_TYPE) { + return established; + } + Call call = expr.getCallExpr(); + Expr indexExpression = call.hasTarget() ? call.getArgs(0) : call.getArgs(1); + ExactMapKey key = exactMapKey(indexExpression, index); + Type resultType = typeMap.get(expr.getId()); + if (key == null + || resultType == null + || !resultType.equals(operandType.getMapType().getValueType())) { + return established; + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.BOOL)) { + return new NativeBooleanMapIndex( + expr.getId(), adapter, established.attr(), source, key.hostValue, key.celValue); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.INT64)) { + return new NativeIntMapIndex( + expr.getId(), adapter, established.attr(), source, key.hostValue, key.celValue); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.UINT64)) { + return new NativeUintMapIndex( + expr.getId(), adapter, established.attr(), source, key.hostValue, key.celValue); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.DOUBLE)) { + return new NativeDoubleMapIndex( + expr.getId(), adapter, established.attr(), source, key.hostValue, key.celValue); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.STRING)) { + return new NativeStringMapIndex( + expr.getId(), adapter, established.attr(), source, key.hostValue, key.celValue); + } + if (hasNullType(expr.getId())) { + return new NativeNullMapIndex( + expr.getId(), adapter, established.attr(), source, key.hostValue, key.celValue); + } + if (resultType.getTypeKindCase() == Type.TypeKindCase.LIST_TYPE + && adapter instanceof ExactAggregateTypeAdapter exactAdapter) { + return new NativeMapListIndex( + expr.getId(), + source, + key.hostValue, + key.celValue, + new CheckedAggregateMaterializer(exactAdapter, resultType)); + } + if (resultType.getTypeKindCase() == Type.TypeKindCase.MAP_TYPE + && adapter instanceof ExactAggregateTypeAdapter exactAdapter) { + return new NativeMapMapIndex( + expr.getId(), + source, + key.hostValue, + key.celValue, + new CheckedAggregateMaterializer(exactAdapter, resultType)); + } + return established; + } + + private ExactMapKey exactMapKey(Expr expression, Interpretable planned) { + if (!(planned instanceof InterpretableConst constant) + || expression.getExprKindCase() != Expr.ExprKindCase.CONST_EXPR) { + return null; + } + Type keyType = typeMap.get(expression.getId()); + if (keyType == null || keyType.getTypeKindCase() != Type.TypeKindCase.PRIMITIVE) { + return null; + } + return switch (keyType.getPrimitive()) { + case STRING -> + expression.getConstExpr().getConstantKindCase() == Constant.ConstantKindCase.STRING_VALUE + ? new ExactMapKey(expression.getConstExpr().getStringValue(), constant.value()) + : null; + case BOOL -> + expression.getConstExpr().getConstantKindCase() == Constant.ConstantKindCase.BOOL_VALUE + ? new ExactMapKey(expression.getConstExpr().getBoolValue(), constant.value()) + : null; + default -> null; + }; + } + + private record ExactMapKey(Object hostValue, Val celValue) {} + + private Interpretable specializeScalarAttribute( + Expr expr, InterpretableAttribute attribute, Attribute partialAttribute) { + if (partialAttribute == null) { + return attribute; + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.BOOL)) { + return new NativeBooleanAttr(expr.getId(), adapter, attribute.attr(), partialAttribute); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.INT64)) { + return new NativeIntAttr(expr.getId(), adapter, attribute.attr(), partialAttribute); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.UINT64)) { + return new NativeUintAttr(expr.getId(), adapter, attribute.attr(), partialAttribute); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.DOUBLE)) { + return new NativeDoubleAttr(expr.getId(), adapter, attribute.attr(), partialAttribute); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.STRING)) { + return new NativeStringAttr(expr.getId(), adapter, attribute.attr(), partialAttribute); + } + return hasNullType(expr.getId()) + ? new NativeNullAttr(expr.getId(), adapter, attribute.attr(), partialAttribute) + : attribute; + } + + private Attribute partialSelectAttribute(Expr expr, Select select, Type operandType) { + return partialQualifiedAttribute( + expr.getId(), select.getOperand(), operandType, select.getField()); + } + + private Attribute partialRawExactSelectAttribute(Select select, Qualifier qualifier) { + Reference reference = topLevelReference(select.getOperand()); + if (reference == null) { + return null; + } + Attribute attribute = + partialAttributeFactory() + .absoluteAttribute(select.getOperand().getId(), reference.getName()); + attribute.addQualifier(qualifier); + return attribute; + } + + private Attribute partialIndexAttribute(Expr expr, Expr operand, Type operandType, Val index) { + return partialQualifiedAttribute(expr.getId(), operand, operandType, index); + } + + private Attribute partialQualifiedAttribute( + long id, Expr operand, Type operandType, Object qualifierValue) { + Reference reference = topLevelReference(operand); + if (reference == null) { + return null; + } + AttributeFactory partial = partialAttributeFactory(); + Attribute attribute = partial.absoluteAttribute(operand.getId(), reference.getName()); + Qualifier qualifier = partial.newQualifier(operandType, id, qualifierValue); + if (qualifier == null) { + return null; + } + attribute.addQualifier(qualifier); + return attribute; + } + + private Reference topLevelReference(Expr expression) { + if (expression.getExprKindCase() != Expr.ExprKindCase.IDENT_EXPR) { + return null; + } + Reference reference = refMap.get(expression.getId()); + if (reference == null || reference.getValue() != Reference.getDefaultInstance().getValue()) { + return null; + } + Type checkedType = typeMap.get(expression.getId()); + return checkedType != null && checkedType.getType() == Type.getDefaultInstance() + ? reference + : null; + } + + private AttributeFactory partialAttributeFactory() { + if (partialAttrFactory == null) { + partialAttrFactory = newPartialAttributeFactory(container, adapter, provider); + } + return partialAttrFactory; + } + + private boolean exactStringMapResult(Expr expr, Type operandType) { + Type resultType = typeMap.get(expr.getId()); + return resultType != null + && operandType != null + && operandType.getTypeKindCase() == Type.TypeKindCase.MAP_TYPE + && operandType.getMapType().getKeyType().getPrimitive() == PrimitiveType.STRING + && resultType.equals(operandType.getMapType().getValueType()); + } + + private Interpretable specializeTopLevelListIndex( + Expr expr, + ResolvedFunction resolvedFunction, + Expr operandExpression, + Type operandType, + Interpretable operand, + InterpretableConst index, + InterpretableAttribute established) { + Call call = expr.getCallExpr(); + Expr indexExpression = call.hasTarget() ? call.getArgs(0) : call.getArgs(1); + Type resultType = typeMap.get(expr.getId()); + Type indexType = typeMap.get(indexExpression.getId()); + if (!nativeListTraversalPlanning() + || resolvedFunction.nativeDescriptor() == null + || !resolvedFunction.fnName.equals(Operator.Index.id) + || !resolvedFunction.overloadId.equals(Overloads.IndexList) + || !(operand instanceof NativeListSourceCapability raw) + || !retainedListSource(operandExpression, raw) + || indexExpression.getExprKindCase() != Expr.ExprKindCase.CONST_EXPR + || indexExpression.getConstExpr().getConstantKindCase() + != Constant.ConstantKindCase.INT64_VALUE + || indexType == null + || indexType.getTypeKindCase() != Type.TypeKindCase.PRIMITIVE + || indexType.getPrimitive() != PrimitiveType.INT64 + || operandType == null + || operandType.getTypeKindCase() != Type.TypeKindCase.LIST_TYPE + || resultType == null + || !resultType.equals(operandType.getListType().getElemType())) { + return established; + } + long indexValue = index.value().intValue(); + if (indexValue < Integer.MIN_VALUE || indexValue > Integer.MAX_VALUE) { + return established; + } + // Top-level list indexing historically resolves the raw identifier first. Under partial + // activation that makes any matching pattern yield the identifier's expression ID, including a + // qualified pattern. Keep that behavior rather than switching this family to qualified + // attribute matching during the ownership migration. + boolean directArrayAccess = raw.exactListSource() || nativePlannerOwnedAggregatePlanning(); + if (hasPrimitiveType(expr.getId(), PrimitiveType.BOOL)) { + return new NativeBooleanListIndex( + expr.getId(), adapter, established.attr(), null, raw, indexValue, directArrayAccess); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.INT64)) { + return new NativeIntListIndex( + expr.getId(), adapter, established.attr(), null, raw, indexValue, directArrayAccess); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.UINT64)) { + return new NativeUintListIndex( + expr.getId(), adapter, established.attr(), null, raw, indexValue, directArrayAccess); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.DOUBLE)) { + return new NativeDoubleListIndex( + expr.getId(), adapter, established.attr(), null, raw, indexValue, directArrayAccess); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.STRING)) { + return new NativeStringListIndex( + expr.getId(), adapter, established.attr(), null, raw, indexValue, directArrayAccess); + } + return hasNullType(expr.getId()) + ? new NativeNullListIndex( + expr.getId(), adapter, established.attr(), null, raw, indexValue, directArrayAccess) + : established; + } + + private Interpretable specializeDynamicTopLevelListIndex( + Expr expr, + ResolvedFunction resolvedFunction, + Expr operandExpression, + Type operandType, + Interpretable operand, + Interpretable index, + InterpretableAttribute established) { + Call call = expr.getCallExpr(); + Expr indexExpression = call.hasTarget() ? call.getArgs(0) : call.getArgs(1); + Type resultType = typeMap.get(expr.getId()); + Type indexType = typeMap.get(indexExpression.getId()); + if (!nativeListTraversalPlanning() + || resolvedFunction.nativeDescriptor() == null + || !resolvedFunction.fnName.equals(Operator.Index.id) + || !resolvedFunction.overloadId.equals(Overloads.IndexList) + || !(operand instanceof NativeListSourceCapability source) + || !(index instanceof NativeIntCapability nativeIndex) + || !retainedListSource(operandExpression, source) + || indexType == null + || indexType.getTypeKindCase() != Type.TypeKindCase.PRIMITIVE + || indexType.getPrimitive() != PrimitiveType.INT64 + || operandType == null + || operandType.getTypeKindCase() != Type.TypeKindCase.LIST_TYPE + || resultType == null + || !resultType.equals(operandType.getListType().getElemType())) { + return established; + } + boolean directArrayAccess = source.exactListSource() || nativePlannerOwnedAggregatePlanning(); + if (hasPrimitiveType(expr.getId(), PrimitiveType.BOOL)) { + return new NativeBooleanListIndex( + expr.getId(), adapter, established.attr(), source, nativeIndex, directArrayAccess); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.INT64)) { + return new NativeIntListIndex( + expr.getId(), adapter, established.attr(), source, nativeIndex, directArrayAccess); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.UINT64)) { + return new NativeUintListIndex( + expr.getId(), adapter, established.attr(), source, nativeIndex, directArrayAccess); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.DOUBLE)) { + return new NativeDoubleListIndex( + expr.getId(), adapter, established.attr(), source, nativeIndex, directArrayAccess); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.STRING)) { + return new NativeStringListIndex( + expr.getId(), adapter, established.attr(), source, nativeIndex, directArrayAccess); + } + return hasNullType(expr.getId()) + ? new NativeNullListIndex( + expr.getId(), adapter, established.attr(), source, nativeIndex, directArrayAccess) + : established; + } + + private Interpretable specializeScalarListLiteralIndex( + Expr expr, + ResolvedFunction resolvedFunction, + Interpretable operand, + InterpretableConst index, + InterpretableAttribute established) { + Call call = expr.getCallExpr(); + Expr indexExpression = call.hasTarget() ? call.getArgs(0) : call.getArgs(1); + if (!nativePlannerOwnedAggregatePlanning() + || resolvedFunction.nativeDescriptor() == null + || !resolvedFunction.fnName.equals(Operator.Index.id) + || !resolvedFunction.overloadId.equals(Overloads.IndexList) + || indexExpression.getExprKindCase() != Expr.ExprKindCase.CONST_EXPR + || indexExpression.getConstExpr().getConstantKindCase() + != Constant.ConstantKindCase.INT64_VALUE + || !hasPrimitiveType(indexExpression.getId(), PrimitiveType.INT64)) { + return established; + } + long indexValue = index.value().intValue(); + if (indexValue < Integer.MIN_VALUE || indexValue > Integer.MAX_VALUE) { + return established; + } + int effectiveIndex = Math.toIntExact(indexValue); + if (operand instanceof NativeBooleanListLiteralCapability list + && hasPrimitiveType(expr.getId(), PrimitiveType.BOOL)) { + return new NativeBooleanListLiteralIndex( + expr.getId(), adapter, established.attr(), list, effectiveIndex); + } + if (operand instanceof NativeIntListLiteralCapability list + && hasPrimitiveType(expr.getId(), PrimitiveType.INT64)) { + return new NativeIntListLiteralIndex( + expr.getId(), adapter, established.attr(), list, effectiveIndex); + } + if (operand instanceof NativeUintListLiteralCapability list + && hasPrimitiveType(expr.getId(), PrimitiveType.UINT64)) { + return new NativeUintListLiteralIndex( + expr.getId(), adapter, established.attr(), list, effectiveIndex); + } + if (operand instanceof NativeDoubleListLiteralCapability list + && hasPrimitiveType(expr.getId(), PrimitiveType.DOUBLE)) { + return new NativeDoubleListLiteralIndex( + expr.getId(), adapter, established.attr(), list, effectiveIndex); + } + return operand instanceof NativeStringListLiteralCapability list + && hasPrimitiveType(expr.getId(), PrimitiveType.STRING) + ? new NativeStringListLiteralIndex( + expr.getId(), adapter, established.attr(), list, effectiveIndex) + : established; + } + + private Interpretable specializeScalarListLiteralSize( + Expr expr, ResolvedFunction resolvedFunction, Interpretable[] arguments) { + if (!nativePlannerOwnedAggregatePlanning() + || resolvedFunction.nativeDescriptor() == null + || (!resolvedFunction.overloadId.equals(Overloads.SizeList) + && !resolvedFunction.overloadId.equals(Overloads.SizeListInst)) + || arguments.length != 1 + || !(arguments[0] instanceof NativeScalarListLiteralCapability) + || !hasPrimitiveType(expr.getId(), PrimitiveType.INT64)) { + return null; + } + return new NativeListLiteralSize( + expr.getId(), + resolvedFunction.fnName, + resolvedFunction.overloadId, + arguments[0], + resolvedFunction.implementation); + } + + private Interpretable specializeScalarListFoldSize( + Expr expr, ResolvedFunction resolvedFunction, Interpretable[] arguments) { + if (!nativeListTraversalPlanning() + || resolvedFunction.nativeDescriptor() == null + || (!resolvedFunction.overloadId.equals(Overloads.SizeList) + && !resolvedFunction.overloadId.equals(Overloads.SizeListInst)) + || arguments.length != 1 + || !(arguments[0] instanceof NativeScalarListFoldCapability) + || !hasPrimitiveType(expr.getId(), PrimitiveType.INT64)) { + return null; + } + return new NativeListFoldSize( + expr.getId(), + resolvedFunction.fnName, + resolvedFunction.overloadId, + arguments[0], + resolvedFunction.implementation); + } + + private Interpretable specializeTopLevelListSize( + Expr expr, ResolvedFunction resolvedFunction, Interpretable[] arguments) { + Call call = expr.getCallExpr(); + Expr operandExpression = call.hasTarget() ? call.getTarget() : call.getArgs(0); + if (!nativeListTraversalPlanning() + || resolvedFunction.nativeDescriptor() == null + || (!resolvedFunction.overloadId.equals(Overloads.SizeList) + && !resolvedFunction.overloadId.equals(Overloads.SizeListInst)) + || arguments.length != 1 + || !(arguments[0] instanceof NativeListSourceCapability source) + || !retainedListSource(operandExpression, source) + || !hasPrimitiveType(expr.getId(), PrimitiveType.INT64) + || !hasListType(operandExpression.getId())) { + return null; + } + return new NativeListSourceSize( + expr.getId(), + resolvedFunction.fnName, + resolvedFunction.overloadId, + arguments[0], + resolvedFunction.implementation, + source.exactListSource() || nativePlannerOwnedAggregatePlanning()); + } + + private Interpretable specializeScalarListFoldIndex( + Expr expr, + ResolvedFunction resolvedFunction, + Interpretable operand, + InterpretableConst index, + InterpretableAttribute established) { + Call call = expr.getCallExpr(); + Expr indexExpression = call.hasTarget() ? call.getArgs(0) : call.getArgs(1); + if (!nativeListTraversalPlanning() + || resolvedFunction.nativeDescriptor() == null + || !resolvedFunction.fnName.equals(Operator.Index.id) + || !resolvedFunction.overloadId.equals(Overloads.IndexList) + || !(operand instanceof NativeScalarListFoldCapability source) + || indexExpression.getExprKindCase() != Expr.ExprKindCase.CONST_EXPR + || indexExpression.getConstExpr().getConstantKindCase() + != Constant.ConstantKindCase.INT64_VALUE + || !hasPrimitiveType(indexExpression.getId(), PrimitiveType.INT64) + || nativeKind(typeMap.get(expr.getId())) != source.elementKind()) { + return established; + } + long indexValue = index.value().intValue(); + if (indexValue < Integer.MIN_VALUE || indexValue > Integer.MAX_VALUE) { + return established; + } + int effectiveIndex = Math.toIntExact(indexValue); + return switch (source.elementKind()) { + case BOOLEAN -> + new NativeBooleanListFoldIndex( + expr.getId(), adapter, established.attr(), source, effectiveIndex); + case INT -> + new NativeIntListFoldIndex( + expr.getId(), adapter, established.attr(), source, effectiveIndex); + case UINT -> + new NativeUintListFoldIndex( + expr.getId(), adapter, established.attr(), source, effectiveIndex); + case DOUBLE -> + new NativeDoubleListFoldIndex( + expr.getId(), adapter, established.attr(), source, effectiveIndex); + case STRING -> + new NativeStringListFoldIndex( + expr.getId(), adapter, established.attr(), source, effectiveIndex); + case NULL -> + new NativeNullListFoldIndex( + expr.getId(), adapter, established.attr(), source, effectiveIndex); + }; + } + + private Interpretable specializeStringListLiteralMembership( + Expr expr, ResolvedFunction resolvedFunction, Interpretable[] arguments) { + Call call = expr.getCallExpr(); + if (!nativePlannerOwnedAggregatePlanning() + || resolvedFunction.nativeDescriptor() == null + || !resolvedFunction.overloadId.equals(Overloads.InList) + || call.hasTarget() + || call.getArgsCount() != 2 + || arguments.length != 2 + || !(arguments[0] instanceof NativeStringCapability) + || !(arguments[1] instanceof NativeStringListLiteralCapability) + || !hasPrimitiveType(expr.getId(), PrimitiveType.BOOL) + || !hasPrimitiveType(call.getArgs(0).getId(), PrimitiveType.STRING)) { + return null; + } + return new NativeStringListLiteralMembership( + expr.getId(), arguments[0], arguments[1], resolvedFunction.implementation); + } + + private Interpretable specializeTopLevelStringListMembership( + Expr expr, ResolvedFunction resolvedFunction, Interpretable[] arguments) { + Call call = expr.getCallExpr(); + if (!nativeListTraversalPlanning() + || resolvedFunction.nativeDescriptor() == null + || !resolvedFunction.overloadId.equals(Overloads.InList) + || call.hasTarget() + || call.getArgsCount() != 2 + || arguments.length != 2 + || !(arguments[0] instanceof NativeStringCapability) + || !(arguments[1] instanceof NativeListSourceCapability source) + || !retainedListSource(call.getArgs(1), source) + || !hasPrimitiveType(expr.getId(), PrimitiveType.BOOL) + || !hasPrimitiveType(call.getArgs(0).getId(), PrimitiveType.STRING)) { + return null; + } + Type listType = typeMap.get(call.getArgs(1).getId()); + if (listType == null + || listType.getTypeKindCase() != Type.TypeKindCase.LIST_TYPE + || listType.getListType().getElemType().getPrimitive() != PrimitiveType.STRING) { + return null; + } + return new NativeStringListMembership( + expr.getId(), + arguments[0], + arguments[1], + resolvedFunction.implementation, + source.exactListSource() || nativePlannerOwnedAggregatePlanning()); + } + + private Interpretable specializeExactScalarSetMembership( + Expr expr, ResolvedFunction resolvedFunction, Interpretable[] arguments) { + Call call = expr.getCallExpr(); + if (!nativeCertifiedHostAggregatePlanning() + || resolvedFunction.nativeDescriptor() == null + || !resolvedFunction.overloadId.equals(Overloads.InList) + || call.hasTarget() + || call.getArgsCount() != 2 + || arguments.length != 2 + || !(arguments[1] instanceof NativeListSourceCapability source) + || !source.exactListSource() + || !retainedListSource(call.getArgs(1), source) + || !hasPrimitiveType(expr.getId(), PrimitiveType.BOOL)) { + return null; + } + Type listType = typeMap.get(call.getArgs(1).getId()); + if (listType == null || listType.getTypeKindCase() != Type.TypeKindCase.LIST_TYPE) { + return null; + } + NativeScalarKind kind = nativeKind(listType.getListType().getElemType()); + if (kind == null + || kind == NativeScalarKind.NULL + || nativeKind(call.getArgs(0).getId()) != kind) { + return null; + } + boolean supportedNeedle = + switch (kind) { + case BOOLEAN -> arguments[0] instanceof NativeBooleanCapability; + case INT -> arguments[0] instanceof NativeIntCapability; + case UINT -> arguments[0] instanceof NativeUintCapability; + case DOUBLE -> arguments[0] instanceof NativeDoubleCapability; + case STRING -> arguments[0] instanceof NativeStringCapability; + //noinspection DataFlowIssue + case NULL -> false; + }; + return supportedNeedle + ? new NativeExactSetMembership( + expr.getId(), arguments[0], arguments[1], resolvedFunction.implementation, kind) + : null; + } + + private Interpretable specializeStringListFoldMembership( + Expr expr, ResolvedFunction resolvedFunction, Interpretable[] arguments) { + Call call = expr.getCallExpr(); + if (!nativeListTraversalPlanning() + || resolvedFunction.nativeDescriptor() == null + || !resolvedFunction.overloadId.equals(Overloads.InList) + || call.hasTarget() + || call.getArgsCount() != 2 + || arguments.length != 2 + || !(arguments[0] instanceof NativeStringCapability) + || !(arguments[1] instanceof NativeScalarListFoldCapability source) + || source.elementKind() != NativeScalarKind.STRING + || !hasPrimitiveType(expr.getId(), PrimitiveType.BOOL) + || !hasPrimitiveType(call.getArgs(0).getId(), PrimitiveType.STRING)) { + return null; + } + Type listType = typeMap.get(call.getArgs(1).getId()); + if (listType == null + || listType.getTypeKindCase() != Type.TypeKindCase.LIST_TYPE + || listType.getListType().getElemType().getPrimitive() != PrimitiveType.STRING) { + return null; + } + return new NativeStringListFoldMembership( + expr.getId(), arguments[0], arguments[1], resolvedFunction.implementation); + } + + /** planCreateList generates a list construction Interpretable. */ + Interpretable planCreateList(Expr expr) { + CreateList list = expr.getListExpr(); + Interpretable[] elems = new Interpretable[list.getElementsCount()]; + boolean[] optionalIndices = new boolean[list.getElementsCount()]; + for (int index : list.getOptionalIndicesList()) { + optionalIndices[index] = true; + } + for (int i = 0; i < list.getElementsCount(); i++) { + Expr elem = list.getElements(i); + Interpretable elemVal = plan(elem); + if (elemVal == null) { + return null; + } + elems[i] = elemVal; + } + Interpretable nativeLiteral = specializeScalarListLiteral(expr, elems, optionalIndices); + return nativeLiteral != null + ? nativeLiteral + : new EvalList(expr.getId(), asEstablished(elems), optionalIndices, adapter); + } + + private Interpretable specializeScalarListLiteral( + Expr expr, Interpretable[] elements, boolean[] optionalIndices) { + if (!nativePlannerOwnedAggregatePlanning()) { + return null; + } + for (boolean optional : optionalIndices) { + if (optional) { + return null; + } + } + Type listType = typeMap.get(expr.getId()); + if (listType == null || listType.getTypeKindCase() != Type.TypeKindCase.LIST_TYPE) { + return null; + } + Type elementType = listType.getListType().getElemType(); + if (elementType.getTypeKindCase() != Type.TypeKindCase.PRIMITIVE) { + return null; + } + boolean constantElements = true; + for (Expr element : expr.getListExpr().getElementsList()) { + constantElements &= element.getExprKindCase() == Expr.ExprKindCase.CONST_EXPR; + } + PrimitiveType primitiveType = elementType.getPrimitive(); + if (primitiveType == PrimitiveType.BOOL && allBooleanCapabilities(elements)) { + return new NativeBooleanListLiteral(expr.getId(), elements, constantElements, adapter); + } + if (primitiveType == PrimitiveType.INT64 && allIntCapabilities(elements)) { + return new NativeIntListLiteral(expr.getId(), elements, constantElements, adapter); + } + if (primitiveType == PrimitiveType.UINT64 && allUintCapabilities(elements)) { + return new NativeUintListLiteral(expr.getId(), elements, constantElements, adapter); + } + if (primitiveType == PrimitiveType.DOUBLE && allDoubleCapabilities(elements)) { + return new NativeDoubleListLiteral(expr.getId(), elements, constantElements, adapter); + } + return primitiveType == PrimitiveType.STRING && allStringCapabilities(elements) + ? new NativeStringListLiteral(expr.getId(), elements, constantElements, adapter) + : null; + } + + private static boolean allBooleanCapabilities(Interpretable[] elements) { + for (Interpretable element : elements) { + if (!(element instanceof NativeBooleanCapability)) { + return false; + } + } + return true; + } + + private static boolean allIntCapabilities(Interpretable[] elements) { + for (Interpretable element : elements) { + if (!(element instanceof NativeIntCapability)) { + return false; + } + } + return true; + } + + private static boolean allUintCapabilities(Interpretable[] elements) { + for (Interpretable element : elements) { + if (!(element instanceof NativeUintCapability)) { + return false; + } + } + return true; + } + + private static boolean allDoubleCapabilities(Interpretable[] elements) { + for (Interpretable element : elements) { + if (!(element instanceof NativeDoubleCapability)) { + return false; + } + } + return true; + } + + private static boolean allStringCapabilities(Interpretable[] elements) { + for (Interpretable element : elements) { + if (!(element instanceof NativeStringCapability)) { + return false; + } + } + return true; + } + + /** planCreateStruct generates a map or object construction Interpretable. */ + Interpretable planCreateStruct(Expr expr) { + CreateStruct str = expr.getStructExpr(); + if (!str.getMessageName().isEmpty()) { + return planCreateObj(expr); + } + List entries = str.getEntriesList(); + Interpretable[] keys = new Interpretable[entries.size()]; + Interpretable[] vals = new Interpretable[entries.size()]; + boolean[] optionalEntries = new boolean[entries.size()]; + for (int i = 0; i < entries.size(); i++) { + Entry entry = entries.get(i); + optionalEntries[i] = entry.getOptionalEntry(); + Interpretable keyVal = plan(entry.getMapKey()); + if (keyVal == null) { + return null; + } + keys[i] = keyVal; + + Interpretable valVal = plan(entry.getValue()); + if (valVal == null) { + return null; + } + vals[i] = valVal; + } + return new EvalMap( + expr.getId(), asEstablished(keys), asEstablished(vals), optionalEntries, adapter); + } + + /** planCreateObj generates an object construction Interpretable. */ + Interpretable planCreateObj(Expr expr) { + CreateStruct obj = expr.getStructExpr(); + String typeName = resolveTypeName(obj.getMessageName()); + if (typeName == null) { + throw new IllegalStateException(String.format("unknown type: %s", obj.getMessageName())); + } + List entries = obj.getEntriesList(); + String[] fields = new String[entries.size()]; + Interpretable[] vals = new Interpretable[entries.size()]; + boolean[] optionalEntries = new boolean[entries.size()]; + for (int i = 0; i < entries.size(); i++) { + Entry entry = entries.get(i); + fields[i] = entry.getFieldKey(); + optionalEntries[i] = entry.getOptionalEntry(); + Interpretable val = plan(entry.getValue()); + if (val == null) { + return null; + } + vals[i] = val; + } + return new EvalObj( + expr.getId(), typeName, fields, asEstablished(vals), optionalEntries, provider); + } + + /** planComprehension generates an Interpretable fold operation. */ + Interpretable planComprehension(Expr expr) { + Comprehension fold = expr.getComprehensionExpr(); + Interpretable directQuantifier = planDirectQuantifier(expr, fold); + if (directQuantifier != null) { + return directQuantifier; + } + MacroMapFold macroMapFold = macroMapFold(fold); + if (macroMapFold != null) { + Interpretable iterRange = plan(fold.getIterRange()); + if (iterRange == null) { + return null; + } + Interpretable filter = null; + if (macroMapFold.filter != null) { + filter = plan(macroMapFold.filter); + if (filter == null) { + return null; + } + } + Interpretable transform = plan(macroMapFold.transform); + if (transform == null) { + return null; + } + return new EvalMapFold( + expr.getId(), + fold.getIterVar(), + fold.getIterVar2(), + asEstablished(iterRange), + asEstablished(filter), + asEstablished(transform), + adapter); + } + + MacroListFold macroListFold = macroListFold(fold); + if (macroListFold != null) { + return planMacroListFold(expr, fold, macroListFold); + } + + Interpretable accu = plan(fold.getAccuInit()); + if (accu == null) { + return null; + } + Interpretable iterRange = plan(fold.getIterRange()); + if (iterRange == null) { + return null; + } + Interpretable cond = plan(fold.getLoopCondition()); + if (cond == null) { + return null; + } + Interpretable step = plan(fold.getLoopStep()); + if (step == null) { + return null; + } + Interpretable result = plan(fold.getResult()); + if (result == null) { + return null; + } + return new EvalFold( + expr.getId(), + fold.getAccuVar(), + asEstablished(accu), + fold.getIterVar(), + fold.getIterVar2(), + asEstablished(iterRange), + asEstablished(cond), + asEstablished(step), + asEstablished(result)); + } + + private Interpretable planMacroListFold( + Expr expr, Comprehension fold, MacroListFold macroListFold) { + Expr rangeExpression = fold.getIterRange(); + Interpretable range = plan(rangeExpression); + if (range == null) { + return null; + } + + Type rangeType = typeMap.get(rangeExpression.getId()); + Type resultType = typeMap.get(expr.getId()); + NativeScalarKind inputKind = + rangeType != null && rangeType.getTypeKindCase() == Type.TypeKindCase.LIST_TYPE + ? nativeKind(rangeType.getListType().getElemType()) + : null; + NativeScalarKind outputKind = + resultType != null && resultType.getTypeKindCase() == Type.TypeKindCase.LIST_TYPE + ? nativeKind(resultType.getListType().getElemType()) + : null; + boolean candidate = + nativeListTraversalPlanning() + && fold.getIterVar2().isEmpty() + && range instanceof NativeListSourceCapability + && retainedListSource(rangeExpression, (NativeListSourceCapability) range) + && inputKind != null + && inputKind != NativeScalarKind.NULL + && outputKind != null; + + NativeLocalVariable previousLocal = nativeLocalVariable; + if (candidate) { + nativeLocalVariable = new NativeLocalVariable(fold.getIterVar(), inputKind); + } + Interpretable filter; + Interpretable transform; + try { + filter = macroListFold.filter != null ? plan(macroListFold.filter) : null; + transform = plan(macroListFold.transform); + } finally { + nativeLocalVariable = previousLocal; + } + if ((macroListFold.filter != null && filter == null) || transform == null) { + return null; + } + + Interpretable establishedRange = asEstablished(range); + Interpretable establishedFilter = asEstablished(filter); + Interpretable establishedTransform = asEstablished(transform); + if (candidate + && (filter == null || filter instanceof NativeBooleanCapability) + && supportsNativeKind(outputKind, transform)) { + return new NativeScalarListFold( + expr.getId(), + fold.getIterVar(), + establishedRange, + establishedFilter, + establishedTransform, + inputKind, + (NativeBooleanCapability) filter, + transform, + outputKind, + adapter); + } + return new EvalListFold( + expr.getId(), + fold.getIterVar(), + fold.getIterVar2(), + establishedRange, + establishedFilter, + establishedTransform, + adapter); + } + + private static boolean supportsNativeKind(NativeScalarKind kind, Interpretable interpretable) { + return switch (kind) { + case BOOLEAN -> interpretable instanceof NativeBooleanCapability; + case INT -> interpretable instanceof NativeIntCapability; + case UINT -> interpretable instanceof NativeUintCapability; + case DOUBLE -> interpretable instanceof NativeDoubleCapability; + case STRING -> interpretable instanceof NativeStringCapability; + case NULL -> interpretable instanceof NativeNullCapability; + }; + } + + private Interpretable planDirectQuantifier(Expr expr, Comprehension fold) { + if (!nativeListTraversalPlanning() + || !hasPrimitiveType(expr.getId(), PrimitiveType.BOOL) + || !fold.getIterVar2().isEmpty()) { + return null; + } + DirectQuantifierPattern pattern = directQuantifierPattern(fold); + if (pattern == null + || referencesIdent(pattern.predicate, fold.getAccuVar()) + || containsComprehension(pattern.predicate)) { + return null; + } + Expr rangeExpression = fold.getIterRange(); + Type rangeType = typeMap.get(rangeExpression.getId()); + if (rangeType == null || rangeType.getTypeKindCase() != Type.TypeKindCase.LIST_TYPE) { + return null; + } + NativeScalarKind elementKind = nativeKind(rangeType.getListType().getElemType()); + if (elementKind == null || elementKind == NativeScalarKind.NULL) { + return null; + } + + NativeLocalVariable previousLocal = nativeLocalVariable; + nativeLocalVariable = new NativeLocalVariable(fold.getIterVar(), elementKind); + Interpretable accumulatorInitial; + Interpretable range; + Interpretable condition; + Interpretable step; + Interpretable result; + try { + accumulatorInitial = plan(fold.getAccuInit()); + range = plan(rangeExpression); + condition = plan(fold.getLoopCondition()); + step = plan(fold.getLoopStep()); + result = plan(fold.getResult()); + } finally { + nativeLocalVariable = previousLocal; + } + if (accumulatorInitial == null || condition == null || step == null || result == null) { + return null; + } + + NativeBooleanCapability predicate = directQuantifierPredicate(pattern, step); + Interpretable establishedAccumulator = asEstablished(accumulatorInitial); + Interpretable establishedRange = asEstablished(range); + Interpretable establishedCondition = asEstablished(condition); + Interpretable establishedStep = asEstablished(step); + Interpretable establishedResult = asEstablished(result); + if (predicate == null || !directQuantifierGlueIsExact(pattern, fold)) { + return new EvalFold( + expr.getId(), + fold.getAccuVar(), + establishedAccumulator, + fold.getIterVar(), + fold.getIterVar2(), + establishedRange, + establishedCondition, + establishedStep, + establishedResult); + } + if (range instanceof NativeScalarListFoldCapability mappedSource + && mappedSource.elementKind() == NativeScalarKind.INT) { + if (pattern.existsOne) { + return new NativeIntMappedExistsOneFold( + expr.getId(), + fold.getAccuVar(), + establishedAccumulator, + fold.getIterVar(), + establishedRange, + establishedCondition, + establishedStep, + establishedResult, + predicate); + } + return new NativeIntMappedQuantifierFold( + expr.getId(), + fold.getAccuVar(), + establishedAccumulator, + fold.getIterVar(), + establishedRange, + establishedCondition, + establishedStep, + establishedResult, + predicate, + pattern.quantifier); + } + if (!(range instanceof NativeListSourceCapability source) + || !retainedListSource(rangeExpression, source)) { + return new EvalFold( + expr.getId(), + fold.getAccuVar(), + establishedAccumulator, + fold.getIterVar(), + fold.getIterVar2(), + establishedRange, + establishedCondition, + establishedStep, + establishedResult); + } + if (pattern.existsOne) { + return new NativeExistsOneFold( + expr.getId(), + fold.getAccuVar(), + establishedAccumulator, + fold.getIterVar(), + establishedRange, + establishedCondition, + establishedStep, + establishedResult, + elementKind, + predicate, + adapter); + } + return new NativeQuantifierFold( + expr.getId(), + fold.getAccuVar(), + establishedAccumulator, + fold.getIterVar(), + establishedRange, + establishedCondition, + establishedStep, + establishedResult, + elementKind, + predicate, + pattern.quantifier, + adapter); + } + + private static NativeBooleanCapability directQuantifierPredicate( + DirectQuantifierPattern pattern, Interpretable step) { + if (pattern.existsOne) { + return step instanceof NativeIntConditional conditional ? conditional.condition : null; + } + if (pattern.quantifier == NativeQuantifier.ALL && step instanceof NativeLogicalAnd logical) { + return logical.rhs instanceof NativeBooleanCapability predicate ? predicate : null; + } + if (pattern.quantifier == NativeQuantifier.EXISTS && step instanceof NativeLogicalOr logical) { + return logical.rhs instanceof NativeBooleanCapability predicate ? predicate : null; + } + return null; + } + + private boolean directQuantifierGlueIsExact(DirectQuantifierPattern pattern, Comprehension fold) { + if (pattern.existsOne) { + return true; + } + Expr condition = fold.getLoopCondition(); + if (!exactStandardImplementation(condition)) { + return false; + } + return pattern.quantifier != NativeQuantifier.EXISTS + || exactStandardImplementation(condition.getCallExpr().getArgs(0)); + } + + private boolean exactStandardImplementation(Expr expression) { + if (expression.getExprKindCase() != Expr.ExprKindCase.CALL_EXPR) { + return false; + } + Reference reference = refMap.get(expression.getId()); + if (reference == null || reference.getOverloadIdCount() != 1) { + return false; + } + String function = expression.getCallExpr().getFunction(); + Overload implementation = disp.findOverload(reference.getOverloadId(0)); + if (implementation == null) { + implementation = disp.findOverload(function); + } + if (implementation == null) { + return false; + } + for (Overload standard : Overload.standardOverloads()) { + if (standard == implementation && standard.operator.equals(function)) { + return true; + } + } + return false; + } + + private DirectQuantifierPattern directQuantifierPattern(Comprehension fold) { + String accumulator = fold.getAccuVar(); + Expr condition = fold.getLoopCondition(); + Expr step = fold.getLoopStep(); + if (isIdent(fold.getResult(), accumulator) + && isBoolConst(fold.getAccuInit(), true) + && matchesCheckedCall( + condition, Operator.NotStrictlyFalse.id, Overloads.NotStrictlyFalse, 1) + && isIdent(condition.getCallExpr().getArgs(0), accumulator) + && matchesCheckedCall(step, Operator.LogicalAnd.id, Overloads.LogicalAnd, 2) + && isIdent(step.getCallExpr().getArgs(0), accumulator)) { + return new DirectQuantifierPattern( + NativeQuantifier.ALL, false, step.getCallExpr().getArgs(1)); + } + if (isIdent(fold.getResult(), accumulator) + && isBoolConst(fold.getAccuInit(), false) + && matchesCheckedCall( + condition, Operator.NotStrictlyFalse.id, Overloads.NotStrictlyFalse, 1) + && matchesCheckedCall( + condition.getCallExpr().getArgs(0), Operator.LogicalNot.id, Overloads.LogicalNot, 1) + && isIdent(condition.getCallExpr().getArgs(0).getCallExpr().getArgs(0), accumulator) + && matchesCheckedCall(step, Operator.LogicalOr.id, Overloads.LogicalOr, 2) + && isIdent(step.getCallExpr().getArgs(0), accumulator)) { + return new DirectQuantifierPattern( + NativeQuantifier.EXISTS, false, step.getCallExpr().getArgs(1)); + } + if (isNonIntConst(fold.getAccuInit(), 0) + || !isBoolConst(condition, true) + || !matchesCheckedCall(fold.getResult(), Operator.Equals.id, Overloads.Equals, 2) + || !isIdent(fold.getResult().getCallExpr().getArgs(0), accumulator) + || isNonIntConst(fold.getResult().getCallExpr().getArgs(1), 1) + || !matchesCheckedCall(step, Operator.Conditional.id, Overloads.Conditional, 3)) { + return null; + } + Call conditional = step.getCallExpr(); + Expr increment = conditional.getArgs(1); + if (!matchesCheckedCall(increment, Operator.Add.id, Overloads.AddInt64, 2) + || !isIdent(increment.getCallExpr().getArgs(0), accumulator) + || isNonIntConst(increment.getCallExpr().getArgs(1), 1) + || !isIdent(conditional.getArgs(2), accumulator)) { + return null; + } + return new DirectQuantifierPattern(null, true, conditional.getArgs(0)); + } + + private boolean matchesCheckedCall( + Expr expression, String function, String overload, int argumentCount) { + if (!isCall(expression, function, argumentCount)) { + return false; + } + Reference reference = refMap.get(expression.getId()); + return reference != null + && reference.getOverloadIdCount() == 1 + && reference.getOverloadId(0).equals(overload); + } + + private static MacroMapFold macroMapFold(Comprehension fold) { + if (!isEmptyMap(fold.getAccuInit()) + || !isBoolConst(fold.getLoopCondition(), true) + || !isIdent(fold.getResult(), fold.getAccuVar())) { + return null; + } + + Expr step = fold.getLoopStep(); + Expr filter = null; + if (isCall(step, Operator.Conditional.id, 3)) { + Call conditional = step.getCallExpr(); + if (!isIdent(conditional.getArgs(2), fold.getAccuVar())) { + return null; + } + filter = conditional.getArgs(0); + step = conditional.getArgs(1); + } + + Expr transform = mapEntryValue(fold.getIterVar(), step); + if (transform == null + || referencesIdent(transform, fold.getAccuVar()) + || (filter != null && referencesIdent(filter, fold.getAccuVar()))) { + return null; + } + return new MacroMapFold(filter, transform); + } + + private MacroListFold macroListFold(Comprehension fold) { + if (!isEmptyList(fold.getAccuInit()) + || !isBoolConst(fold.getLoopCondition(), true) + || !isIdent(fold.getResult(), fold.getAccuVar())) { + return null; + } + + Expr step = fold.getLoopStep(); + Expr filter = null; + if (matchesCheckedCall(step, Operator.Conditional.id, Overloads.Conditional, 3)) { + Call conditional = step.getCallExpr(); + if (!isIdent(conditional.getArgs(2), fold.getAccuVar())) { + return null; + } + filter = conditional.getArgs(0); + step = conditional.getArgs(1); + } + + Expr transform = appendedValue(fold.getAccuVar(), step); + if (transform == null + || referencesIdent(transform, fold.getAccuVar()) + || (filter != null && referencesIdent(filter, fold.getAccuVar()))) { + return null; + } + return new MacroListFold(filter, transform); + } + + private Expr appendedValue(String accuVar, Expr step) { + if (!matchesCheckedCall(step, Operator.Add.id, Overloads.AddList, 2)) { + return null; + } + Call add = step.getCallExpr(); + if (!isIdent(add.getArgs(0), accuVar)) { + return null; + } + Expr list = add.getArgs(1); + if (list.getExprKindCase() != Expr.ExprKindCase.LIST_EXPR + || list.getListExpr().getElementsCount() != 1 + || list.getListExpr().getOptionalIndicesCount() != 0) { + return null; + } + return list.getListExpr().getElements(0); + } + + private boolean retainedListSource(Expr expression, NativeListSourceCapability source) { + return topLevelReference(expression) != null + || source instanceof NativeMapListIndex + || source instanceof NativeExactListFieldAttr; + } + + private static boolean isCall(Expr expr, String function, int argCount) { + return expr.getExprKindCase() == Expr.ExprKindCase.CALL_EXPR + && expr.getCallExpr().getFunction().equals(function) + && expr.getCallExpr().getArgsCount() == argCount + && !expr.getCallExpr().hasTarget(); + } + + private static boolean isIdent(Expr expr, String name) { + return expr.getExprKindCase() == Expr.ExprKindCase.IDENT_EXPR + && expr.getIdentExpr().getName().equals(name); + } + + private static boolean isEmptyList(Expr expr) { + return expr.getExprKindCase() == Expr.ExprKindCase.LIST_EXPR + && expr.getListExpr().getElementsCount() == 0 + && expr.getListExpr().getOptionalIndicesCount() == 0; + } + + private static boolean isEmptyMap(Expr expr) { + return expr.getExprKindCase() == Expr.ExprKindCase.STRUCT_EXPR + && expr.getStructExpr().getMessageName().isEmpty() + && expr.getStructExpr().getEntriesCount() == 0; + } + + private static Expr mapEntryValue(String keyVar, Expr step) { + if (step.getExprKindCase() != Expr.ExprKindCase.STRUCT_EXPR + || !step.getStructExpr().getMessageName().isEmpty() + || step.getStructExpr().getEntriesCount() != 1) { + return null; + } + Entry entry = step.getStructExpr().getEntries(0); + if (!isIdent(entry.getMapKey(), keyVar)) { + return null; + } + return entry.getValue(); + } + + private static boolean isBoolConst(Expr expr, boolean value) { + return expr.getExprKindCase() == Expr.ExprKindCase.CONST_EXPR + && expr.getConstExpr().getConstantKindCase() == Constant.ConstantKindCase.BOOL_VALUE + && expr.getConstExpr().getBoolValue() == value; + } + + private static boolean isNonIntConst(Expr expr, long value) { + return expr.getExprKindCase() != Expr.ExprKindCase.CONST_EXPR + || expr.getConstExpr().getConstantKindCase() != Constant.ConstantKindCase.INT64_VALUE + || expr.getConstExpr().getInt64Value() != value; + } + + private static boolean referencesIdent(Expr expr, String name) { + switch (expr.getExprKindCase()) { + case IDENT_EXPR: + return expr.getIdentExpr().getName().equals(name); + case SELECT_EXPR: + return referencesIdent(expr.getSelectExpr().getOperand(), name); + case CALL_EXPR: + Call call = expr.getCallExpr(); + if (call.hasTarget() && referencesIdent(call.getTarget(), name)) { + return true; + } + for (Expr arg : call.getArgsList()) { + if (referencesIdent(arg, name)) { + return true; + } + } + return false; + case LIST_EXPR: + for (Expr elem : expr.getListExpr().getElementsList()) { + if (referencesIdent(elem, name)) { + return true; + } + } + return false; + case STRUCT_EXPR: + for (Entry entry : expr.getStructExpr().getEntriesList()) { + if (referencesIdent(entry.getValue(), name)) { + return true; + } + } + return false; + case COMPREHENSION_EXPR: + Comprehension comprehension = expr.getComprehensionExpr(); + return referencesIdent(comprehension.getIterRange(), name) + || referencesIdent(comprehension.getAccuInit(), name) + || (!comprehension.getIterVar().equals(name) + && !comprehension.getIterVar2().equals(name) + && !comprehension.getAccuVar().equals(name) + && referencesIdent(comprehension.getLoopCondition(), name)) + || (!comprehension.getIterVar().equals(name) + && !comprehension.getIterVar2().equals(name) + && !comprehension.getAccuVar().equals(name) + && referencesIdent(comprehension.getLoopStep(), name)) + || (!comprehension.getAccuVar().equals(name) + && referencesIdent(comprehension.getResult(), name)); + default: + return false; + } + } + + private static boolean containsComprehension(Expr expr) { + return switch (expr.getExprKindCase()) { + case COMPREHENSION_EXPR -> true; + case SELECT_EXPR -> containsComprehension(expr.getSelectExpr().getOperand()); + case CALL_EXPR -> { + Expr.Call call = expr.getCallExpr(); + if (call.hasTarget() && containsComprehension(call.getTarget())) { + yield true; + } + boolean found = false; + for (Expr arg : call.getArgsList()) { + if (containsComprehension(arg)) { + found = true; + break; + } + } + yield found; + } + case LIST_EXPR -> + expr.getListExpr().getElementsList().stream().anyMatch(Planner::containsComprehension); + case STRUCT_EXPR -> + expr.getStructExpr().getEntriesList().stream() + .anyMatch(entry -> containsComprehension(entry.getValue())); + case IDENT_EXPR, CONST_EXPR, EXPRKIND_NOT_SET -> false; + }; + } + + private record MacroListFold(Expr filter, Expr transform) {} + + private record MacroMapFold(Expr filter, Expr transform) {} + + /** planConst generates a constant valued Interpretable. */ + Interpretable planConst(Expr expr) { + Val val = constValue(expr.getConstExpr()); + if (val == null) { + return null; + } + if (nativeScalarPlanning()) { + if (hasPrimitiveType(expr.getId(), PrimitiveType.INT64)) { + return new NativeIntConst(expr.getId(), val.intValue()); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.UINT64)) { + return new NativeUintConst(expr.getId(), val.intValue()); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.BOOL)) { + return new NativeBooleanConst(expr.getId(), val.booleanValue()); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.DOUBLE)) { + return new NativeDoubleConst(expr.getId(), val.doubleValue()); + } + if (hasPrimitiveType(expr.getId(), PrimitiveType.STRING)) { + return new NativeStringConst(expr.getId(), (String) val.value()); + } + if (hasNullType(expr.getId())) { + return new NativeNullConst(expr.getId()); + } + } + return newConstValue(expr.getId(), val); + } + + /** constValue converts a proto Constant value to a ref.Val. */ + @SuppressWarnings("deprecation") + static Val constValue(Constant c) { + return switch (c.getConstantKindCase()) { + case BOOL_VALUE -> boolOf(c.getBoolValue()); + case BYTES_VALUE -> bytesOf(c.getBytesValue()); + case DOUBLE_VALUE -> doubleOf(c.getDoubleValue()); + case DURATION_VALUE -> durationOf(c.getDurationValue()); + case INT64_VALUE -> intOf(c.getInt64Value()); + case NULL_VALUE -> NullT.NullValue; + case STRING_VALUE -> stringOf(c.getStringValue()); + case TIMESTAMP_VALUE -> timestampOf(c.getTimestampValue()); + case UINT64_VALUE -> uintOf(c.getUint64Value()); + default -> + throw new IllegalArgumentException( + String.format( + "unknown constant type: '%s' of kind '%s'", c, c.getConstantKindCase())); + }; + } + + /** + * resolveTypeName takes a qualified string constructed at parse time, applies the proto namespace + * resolution rules to it in a scan over possible matching types in the TypeProvider. + */ + String resolveTypeName(String typeName) { + for (String qualifiedTypeName : container.resolveCandidateNames(typeName)) { + if (provider.findType(qualifiedTypeName) != null) { + return qualifiedTypeName; + } + } + return null; + } + + static class ResolvedFunction { + final Expr target; + final String fnName; + final String overloadId; + final Overload implementation; + + ResolvedFunction(Expr target, String fnName, String overloadId, Overload implementation) { + this.target = target; + this.fnName = fnName; + this.overloadId = overloadId; + this.implementation = implementation; + } + + NativeOverloadDescriptor nativeDescriptor() { + return null; + } + } + + static final class NativeResolvedFunction extends ResolvedFunction { + private final NativeOverloadDescriptor nativeDescriptor; + + NativeResolvedFunction( + Expr target, + String fnName, + String overloadId, + Overload implementation, + NativeOverloadDescriptor nativeDescriptor) { + super(target, fnName, overloadId, implementation); + this.nativeDescriptor = nativeDescriptor; + } + + @Override + NativeOverloadDescriptor nativeDescriptor() { + return nativeDescriptor; + } + } + + /** + * resolveFunction determines the call target, function name, and overload name from a given Expr + * value. + * + *

The resolveFunction resolves ambiguities where a function may either be a receiver-style + * invocation or a qualified global function name. + * + *

    + *
  • The target expression may only consist of ident and select expressions. + *
  • The function is declared in the environment using its fully-qualified name. + *
  • The fully-qualified function name matches the string serialized target value. + *
+ */ + ResolvedFunction resolveFunction(Expr expr) { + // Note: similar logic exists within the `checker/checker.go`. If making changes here + // please consider the impact on checker.go and consolidate implementations or mirror code + // as appropriate. + Call call = expr.getCallExpr(); + Expr target = call.hasTarget() ? call.getTarget() : null; + String fnName = call.getFunction(); + + // Checked expressions always have a reference map entry, and _should_ have the fully + // qualified + // function name as the fnName value. + Reference oRef = refMap.get(expr.getId()); + if (oRef != null) { + if (oRef.getOverloadIdCount() == 1) { + return resolvedFunction(target, fnName, oRef.getOverloadId(0)); + } + // Note, this namespaced function name will not appear as a fully qualified name in ASTs + // built and stored before cel-go v0.5.0; however, this functionality did not work at all + // before the v0.5.0 release. + return resolvedFunction(target, fnName, ""); + } + + // Parse-only expressions need to handle the same logic as is normally performed at check + // time, + // but with potentially much less information. The only reliable source of information about + // which functions are configured is the dispatcher. + if (target == null) { + // If the user has a parse-only expression, then it should have been configured as such in + // the interpreter dispatcher as it may have been omitted from the checker environment. + for (String qualifiedName : container.resolveCandidateNames(fnName)) { + if (disp.findOverload(qualifiedName) != null) { + return resolvedFunction(null, qualifiedName, ""); + } + } + // It's possible that the overload was not found, but this situation is accounted for in + // the planCall phase; however, the leading dot used for denoting fully-qualified + // namespaced identifiers must be stripped, as all declarations already use fully-qualified + // names. This stripping behavior is handled automatically by the ResolveCandidateNames + // call. + return resolvedFunction(null, stripLeadingDot(fnName), ""); + } + + // Handle the situation where the function target actually indicates a qualified function + // name. + String qualifiedPrefix = toQualifiedName(target); + if (qualifiedPrefix != null) { + String maybeQualifiedName = qualifiedPrefix + "." + fnName; + for (String qualifiedName : container.resolveCandidateNames(maybeQualifiedName)) { + if (disp.findOverload(qualifiedName) != null) { + // Clear the target to ensure the proper arity is used for finding the + // implementation. + return resolvedFunction(null, qualifiedName, ""); + } + } + } + // In the default case, the function is exactly as it was advertised: a receiver call on with + // an expression-based target with the given simple function name. + return resolvedFunction(target, fnName, ""); + } + + private ResolvedFunction resolvedFunction(Expr target, String function, String overloadId) { + Overload implementation = null; + if (overloadId != null && !overloadId.isEmpty()) { + implementation = disp.findOverload(overloadId); + } + if (implementation == null) { + implementation = disp.findOverload(function); + } + NativeOverloadDescriptor nativeDescriptor = + policy.nativeSpecializationPermitted() + ? StandardNativeOverloadCatalog.find(implementation, function, overloadId) + : null; + return nativeDescriptor != null + ? new NativeResolvedFunction(target, function, overloadId, implementation, nativeDescriptor) + : new ResolvedFunction(target, function, overloadId, implementation); + } + + private Interpretable[] asEstablished(Interpretable[] arguments) { + if (!policy.nativeSpecializationPermitted()) { + return arguments; + } + for (int i = 0; i < arguments.length; i++) { + Interpretable argument = arguments[i]; + if (NativeIsland.supports(argument)) { + arguments[i] = new NativeIsland(argument, adapter); + } + } + return arguments; + } + + private Interpretable asEstablished(Interpretable argument) { + if (policy.nativeSpecializationPermitted() && NativeIsland.supports(argument)) { + return new NativeIsland(argument, adapter); + } + return argument; + } + + private boolean nativeScalarPlanning() { + return policy.nativeSpecializationPermitted() && adapter instanceof StandardScalarTypeAdapter; + } + + private boolean nativePlannerOwnedAggregatePlanning() { + return nativeScalarPlanning() + && (adapter == DefaultTypeAdapter.Instance || adapter instanceof ProtoTypeRegistry); + } + + private boolean nativeCertifiedHostAggregatePlanning() { + return nativeScalarPlanning() && adapter instanceof ExactAggregateTypeAdapter; + } + + private boolean nativeListTraversalPlanning() { + return nativePlannerOwnedAggregatePlanning() || nativeCertifiedHostAggregatePlanning(); + } + + private NativeScalarKind nativeKind(long id) { + return nativeKind(typeMap.get(id)); + } + + private static NativeScalarKind nativeKind(Type type) { + if (type == null) { + return null; + } + if (Decls.Null.equals(type)) { + return NativeScalarKind.NULL; + } + if (type.getTypeKindCase() != Type.TypeKindCase.PRIMITIVE) { + return null; + } + return switch (type.getPrimitive()) { + case BOOL -> NativeScalarKind.BOOLEAN; + case INT64 -> NativeScalarKind.INT; + case UINT64 -> NativeScalarKind.UINT; + case DOUBLE -> NativeScalarKind.DOUBLE; + case STRING -> NativeScalarKind.STRING; + default -> null; + }; + } + + private boolean hasPrimitiveType(long id, PrimitiveType primitiveType) { + Type type = typeMap.get(id); + return type != null + && type.getTypeKindCase() == Type.TypeKindCase.PRIMITIVE + && type.getPrimitive() == primitiveType; + } + + private boolean hasListType(long id) { + Type type = typeMap.get(id); + return type != null && type.getTypeKindCase() == Type.TypeKindCase.LIST_TYPE; + } + + private static boolean isAggregateType(Type type) { + if (type == null) { + return false; + } + return type.getTypeKindCase() == Type.TypeKindCase.LIST_TYPE + || type.getTypeKindCase() == Type.TypeKindCase.MAP_TYPE; + } + + private boolean hasNullType(long id) { + return org.projectnessie.cel.checker.Decls.Null.equals(typeMap.get(id)); + } + + InterpretableAttribute relativeAttr(long id, Interpretable eval) { + InterpretableAttribute eAttr; + if (eval instanceof InterpretableAttribute) { + eAttr = (InterpretableAttribute) eval; + } else { + eAttr = new EvalAttr(adapter, attrFactory.relativeAttribute(id, eval)); + } + Interpretable decAttr = decorate(eAttr); + if (decAttr == null) { + return null; + } + if (!(decAttr instanceof InterpretableAttribute)) { + throw new IllegalStateException( + String.format( + "invalid attribute decoration: %s(%s)", decAttr, decAttr.getClass().getName())); + } + eAttr = (InterpretableAttribute) decAttr; + return eAttr; + } + + /** + * toQualifiedName converts an expression AST into a qualified name if possible, with a boolean + * 'found' value that indicates if the conversion is successful. + */ + String toQualifiedName(Expr operand) { + // If the checker identified the expression as an attribute by the type-checker, then it can't + // possibly be part of qualified name in a namespace. + if (refMap.containsKey(operand.getId())) { + return ""; + } + // Since functions cannot be both namespaced and receiver functions, if the operand is not an + // qualified variable name, return the (possibly) qualified name given the expressions. + return Container.toQualifiedName(operand); + } + + String stripLeadingDot(String name) { + return name.startsWith(".") ? name.substring(1) : name; + } + + private record NativeLocalVariable(String name, NativeScalarKind kind) {} + + private record DirectQuantifierPattern( + NativeQuantifier quantifier, boolean existsOne, Expr predicate) {} +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/PlanningPolicy.java b/core/src/main/java/org/projectnessie/cel/interpreter/PlanningPolicy.java new file mode 100644 index 00000000..bd2e3975 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/PlanningPolicy.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +/** Immutable planning-time permission for native specialization. */ +enum PlanningPolicy { + ESTABLISHED_ONLY(false), + NATIVE_SPECIALIZATION_PERMITTED(true); + + private final boolean nativeSpecializationPermitted; + + PlanningPolicy(boolean nativeSpecializationPermitted) { + this.nativeSpecializationPermitted = nativeSpecializationPermitted; + } + + static PlanningPolicy nativeSpecialization(boolean permitted) { + return permitted ? NATIVE_SPECIALIZATION_PERMITTED : ESTABLISHED_ONLY; + } + + boolean nativeSpecializationPermitted() { + return nativeSpecializationPermitted; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/ResolvedValue.java b/core/src/main/java/org/projectnessie/cel/interpreter/ResolvedValue.java index 25f238d9..db8da616 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/ResolvedValue.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/ResolvedValue.java @@ -17,6 +17,8 @@ import java.util.Objects; +@SuppressWarnings("DeprecatedIsStillUsed") +@Deprecated(forRemoval = true) public final class ResolvedValue { public static final ResolvedValue NULL_VALUE = new ResolvedValue(null, true); public static final ResolvedValue ABSENT = new ResolvedValue(null, false); @@ -63,4 +65,25 @@ public int hashCode() { public String toString() { return "ResolvedValue{present=" + present + ", value=" + value + '}'; } + + static Object mapLegacy(Object o) { + if (o instanceof ResolvedValue resolvedValue) { + if (resolvedValue.present()) { + return resolvedValue.value(); + } + return ActivationFunction.ABSENT; + } + return Objects.requireNonNullElse(o, ActivationFunction.ABSENT); + } + + static ResolvedValue mapTo(Object result) { + if (result instanceof ResolvedValue) { + return (ResolvedValue) result; + } else if (result == null) { + return ResolvedValue.NULL_VALUE; + } else if (result == ActivationFunction.ABSENT) { + return ResolvedValue.ABSENT; + } + return ResolvedValue.resolvedValue(result); + } } diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/StandardNativeOverloadCatalog.java b/core/src/main/java/org/projectnessie/cel/interpreter/StandardNativeOverloadCatalog.java new file mode 100644 index 00000000..91e91e16 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/StandardNativeOverloadCatalog.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.Overloads.AddDouble; +import static org.projectnessie.cel.common.types.Overloads.AddInt64; +import static org.projectnessie.cel.common.types.Overloads.AddList; +import static org.projectnessie.cel.common.types.Overloads.AddString; +import static org.projectnessie.cel.common.types.Overloads.DivideDouble; +import static org.projectnessie.cel.common.types.Overloads.DivideInt64; +import static org.projectnessie.cel.common.types.Overloads.GreaterBool; +import static org.projectnessie.cel.common.types.Overloads.GreaterDouble; +import static org.projectnessie.cel.common.types.Overloads.GreaterEqualsBool; +import static org.projectnessie.cel.common.types.Overloads.GreaterEqualsDouble; +import static org.projectnessie.cel.common.types.Overloads.GreaterEqualsInt64; +import static org.projectnessie.cel.common.types.Overloads.GreaterEqualsString; +import static org.projectnessie.cel.common.types.Overloads.GreaterInt64; +import static org.projectnessie.cel.common.types.Overloads.GreaterString; +import static org.projectnessie.cel.common.types.Overloads.InList; +import static org.projectnessie.cel.common.types.Overloads.InMap; +import static org.projectnessie.cel.common.types.Overloads.IndexList; +import static org.projectnessie.cel.common.types.Overloads.IndexMap; +import static org.projectnessie.cel.common.types.Overloads.LessBool; +import static org.projectnessie.cel.common.types.Overloads.LessDouble; +import static org.projectnessie.cel.common.types.Overloads.LessEqualsBool; +import static org.projectnessie.cel.common.types.Overloads.LessEqualsDouble; +import static org.projectnessie.cel.common.types.Overloads.LessEqualsInt64; +import static org.projectnessie.cel.common.types.Overloads.LessEqualsString; +import static org.projectnessie.cel.common.types.Overloads.LessInt64; +import static org.projectnessie.cel.common.types.Overloads.LessString; +import static org.projectnessie.cel.common.types.Overloads.LogicalNot; +import static org.projectnessie.cel.common.types.Overloads.ModuloInt64; +import static org.projectnessie.cel.common.types.Overloads.MultiplyDouble; +import static org.projectnessie.cel.common.types.Overloads.MultiplyInt64; +import static org.projectnessie.cel.common.types.Overloads.NegateDouble; +import static org.projectnessie.cel.common.types.Overloads.NegateInt64; +import static org.projectnessie.cel.common.types.Overloads.SizeList; +import static org.projectnessie.cel.common.types.Overloads.SizeListInst; +import static org.projectnessie.cel.common.types.Overloads.SizeMap; +import static org.projectnessie.cel.common.types.Overloads.SizeMapInst; +import static org.projectnessie.cel.common.types.Overloads.SubtractDouble; +import static org.projectnessie.cel.common.types.Overloads.SubtractInt64; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.interpreter.functions.Overload; + +/** Exact standard-overload provenance used only while planning checked expressions. */ +final class StandardNativeOverloadCatalog { + private StandardNativeOverloadCatalog() {} + + static NativeOverloadDescriptor find( + Overload implementation, String function, String overloadId) { + if (implementation == null || function == null || overloadId == null) { + return null; + } + for (NativeOverloadDescriptor descriptor : descriptors(implementation)) { + if (descriptor.matches(function, overloadId)) { + return descriptor; + } + } + return null; + } + + static List descriptors(Overload implementation) { + List descriptors = Holder.BY_IMPLEMENTATION.get(implementation); + return descriptors != null ? descriptors : List.of(); + } + + private static final class Holder { + private static final Map> BY_IMPLEMENTATION = + createCatalog(); + } + + private static Map> createCatalog() { + Map implementations = new HashMap<>(); + for (Overload implementation : Overload.standardOverloads()) { + Overload previous = implementations.put(implementation.operator, implementation); + if (previous != null) { + throw new IllegalStateException( + "duplicate standard overload implementation: " + implementation.operator); + } + } + + IdentityHashMap> mutable = new IdentityHashMap<>(); + bind(mutable, implementations, Operator.LogicalNot.id, LogicalNot); + bind(mutable, implementations, Operator.Negate.id, NegateInt64, NegateDouble); + bind(mutable, implementations, Operator.Add.id, AddInt64, AddDouble, AddString, AddList); + bind(mutable, implementations, Operator.Subtract.id, SubtractInt64, SubtractDouble); + bind(mutable, implementations, Operator.Multiply.id, MultiplyInt64, MultiplyDouble); + bind(mutable, implementations, Operator.Divide.id, DivideInt64, DivideDouble); + bind(mutable, implementations, Operator.Modulo.id, ModuloInt64); + bind(mutable, implementations, Operator.Less.id, LessBool, LessInt64, LessDouble, LessString); + bind( + mutable, + implementations, + Operator.LessEquals.id, + LessEqualsBool, + LessEqualsInt64, + LessEqualsDouble, + LessEqualsString); + bind( + mutable, + implementations, + Operator.Greater.id, + GreaterBool, + GreaterInt64, + GreaterDouble, + GreaterString); + bind( + mutable, + implementations, + Operator.GreaterEquals.id, + GreaterEqualsBool, + GreaterEqualsInt64, + GreaterEqualsDouble, + GreaterEqualsString); + bind(mutable, implementations, Operator.Index.id, IndexList, IndexMap); + bind(mutable, implementations, Operator.In.id, InList, InMap); + bind( + mutable, + implementations, + org.projectnessie.cel.common.types.Overloads.Size, + SizeList, + SizeListInst, + SizeMap, + SizeMapInst); + + IdentityHashMap> immutable = + new IdentityHashMap<>(mutable.size()); + mutable.forEach( + (implementation, descriptors) -> immutable.put(implementation, List.copyOf(descriptors))); + return immutable; + } + + private static void bind( + IdentityHashMap> catalog, + Map implementations, + String function, + String... overloadIds) { + Overload implementation = implementations.get(function); + if (implementation == null) { + throw new IllegalStateException("missing standard overload implementation: " + function); + } + List descriptors = + catalog.computeIfAbsent(implementation, ignored -> new ArrayList<>()); + for (String overloadId : overloadIds) { + descriptors.add(new NativeOverloadDescriptor(function, overloadId)); + } + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/ValueSignal.java b/core/src/main/java/org/projectnessie/cel/interpreter/ValueSignal.java new file mode 100644 index 00000000..28f8b418 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/ValueSignal.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static java.util.Objects.requireNonNull; + +import org.projectnessie.cel.common.types.ref.Val; + +/** Stackless transport for an already produced CEL value inside a typed evaluation island. */ +final class ValueSignal extends RuntimeException { + final Val value; + + ValueSignal(Val value) { + super(null, null, false, false); + this.value = requireNonNull(value, "value"); + } + + static ValueSignal signal(Val value) { + return new ValueSignal(value); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/functions/Overload.java b/core/src/main/java/org/projectnessie/cel/interpreter/functions/Overload.java index 92b6596e..71498da2 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/functions/Overload.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/functions/Overload.java @@ -52,8 +52,8 @@ /** * Overload defines a named overload of a function, indicating an operand trait which must be - * present on the first argument to the overload as well as one of either a unary, binary, or - * function implementation. + * present on the first argument to the overload as well as one or more fixed-arity or generic + * function implementations. * *

The majority of operators within the expression language are unary or binary and the * specializations simplify the call contract for implementers of types with operator overloads. Any @@ -75,6 +75,15 @@ public final class Overload { /** Binary defines the overload with a BinaryOp implementation. May be nil. */ public final BinaryOp binary; + /** Ternary defines the overload with a TernaryOp implementation. May be nil. */ + public final TernaryOp ternary; + + /** Quaternary defines the overload with a QuaternaryOp implementation. May be nil. */ + public final QuaternaryOp quaternary; + + /** Quinary defines the overload with a QuinaryOp implementation. May be nil. */ + public final QuinaryOp quinary; + /** Function defines the overload with a FunctionOp implementation. May be nil. */ public final FunctionOp function; @@ -91,7 +100,7 @@ public static Overload unary(Operator operator, Trait trait, UnaryOp op) { } public static Overload unary(String operator, Trait trait, UnaryOp op) { - return new Overload(operator, trait, op, null, null); + return new Overload(operator, trait, op, null, null, null, null, null); } public static Overload binary(Operator operator, BinaryOp op) { @@ -107,7 +116,31 @@ public static Overload binary(Operator operator, Trait trait, BinaryOp op) { } public static Overload binary(String operator, Trait trait, BinaryOp op) { - return new Overload(operator, trait, null, op, null); + return new Overload(operator, trait, null, op, null, null, null, null); + } + + public static Overload ternary(String operator, TernaryOp op) { + return ternary(operator, null, op); + } + + public static Overload ternary(String operator, Trait trait, TernaryOp op) { + return new Overload(operator, trait, null, null, op, null, null, null); + } + + public static Overload quaternary(String operator, QuaternaryOp op) { + return quaternary(operator, null, op); + } + + public static Overload quaternary(String operator, Trait trait, QuaternaryOp op) { + return new Overload(operator, trait, null, null, null, op, null, null); + } + + public static Overload quinary(String operator, QuinaryOp op) { + return quinary(operator, null, op); + } + + public static Overload quinary(String operator, Trait trait, QuinaryOp op) { + return new Overload(operator, trait, null, null, null, null, op, null); } public static Overload function(String operator, FunctionOp op) { @@ -115,20 +148,63 @@ public static Overload function(String operator, FunctionOp op) { } public static Overload function(String operator, Trait trait, FunctionOp op) { - return new Overload(operator, trait, null, null, op); + return new Overload(operator, trait, null, null, null, null, null, op); } public static Overload overload( String operator, Trait trait, UnaryOp unary, BinaryOp binary, FunctionOp function) { - return new Overload(operator, trait, unary, binary, function); + return new Overload(operator, trait, unary, binary, null, null, null, function); + } + + public static Overload overload( + String operator, + Trait trait, + UnaryOp unary, + BinaryOp binary, + TernaryOp ternary, + FunctionOp function) { + return new Overload(operator, trait, unary, binary, ternary, null, null, function); + } + + public static Overload overload( + String operator, + Trait trait, + UnaryOp unary, + BinaryOp binary, + TernaryOp ternary, + QuaternaryOp quaternary, + FunctionOp function) { + return new Overload(operator, trait, unary, binary, ternary, quaternary, null, function); + } + + public static Overload overload( + String operator, + Trait trait, + UnaryOp unary, + BinaryOp binary, + TernaryOp ternary, + QuaternaryOp quaternary, + QuinaryOp quinary, + FunctionOp function) { + return new Overload(operator, trait, unary, binary, ternary, quaternary, quinary, function); } private Overload( - String operator, Trait operandTrait, UnaryOp unary, BinaryOp binary, FunctionOp function) { + String operator, + Trait operandTrait, + UnaryOp unary, + BinaryOp binary, + TernaryOp ternary, + QuaternaryOp quaternary, + QuinaryOp quinary, + FunctionOp function) { this.operator = operator; this.operandTrait = operandTrait; this.unary = unary; this.binary = binary; + this.ternary = ternary; + this.quaternary = quaternary; + this.quinary = quinary; this.function = function; } @@ -143,7 +219,16 @@ public String toString() { if (binary != null) { sb.append(", binary"); } - if (binary != null) { + if (ternary != null) { + sb.append(", ternary"); + } + if (quaternary != null) { + sb.append(", quaternary"); + } + if (quinary != null) { + sb.append(", quinary"); + } + if (function != null) { sb.append(", function"); } sb.append('}'); @@ -152,6 +237,14 @@ public String toString() { /** StandardOverloads returns the definitions of the built-in overloads. */ public static Overload[] standardOverloads() { + return StandardOverloadsHolder.OVERLOADS.clone(); + } + + private static final class StandardOverloadsHolder { + private static final Overload[] OVERLOADS = createStandardOverloads(); + } + + private static Overload[] createStandardOverloads() { return new Overload[] { // Logical not (!a) unary( diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/functions/QuaternaryOp.java b/core/src/main/java/org/projectnessie/cel/interpreter/functions/QuaternaryOp.java new file mode 100644 index 00000000..d7793d35 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/functions/QuaternaryOp.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter.functions; + +import org.projectnessie.cel.common.types.ref.Val; + +/** QuaternaryOp is a function that takes four values and produces an output. */ +@FunctionalInterface +public interface QuaternaryOp { + Val invoke(Val first, Val second, Val third, Val fourth); +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/functions/QuinaryOp.java b/core/src/main/java/org/projectnessie/cel/interpreter/functions/QuinaryOp.java new file mode 100644 index 00000000..c06cb22b --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/functions/QuinaryOp.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter.functions; + +import org.projectnessie.cel.common.types.ref.Val; + +/** QuinaryOp is a function that takes five values and produces an output. */ +@FunctionalInterface +public interface QuinaryOp { + Val invoke(Val first, Val second, Val third, Val fourth, Val fifth); +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/functions/TernaryOp.java b/core/src/main/java/org/projectnessie/cel/interpreter/functions/TernaryOp.java new file mode 100644 index 00000000..d5d33ec9 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/functions/TernaryOp.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter.functions; + +import org.projectnessie.cel.common.types.ref.Val; + +/** TernaryOp is a function that takes three values and produces an output. */ +@FunctionalInterface +public interface TernaryOp { + Val invoke(Val first, Val second, Val third); +} diff --git a/core/src/main/java/org/projectnessie/cel/parser/CelExprBuilder.java b/core/src/main/java/org/projectnessie/cel/parser/CelExprBuilder.java new file mode 100644 index 00000000..3d88b07d --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/parser/CelExprBuilder.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.parser; + +import com.google.api.expr.v1alpha1.Expr; +import org.projectnessie.cel.common.operators.Operator; + +public interface CelExprBuilder { + Expr visitExpr(Node node); + + Expr visitBalanced(Node node, Operator operator); + + Expr visitBinary(Node node); + + Expr visitUnary(Node node); + + Expr visitMember(Node node); + + Expr visitPrimary(Node node); + + Expr visitLiteral(Node node); + + Expr visitIdentifier(Token token); +} diff --git a/core/src/main/java/org/projectnessie/cel/parser/ParseError.java b/core/src/main/java/org/projectnessie/cel/parser/CelExprNode.java similarity index 62% rename from core/src/main/java/org/projectnessie/cel/parser/ParseError.java rename to core/src/main/java/org/projectnessie/cel/parser/CelExprNode.java index 16103c77..2abb98a5 100644 --- a/core/src/main/java/org/projectnessie/cel/parser/ParseError.java +++ b/core/src/main/java/org/projectnessie/cel/parser/CelExprNode.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Authors of CEL-Java + * Copyright (C) 2026 The Authors of CEL-Java * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,17 +15,8 @@ */ package org.projectnessie.cel.parser; -import org.projectnessie.cel.common.Location; +import com.google.api.expr.v1alpha1.Expr; -public final class ParseError extends RuntimeException { - private final Location location; - - public ParseError(Location location, String message) { - super(message); - this.location = location; - } - - public Location getLocation() { - return location; - } +public interface CelExprNode { + Expr toCelExpr(CelExprBuilder builder); } diff --git a/core/src/main/java/org/projectnessie/cel/parser/ExprHelper.java b/core/src/main/java/org/projectnessie/cel/parser/ExprHelper.java index 286e3e32..5a24fd85 100644 --- a/core/src/main/java/org/projectnessie/cel/parser/ExprHelper.java +++ b/core/src/main/java/org/projectnessie/cel/parser/ExprHelper.java @@ -40,6 +40,9 @@ public interface ExprHelper { /** LiteralInt creates an Expr value for an int literal. */ Expr literalInt(long value); + /** LiteralNull creates an Expr value for a null literal. */ + Expr literalNull(); + /** LiteralString creates am Expr value for a string literal. */ Expr literalString(String value); @@ -100,6 +103,17 @@ Expr fold( Expr step, Expr result); + /** Fold creates a fold comprehension instruction with two iteration variables. */ + Expr fold( + String iterVar, + String iterVar2, + Expr iterRange, + String accuVar, + Expr accuInit, + Expr condition, + Expr step, + Expr result); + /** Ident creates an identifier Expr value. */ Expr ident(String name); diff --git a/core/src/main/java/org/projectnessie/cel/parser/ExprHelperImpl.java b/core/src/main/java/org/projectnessie/cel/parser/ExprHelperImpl.java index 457a576f..0c39b695 100644 --- a/core/src/main/java/org/projectnessie/cel/parser/ExprHelperImpl.java +++ b/core/src/main/java/org/projectnessie/cel/parser/ExprHelperImpl.java @@ -60,6 +60,12 @@ public Expr literalInt(long value) { return parserHelper.newLiteralInt(nextMacroID(), value); } + // LiteralNull implements the ExprHelper interface method. + @Override + public Expr literalNull() { + return parserHelper.newLiteralNull(nextMacroID()); + } + // LiteralString implements the ExprHelper interface method. @Override public Expr literalString(String value) { @@ -121,6 +127,20 @@ public Expr fold( nextMacroID(), iterVar, iterRange, accuVar, accuInit, condition, step, result); } + @Override + public Expr fold( + String iterVar, + String iterVar2, + Expr iterRange, + String accuVar, + Expr accuInit, + Expr condition, + Expr step, + Expr result) { + return parserHelper.newComprehension( + nextMacroID(), iterVar, iterVar2, iterRange, accuVar, accuInit, condition, step, result); + } + // Ident implements the ExprHelper interface method. @Override public Expr ident(String name) { diff --git a/core/src/main/java/org/projectnessie/cel/parser/Helper.java b/core/src/main/java/org/projectnessie/cel/parser/Helper.java index edea6552..bad63c29 100644 --- a/core/src/main/java/org/projectnessie/cel/parser/Helper.java +++ b/core/src/main/java/org/projectnessie/cel/parser/Helper.java @@ -27,6 +27,7 @@ import com.google.api.expr.v1alpha1.Expr.Select; import com.google.api.expr.v1alpha1.SourceInfo; import com.google.protobuf.ByteString; +import com.google.protobuf.NullValue; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -35,8 +36,6 @@ import org.agrona.collections.LongArrayList; import org.projectnessie.cel.common.Location; import org.projectnessie.cel.common.Source; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.ParserRuleContext; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.Token; final class Helper { private final Source source; @@ -77,6 +76,10 @@ Expr newLiteralInt(Object ctx, long value) { return newLiteral(ctx, Constant.newBuilder().setInt64Value(value)); } + Expr newLiteralNull(Object ctx) { + return newLiteral(ctx, Constant.newBuilder().setNullValue(NullValue.NULL_VALUE)); + } + Expr newLiteralUint(Object ctx, long value) { return newLiteral(ctx, Constant.newBuilder().setUint64Value(value)); } @@ -118,8 +121,13 @@ Expr newReceiverCall(Object ctx, String function, Expr target, List args) } Expr newList(Object ctx, List elements) { + return newList(ctx, elements, List.of()); + } + + Expr newList(Object ctx, List elements, List optionalIndices) { return newExprBuilder(ctx) - .setListExpr(CreateList.newBuilder().addAllElements(elements)) + .setListExpr( + CreateList.newBuilder().addAllElements(elements).addAllOptionalIndices(optionalIndices)) .build(); } @@ -130,7 +138,16 @@ Expr newMap(Object ctx, List entries) { } Entry newMapEntry(long entryID, Expr key, Expr value) { - return Entry.newBuilder().setId(entryID).setMapKey(key).setValue(value).build(); + return newMapEntry(entryID, key, value, false); + } + + Entry newMapEntry(long entryID, Expr key, Expr value, boolean optional) { + return Entry.newBuilder() + .setId(entryID) + .setMapKey(key) + .setValue(value) + .setOptionalEntry(optional) + .build(); } Expr newObject(Object ctx, String typeName, List entries) { @@ -140,12 +157,35 @@ Expr newObject(Object ctx, String typeName, List entries) { } Entry newObjectField(long fieldID, String field, Expr value) { - return Entry.newBuilder().setId(fieldID).setFieldKey(field).setValue(value).build(); + return newObjectField(fieldID, field, value, false); + } + + Entry newObjectField(long fieldID, String field, Expr value, boolean optional) { + return Entry.newBuilder() + .setId(fieldID) + .setFieldKey(field) + .setValue(value) + .setOptionalEntry(optional) + .build(); + } + + Expr newComprehension( + Object ctx, + String iterVar, + Expr iterRange, + String accuVar, + Expr accuInit, + Expr condition, + Expr step, + Expr result) { + return newComprehension( + ctx, iterVar, "", iterRange, accuVar, accuInit, condition, step, result); } Expr newComprehension( Object ctx, String iterVar, + String iterVar2, Expr iterRange, String accuVar, Expr accuInit, @@ -158,6 +198,7 @@ Expr newComprehension( .setAccuVar(accuVar) .setAccuInit(accuInit) .setIterVar(iterVar) + .setIterVar2(iterVar2) .setIterRange(iterRange) .setLoopCondition(condition) .setLoopStep(step) @@ -177,12 +218,8 @@ private Builder newExprBuilder(Object ctx) { long id(Object ctx) { Location location; - if (ctx instanceof ParserRuleContext) { - Token token = ((ParserRuleContext) ctx).start; - location = source.newLocation(token.getLine(), token.getCharPositionInLine()); - } else if (ctx instanceof Token) { - Token token = (Token) ctx; - location = source.newLocation(token.getLine(), token.getCharPositionInLine()); + if (ctx instanceof Node node) { + location = source.newLocation(node.getBeginLine(), node.getBeginColumn() - 1); } else if (ctx instanceof Location) { location = (Location) ctx; } else { diff --git a/core/src/main/java/org/projectnessie/cel/parser/Macro.java b/core/src/main/java/org/projectnessie/cel/parser/Macro.java index ba507490..ac7e4c52 100644 --- a/core/src/main/java/org/projectnessie/cel/parser/Macro.java +++ b/core/src/main/java/org/projectnessie/cel/parser/Macro.java @@ -15,10 +15,11 @@ */ package org.projectnessie.cel.parser; -import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import com.google.api.expr.v1alpha1.Expr; +import com.google.api.expr.v1alpha1.Expr.CreateList; +import com.google.api.expr.v1alpha1.Expr.CreateStruct.Entry; import com.google.api.expr.v1alpha1.Expr.ExprKindCase; import com.google.api.expr.v1alpha1.Expr.Select; import java.util.List; @@ -26,6 +27,7 @@ import org.projectnessie.cel.common.ErrorWithLocation; import org.projectnessie.cel.common.Location; import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.Overloads; public final class Macro { /** AccumulatorName is the traditional variable name assigned to the fold accumulator variable. */ @@ -33,7 +35,7 @@ public final class Macro { /** AllMacros includes the list of all spec-supported macros. */ public static final List AllMacros = - asList( + List.of( /* The macro "has(m.f)" which tests the presence of a field, avoiding the need to specify * the field as a string. */ @@ -43,16 +45,20 @@ public final class Macro { * predicate holds. */ newReceiverMacro(Operator.All.id, 2, Macro::makeAll), + newReceiverMacro(Operator.All.id, 3, Macro::makeAll2), /* The macro "range.exists(var, predicate)", which is true if for at least one element in * range the predicate holds. */ newReceiverMacro(Operator.Exists.id, 2, Macro::makeExists), + newReceiverMacro(Operator.Exists.id, 3, Macro::makeExists2), /* The macro "range.exists_one(var, predicate)", which is true if for exactly one element * in range the predicate holds. */ newReceiverMacro(Operator.ExistsOne.id, 2, Macro::makeExistsOne), + newReceiverMacro(Operator.ExistsOne.id, 3, Macro::makeExistsOne2), + newReceiverMacro("existsOne", 3, Macro::makeExistsOne2), /* The macro "range.map(var, function)", applies the function to the vars in the range. */ newReceiverMacro(Operator.Map.id, 2, Macro::makeMap), @@ -65,7 +71,26 @@ public final class Macro { /* The macro "range.filter(var, predicate)", filters out the variables for which the * predicate is false. */ - newReceiverMacro(Operator.Filter.id, 2, Macro::makeFilter)); + newReceiverMacro(Operator.Filter.id, 2, Macro::makeFilter), + + /* The macro "range.transformList(index, value, function)" transforms entries to a list. */ + newReceiverMacro("transformList", 3, Macro::makeTransformList), + newReceiverMacro("transformList", 4, Macro::makeTransformList), + + /* The macro "range.transformMap(key, value, function)" transforms map values. */ + newReceiverMacro("transformMap", 3, Macro::makeTransformMap), + newReceiverMacro("transformMap", 4, Macro::makeTransformMap), + + /* The macro "cel.bind(var, value, result)" binds a local variable for use in result. */ + newReceiverMacro("bind", 3, Macro::makeBind)); + + /** TestOnlyBlockMacros includes the test-only macros used by CEL-Spec block conformance tests. */ + public static final List TestOnlyBlockMacros = + List.of( + newReceiverMacro("block", 2, Macro::makeBlock), + newReceiverMacro("index", 1, Macro::makeIndex), + newReceiverMacro("iterVar", 2, Macro::makeIterVar), + newReceiverMacro("accuVar", 2, Macro::makeAccuVar)); /** NoMacros list. */ public static List MoMacros = emptyList(); @@ -138,14 +163,26 @@ static Expr makeAll(ExprHelper eh, Expr target, List args) { return makeQuantifier(QuantifierKind.quantifierAll, eh, target, args); } + static Expr makeAll2(ExprHelper eh, Expr target, List args) { + return makeQuantifier2(QuantifierKind.quantifierAll, eh, target, args); + } + static Expr makeExists(ExprHelper eh, Expr target, List args) { return makeQuantifier(QuantifierKind.quantifierExists, eh, target, args); } + static Expr makeExists2(ExprHelper eh, Expr target, List args) { + return makeQuantifier2(QuantifierKind.quantifierExists, eh, target, args); + } + static Expr makeExistsOne(ExprHelper eh, Expr target, List args) { return makeQuantifier(QuantifierKind.quantifierExistsOne, eh, target, args); } + static Expr makeExistsOne2(ExprHelper eh, Expr target, List args) { + return makeQuantifier2(QuantifierKind.quantifierExistsOne, eh, target, args); + } + static Expr makeQuantifier(QuantifierKind kind, ExprHelper eh, Expr target, List args) { String v = extractIdent(args.get(0)); if (v == null) { @@ -194,6 +231,59 @@ static Expr makeQuantifier(QuantifierKind kind, ExprHelper eh, Expr target, List return eh.fold(v, target, AccumulatorName, init, condition, step, result); } + static Expr makeQuantifier2(QuantifierKind kind, ExprHelper eh, Expr target, List args) { + String v = extractIdent(args.get(0)); + if (v == null) { + Location location = eh.offsetLocation(args.get(0).getId()); + throw new ErrorWithLocation(location, "argument must be a simple name"); + } + String v2 = extractIdent(args.get(1)); + if (v2 == null) { + Location location = eh.offsetLocation(args.get(1).getId()); + throw new ErrorWithLocation(location, "argument must be a simple name"); + } + + Supplier accuIdent = () -> eh.ident(AccumulatorName); + + Expr init; + Expr condition; + Expr step; + Expr result; + switch (kind) { + case quantifierAll: + init = eh.literalBool(true); + condition = eh.globalCall(Operator.NotStrictlyFalse.id, accuIdent.get()); + step = eh.globalCall(Operator.LogicalAnd.id, accuIdent.get(), args.get(2)); + result = accuIdent.get(); + break; + case quantifierExists: + init = eh.literalBool(false); + condition = + eh.globalCall( + Operator.NotStrictlyFalse.id, + eh.globalCall(Operator.LogicalNot.id, accuIdent.get())); + step = eh.globalCall(Operator.LogicalOr.id, accuIdent.get(), args.get(2)); + result = accuIdent.get(); + break; + case quantifierExistsOne: + Expr zeroExpr = eh.literalInt(0); + Expr oneExpr = eh.literalInt(1); + init = zeroExpr; + condition = eh.literalBool(true); + step = + eh.globalCall( + Operator.Conditional.id, + args.get(2), + eh.globalCall(Operator.Add.id, accuIdent.get(), oneExpr), + accuIdent.get()); + result = eh.globalCall(Operator.Equals.id, accuIdent.get(), oneExpr); + break; + default: + throw new ErrorWithLocation(null, String.format("unrecognized quantifier '%s'", kind)); + } + return eh.fold(v, v2, target, AccumulatorName, init, condition, step, result); + } + static Expr makeMap(ExprHelper eh, Expr target, List args) { String v = extractIdent(args.get(0)); if (v == null) { @@ -237,6 +327,185 @@ static Expr makeFilter(ExprHelper eh, Expr target, List args) { return eh.fold(v, target, AccumulatorName, init, condition, step, accuExpr); } + static Expr makeTransformList(ExprHelper eh, Expr target, List args) { + String v = extractIdent(args.get(0)); + if (v == null) { + Location location = eh.offsetLocation(args.get(0).getId()); + throw new ErrorWithLocation(location, "argument must be a simple name"); + } + String v2 = extractIdent(args.get(1)); + if (v2 == null) { + Location location = eh.offsetLocation(args.get(1).getId()); + throw new ErrorWithLocation(location, "argument must be a simple name"); + } + + Expr fn; + Expr filter; + + if (args.size() == 4) { + filter = args.get(2); + fn = args.get(3); + } else { + filter = null; + fn = args.get(2); + } + + Expr accuExpr = eh.ident(AccumulatorName); + Expr init = eh.newList(); + Expr condition = eh.literalBool(true); + Expr step = eh.globalCall(Operator.Add.id, accuExpr, eh.newList(fn)); + + if (filter != null) { + step = eh.globalCall(Operator.Conditional.id, filter, step, accuExpr); + } + return eh.fold(v, v2, target, AccumulatorName, init, condition, step, accuExpr); + } + + static Expr makeTransformMap(ExprHelper eh, Expr target, List args) { + String v = extractIdent(args.get(0)); + if (v == null) { + Location location = eh.offsetLocation(args.get(0).getId()); + throw new ErrorWithLocation(location, "argument must be a simple name"); + } + String v2 = extractIdent(args.get(1)); + if (v2 == null) { + Location location = eh.offsetLocation(args.get(1).getId()); + throw new ErrorWithLocation(location, "argument must be a simple name"); + } + + Expr fn; + Expr filter; + + if (args.size() == 4) { + filter = args.get(2); + fn = args.get(3); + } else { + filter = null; + fn = args.get(2); + } + + Expr accuExpr = eh.ident(AccumulatorName); + Expr init = eh.newMap(emptyList()); + Expr condition = eh.literalBool(true); + Entry transformedEntry = eh.newMapEntry(eh.ident(v), fn); + Expr step = eh.newMap(List.of(transformedEntry)); + + if (filter != null) { + step = eh.globalCall(Operator.Conditional.id, filter, step, accuExpr); + } + return eh.fold(v, v2, target, AccumulatorName, init, condition, step, accuExpr); + } + + static Expr makeBind(ExprHelper eh, Expr target, List args) { + if (target == null + || target.getExprKindCase() != ExprKindCase.IDENT_EXPR + || !"cel".equals(target.getIdentExpr().getName())) { + Location location = target != null ? eh.offsetLocation(target.getId()) : null; + throw new ErrorWithLocation( + location, "cel.bind() must be called with receiver identifier cel"); + } + + String v = extractIdent(args.get(0)); + if (v == null) { + Location location = eh.offsetLocation(args.get(0).getId()); + throw new ErrorWithLocation(location, "argument must be a simple name"); + } + + Expr accuExpr = eh.ident(AccumulatorName); + Expr dynNull = eh.globalCall(Overloads.TypeConvertDyn, eh.literalNull()); + return eh.fold( + v, + eh.newList(args.get(1)), + AccumulatorName, + dynNull, + eh.literalBool(true), + args.get(2), + accuExpr); + } + + static Expr makeBlock(ExprHelper eh, Expr target, List args) { + validateCelReceiver(eh, target, "cel.block()"); + + Expr bindings = args.get(0); + if (bindings.getExprKindCase() != ExprKindCase.LIST_EXPR) { + Location location = eh.offsetLocation(bindings.getId()); + throw new ErrorWithLocation(location, "cel.block() first argument must be a list literal"); + } + + CreateList list = bindings.getListExpr(); + Expr result = args.get(1); + for (int i = list.getElementsCount() - 1; i >= 0; i--) { + result = makeLocalBinding(eh, indexName(i), list.getElements(i), result); + } + return result; + } + + static Expr makeIndex(ExprHelper eh, Expr target, List args) { + validateCelReceiver(eh, target, "cel.index()"); + return eh.ident(indexName(extractIntegerArgument(eh, args.get(0), "cel.index()"))); + } + + static Expr makeIterVar(ExprHelper eh, Expr target, List args) { + validateCelReceiver(eh, target, "cel.iterVar()"); + return eh.ident( + String.format( + "@it:%d:%d", + extractIntegerArgument(eh, args.get(0), "cel.iterVar()"), + extractIntegerArgument(eh, args.get(1), "cel.iterVar()"))); + } + + static Expr makeAccuVar(ExprHelper eh, Expr target, List args) { + validateCelReceiver(eh, target, "cel.accuVar()"); + return eh.ident( + String.format( + "@ac:%d:%d", + extractIntegerArgument(eh, args.get(0), "cel.accuVar()"), + extractIntegerArgument(eh, args.get(1), "cel.accuVar()"))); + } + + private static Expr makeLocalBinding(ExprHelper eh, String variable, Expr value, Expr result) { + Expr accuExpr = eh.ident(AccumulatorName); + Expr dynNull = eh.globalCall(Overloads.TypeConvertDyn, eh.literalNull()); + return eh.fold( + variable, + eh.newList(value), + AccumulatorName, + dynNull, + eh.literalBool(true), + result, + accuExpr); + } + + private static void validateCelReceiver(ExprHelper eh, Expr target, String macroName) { + if (target != null + && target.getExprKindCase() == ExprKindCase.IDENT_EXPR + && "cel".equals(target.getIdentExpr().getName())) { + return; + } + + Location location = target != null ? eh.offsetLocation(target.getId()) : null; + throw new ErrorWithLocation( + location, macroName + " must be called with receiver identifier cel"); + } + + private static int extractIntegerArgument(ExprHelper eh, Expr expr, String macroName) { + if (expr.getExprKindCase() != ExprKindCase.CONST_EXPR || !expr.getConstExpr().hasInt64Value()) { + Location location = eh.offsetLocation(expr.getId()); + throw new ErrorWithLocation(location, macroName + " argument must be an integer literal"); + } + + long value = expr.getConstExpr().getInt64Value(); + if (value < 0 || value > Integer.MAX_VALUE) { + Location location = eh.offsetLocation(expr.getId()); + throw new ErrorWithLocation(location, macroName + " argument out of range"); + } + return (int) value; + } + + private static String indexName(int index) { + return "@index" + index; + } + static String extractIdent(Expr e) { if (e.getExprKindCase() == ExprKindCase.IDENT_EXPR) { return e.getIdentExpr().getName(); diff --git a/core/src/main/java/org/projectnessie/cel/parser/Parser.java b/core/src/main/java/org/projectnessie/cel/parser/Parser.java index 832cbecd..ba8207c2 100644 --- a/core/src/main/java/org/projectnessie/cel/parser/Parser.java +++ b/core/src/main/java/org/projectnessie/cel/parser/Parser.java @@ -16,6 +16,28 @@ package org.projectnessie.cel.parser; import static org.projectnessie.cel.parser.Macro.AllMacros; +import static org.projectnessie.cel.parser.Token.TokenType.BYTES; +import static org.projectnessie.cel.parser.Token.TokenType.COLON; +import static org.projectnessie.cel.parser.Token.TokenType.COMMA; +import static org.projectnessie.cel.parser.Token.TokenType.DOT; +import static org.projectnessie.cel.parser.Token.TokenType.EOF; +import static org.projectnessie.cel.parser.Token.TokenType.EXCLAM; +import static org.projectnessie.cel.parser.Token.TokenType.FALSE; +import static org.projectnessie.cel.parser.Token.TokenType.IDENTIFIER; +import static org.projectnessie.cel.parser.Token.TokenType.LBRACE; +import static org.projectnessie.cel.parser.Token.TokenType.LBRACKET; +import static org.projectnessie.cel.parser.Token.TokenType.LPAREN; +import static org.projectnessie.cel.parser.Token.TokenType.MINUS; +import static org.projectnessie.cel.parser.Token.TokenType.NULL; +import static org.projectnessie.cel.parser.Token.TokenType.NUM_FLOAT; +import static org.projectnessie.cel.parser.Token.TokenType.NUM_INT; +import static org.projectnessie.cel.parser.Token.TokenType.NUM_UINT; +import static org.projectnessie.cel.parser.Token.TokenType.QUESTIONMARK; +import static org.projectnessie.cel.parser.Token.TokenType.RBRACE; +import static org.projectnessie.cel.parser.Token.TokenType.RBRACKET; +import static org.projectnessie.cel.parser.Token.TokenType.RPAREN; +import static org.projectnessie.cel.parser.Token.TokenType.STRING; +import static org.projectnessie.cel.parser.Token.TokenType.TRUE; import com.google.api.expr.v1alpha1.Constant; import com.google.api.expr.v1alpha1.Expr; @@ -27,7 +49,6 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; -import java.util.BitSet; import java.util.Collections; import java.util.List; import java.util.Set; @@ -37,54 +58,13 @@ import org.projectnessie.cel.common.Source; import org.projectnessie.cel.common.operators.Operator; import org.projectnessie.cel.parser.Helper.Balancer; -import org.projectnessie.cel.parser.gen.CELLexer; -import org.projectnessie.cel.parser.gen.CELParser; -import org.projectnessie.cel.parser.gen.CELParser.BoolFalseContext; -import org.projectnessie.cel.parser.gen.CELParser.BoolTrueContext; -import org.projectnessie.cel.parser.gen.CELParser.BytesContext; -import org.projectnessie.cel.parser.gen.CELParser.CalcContext; -import org.projectnessie.cel.parser.gen.CELParser.ConditionalAndContext; -import org.projectnessie.cel.parser.gen.CELParser.ConditionalOrContext; -import org.projectnessie.cel.parser.gen.CELParser.ConstantLiteralContext; -import org.projectnessie.cel.parser.gen.CELParser.CreateListContext; -import org.projectnessie.cel.parser.gen.CELParser.CreateMessageContext; -import org.projectnessie.cel.parser.gen.CELParser.CreateStructContext; -import org.projectnessie.cel.parser.gen.CELParser.DoubleContext; -import org.projectnessie.cel.parser.gen.CELParser.ExprContext; -import org.projectnessie.cel.parser.gen.CELParser.ExprListContext; -import org.projectnessie.cel.parser.gen.CELParser.FieldInitializerListContext; -import org.projectnessie.cel.parser.gen.CELParser.IdentOrGlobalCallContext; -import org.projectnessie.cel.parser.gen.CELParser.IndexContext; -import org.projectnessie.cel.parser.gen.CELParser.IntContext; -import org.projectnessie.cel.parser.gen.CELParser.LogicalNotContext; -import org.projectnessie.cel.parser.gen.CELParser.MapInitializerListContext; -import org.projectnessie.cel.parser.gen.CELParser.MemberExprContext; -import org.projectnessie.cel.parser.gen.CELParser.NegateContext; -import org.projectnessie.cel.parser.gen.CELParser.NestedContext; -import org.projectnessie.cel.parser.gen.CELParser.NullContext; -import org.projectnessie.cel.parser.gen.CELParser.PrimaryExprContext; -import org.projectnessie.cel.parser.gen.CELParser.RelationContext; -import org.projectnessie.cel.parser.gen.CELParser.SelectOrCallContext; -import org.projectnessie.cel.parser.gen.CELParser.StartContext; -import org.projectnessie.cel.parser.gen.CELParser.StringContext; -import org.projectnessie.cel.parser.gen.CELParser.UintContext; -import org.projectnessie.cel.parser.gen.CELParser.UnaryContext; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.ANTLRErrorListener; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.CommonTokenStream; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.DefaultErrorStrategy; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.IntStream; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.ParserRuleContext; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.RecognitionException; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.Recognizer; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.RuleContext; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.Token; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.atn.ATNConfigSet; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.dfa.DFA; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.tree.AbstractParseTreeVisitor; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.tree.ErrorNode; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.tree.ParseTree; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.tree.ParseTreeListener; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.tree.TerminalNode; +import org.projectnessie.cel.parser.ast.ConstantLiteral; +import org.projectnessie.cel.parser.ast.ExprList; +import org.projectnessie.cel.parser.ast.Field; +import org.projectnessie.cel.parser.ast.FieldInitializerList; +import org.projectnessie.cel.parser.ast.ListInitializerList; +import org.projectnessie.cel.parser.ast.MapInitializerList; +import org.projectnessie.cel.parser.ast.Start; public final class Parser { @@ -131,37 +111,27 @@ public static ParseResult parse(Options options, Source source) { } ParseResult parse(Source source) { - StringCharStream charStream = new StringCharStream(source.content(), source.description()); - CELLexer lexer = new CELLexer(charStream); - CELParser parser = new CELParser(new CommonTokenStream(lexer, 0)); - - RecursionListener parserListener = new RecursionListener(options.getMaxRecursionDepth()); - - parser.addParseListener(parserListener); - - parser.setErrorHandler(new RecoveryLimitErrorStrategy(options.getErrorRecoveryLimit())); - Helper helper = new Helper(source); Errors errors = new Errors(source); - - InnerParser inner = new InnerParser(helper, errors); - - lexer.addErrorListener(inner); - parser.addErrorListener(inner); - Expr expr = null; - try { - if (charStream.size() > options.getExpressionSizeCodePointLimit()) { - errors.reportError( - Location.NoLocation, - "expression code point size exceeds limit: size: %d, limit %d", - charStream.size(), - options.getExpressionSizeCodePointLimit()); - } else { - expr = inner.exprVisit(parser.start()); + + int codePointCount = source.content().codePointCount(0, source.content().length()); + if (codePointCount > options.getExpressionSizeCodePointLimit()) { + errors.reportError( + Location.NoLocation, + "expression code point size exceeds limit: size: %d, limit %d", + codePointCount, + options.getExpressionSizeCodePointLimit()); + } else { + CelGrammarParser parser = new CelGrammarParser(source.description(), source.content()); + try { + parser.Start(); + expr = new AstBuilder(helper, errors).exprVisit(firstExpressionNode(parser.rootNode())); + } catch (ParseException e) { + errors.syntaxError(location(e.getLocation()), e.getMessage()); + } catch (RecursionError e) { + errors.reportError(e, Location.NoLocation, "%s", e.getMessage()); } - } catch (RecoveryLimitError | RecursionError e) { - errors.reportError(e, Location.NoLocation, "%s", e.getMessage()); } if (errors.hasErrors()) { @@ -171,6 +141,24 @@ ParseResult parse(Source source) { return new ParseResult(expr, errors, helper.getSourceInfo()); } + private static Node firstExpressionNode(Node root) { + if (root instanceof Start) { + for (Node child : root.children()) { + if (!isToken(child, EOF)) { + return child; + } + } + } + return root; + } + + private static Location location(Node node) { + if (node == null) { + return Location.NoLocation; + } + return Location.newLocation(node.getBeginLine(), node.getBeginColumn() - 1); + } + public static final class ParseResult { private final Expr expr; private final Errors errors; @@ -199,404 +187,339 @@ public boolean hasErrors() { } } - static final class RecursionListener implements ParseTreeListener { - private final int maxDepth; - private int depth; - - RecursionListener(int maxDepth) { - this.maxDepth = maxDepth; - } - - @Override - public void visitTerminal(TerminalNode node) {} - - @Override - public void visitErrorNode(ErrorNode node) {} - - @Override - public void enterEveryRule(ParserRuleContext ctx) { - if (ctx != null && ctx.getRuleIndex() == CELParser.RULE_expr) { - if (this.depth >= this.maxDepth) { - this.depth++; - throw new RecursionError( - String.format("expression recursion limit exceeded: %d", maxDepth)); - } - this.depth++; - } - } - - @Override - public void exitEveryRule(ParserRuleContext ctx) { - if (ctx != null && ctx.getRuleIndex() == CELParser.RULE_expr) { - depth--; - } - } - } - static final class RecursionError extends RuntimeException { - public RecursionError(String message) { + RecursionError(String message) { super(message); } } - static final class RecoveryLimitError extends RecognitionException { - public RecoveryLimitError( - String message, Recognizer recognizer, IntStream input, ParserRuleContext ctx) { - super(message, recognizer, input, ctx); - } - } - - static final class RecoveryLimitErrorStrategy extends DefaultErrorStrategy { - private final int maxAttempts; - private int attempts; - - private RecoveryLimitErrorStrategy(int maxAttempts) { - this.maxAttempts = maxAttempts; - } - - @Override - public void recover( - org.projectnessie.cel.shaded.org.antlr.v4.runtime.Parser recognizer, - RecognitionException e) { - checkAttempts(recognizer); - super.recover(recognizer, e); - } - - @Override - public Token recoverInline(org.projectnessie.cel.shaded.org.antlr.v4.runtime.Parser recognizer) - throws RecognitionException { - checkAttempts(recognizer); - return super.recoverInline(recognizer); - } - - void checkAttempts(org.projectnessie.cel.shaded.org.antlr.v4.runtime.Parser recognizer) - throws RecognitionException { - if (attempts >= maxAttempts) { - attempts++; - String msg = String.format("error recovery attempt limit exceeded: %d", maxAttempts); - recognizer.notifyErrorListeners(null, msg, null); - throw new RecoveryLimitError(msg, recognizer, null, null); - } - attempts++; - } - } - - final class InnerParser extends AbstractParseTreeVisitor implements ANTLRErrorListener { - + final class AstBuilder implements CelExprBuilder { private final Helper helper; private final Errors errors; + private int depth; - InnerParser(Helper helper, Errors errors) { + AstBuilder(Helper helper, Errors errors) { this.helper = helper; this.errors = errors; } - @Override - public void syntaxError( - Recognizer recognizer, - Object offendingSymbol, - int line, - int charPositionInLine, - String msg, - RecognitionException e) { - errors.syntaxError(Location.newLocation(line, charPositionInLine), msg); - } - - @Override - public void reportAmbiguity( - org.projectnessie.cel.shaded.org.antlr.v4.runtime.Parser recognizer, - DFA dfa, - int startIndex, - int stopIndex, - boolean exact, - BitSet ambigAlts, - ATNConfigSet configs) { - // empty - } - - @Override - public void reportAttemptingFullContext( - org.projectnessie.cel.shaded.org.antlr.v4.runtime.Parser recognizer, - DFA dfa, - int startIndex, - int stopIndex, - BitSet conflictingAlts, - ATNConfigSet configs) { - // empty - } - - @Override - public void reportContextSensitivity( - org.projectnessie.cel.shaded.org.antlr.v4.runtime.Parser recognizer, - DFA dfa, - int startIndex, - int stopIndex, - int prediction, - ATNConfigSet configs) { - // empty - } - - Expr reportError(Object ctx, String message) { - return reportError(ctx, "%s", message); - } - - Expr reportError(Object ctx, String format, Object... args) { - Location location; - if (ctx instanceof Location) { - location = (Location) ctx; - } else if (ctx instanceof Token || ctx instanceof ParserRuleContext) { - Expr err = helper.newExpr(ctx); - location = helper.getLocation(err.getId()); - } else { - location = Location.NoLocation; + Expr exprVisit(Node node) { + if (node == null) { + return reportError(Location.NoLocation, "unknown parse element encountered: <>"); } - Expr err = helper.newExpr(ctx); - // Provide arguments to the report error. - errors.reportError(location, format, args); - return err; - } - - public Expr exprVisit(ParseTree tree) { - Object r = visit(tree); - return (Expr) r; - } - - @Override - public Object visit(ParseTree tree) { - if (tree instanceof RuleContext) { - RuleContext ruleContext = (RuleContext) tree; - int ruleIndex = ruleContext.getRuleIndex(); - switch (ruleIndex) { - case CELParser.RULE_start: - return visitStart((StartContext) tree); - case CELParser.RULE_expr: - return visitExpr((ExprContext) tree); - case CELParser.RULE_conditionalOr: - return visitConditionalOr((ConditionalOrContext) tree); - case CELParser.RULE_conditionalAnd: - return visitConditionalAnd((ConditionalAndContext) tree); - case CELParser.RULE_relation: - return visitRelation((RelationContext) tree); - case CELParser.RULE_calc: - return visitCalc((CalcContext) tree); - case CELParser.RULE_unary: - if (tree instanceof LogicalNotContext) { - return visitLogicalNot((LogicalNotContext) tree); - } else if (tree instanceof NegateContext) { - return visitNegate((NegateContext) tree); - } else if (tree instanceof MemberExprContext) { - return visitMemberExpr((MemberExprContext) tree); - } - return visitUnary((UnaryContext) tree); - case CELParser.RULE_member: - if (tree instanceof CreateMessageContext) { - return visitCreateMessage((CreateMessageContext) tree); - } else if (tree instanceof PrimaryExprContext) { - return visitPrimaryExpr((PrimaryExprContext) tree); - } else if (tree instanceof SelectOrCallContext) { - return visitSelectOrCall((SelectOrCallContext) tree); - } else if (tree instanceof IndexContext) { - return visitIndex((IndexContext) tree); - } - break; - case CELParser.RULE_primary: - if (tree instanceof CreateListContext) { - return visitCreateList((CreateListContext) tree); - } else if (tree instanceof CreateStructContext) { - return visitCreateStruct((CreateStructContext) tree); - } - break; - case CELParser.RULE_fieldInitializerList: - case CELParser.RULE_mapInitializerList: - return visitMapInitializerList((MapInitializerListContext) tree); - // case CELParser.RULE_exprList: - // case CELParser.RULE_literal: - default: - return reportError(tree, "parser rule '%d'", ruleIndex); - } + if (depth >= options.getMaxRecursionDepth()) { + throw new RecursionError( + String.format( + "expression recursion limit exceeded: %d", options.getMaxRecursionDepth())); } - - // Report at least one error if the parser reaches an unknown parse element. - // Typically, this happens if the parser has already encountered a syntax error elsewhere. - if (!errors.hasErrors()) { - String txt = "<>"; - if (tree != null) { - txt = String.format("<<%s>>", tree.getClass().getSimpleName()); - } - return reportError(Location.NoLocation, "unknown parse element encountered: %s", txt); + depth++; + try { + return doExprVisit(node); + } finally { + depth--; } - return helper.newExpr(Location.NoLocation); } - private Object visitStart(StartContext ctx) { - return visit(ctx.expr()); + private Expr doExprVisit(Node node) { + if (node instanceof CelExprNode) { + return ((CelExprNode) node).toCelExpr(this); + } + return reportError( + node, "unknown parse element encountered: <<%s>>", node.getClass().getSimpleName()); } - private Expr visitExpr(ExprContext ctx) { - Expr result = exprVisit(ctx.e); - if (ctx.op == null) { - return result; + @Override + public Expr visitExpr(Node node) { + List children = significantChildren(node); + int question = indexOf(children, QUESTIONMARK); + if (question < 0) { + return exprVisit(firstExpressionChild(children, node)); } - long opID = helper.id(ctx.op); - Expr ifTrue = exprVisit(ctx.e1); - Expr ifFalse = exprVisit(ctx.e2); - return globalCallOrMacro(opID, Operator.Conditional.id, result, ifTrue, ifFalse); + Expr condition = exprVisit(children.get(0)); + long opID = helper.id(children.get(question)); + Expr ifTrue = exprVisit(children.get(question + 1)); + Expr ifFalse = exprVisit(children.get(question + 3)); + return globalCallOrMacro(opID, Operator.Conditional.id, condition, ifTrue, ifFalse); } - private Expr visitConditionalAnd(ConditionalAndContext ctx) { - Expr result = exprVisit(ctx.e); - if (ctx.ops == null || ctx.ops.isEmpty()) { - return result; - } - Balancer b = helper.newBalancer(Operator.LogicalAnd.id, result); - List rest = ctx.e1; - for (int i = 0; i < ctx.ops.size(); i++) { - Token op = ctx.ops.get(i); - if (i >= rest.size()) { - return reportError(ctx, "unexpected character, wanted '&&'"); + @Override + public Expr visitBalanced(Node node, Operator operator) { + List children = significantChildren(node); + if (children.size() == 1) { + return exprVisit(children.get(0)); + } + Expr result = exprVisit(children.get(0)); + Balancer balancer = helper.newBalancer(operator.id, result); + for (int i = 1; i < children.size(); i += 2) { + Node op = children.get(i); + if (i + 1 >= children.size()) { + return reportError(node, "unexpected character, wanted '%s'", tokenText(op)); } - Expr next = exprVisit(rest.get(i)); - long opID = helper.id(op); - b.addTerm(opID, next); + Expr next = exprVisit(children.get(i + 1)); + balancer.addTerm(helper.id(op), next); } - return b.balance(); + return balancer.balance(); } - private Expr visitConditionalOr(ConditionalOrContext ctx) { - Expr result = exprVisit(ctx.e); - if (ctx.ops == null || ctx.ops.isEmpty()) { - return result; - } - Balancer b = helper.newBalancer(Operator.LogicalOr.id, result); - List rest = ctx.e1; - for (int i = 0; i < ctx.ops.size(); i++) { - Token op = ctx.ops.get(i); - if (i >= rest.size()) { - return reportError(ctx, "unexpected character, wanted '||'"); + @Override + public Expr visitBinary(Node node) { + List children = significantChildren(node); + if (children.size() == 1) { + return exprVisit(children.get(0)); + } + Expr result = exprVisit(children.get(0)); + for (int i = 1; i < children.size(); i += 2) { + Node opNode = children.get(i); + if (i + 1 >= children.size()) { + return reportError(node, "operator not found"); + } + Operator op = Operator.find(tokenText(opNode)); + if (op == null) { + return reportError(opNode, "operator not found"); } - Expr next = exprVisit(rest.get(i)); - long opID = helper.id(op); - b.addTerm(opID, next); + long opID = helper.id(opNode); + Expr rhs = exprVisit(children.get(i + 1)); + result = globalCallOrMacro(opID, op.id, result, rhs); } - return b.balance(); + return result; } - private Expr visitRelation(RelationContext ctx) { - if (ctx.calc() != null) { - return exprVisit(ctx.calc()); + @Override + public Expr visitUnary(Node node) { + List children = significantChildren(node); + int opCount = 0; + while (opCount < children.size() + && (isToken(children.get(opCount), MINUS) || isToken(children.get(opCount), EXCLAM))) { + opCount++; } - String opText = ""; - if (ctx.op != null) { - opText = ctx.op.getText(); + Node operand = children.get(opCount); + if (opCount == 0) { + return exprVisit(operand); } - Operator op = Operator.find(opText); - if (op != null) { - Expr lhs = exprVisit(ctx.relation(0)); - long opID = helper.id(ctx.op); - Expr rhs = exprVisit(ctx.relation(1)); - return globalCallOrMacro(opID, op.id, lhs, rhs); + Node op = children.get(0); + boolean logicalNot = isToken(op, EXCLAM); + if (opCount % 2 == 0) { + return exprVisit(operand); } - return reportError(ctx, "operator not found"); + if (!logicalNot && isNegativeNumericLiteral(operand)) { + return visitNegativeNumericLiteral(op, operand); + } + return globalCallOrMacro( + helper.id(op), + logicalNot ? Operator.LogicalNot.id : Operator.Negate.id, + exprVisit(operand)); } - private Expr visitCalc(CalcContext ctx) { - if (ctx.unary() != null) { - return exprVisit(ctx.unary()); - } - String opText = ""; - if (ctx.op != null) { - opText = ctx.op.getText(); + @Override + public Expr visitPrimary(Node node) { + List children = significantChildren(node); + if (children.isEmpty()) { + return reportError(node, "invalid primary expression"); + } + Node first = children.get(0); + if (isToken(first, DOT) || isToken(first, IDENTIFIER)) { + return visitIdentOrGlobalCall(children, node); + } else if (isToken(first, LPAREN)) { + return exprVisit(children.get(1)); + } else if (isToken(first, LBRACKET)) { + long listID = helper.id(first); + ListInitializerList list = firstChildOfType(children, ListInitializerList.class); + ListElements elements = + list != null ? listElements(list) : listElements(children.subList(1, children.size())); + return helper.newList(listID, elements.expressions(), elements.optionalIndices()); + } else if (isToken(first, LBRACE)) { + return helper.newMap( + helper.id(first), mapEntries(firstChildOfType(children, MapInitializerList.class))); + } else if (first instanceof ConstantLiteral || isLiteralToken(first)) { + return exprVisit(first); + } + return reportError(node, "invalid primary expression"); + } + + private Expr visitIdentOrGlobalCall(List children, Node ctx) { + int i = 0; + String prefix = ""; + if (isToken(children.get(i), DOT)) { + prefix = "."; + i++; + } + if (i >= children.size() || !isToken(children.get(i), IDENTIFIER)) { + return helper.newExpr(ctx); } - Operator op = Operator.find(opText); - if (op != null) { - Expr lhs = exprVisit(ctx.calc(0)); - long opID = helper.id(ctx.op); - Expr rhs = exprVisit(ctx.calc(1)); - return globalCallOrMacro(opID, op.id, lhs, rhs); + Token ident = (Token) children.get(i++); + String name = prefix + tokenText(ident); + if (reservedIds.contains(tokenText(ident))) { + return reportError(ident, "reserved identifier: %s", tokenText(ident)); } - return reportError(ctx, "operator not found"); - } - - private Expr visitLogicalNot(LogicalNotContext ctx) { - if (ctx.ops.size() % 2 == 0) { - return exprVisit(ctx.member()); + if (i < children.size() && isToken(children.get(i), LPAREN)) { + Node open = children.get(i); + return globalCallOrMacro( + helper.id(open), name, expressionsBetween(children, i + 1, RPAREN)); } - long opID = helper.id(ctx.ops.get(0)); - Expr target = exprVisit(ctx.member()); - return globalCallOrMacro(opID, Operator.LogicalNot.id, target); + return helper.newIdent(ident, name); } - private Expr visitMemberExpr(MemberExprContext ctx) { - if (ctx.member() instanceof PrimaryExprContext) { - return visitPrimaryExpr((PrimaryExprContext) ctx.member()); - } else if (ctx.member() instanceof SelectOrCallContext) { - return visitSelectOrCall((SelectOrCallContext) ctx.member()); - } else if (ctx.member() instanceof IndexContext) { - return visitIndex((IndexContext) ctx.member()); - } else if (ctx.member() instanceof CreateMessageContext) { - return visitCreateMessage((CreateMessageContext) ctx.member()); - } - return reportError(ctx, "unsupported simple expression"); + @Override + public Expr visitIdentifier(Token token) { + return identOrReserved(token, tokenText(token)); } - private Expr visitPrimaryExpr(PrimaryExprContext ctx) { - if (ctx.primary() instanceof NestedContext) { - return visitNested((NestedContext) ctx.primary()); - } else if (ctx.primary() instanceof IdentOrGlobalCallContext) { - return visitIdentOrGlobalCall((IdentOrGlobalCallContext) ctx.primary()); - } else if (ctx.primary() instanceof CreateListContext) { - return visitCreateList((CreateListContext) ctx.primary()); - } else if (ctx.primary() instanceof CreateStructContext) { - return visitCreateStruct((CreateStructContext) ctx.primary()); - } else if (ctx.primary() instanceof ConstantLiteralContext) { - return visitConstantLiteral((ConstantLiteralContext) ctx.primary()); + private Expr identOrReserved(Token token, String name) { + if (reservedIds.contains(name)) { + return reportError(token, "reserved identifier: %s", name); } - - return reportError(ctx, "invalid primary expression"); + return helper.newIdent(token, name); } - private Expr visitConstantLiteral(ConstantLiteralContext ctx) { - if (ctx.literal() instanceof IntContext) { - return visitInt((IntContext) ctx.literal()); - } else if (ctx.literal() instanceof UintContext) { - return visitUint((UintContext) ctx.literal()); - } else if (ctx.literal() instanceof DoubleContext) { - return visitDouble((DoubleContext) ctx.literal()); - } else if (ctx.literal() instanceof StringContext) { - return visitString((StringContext) ctx.literal()); - } else if (ctx.literal() instanceof BytesContext) { - return visitBytes((BytesContext) ctx.literal()); - } else if (ctx.literal() instanceof BoolFalseContext) { - return visitBoolFalse((BoolFalseContext) ctx.literal()); - } else if (ctx.literal() instanceof BoolTrueContext) { - return visitBoolTrue((BoolTrueContext) ctx.literal()); - } else if (ctx.literal() instanceof NullContext) { - return visitNull((NullContext) ctx.literal()); + @Override + public Expr visitMember(Node node) { + List children = significantChildren(node); + Expr operand = exprVisit(children.get(0)); + int i = 1; + while (i < children.size()) { + Node op = children.get(i++); + if (isToken(op, DOT)) { + if (i >= children.size()) { + return helper.newExpr(node); + } + boolean optional = false; + Node optionalNode = null; + if (isToken(children.get(i), QUESTIONMARK)) { + optional = true; + optionalNode = children.get(i++); + } + String id = fieldName(children.get(i++)); + if (i < children.size() && isToken(children.get(i), LPAREN)) { + if (optional) { + return reportError(optionalNode, "optional select does not support function calls"); + } + Node open = children.get(i++); + long openID = helper.id(open); + List args = expressionsBetween(children, i, RPAREN); + while (i < children.size() && !isToken(children.get(i), RPAREN)) { + i++; + } + if (i < children.size()) { + i++; + } + operand = receiverCallOrMacro(openID, id, operand, args); + } else if (optional) { + operand = + globalCallOrMacro( + helper.id(optionalNode), + Operator.OptionalSelect.id, + operand, + helper.newLiteralString(optionalNode, id)); + } else { + operand = helper.newSelect(op, operand, id); + } + } else if (isToken(op, LBRACKET)) { + long opID = helper.id(op); + boolean optional = false; + if (isToken(children.get(i), QUESTIONMARK)) { + optional = true; + opID = helper.id(children.get(i++)); + } + Expr index = exprVisit(children.get(i++)); + if (i < children.size() && isToken(children.get(i), RBRACKET)) { + i++; + } + operand = + globalCallOrMacro( + opID, optional ? Operator.OptionalIndex.id : Operator.Index.id, operand, index); + } else if (isToken(op, LBRACE)) { + String messageName = extractQualifiedName(operand); + FieldInitializerList fields = + firstChildOfType(children.subList(i, children.size()), FieldInitializerList.class); + if (messageName != null) { + operand = helper.newObject(helper.id(op), messageName, objectFields(fields)); + } else { + operand = helper.newExpr(helper.id(op)); + } + while (i < children.size() && !isToken(children.get(i), RBRACE)) { + i++; + } + if (i < children.size()) { + i++; + } + } else { + return reportError(op, "unsupported member expression"); + } } - return reportError(ctx, "invalid literal"); + return operand; } - private Expr visitInt(IntContext ctx) { - String text = ctx.tok.getText(); + @Override + public Expr visitLiteral(Node node) { + Node token = node; + if (node instanceof ConstantLiteral) { + List children = significantChildren(node); + token = children.get(children.size() - 1); + } + if (isToken(token, NUM_INT)) { + return intLiteral(token); + } else if (isToken(token, NUM_UINT)) { + return uintLiteral(token); + } else if (isToken(token, NUM_FLOAT)) { + return doubleLiteral(token); + } else if (isToken(token, STRING)) { + return helper.newLiteralString(token, unquoteString(token, tokenText(token))); + } else if (isToken(token, BYTES)) { + return helper.newLiteralBytes(token, unquoteBytes(token, tokenText(token).substring(1))); + } else if (isToken(token, FALSE)) { + return helper.newLiteralBool(token, false); + } else if (isToken(token, TRUE)) { + return helper.newLiteralBool(token, true); + } else if (isToken(token, NULL)) { + return helper.newLiteral(token, Constant.newBuilder().setNullValue(NullValue.NULL_VALUE)); + } + return reportError(node, "invalid literal"); + } + + private boolean isNegativeNumericLiteral(Node operand) { + return isToken(operand, NUM_INT) + || isToken(operand, NUM_FLOAT) + || (operand instanceof ConstantLiteral + && significantChildren(operand).stream() + .anyMatch(child -> isToken(child, NUM_INT) || isToken(child, NUM_FLOAT))); + } + + private Expr visitNegativeNumericLiteral(Node op, Node operand) { + Node token = operand; + if (operand instanceof ConstantLiteral) { + List children = significantChildren(operand); + token = children.get(children.size() - 1); + } + if (isToken(token, NUM_INT)) { + return intLiteral(op, "-" + tokenText(token)); + } else if (isToken(token, NUM_FLOAT)) { + return doubleLiteral(op, "-" + tokenText(token)); + } + return globalCallOrMacro(helper.id(op), Operator.Negate.id, exprVisit(operand)); + } + + private Expr intLiteral(Node token) { + return intLiteral(token, tokenText(token)); + } + + private Expr intLiteral(Node token, String text) { int base = 10; - if (text.startsWith("0x")) { + if (text.startsWith("-0x")) { + base = 16; + text = "-" + text.substring(3); + } else if (text.startsWith("0x")) { base = 16; text = text.substring(2); } - if (ctx.sign != null) { - text = ctx.sign.getText() + text; - } try { - long i = Long.parseLong(text, base); - return helper.newLiteralInt(ctx, i); + return helper.newLiteralInt(token, Long.parseLong(text, base)); } catch (Exception e) { - return reportError(ctx, "invalid int literal"); + return reportError(token, "invalid int literal"); } } - private Expr visitUint(UintContext ctx) { - String text = ctx.tok.getText(); - // trim the 'u' designator included in the uint literal. + private Expr uintLiteral(Node token) { + String text = tokenText(token); text = text.substring(0, text.length() - 1); int base = 10; if (text.startsWith("0x")) { @@ -604,63 +527,138 @@ private Expr visitUint(UintContext ctx) { text = text.substring(2); } try { - long i = Long.parseUnsignedLong(text, base); - return helper.newLiteralUint(ctx, i); + return helper.newLiteralUint(token, Long.parseUnsignedLong(text, base)); } catch (Exception e) { - return reportError(ctx, "invalid int literal"); + return reportError(token, "invalid int literal"); } } - private Expr visitDouble(DoubleContext ctx) { - String txt = ctx.tok.getText(); - if (ctx.sign != null) { - txt = ctx.sign.getText() + txt; - } + private Expr doubleLiteral(Node token) { + return doubleLiteral(token, tokenText(token)); + } + + private Expr doubleLiteral(Node token, String text) { try { - double f = Double.parseDouble(txt); - return helper.newLiteralDouble(ctx, f); + return helper.newLiteralDouble(token, Double.parseDouble(text)); } catch (Exception e) { - return reportError(ctx, "invalid double literal"); + return reportError(token, "invalid double literal"); } } - private Expr visitString(StringContext ctx) { - String s = unquoteString(ctx, ctx.getText()); - return helper.newLiteralString(ctx, s); - } - - private Expr visitBytes(BytesContext ctx) { - ByteString b = unquoteBytes(ctx, ctx.tok.getText().substring(1)); - return helper.newLiteralBytes(ctx, b); + private List expressionsIn(ExprList list) { + if (list == null) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (Node child : significantChildren(list)) { + if (!isToken(child, COMMA)) { + result.add(exprVisit(child)); + } + } + return result; } - private Expr visitBoolFalse(BoolFalseContext ctx) { - return helper.newLiteralBool(ctx, false); + private ListElements listElements(ListInitializerList list) { + if (list == null) { + return new ListElements(Collections.emptyList(), Collections.emptyList()); + } + return listElements(significantChildren(list)); } - private Expr visitBoolTrue(BoolTrueContext ctx) { - return helper.newLiteralBool(ctx, true); + private ListElements listElements(List children) { + List expressions = new ArrayList<>(); + List optionalIndices = new ArrayList<>(); + boolean optional = false; + for (Node child : children) { + if (isToken(child, COMMA) || isToken(child, RBRACKET)) { + continue; + } + if (isToken(child, QUESTIONMARK)) { + optional = true; + continue; + } + if (optional) { + optionalIndices.add(expressions.size()); + optional = false; + } + expressions.add(exprVisit(child)); + } + return new ListElements(expressions, optionalIndices); } - private Expr visitNull(NullContext ctx) { - return helper.newLiteral(ctx, Constant.newBuilder().setNullValue(NullValue.NULL_VALUE)); + private List expressionsBetween(List children, int start, Token.TokenType end) { + List result = new ArrayList<>(); + for (int i = start; i < children.size() && !isToken(children.get(i), end); i++) { + Node child = children.get(i); + if (isToken(child, COMMA)) { + continue; + } + if (child instanceof ExprList) { + result.addAll(expressionsIn((ExprList) child)); + } else { + result.add(exprVisit(child)); + } + } + return result; } - private List visitList(ExprListContext ctx) { - if (ctx == null) { + private List objectFields(FieldInitializerList fields) { + if (fields == null) { return Collections.emptyList(); } - return visitSlice(ctx.e); + List children = significantChildren(fields); + List result = new ArrayList<>(); + for (int i = 0; i < children.size(); ) { + boolean optional = false; + if (isToken(children.get(i), QUESTIONMARK)) { + optional = true; + i++; + } + Node field = children.get(i++); + if (i >= children.size() || !isToken(children.get(i), COLON)) { + break; + } + Node colon = children.get(i++); + if (i >= children.size()) { + break; + } + long colonID = helper.id(colon); + Expr value = exprVisit(children.get(i++)); + result.add(helper.newObjectField(colonID, fieldName(field), value, optional)); + if (i < children.size() && isToken(children.get(i), COMMA)) { + i++; + } + } + return result; } - private List visitSlice(List expressions) { - if (expressions == null) { + private List mapEntries(MapInitializerList entries) { + if (entries == null) { return Collections.emptyList(); } - List result = new ArrayList<>(expressions.size()); - for (ExprContext e : expressions) { - Expr ex = exprVisit(e); - result.add(ex); + List children = significantChildren(entries); + List result = new ArrayList<>(); + for (int i = 0; i < children.size(); ) { + boolean optional = false; + if (isToken(children.get(i), QUESTIONMARK)) { + optional = true; + i++; + } + Node keyNode = children.get(i++); + if (i >= children.size() || !isToken(children.get(i), COLON)) { + break; + } + Node colon = children.get(i++); + long colonID = helper.id(colon); + Expr key = exprVisit(keyNode); + if (i >= children.size()) { + break; + } + Expr value = exprVisit(children.get(i++)); + result.add(helper.newMapEntry(colonID, key, value, optional)); + if (i < children.size() && isToken(children.get(i), COMMA)) { + i++; + } } return result; } @@ -677,145 +675,11 @@ String extractQualifiedName(Expr e) { String prefix = extractQualifiedName(s.getOperand()); return prefix + "." + s.getField(); } - // TODO: Add a method to Source to get location from character offset. Location location = helper.getLocation(e.getId()); reportError(location, "expected a qualified name"); return null; } - // Visit a parse tree of field initializers. - List visitIFieldInitializerList(FieldInitializerListContext ctx) { - if (ctx == null || ctx.fields == null) { - // This is the result of a syntax error handled elswhere, return empty. - return Collections.emptyList(); - } - - List result = new ArrayList<>(ctx.fields.size()); - List cols = ctx.cols; - List vals = ctx.values; - for (int i = 0; i < ctx.fields.size(); i++) { - Token f = ctx.fields.get(i); - if (i >= cols.size() || i >= vals.size()) { - // This is the result of a syntax error detected elsewhere. - return Collections.emptyList(); - } - long initID = helper.id(cols.get(i)); - Expr value = exprVisit(vals.get(i)); - Entry field = helper.newObjectField(initID, f.getText(), value); - result.add(field); - } - return result; - } - - private Expr visitIdentOrGlobalCall(IdentOrGlobalCallContext ctx) { - String identName = ""; - if (ctx.leadingDot != null) { - identName = "."; - } - // Handle the error case where no valid identifier is specified. - if (ctx.id == null) { - return helper.newExpr(ctx); - } - // Handle reserved identifiers. - String id = ctx.id.getText(); - if (reservedIds.contains(id)) { - return reportError(ctx, "reserved identifier: %s", id); - } - identName += id; - if (ctx.op != null) { - long opID = helper.id(ctx.op); - return globalCallOrMacro(opID, identName, visitList(ctx.args)); - } - return helper.newIdent(ctx.id, identName); - } - - private Expr visitNested(NestedContext ctx) { - return exprVisit(ctx.e); - } - - private Expr visitSelectOrCall(SelectOrCallContext ctx) { - Expr operand = exprVisit(ctx.member()); - // Handle the error case where no valid identifier is specified. - if (ctx.id == null) { - return helper.newExpr(ctx); - } - String id = ctx.id.getText(); - if (ctx.open != null) { - long opID = helper.id(ctx.open); - return receiverCallOrMacro(opID, id, operand, visitList(ctx.args)); - } - return helper.newSelect(ctx.op, operand, id); - } - - private List visitMapInitializerList(MapInitializerListContext ctx) { - if (ctx == null || ctx.keys.isEmpty()) { - // This is the result of a syntax error handled elswhere, return empty. - return Collections.emptyList(); - } - - List result = new ArrayList<>(ctx.cols.size()); - List keys = ctx.keys; - List vals = ctx.values; - for (int i = 0; i < ctx.cols.size(); i++) { - Token col = ctx.cols.get(i); - long colID = helper.id(col); - if (i >= keys.size() || i >= vals.size()) { - // This is the result of a syntax error detected elsewhere. - return Collections.emptyList(); - } - Expr key = exprVisit(keys.get(i)); - Expr value = exprVisit(vals.get(i)); - Entry entry = helper.newMapEntry(colID, key, value); - result.add(entry); - } - return result; - } - - private Expr visitNegate(NegateContext ctx) { - if (ctx.ops.size() % 2 == 0) { - return exprVisit(ctx.member()); - } - long opID = helper.id(ctx.ops.get(0)); - Expr target = exprVisit(ctx.member()); - return globalCallOrMacro(opID, Operator.Negate.id, target); - } - - private Expr visitIndex(IndexContext ctx) { - Expr target = exprVisit(ctx.member()); - long opID = helper.id(ctx.op); - Expr index = exprVisit(ctx.index); - return globalCallOrMacro(opID, Operator.Index.id, target, index); - } - - private Expr visitUnary(UnaryContext ctx) { - return helper.newLiteralString(ctx, "<>"); - } - - private Expr visitCreateList(CreateListContext ctx) { - long listID = helper.id(ctx.op); - return helper.newList(listID, visitList(ctx.elems)); - } - - private Expr visitCreateMessage(CreateMessageContext ctx) { - Expr target = exprVisit(ctx.member()); - long objID = helper.id(ctx.op); - String messageName = extractQualifiedName(target); - if (messageName != null) { - List entries = visitIFieldInitializerList(ctx.entries); - return helper.newObject(objID, messageName, entries); - } - return helper.newExpr(objID); - } - - private Expr visitCreateStruct(CreateStructContext ctx) { - long structID = helper.id(ctx.op); - if (ctx.entries != null) { - return helper.newMap(structID, visitMapInitializerList(ctx.entries)); - } else { - return helper.newMap(structID, Collections.emptyList()); - } - } - Expr globalCallOrMacro(long exprID, String function, Expr... args) { return globalCallOrMacro(exprID, function, Arrays.asList(args)); } @@ -872,12 +736,111 @@ ByteString unquoteBytes(Object ctx, String value) { String unquoteString(Object ctx, String value) { try { ByteBuffer buf = Unescape.unescape(value, false); - return Unescape.toUtf8(buf); } catch (Exception e) { reportError(ctx, e.toString()); return value; } } + + Expr reportError(Object ctx, String message) { + return reportError(ctx, "%s", message); + } + + Expr reportError(Object ctx, String format, Object... args) { + Location loc = Location.NoLocation; + if (ctx instanceof Location) { + loc = (Location) ctx; + } else if (ctx instanceof Node) { + loc = location((Node) ctx); + } + Expr err = helper.newExpr(ctx); + errors.reportError(loc, format, args); + return err; + } + + private String fieldName(Node node) { + if (node instanceof Field) { + return fieldName(significantChildren(node).get(0)); + } + String text = tokenText(node); + if (text.length() >= 2 && text.charAt(0) == '`' && text.charAt(text.length() - 1) == '`') { + return text.substring(1, text.length() - 1); + } + return text; + } + + private Node firstExpressionChild(List children, Node ctx) { + for (Node child : children) { + if (!isStructuralToken(child)) { + return child; + } + } + return ctx; + } + } + + private static List significantChildren(Node node) { + if (node == null) { + return Collections.emptyList(); + } + List children = new ArrayList<>(); + for (Node child : node.children()) { + if (!isToken(child, EOF)) { + children.add(child); + } + } + return children; + } + + private static boolean isLiteralToken(Node node) { + return isToken(node, NUM_INT) + || isToken(node, NUM_UINT) + || isToken(node, NUM_FLOAT) + || isToken(node, STRING) + || isToken(node, BYTES) + || isToken(node, TRUE) + || isToken(node, FALSE) + || isToken(node, NULL); + } + + private static boolean isStructuralToken(Node node) { + return isToken(node, COMMA) + || isToken(node, COLON) + || isToken(node, LPAREN) + || isToken(node, RPAREN) + || isToken(node, LBRACKET) + || isToken(node, RBRACKET) + || isToken(node, LBRACE) + || isToken(node, RBRACE); + } + + private static boolean isToken(Node node, Token.TokenType type) { + return node instanceof Token && node.getType() == type; + } + + private static String tokenText(Node node) { + return node == null ? "" : node.getSource(); + } + + private static int indexOf(List children, Token.TokenType type) { + for (int i = 0; i < children.size(); i++) { + if (isToken(children.get(i), type)) { + return i; + } + } + return -1; } + + @SuppressWarnings("unchecked") + private static T firstChildOfType(List children, Class type) { + for (Node child : children) { + if (type.isInstance(child)) { + return (T) child; + } + } + return null; + } + + private record ListElements(List expressions, List optionalIndices) {} } diff --git a/core/src/main/java/org/projectnessie/cel/parser/StringCharStream.java b/core/src/main/java/org/projectnessie/cel/parser/StringCharStream.java deleted file mode 100644 index bc29ec06..00000000 --- a/core/src/main/java/org/projectnessie/cel/parser/StringCharStream.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2021 The Authors of CEL-Java - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.projectnessie.cel.parser; - -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.CharStream; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.IntStream; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.misc.Interval; - -public final class StringCharStream implements CharStream { - - private final String buf; - private final String src; - private int pos; - - public StringCharStream(String buf, String src) { - this.buf = buf; - this.src = src; - } - - @Override - public void consume() { - if (pos >= buf.length()) { - throw new RuntimeException("cannot consume EOF"); - } - pos++; - } - - @Override - public int LA(int offset) { - if (offset == 0) { - return 0; - } - if (offset < 0) { - offset++; - } - pos = pos + offset - 1; - if (pos < 0 || pos >= buf.length()) { - return IntStream.EOF; - } - return buf.charAt(pos); - } - - @Override - public int mark() { - return -1; - } - - @Override - public void release(int marker) {} - - @Override - public int index() { - return pos; - } - - @Override - public void seek(int index) { - if (index <= pos) { - pos = index; - return; - } - pos = Math.min(index, buf.length()); - } - - @Override - public int size() { - return buf.length(); - } - - @Override - public String getSourceName() { - return src; - } - - @Override - public String getText(Interval interval) { - int start = interval.a; - int stop = interval.b; - if (stop >= buf.length()) { - stop = buf.length() - 1; - } - if (start >= buf.length()) { - return ""; - } - return buf.substring(start, stop + 1); - } - - @Override - public String toString() { - return buf; - } -} diff --git a/core/src/main/java/org/projectnessie/cel/parser/Unescape.java b/core/src/main/java/org/projectnessie/cel/parser/Unescape.java index dc260f3f..a6a8ef35 100644 --- a/core/src/main/java/org/projectnessie/cel/parser/Unescape.java +++ b/core/src/main/java/org/projectnessie/cel/parser/Unescape.java @@ -90,7 +90,7 @@ public static ByteBuffer unescape(String value, boolean isBytes) { } // Otherwise the string contains escape characters. - ByteBuffer buf = ByteBuffer.allocate(value.length() * 3 / 2); + ByteBuffer buf = ByteBuffer.allocate(value.length() * 4); for (int i = 0; i < n; i++) { char c = value.charAt(i); if (c == '\\') { @@ -215,7 +215,11 @@ public static ByteBuffer unescape(String value, boolean isBytes) { } else { // not an escape sequence if (!isBytes) { - encodeCodePoint(buf, c, cb, enc); + int codePoint = value.codePointAt(i); + encodeCodePoint(buf, codePoint, cb, enc); + if (Character.charCount(codePoint) == 2) { + i++; + } } else { buf.put((byte) c); } diff --git a/core/src/test/java/org/projectnessie/ListContainsTest.java b/core/src/test/java/org/projectnessie/ListContainsTest.java index dfe904e6..e4bb2f04 100644 --- a/core/src/test/java/org/projectnessie/ListContainsTest.java +++ b/core/src/test/java/org/projectnessie/ListContainsTest.java @@ -17,8 +17,8 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto; import com.google.api.expr.v1alpha1.UnknownSet; +import dev.cel.expr.conformance.proto3.TestAllTypes; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; @@ -45,7 +45,7 @@ public void dynamicProtobufFieldLookupSuccess() throws ScriptException { arguments.put("this", 1); arguments.put("rules", rule); - String result = script.execute(String.class, arguments); + String result = script.executeWithActivation(String.class, arguments); assertThat(result).isEqualTo(""); } @@ -66,17 +66,14 @@ public void dynamicProtobufFieldLookupFailure() throws ScriptException { arguments.put("this", 15); arguments.put("rules", rule); - String result = script.execute(String.class, arguments); + String result = script.executeWithActivation(String.class, arguments); assertThat(result).isEqualTo("value must be in list"); } @Test public void uintInList() throws ScriptException { - TestAllTypesProto.TestAllTypes rule = - TestAllTypesProto.TestAllTypes.newBuilder() - .addRepeatedFixed32(2) - .addRepeatedFixed32(3) - .build(); + TestAllTypes rule = + TestAllTypes.newBuilder().addRepeatedFixed32(2).addRepeatedFixed32(3).build(); ScriptHost scriptHost = ScriptHost.newBuilder().build(); Script script = scriptHost @@ -89,17 +86,14 @@ public void uintInList() throws ScriptException { Map arguments = new HashMap<>(); arguments.put("rules", rule); - Boolean result = script.execute(Boolean.class, arguments); + Boolean result = script.executeWithActivation(Boolean.class, arguments); assertThat(result).isTrue(); } @Test public void uintNotInList() throws ScriptException { - TestAllTypesProto.TestAllTypes rule = - TestAllTypesProto.TestAllTypes.newBuilder() - .addRepeatedFixed32(2) - .addRepeatedFixed32(3) - .build(); + TestAllTypes rule = + TestAllTypes.newBuilder().addRepeatedFixed32(2).addRepeatedFixed32(3).build(); ScriptHost scriptHost = ScriptHost.newBuilder().build(); Script script = scriptHost @@ -112,7 +106,7 @@ public void uintNotInList() throws ScriptException { Map arguments = new HashMap<>(); arguments.put("rules", rule); - Boolean result = script.execute(Boolean.class, arguments); + Boolean result = script.executeWithActivation(Boolean.class, arguments); assertThat(result).isFalse(); } } diff --git a/core/src/test/java/org/projectnessie/cel/CELTest.java b/core/src/test/java/org/projectnessie/cel/CELTest.java index 992ce828..8b58ac2d 100644 --- a/core/src/test/java/org/projectnessie/cel/CELTest.java +++ b/core/src/test/java/org/projectnessie/cel/CELTest.java @@ -126,6 +126,103 @@ void AstToProto() { assertThat(ast3.getExpr()).isEqualTo(astIss2.getAst().getExpr()); } + @Test + void comprehensionLocalVariablesShadowNamespacedIdentifiers() { + Env env = + newEnv( + container("com.example"), + declarations( + Decls.newVar("com.example.y", Decls.Int), + Decls.newVar("y", Decls.String), + Decls.newVar("com.example.y.z", Decls.Int))); + + AstIssuesTuple astIss = env.compile("[0].exists(y, y == 0)"); + assertThat(astIss.hasIssues()).isFalse(); + assertThat(env.program(astIss.getAst()).eval(mapOf("com.example.y", 42L)).getVal()) + .isSameAs(True); + + astIss = env.compile("['compre'].exists(y, .y == 'y')"); + assertThat(astIss.hasIssues()).isFalse(); + assertThat(env.program(astIss.getAst()).eval(mapOf("y", "y")).getVal()).isSameAs(True); + + astIss = env.compile("[{'z': 0}].exists(y, y.z == 0)"); + assertThat(astIss.hasIssues()).isFalse(); + assertThat(env.program(astIss.getAst()).eval(mapOf("com.example.y.z", 42L)).getVal()) + .isSameAs(True); + } + + @Test + void bindMacroIntroducesLocalVariable() { + Env env = + newEnv(container("com.example"), declarations(Decls.newVar("com.example.x", Decls.Int))); + + AstIssuesTuple astIss = env.compile("cel.bind(x, 1, x + 1)"); + assertThat(astIss.hasIssues()).isFalse(); + assertThat( + env.program(astIss.getAst()) + .eval(mapOf("com.example.x", 42L)) + .getVal() + .equal(IntT.intOf(2))) + .isSameAs(True); + + astIss = env.compile("cel.bind(x, {'y': 0}, x.y == 0)"); + assertThat(astIss.hasIssues()).isFalse(); + assertThat(env.program(astIss.getAst()).eval(mapOf("com.example.x.y", 42L)).getVal()) + .isSameAs(True); + } + + @Test + void blockMacrosIntroduceSequentialLocalVariables() { + Env env = newEnv(macros(Macro.TestOnlyBlockMacros)); + + AstIssuesTuple astIss = + env.compile("cel.block([1, cel.index(0) + 1, cel.index(1) + 1], cel.index(2))"); + + assertThat(astIss.hasIssues()).isFalse(); + assertThat(env.program(astIss.getAst()).eval(emptyMap()).getVal().equal(IntT.intOf(3))) + .isSameAs(True); + } + + @Test + void blockMacrosCanNameComprehensionVariables() { + Env env = newEnv(macros(Macro.TestOnlyBlockMacros)); + + AstIssuesTuple astIss = + env.compile( + "[1, 2].map(cel.iterVar(0, 0), " + + "[cel.iterVar(0, 0) + cel.iterVar(0, 0)])[1][0] == 4"); + + assertThat(astIss.hasIssues()).isFalse(); + assertThat(env.program(astIss.getAst()).eval(emptyMap()).getVal()).isSameAs(True); + } + + @Test + void twoVariableComprehensionsBindListIndexAndValue() { + Env env = newEnv(); + + AstIssuesTuple astIss = env.compile("[2, 4, 6].transformList(i, v, i + v)[2] == 8"); + + assertThat(astIss.hasIssues()).isFalse(); + assertThat(env.program(astIss.getAst()).eval(emptyMap()).getVal()).isSameAs(True); + assertThat( + env.program(astIss.getAst(), evalOptions(OptExhaustiveEval)).eval(emptyMap()).getVal()) + .isSameAs(True); + } + + @Test + void twoVariableComprehensionsBindMapKeyAndValue() { + Env env = newEnv(); + + AstIssuesTuple astIss = + env.compile("{'foo': 'bar'}.transformMap(k, v, k + v)['foo'] == 'foobar'"); + + assertThat(astIss.hasIssues()).isFalse(); + assertThat(env.program(astIss.getAst()).eval(emptyMap()).getVal()).isSameAs(True); + assertThat( + env.program(astIss.getAst(), evalOptions(OptExhaustiveEval)).eval(emptyMap()).getVal()) + .isSameAs(True); + } + @Test void AstToString() { Env stdEnv = newEnv(); @@ -198,6 +295,7 @@ void Abbrevs_Parsed() { } @Test + @SuppressWarnings("removal") void Abbrevs_Disambiguation() { Env env = newEnv( @@ -244,6 +342,7 @@ void CustomEnv() { } @Test + @SuppressWarnings("removal") void CustomEnvCanSubsetStandardLibrary() { Env withoutStdLib = newCustomEnv(declarations(Decls.newVar("resource.name", Decls.String))); AstIssuesTuple missingStartsWith = @@ -556,15 +655,13 @@ void GlobalVars() { if (args.length != 3) { return newErr("invalid arguments to 'get'"); } - if (!(args[0] instanceof Mapper)) { + if (!(args[0] instanceof Mapper attrs)) { return newErr( "invalid operand of type '%s' to obj.get(key, def)", args[0].type()); } - Mapper attrs = (Mapper) args[0]; - if (!(args[1] instanceof StringT)) { + if (!(args[1] instanceof StringT key)) { return newErr("invalid key of type '%s' to obj.get(key, def)", args[1].type()); } - StringT key = (StringT) args[1]; Val defVal = args[2]; if (attrs.contains(key) == True) { return attrs.get(key); @@ -741,9 +838,7 @@ void EnvExtension() { Env e2 = e.extend( customTypeAdapter(DefaultTypeAdapter.Instance), - types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance())); + types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance())); assertThat(e).isNotEqualTo(e2); assertThat(e.getTypeAdapter()).isNotEqualTo(e2.getTypeAdapter()); assertThat(e.getTypeProvider()).isNotEqualTo(e2.getTypeProvider()); @@ -756,22 +851,18 @@ void EnvExtension() { void EnvExtensionIsolation() { Env baseEnv = newEnv( - container("google.api.expr.test.v1"), + container("cel.expr.conformance"), declarations( Decls.newVar("age", Decls.Int), Decls.newVar("gender", Decls.String), Decls.newVar("country", Decls.String))); Env env1 = baseEnv.extend( - types( - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes - .getDefaultInstance()), + types(dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()), declarations(Decls.newVar("name", Decls.String))); Env env2 = baseEnv.extend( - types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()), + types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()), declarations(Decls.newVar("group", Decls.String))); AstIssuesTuple astIss = env2.compile("size(group) > 10 && !has(proto3.TestAllTypes{}.single_int32)"); @@ -831,10 +922,9 @@ void CustomInterpreterDecorator() { i -> { lastInstruction.set(i); // Only optimize the instruction if it is a call. - if (!(i instanceof InterpretableCall)) { + if (!(i instanceof InterpretableCall call)) { return i; } - InterpretableCall call = (InterpretableCall) i; // Only optimize the math functions when they have constant arguments. switch (call.function()) { case "_+_": diff --git a/core/src/test/java/org/projectnessie/cel/NativePlanRootTest.java b/core/src/test/java/org/projectnessie/cel/NativePlanRootTest.java new file mode 100644 index 00000000..18c2a34d --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/NativePlanRootTest.java @@ -0,0 +1,702 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EvalOption.OptDisableNativeEval; +import static org.projectnessie.cel.EvalOption.OptExhaustiveEval; +import static org.projectnessie.cel.EvalOption.OptTrackState; +import static org.projectnessie.cel.ProgramOption.customDecorator; +import static org.projectnessie.cel.ProgramOption.evalOptions; +import static org.projectnessie.cel.ProgramOption.functions; +import static org.projectnessie.cel.checker.Decls.Int; +import static org.projectnessie.cel.checker.Decls.newFunction; +import static org.projectnessie.cel.checker.Decls.newOverload; +import static org.projectnessie.cel.checker.Decls.newVar; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.UnknownT.unknownOf; +import static org.projectnessie.cel.interpreter.Activation.newActivation; +import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.NullT; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.Activation; +import org.projectnessie.cel.interpreter.ActivationFunction; +import org.projectnessie.cel.interpreter.Interpretable; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableCall; +import org.projectnessie.cel.interpreter.functions.Overload; + +class NativePlanRootTest { + + @TestFactory + Stream scalarConstantAndIdentifierRootsBelongOnlyToTheIntegratedPlanner() { + Map nullInput = new HashMap<>(); + nullInput.put("n", null); + List cases = + List.of( + new RootCase("x", Map.of("x", 41L)), + new RootCase("s", Map.of("s", "value")), + new RootCase("d", Map.of("d", 1.5d)), + new RootCase("b", Map.of("b", true)), + new RootCase("n", nullInput), + new RootCase("41", Map.of()), + new RootCase("'value'", Map.of()), + new RootCase("1.5", Map.of()), + new RootCase("true", Map.of()), + new RootCase("null", Map.of())); + + return integratedRootTests(cases); + } + + @TestFactory + Stream strictScalarOperationRootsBelongOnlyToTheIntegratedPlanner() { + Map values = Map.of("x", 42L, "d", 42.5d, "s", "value", "b", false); + return integratedRootTests( + Stream.of( + "!b", + "-x", + "-d", + "x + 1", + "x - 1", + "x * 2", + "x / 2", + "x % 2", + "d + 1.0", + "d - 1.0", + "d * 2.0", + "d / 2.0", + "s + '-suffix'", + "b < true", + "b <= true", + "b > true", + "b >= true", + "x < 43", + "x <= 42", + "x > 41", + "x >= 42", + "d < 43.0", + "d <= 42.5", + "d > 41.0", + "d >= 42.5", + "s < 'z'", + "s <= 'value'", + "s > 'a'", + "s >= 'value'") + .map(expression -> new RootCase(expression, values)) + .toList()); + } + + @TestFactory + Stream scalarControlRootsBelongOnlyToTheIntegratedPlanner() { + Map values = new HashMap<>(); + values.put("x", 42L); + values.put("y", 41L); + values.put("d", 42.5d); + values.put("e", 42.5d); + values.put("s", "value"); + values.put("t", "other"); + values.put("b", true); + values.put("c", false); + values.put("n", null); + + return integratedRootTests( + Stream.of( + "b == c", + "b != c", + "x == y", + "x != y", + "d == e", + "d != e", + "s == t", + "s != t", + "n == null", + "n != null", + "b && c", + "b || c", + "b ? c : true", + "b ? x : y", + "b ? d : e", + "b ? s : t", + "b ? n : null") + .map(expression -> new RootCase(expression, values)) + .toList()); + } + + @TestFactory + Stream mapSelectorRootsBelongOnlyToTheIntegratedPlanner() { + Map nullValues = new HashMap<>(); + nullValues.put("answer", null); + List cases = + List.of( + new RootCase("bools.answer", Map.of("bools", Map.of("answer", true))), + new RootCase("bools['answer']", Map.of("bools", Map.of("answer", true))), + new RootCase("ints.answer", Map.of("ints", Map.of("answer", 41L))), + new RootCase("ints['answer']", Map.of("ints", Map.of("answer", 41L))), + new RootCase("doubles.answer", Map.of("doubles", Map.of("answer", -0.0d))), + new RootCase("doubles['answer']", Map.of("doubles", Map.of("answer", -0.0d))), + new RootCase("strings.answer", Map.of("strings", Map.of("answer", "value"))), + new RootCase("strings['answer']", Map.of("strings", Map.of("answer", "value"))), + new RootCase("nulls.answer", Map.of("nulls", nullValues)), + new RootCase("nulls['answer']", Map.of("nulls", nullValues))); + + return integratedRootTests(mapSelectorEnv(), cases); + } + + @TestFactory + Stream topLevelListConsumerRootsBelongOnlyToTheIntegratedPlanner() { + List nullValues = new ArrayList<>(); + nullValues.add(null); + List cases = + List.of( + new RootCase("flags[1]", Map.of("flags", List.of(true, false))), + new RootCase("numbers[1]", Map.of("numbers", new long[] {7L, 41L})), + new RootCase("doubles[1]", Map.of("doubles", new double[] {1.0d, -0.0d})), + new RootCase("words[1]", Map.of("words", new String[] {"zero", "value"})), + new RootCase("nulls[0]", Map.of("nulls", nullValues)), + new RootCase( + "wordTarget in words", + Map.of("wordTarget", "value", "words", new String[] {"zero", "value"}))); + + return integratedRootTests(listEnv(), cases); + } + + @TestFactory + Stream scalarListLiteralConsumerRootsBelongOnlyToTheIntegratedPlanner() { + Map values = + Map.of( + "b", true, "c", false, "x", 41L, "y", 42L, "d", 1.0d, "e", -0.0d, "s", "value", "t", + "other"); + return integratedRootTests( + List.of( + new RootCase("[b, c][1]", values), + new RootCase("[x, y][0]", values), + new RootCase("[d, e][1]", values), + new RootCase("[s, t][0]", values), + new RootCase("size([x, y])", values), + new RootCase("[x, y].size()", values), + new RootCase("s in [t, s]", values))); + } + + @TestFactory + Stream canonicalQuantifierRootsBelongOnlyToTheIntegratedPlanner() { + return integratedRootTests( + listEnv(), + List.of( + new RootCase( + "flags.exists(value, value)", Map.of("flags", new Boolean[] {false, true})), + new RootCase("flags.all(value, value)", Map.of("flags", new Boolean[] {true, true})), + new RootCase( + "flags.exists_one(value, value)", Map.of("flags", new Boolean[] {false, true})), + new RootCase( + "numbers.exists(value, value == 2)", Map.of("numbers", new long[] {1L, 2L})), + new RootCase("numbers.all(value, value > 0)", Map.of("numbers", new long[] {1L, 2L})), + new RootCase( + "numbers.exists_one(value, value == 2)", Map.of("numbers", new long[] {1L, 2L})), + new RootCase( + "doubles.exists(value, value == 1.5)", + Map.of("doubles", new double[] {1.0d, 1.5d})), + new RootCase( + "doubles.all(value, value > 0.0)", Map.of("doubles", new double[] {1.0d, 1.5d})), + new RootCase( + "doubles.exists_one(value, value == 1.5)", + Map.of("doubles", new double[] {1.0d, 1.5d})), + new RootCase( + "words.exists(value, value == 'value')", + Map.of("words", new String[] {"other", "value"})), + new RootCase( + "words.all(value, value != 'absent')", + Map.of("words", new String[] {"other", "value"})), + new RootCase( + "words.exists_one(value, value == 'value')", + Map.of("words", new String[] {"other", "value"})))); + } + + @TestFactory + Stream canonicalListFoldConsumersBelongOnlyToTheIntegratedPlanner() { + return integratedRootTests( + listEnv(), + List.of( + new RootCase( + "size(numbers.filter(value, value > 0))", + Map.of("numbers", new long[] {-1L, 1L, 2L})), + new RootCase( + "numbers.map(value, value + 1).size()", Map.of("numbers", new long[] {1L, 2L})), + new RootCase( + "size(numbers.map(value, value > 0, value + 1))", + Map.of("numbers", new long[] {-1L, 1L, 2L})), + new RootCase( + "flags.filter(value, value)[0]", Map.of("flags", new Boolean[] {false, true})), + new RootCase( + "numbers.map(value, value + 1)[0]", Map.of("numbers", new long[] {1L, 2L})), + new RootCase( + "doubles.map(value, value + 0.5)[0]", Map.of("doubles", new double[] {1.0d, 2.0d})), + new RootCase( + "words.map(value, value + '!')[0]", Map.of("words", new String[] {"cel", "java"})), + new RootCase("numbers.map(value, null)[0]", Map.of("numbers", new long[] {1L, 2L})), + new RootCase( + "numbers.map(value, value > 0, value + 1)[0]", + Map.of("numbers", new long[] {-1L, 1L, 2L})))); + } + + @TestFactory + Stream canonicalMappedStringMembershipBelongsOnlyToTheIntegratedPlanner() { + return integratedRootTests( + listEnv(), + List.of( + new RootCase( + "wordTarget in words.map(value, value)", + Map.of("wordTarget", "cel", "words", new String[] {"other", "cel"})), + new RootCase( + "wordTarget in words.map(value, value + '!')", + Map.of("wordTarget", "cel!", "words", new String[] {"other", "cel"})), + new RootCase( + "wordTarget in words.map(value, value != 'skip', value + '!')", + Map.of("wordTarget", "cel!", "words", new String[] {"other", "skip", "cel"})), + new RootCase( + "wordTarget in words.filter(value, value != 'skip')", + Map.of("wordTarget", "cel", "words", new String[] {"other", "skip", "cel"})))); + } + + @TestFactory + Stream canonicalMappedIntegerQuantifiersBelongOnlyToTheIntegratedPlanner() { + Map input = Map.of("numbers", new long[] {1L, 2L, 3L}); + return integratedRootTests( + listEnv(), + Stream.of( + "numbers.map(value, value + 1).exists(mapped, mapped == 3)", + "numbers.map(value, value + 1).all(mapped, mapped > 1)", + "numbers.map(value, value + 1).exists_one(mapped, mapped == 3)", + "numbers.map(value, value > 1, value + 1).exists(mapped, mapped == 3)", + "numbers.map(value, value > 1, value + 1).all(mapped, mapped > 2)", + "numbers.map(value, value > 1, value + 1).exists_one(mapped, mapped == 3)", + "numbers.filter(value, value > 1).exists(mapped, mapped == 2)", + "numbers.filter(value, value > 1).all(mapped, mapped > 1)", + "numbers.filter(value, value > 1).exists_one(mapped, mapped == 2)") + .map(expression -> new RootCase(expression, input)) + .toList()); + } + + @TestFactory + Stream mapSelectorsPreserveEstablishedCompatibilityResults() { + Map nullValue = new HashMap<>(); + nullValue.put("answer", null); + Map nullSource = new HashMap<>(); + nullSource.put("ints", null); + List cases = + List.of( + new CompatibilityCase("native value", Map.of("ints", Map.of("answer", 41L))), + new CompatibilityCase("boxed value", Map.of("ints", Map.of("answer", 41))), + new CompatibilityCase("wrapped value", Map.of("ints", Map.of("answer", intOf(41L)))), + new CompatibilityCase( + "wrapped source", + Map.of("ints", DefaultTypeAdapter.Instance.nativeToValue(Map.of("answer", 41L)))), + new CompatibilityCase( + "carried error", Map.of("ints", Map.of("answer", newErr("carried")))), + new CompatibilityCase( + "carried unknown", Map.of("ints", Map.of("answer", unknownOf(51L)))), + new CompatibilityCase("wrong value", Map.of("ints", Map.of("answer", "wrong"))), + new CompatibilityCase("present null value", Map.of("ints", nullValue)), + new CompatibilityCase("absent key", Map.of("ints", Map.of("other", 41L))), + new CompatibilityCase("source error", Map.of("ints", newErr("source"))), + new CompatibilityCase("source unknown", Map.of("ints", unknownOf(52L))), + new CompatibilityCase("wrong source", Map.of("ints", "wrong source")), + new CompatibilityCase("present null source", nullSource), + new CompatibilityCase("absent source", Map.of())); + + return Stream.of("ints.answer", "ints['answer']") + .flatMap( + expression -> + cases.stream() + .map( + testCase -> + DynamicTest.dynamicTest( + expression + ": " + testCase.label(), + () -> { + Env env = mapSelectorEnv(); + Ast ast = compile(env, expression); + Prog integrated = (Prog) env.program(ast); + Prog established = + (Prog) env.program(ast, evalOptions(OptDisableNativeEval)); + + assertIntegratedRoot(integrated, established); + assertEquivalent( + integrated.eval(testCase.input()).getVal(), + established.eval(testCase.input()).getVal()); + }))); + } + + @TestFactory + Stream mapSelectorRootsResolveTheirSourceOnceWithoutRepeatedKeyLookup() { + return Stream.of("ints.answer", "ints['answer']") + .map( + expression -> + DynamicTest.dynamicTest( + expression, + () -> { + Env env = mapSelectorEnv(); + Ast ast = compile(env, expression); + Prog integrated = (Prog) env.program(ast); + CountingMap values = new CountingMap(); + values.put("answer", 41L); + AtomicInteger resolutions = new AtomicInteger(); + Activation activation = + newActivation( + (ActivationFunction) + name -> { + resolutions.incrementAndGet(); + return name.equals("ints") ? values : ActivationFunction.ABSENT; + }); + + assertThat(integrated.interpretable.getClass().getSimpleName()) + .isEqualTo("NativeIsland"); + assertThat(integrated.interpretable.eval(activation).intValue()) + .isEqualTo(41L); + assertThat(resolutions).hasValue(1); + assertThat(values.gets()).isEqualTo(expression.indexOf('[') < 0 ? 1 : 0); + })); + } + + @TestFactory + Stream nullMapSelectorsDistinguishPresentNullFromAnAbsentKey() { + return Stream.of("nulls.answer", "nulls['answer']") + .map( + expression -> + DynamicTest.dynamicTest( + expression, + () -> { + Env env = mapSelectorEnv(); + Ast ast = compile(env, expression); + Prog integrated = (Prog) env.program(ast); + Prog established = (Prog) env.program(ast, evalOptions(OptDisableNativeEval)); + Map presentNull = new HashMap<>(); + presentNull.put("answer", null); + + Val actualPresent = integrated.eval(Map.of("nulls", presentNull)).getVal(); + Val expectedPresent = established.eval(Map.of("nulls", presentNull)).getVal(); + Val actualAbsent = + integrated + .eval(Map.of("nulls", Map.of("other", "not selected"))) + .getVal(); + Val expectedAbsent = + established + .eval(Map.of("nulls", Map.of("other", "not selected"))) + .getVal(); + + assertIntegratedRoot(integrated, established); + assertEquivalent(actualPresent, expectedPresent); + assertThat(actualPresent).isSameAs(NullT.NullValue); + assertEquivalent(actualAbsent, expectedAbsent); + assertThat(actualAbsent).isInstanceOf(Err.class); + })); + } + + private static void assertIntegratedRoot(Prog integrated, Prog established) { + assertThat(integrated.interpretable.getClass().getSimpleName()).isEqualTo("NativeIsland"); + assertThat(established.interpretable.getClass().getSimpleName()).isNotEqualTo("NativeIsland"); + assertThat(containsNativeNode(established.interpretable)).isFalse(); + } + + private static void assertEquivalent(Val actual, Val expected) { + assertThat(actual.getClass()).isEqualTo(expected.getClass()); + assertThat(actual.type()).isSameAs(expected.type()); + assertThat(actual.toString()).isEqualTo(expected.toString()); + if (actual instanceof Err actualError && expected instanceof Err expectedError) { + assertThat(actualError.hasCause()).isEqualTo(expectedError.hasCause()); + if (actualError.hasCause()) { + assertThat(actualError.getCause().getClass()) + .isEqualTo(expectedError.getCause().getClass()); + assertThat(actualError.getCause().getMessage()) + .isEqualTo(expectedError.getCause().getMessage()); + } + return; + } + if (actual.value() instanceof Double actualDouble + && expected.value() instanceof Double expectedDouble) { + assertThat(Double.doubleToRawLongBits(actualDouble)) + .isEqualTo(Double.doubleToRawLongBits(expectedDouble)); + } else { + assertThat(actual.value()).isEqualTo(expected.value()); + } + } + + private static Stream integratedRootTests(List cases) { + return integratedRootTests(env(DefaultTypeAdapter.Instance), cases); + } + + private static Stream integratedRootTests(Env env, List cases) { + return cases.stream() + .map( + testCase -> + DynamicTest.dynamicTest( + testCase.expression(), + () -> { + Ast ast = compile(env, testCase.expression()); + Prog integrated = (Prog) env.program(ast); + Prog established = (Prog) env.program(ast, evalOptions(OptDisableNativeEval)); + + assertIntegratedRoot(integrated, established); + assertThat(integrated.interpretable.id()).isEqualTo(ast.getExpr().getId()); + assertThat(integrated.cost()).isEqualTo(established.cost()); + Val actual = integrated.eval(testCase.input()).getVal(); + Val expected = established.eval(testCase.input()).getVal(); + assertEquivalent(actual, expected); + })); + } + + @Test + void storesEitherEvaluatorAsOneInterpretableWithTheCheckedRootIdentityAndCost() { + Env env = env(DefaultTypeAdapter.Instance); + Ast ast = compile(env, "x + 1"); + Prog nativeProgram = (Prog) env.program(ast); + Prog currentProgram = (Prog) env.program(ast, evalOptions(OptDisableNativeEval)); + + assertThat(nativeProgram.interpretable.getClass().getSimpleName()).isEqualTo("NativeIsland"); + assertThat(currentProgram.interpretable.getClass().getSimpleName()) + .isNotEqualTo("NativeIsland"); + assertThat(nativeProgram.interpretable.id()).isEqualTo(ast.getExpr().getId()); + assertThat(currentProgram.interpretable.id()).isEqualTo(ast.getExpr().getId()); + assertThat(nativeProgram.cost()).isEqualTo(currentProgram.cost()); + } + + @Test + void adaptsANativeSuccessExactlyOnceAtTheExecutableRoot() { + CountingAdapter adapter = new CountingAdapter(); + Env env = env(adapter); + Prog program = (Prog) env.program(compile(env, "x + 1")); + Interpretable root = program.interpretable; + + adapter.reset(); + assertThat(root.eval(newActivation(Map.of("x", 41L))).intValue()).isEqualTo(42L); + assertThat(adapter.count()).isEqualTo(1); + + adapter.reset(); + assertThat(program.eval(Map.of("x", 41L)).getVal().intValue()).isEqualTo(42L); + assertThat(adapter.count()).isEqualTo(1); + } + + @Test + void returnsCarriedCompatibilityValuesWithoutRootReadaptation() { + CountingAdapter adapter = new CountingAdapter(); + Env env = env(adapter); + Prog intProgram = (Prog) env.program(compile(env, "x")); + Prog stringProgram = (Prog) env.program(compile(env, "s")); + Val error = newErr("carried error"); + Val unknown = unknownOf(42L); + Val nullString = stringOf(null); + + assertCarried(intProgram, adapter, "x", error); + assertCarried(intProgram, adapter, "x", unknown); + assertCarried(stringProgram, adapter, "s", nullString); + } + + @Test + void statefulProgramFactoriesAlwaysCreateAnExecutableRootForEvalAndCost() { + Env env = env(DefaultTypeAdapter.Instance); + Ast ast = compile(env, "x + 1"); + Program current = env.program(ast, evalOptions(OptDisableNativeEval)); + Program tracked = env.program(ast, evalOptions(OptTrackState)); + Program exhaustive = env.program(ast, evalOptions(OptExhaustiveEval)); + + assertThat(tracked.eval(Map.of("x", 41L)).getVal().intValue()).isEqualTo(42L); + assertThat(exhaustive.eval(Map.of("x", 41L)).getVal().intValue()).isEqualTo(42L); + assertThat(estimateCost(tracked)).isEqualTo(estimateCost(current)); + assertThat(estimateCost(exhaustive)).isEqualTo(estimateCost(current)); + } + + @Test + void onlyTheEmptyEvalOptionSubsetPermitsNativePlanning() { + EvalOption[] values = EvalOption.values(); + assertThat(values).hasSize(5); + Env env = + newEnv( + declarations( + newVar("x", Int), + newFunction("opaque", newOverload("opaque_int", List.of(Int), Int)))); + Ast ast = compile(env, "opaque(x + 1)"); + Overload opaque = Overload.unary("opaque_int", value -> value); + + for (int mask = 0; mask < 1 << values.length; mask++) { + Set subset = EnumSet.noneOf(EvalOption.class); + for (int bit = 0; bit < values.length; bit++) { + if ((mask & (1 << bit)) != 0) { + subset.add(values[bit]); + } + } + + assertThat(CEL.nativePlanningPermitted(subset, List.of())) + .as(subset.toString()) + .isEqualTo(subset.isEmpty()); + + Program program = + env.program(ast, functions(opaque), evalOptions(subset.toArray(EvalOption[]::new))); + assertThat(program.eval(Map.of("x", 41L)).getVal().intValue()) + .as(subset.toString()) + .isEqualTo(42L); + + if (program instanceof Prog prog) { + assertThat(containsNativeNode(prog.interpretable)) + .as(subset.toString()) + .isEqualTo(subset.isEmpty()); + } + } + } + + @Test + void customDecoratorsDisableNativePlanningBeforeAnyNodeIsBuilt() { + Env env = env(DefaultTypeAdapter.Instance); + Ast ast = compile(env, "x + 1"); + List observed = new ArrayList<>(); + + Prog program = + (Prog) + env.program( + ast, + customDecorator( + node -> { + observed.add(node.getClass().getName()); + return node; + })); + + assertThat(CEL.nativePlanningPermitted(Set.of(), List.of(node -> node))).isFalse(); + assertThat(observed).noneMatch(name -> name.contains("Native")); + assertThat(containsNativeNode(program.interpretable)).isFalse(); + } + + private static boolean containsNativeNode(Interpretable node) { + if (node.getClass().getSimpleName().startsWith("Native")) { + return true; + } + if (node instanceof InterpretableCall call) { + for (Interpretable argument : call.args()) { + if (containsNativeNode(argument)) { + return true; + } + } + } + return false; + } + + private static void assertCarried( + Prog program, CountingAdapter adapter, String variable, Val carried) { + adapter.reset(); + assertThat(program.interpretable.eval(newActivation(Map.of(variable, carried)))) + .isSameAs(carried); + assertThat(adapter.count()).isEqualTo(1); + + adapter.reset(); + assertThat(program.eval(Map.of(variable, carried)).getVal()).isSameAs(carried); + assertThat(adapter.count()).isEqualTo(1); + } + + private static Env env(StandardScalarTypeAdapter adapter) { + return newEnv( + customTypeAdapter(adapter), + declarations( + Decls.newVar("x", Decls.Int), + Decls.newVar("y", Decls.Int), + Decls.newVar("s", Decls.String), + Decls.newVar("t", Decls.String), + Decls.newVar("d", Decls.Double), + Decls.newVar("e", Decls.Double), + Decls.newVar("b", Decls.Bool), + Decls.newVar("c", Decls.Bool), + Decls.newVar("n", Decls.Null))); + } + + private static Env mapSelectorEnv() { + return newEnv( + declarations( + Decls.newVar("bools", Decls.newMapType(Decls.String, Decls.Bool)), + Decls.newVar("ints", Decls.newMapType(Decls.String, Decls.Int)), + Decls.newVar("doubles", Decls.newMapType(Decls.String, Decls.Double)), + Decls.newVar("strings", Decls.newMapType(Decls.String, Decls.String)), + Decls.newVar("nulls", Decls.newMapType(Decls.String, Decls.Null)))); + } + + private static Env listEnv() { + return newEnv( + declarations( + Decls.newVar("flags", Decls.newListType(Decls.Bool)), + Decls.newVar("numbers", Decls.newListType(Decls.Int)), + Decls.newVar("doubles", Decls.newListType(Decls.Double)), + Decls.newVar("words", Decls.newListType(Decls.String)), + Decls.newVar("nulls", Decls.newListType(Decls.Null)), + Decls.newVar("wordTarget", Decls.String))); + } + + private static Ast compile(Env env, String expression) { + AstIssuesTuple result = env.compile(expression); + assertThat(result.hasIssues()).as(expression).isFalse(); + return result.getAst(); + } + + private static final class CountingAdapter implements StandardScalarTypeAdapter { + private final AtomicInteger conversions = new AtomicInteger(); + + @Override + public Val nativeToValue(Object value) { + conversions.incrementAndGet(); + return DefaultTypeAdapter.Instance.nativeToValue(value); + } + + void reset() { + conversions.set(0); + } + + int count() { + return conversions.get(); + } + } + + private static final class CountingMap extends HashMap { + private int gets; + + @Override + public Object get(Object key) { + gets++; + return super.get(key); + } + + int gets() { + return gets; + } + } + + private record RootCase(String expression, Map input) {} + + private record CompatibilityCase(String label, Map input) {} +} diff --git a/core/src/test/java/org/projectnessie/cel/NativePlanTest.java b/core/src/test/java/org/projectnessie/cel/NativePlanTest.java new file mode 100644 index 00000000..a281f327 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/NativePlanTest.java @@ -0,0 +1,4198 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.projectnessie.cel.CEL.attributePattern; +import static org.projectnessie.cel.CEL.partialVars; +import static org.projectnessie.cel.Env.newCustomEnv; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EnvOption.types; +import static org.projectnessie.cel.EvalOption.OptDisableNativeEval; +import static org.projectnessie.cel.EvalOption.OptOptimize; +import static org.projectnessie.cel.EvalOption.OptPartialEval; +import static org.projectnessie.cel.ProgramOption.customDecorator; +import static org.projectnessie.cel.ProgramOption.evalOptions; +import static org.projectnessie.cel.ProgramOption.functions; +import static org.projectnessie.cel.ProgramOption.globals; +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.UnknownT.unknownOf; +import static org.projectnessie.cel.extension.OptionalLib.optionals; +import static org.projectnessie.cel.interpreter.Activation.newActivation; + +import com.google.protobuf.DynamicMessage; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; +import org.jspecify.annotations.NonNull; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.IntT; +import org.projectnessie.cel.common.types.Overloads; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.pb.ProtoTypeRegistry; +import org.projectnessie.cel.common.types.ref.StandardScalarFieldProvider; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Adder; +import org.projectnessie.cel.common.types.traits.Trait; +import org.projectnessie.cel.interpreter.ActivationFunction; +import org.projectnessie.cel.interpreter.Interpretable; +import org.projectnessie.cel.interpreter.functions.Overload; + +class NativePlanTest { + private final Env env = + newEnv( + declarations( + Decls.newVar("x", Decls.Int), + Decls.newVar("y", Decls.Int), + Decls.newVar("d", Decls.Double), + Decls.newVar("e", Decls.Double), + Decls.newVar("b", Decls.Bool), + Decls.newVar("c", Decls.Bool), + Decls.newVar("s", Decls.String), + Decls.newVar("n", Decls.Null), + Decls.newVar("attrs", Decls.newMapType(Decls.String, Decls.Int)), + Decls.newVar("labels", Decls.newMapType(Decls.String, Decls.String)), + Decls.newVar("numbers", Decls.newListType(Decls.Int)), + Decls.newVar("doubles", Decls.newListType(Decls.Double)), + Decls.newVar("words", Decls.newListType(Decls.String)), + Decls.newVar("flags", Decls.newListType(Decls.Bool)), + Decls.newVar("key", Decls.String), + Decls.newVar("target", Decls.Int), + Decls.newVar("wordTarget", Decls.String))); + + @Test + void builtInAdaptersDeclareStandardScalarSemantics() { + assertThat(DefaultTypeAdapter.Instance).isInstanceOf(StandardScalarTypeAdapter.class); + assertThat(ProtoTypeRegistry.newRegistry()).isInstanceOf(StandardScalarTypeAdapter.class); + assertThat(ProtoTypeRegistry.newRegistry()).isInstanceOf(StandardScalarFieldProvider.class); + } + + static Stream supportedScalarEvaluations() { + return Stream.of( + new Evaluation("x", Map.of("x", 41L)), + new Evaluation("x", Map.of("x", "not an int")), + new Evaluation("x + 1", Map.of("x", intOf(41L))), + new Evaluation("d + 1.0", Map.of("d", doubleOf(41.5d))), + new Evaluation("b && true", Map.of("b", True)), + new Evaluation("s + '!'", Map.of("s", stringOf("hello"))), + new Evaluation("s", Map.of("s", stringOf(null))), + new Evaluation("b ? s : 'unused'", Map.of("b", true, "s", stringOf(null))), + new Evaluation("s + ' suffix'", Map.of("s", stringOf(null))), + new Evaluation("'prefix ' + s", Map.of("s", stringOf(null))), + new Evaluation("x + 1", Map.of("x", 41L)), + new Evaluation("(x + 1) * (y - 2) / 3 % 5", Map.of("x", 8L, "y", 7L)), + new Evaluation("-(x + 1)", Map.of("x", 41L)), + new Evaluation("d / 3.0 + 0.5", Map.of("d", -0.0d)), + new Evaluation("s + ' world' + '!'", Map.of("s", "hello")), + new Evaluation("b && x < y", Map.of("b", false, "x", 1L, "y", 2L)), + new Evaluation("b || x >= y", Map.of("b", true, "x", 1L, "y", 2L)), + new Evaluation("!(x == y)", Map.of("x", 1L, "y", 2L)), + new Evaluation("b ? x + 1 : y + 2", Map.of("b", true, "x", 1L, "y", 9L)), + new Evaluation("d == -0.0", Map.of("d", 0.0d)), + new Evaluation("d != d", Map.of("d", Double.NaN)), + new Evaluation("b ? x / y : x + y", Map.of("b", true, "x", 1L, "y", 0L)), + new Evaluation("b ? x : y", Map.of("b", "not a bool", "x", 1L, "y", 2L)), + new Evaluation("x / y", Map.of("x", 1L, "y", 0L)), + new Evaluation("x + 1", Map.of("x", Long.MAX_VALUE)), + new Evaluation("x + 1", Map.of("x", "not an int")), + new Evaluation("x + 1", Map.of("x", IntT.IntType)), + new Evaluation("x", Map.of())); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("supportedScalarEvaluations") + void evaluatesSupportedScalarTreesLikeTheExistingInterpreter(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @ParameterizedTest(name = "{index}: {0}") + @ValueSource(strings = {"s == 'value'", "s < 'value'"}) + void nullBackedStringOperationsUseTheCurrentCompatibilityPath(String expression) { + Programs programs = programs(expression); + Object input = Map.of("s", stringOf(null)); + + Throwable nativeFailure = catchThrowable(() -> programs.nativeProgram.eval(input)); + Throwable interpreterFailure = catchThrowable(() -> programs.interpreterProgram.eval(input)); + + assertThat(nativeFailure).isNotNull(); + assertThat(interpreterFailure).isNotNull(); + assertThat(nativeFailure.getClass()).isEqualTo(interpreterFailure.getClass()); + assertThat(nativeFailure.getMessage()).isEqualTo(interpreterFailure.getMessage()); + } + + @Test + void preservesNullAndAbsentAsDifferentActivationStates() { + Map presentNull = new java.util.HashMap<>(); + presentNull.put("n", null); + + Programs programs = programs("n == null"); + assertEquivalent( + programs.nativeProgram.eval(presentNull).getVal(), + programs.interpreterProgram.eval(presentNull).getVal()); + assertEquivalent( + programs.nativeProgram.eval(Map.of()).getVal(), + programs.interpreterProgram.eval(Map.of()).getVal()); + } + + static Stream checkedMapSelectorEvaluations() { + Val adaptedMap = DefaultTypeAdapter.Instance.nativeToValue(Map.of("answer", 50_021L)); + Map presentNull = new java.util.HashMap<>(); + presentNull.put("answer", null); + Map presentNullString = new java.util.HashMap<>(); + presentNullString.put("name", stringOf(null)); + return Stream.of( + new Evaluation("attrs.answer", Map.of("attrs", Map.of("answer", 50_021L))), + new Evaluation( + "attrs.answer == target", + Map.of("attrs", Map.of("answer", 50_021L), "target", 50_021L)), + new Evaluation("labels.name == 'cel'", Map.of("labels", Map.of("name", "cel"))), + new Evaluation("attrs.missing", Map.of("attrs", Map.of())), + new Evaluation("attrs.answer", Map.of("attrs", Map.of("answer", "wrong"))), + new Evaluation("attrs.answer", Map.of("attrs", newErr("map error"))), + new Evaluation("attrs.answer", Map.of("attrs", unknownOf(87L))), + new Evaluation("attrs.answer", Map.of("attrs", "wrong container")), + new Evaluation("attrs.answer", Map.of()), + new Evaluation("attrs.answer == 50021", Map.of("attrs", adaptedMap)), + new Evaluation("attrs.answer", Map.of("attrs", presentNull)), + new Evaluation("labels.name", Map.of("labels", presentNullString))); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("checkedMapSelectorEvaluations") + void evaluatesCheckedMapSelectorsWithoutTerminalValueAdaptation(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void mapSelectorsPreservePartialQualifierMatchingAndResolutionCounts() { + Ast ast = compile("attrs.answer == target"); + Prog nativeProgram = (Prog) env.program(ast); + Prog interpreterProgram = + (Prog) env.program(ast, evalOptions(OptPartialEval, OptDisableNativeEval)); + Object input = + partialVars( + Map.of("attrs", Map.of("answer", 50_021L), "target", 50_021L), + attributePattern("attrs").qualString("answer")); + + assertIntegratedPlan(nativeProgram, "attrs.answer == target"); + assertEquivalent(nativeProgram.eval(input).getVal(), interpreterProgram.eval(input).getVal()); + } + + @Test + void dynamicMapKeysRemainOnTheCurrentEvaluator() { + Ast ast = compile("attrs[key]"); + Prog program = (Prog) env.program(ast); + + assertCurrentPlan(program); + assertThat(program.eval(Map.of("attrs", Map.of("answer", 50_021L), "key", "answer")).getVal()) + .isEqualTo(intOf(50_021L)); + } + + static Stream constantListIndexEvaluations() { + Val adaptedList = DefaultTypeAdapter.Instance.nativeToValue(List.of(7L, 50_021L)); + return Stream.of( + new Evaluation("numbers[1]", Map.of("numbers", new long[] {7L, 50_021L, 9L})), + new Evaluation("numbers[1]", Map.of("numbers", new int[] {7, 50_021, 9})), + new Evaluation( + "numbers[1] == target", Map.of("numbers", List.of(7L, 50_021L), "target", 50_021L)), + new Evaluation("doubles[1] == -0.0", Map.of("doubles", new double[] {1.0d, -0.0d})), + new Evaluation("numbers[-1]", Map.of("numbers", new long[] {7L})), + new Evaluation("numbers[3]", Map.of("numbers", new long[] {7L})), + new Evaluation("numbers[0]", Map.of("numbers", List.of("wrong element"))), + new Evaluation("numbers[0]", Map.of("numbers", "wrong container")), + new Evaluation("numbers[0]", Map.of("numbers", newErr("list error"))), + new Evaluation("numbers[0]", Map.of("numbers", unknownOf(96L))), + new Evaluation("numbers[0]", Map.of()), + new Evaluation("numbers[1]", Map.of("numbers", adaptedList))); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("constantListIndexEvaluations") + void evaluatesConstantListIndexesWithoutPrimitiveArrayBoxing(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + static Stream boolListLiteralIndexEvaluations() { + return Stream.of( + new Evaluation("[true, false, true][0]", Map.of()), + new Evaluation("[b, c, true][0]", Map.of("b", true, "c", false)), + new Evaluation("[b && true, c || false][1]", Map.of("b", true, "c", false)), + new Evaluation("[b, c][1] == true", Map.of("b", false, "c", true)), + new Evaluation("[b, c][-1]", Map.of("b", true, "c", false)), + new Evaluation("[b, c][2]", Map.of("b", true, "c", false)), + new Evaluation("[b, c][0]", Map.of("b", newErr("first"), "c", false)), + new Evaluation("[b, c][0]", Map.of("b", unknownOf(80L), "c", false)), + new Evaluation( + "[b, c][0]", partialVars(Map.of("b", true, "c", false), attributePattern("b"))), + new Evaluation("[b, c][0]", Map.of("b", 42L, "c", false)), + new Evaluation("[b, c][1]", Map.of("b", 42L, "c", false)), + new Evaluation("[b, c][0]", Map.of("b", 42L, "c", newErr("later"))), + new Evaluation("[b && true, c][0]", Map.of("b", 42L, "c", false)), + new Evaluation("[b, c][0]", Map.of("c", false))); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("boolListLiteralIndexEvaluations") + void evaluatesCheckedBoolListLiteralIndexesWithoutMaterializingTheList(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void boolListLiteralIndexesPreserveConstructionBoundsAndLocalShortCircuiting() { + for (String expression : List.of("[b, c][0]", "[b, c][2]", "[b && x > 0, c][0]")) { + Programs programs = programs(expression); + List nativeOrder = new ArrayList<>(); + List interpreterOrder = new ArrayList<>(); + + assertEquivalent( + programs + .nativeProgram + .eval(orderedBoolListActivation(nativeOrder, false, false)) + .getVal(), + programs + .interpreterProgram + .eval(orderedBoolListActivation(interpreterOrder, false, false)) + .getVal()); + assertThat(nativeOrder).as(expression).containsExactly("b", "c"); + assertThat(interpreterOrder).as(expression).containsExactly("b", "c"); + } + + Programs programs = programs("[b, c][0]"); + List nativeOrder = new ArrayList<>(); + List interpreterOrder = new ArrayList<>(); + assertEquivalent( + programs + .nativeProgram + .eval(orderedBoolListActivation(nativeOrder, newErr("first"), false)) + .getVal(), + programs + .interpreterProgram + .eval(orderedBoolListActivation(interpreterOrder, newErr("first"), false)) + .getVal()); + assertThat(nativeOrder).containsExactly("b"); + assertThat(interpreterOrder).containsExactly("b"); + } + + static Stream boolListLiteralSizeEvaluations() { + return Stream.of( + new Evaluation("size([true, false, true])", Map.of()), + new Evaluation("size([b, c, true])", Map.of("b", true, "c", false)), + new Evaluation("[b, c, true].size()", Map.of("b", true, "c", false)), + new Evaluation("size([b && true, c || false]) + 1", Map.of("b", true, "c", false)), + new Evaluation("size([b, c])", Map.of("b", 42L, "c", false)), + new Evaluation("size([b, c])", Map.of("b", 42L, "c", newErr("later"))), + new Evaluation("size([b, c])", Map.of("b", newErr("first"), "c", false)), + new Evaluation("size([b, c])", Map.of("b", unknownOf(81L), "c", false)), + new Evaluation( + "size([b, c])", partialVars(Map.of("b", true, "c", false), attributePattern("b"))), + new Evaluation("size([b && true, c])", Map.of("b", 42L, "c", false)), + new Evaluation("size([b, c])", Map.of("c", false))); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("boolListLiteralSizeEvaluations") + void evaluatesCheckedBoolListLiteralSizesWithoutMaterializingTheList(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void boolListLiteralSizePreservesElementEvaluationAndEarlyTermination() { + Programs programs = programs("size([b && x > 0, c])"); + List nativeOrder = new ArrayList<>(); + List interpreterOrder = new ArrayList<>(); + + assertEquivalent( + programs.nativeProgram.eval(orderedBoolListActivation(nativeOrder, false, false)).getVal(), + programs + .interpreterProgram + .eval(orderedBoolListActivation(interpreterOrder, false, false)) + .getVal()); + assertThat(nativeOrder).containsExactly("b", "c"); + assertThat(interpreterOrder).containsExactly("b", "c"); + + programs = programs("size([b, c])"); + nativeOrder.clear(); + interpreterOrder.clear(); + assertEquivalent( + programs + .nativeProgram + .eval(orderedBoolListActivation(nativeOrder, newErr("first"), false)) + .getVal(), + programs + .interpreterProgram + .eval(orderedBoolListActivation(interpreterOrder, newErr("first"), false)) + .getVal()); + assertThat(nativeOrder).containsExactly("b"); + assertThat(interpreterOrder).containsExactly("b"); + } + + static Stream intListLiteralIndexEvaluations() { + return Stream.of( + new Evaluation("[x, y, 3][0]", Map.of("x", 50_021L, "y", 7L)), + new Evaluation("[x + 1, y + 2, 3][1]", Map.of("x", 40L, "y", 40L)), + new Evaluation("[x + 1, y + 2, 3][1] == target", Map.of("x", 40L, "y", 40L, "target", 42L)), + new Evaluation("[x, y][-1]", Map.of("x", 1L, "y", 2L)), + new Evaluation("[x, y][2]", Map.of("x", 1L, "y", 2L)), + new Evaluation("[x, y][0]", Map.of("x", newErr("first"), "y", 2L)), + new Evaluation("[x, y][0]", Map.of("x", unknownOf(73L), "y", 2L)), + new Evaluation("[x, y][0]", Map.of("x", 1L, "y", newErr("second"))), + new Evaluation("[x, y][0]", Map.of("x", 1L, "y", unknownOf(74L))), + new Evaluation("[x, y][2]", Map.of("x", newErr("first"), "y", 2L)), + new Evaluation("[x, y][0]", partialVars(Map.of("x", 1L, "y", 2L), attributePattern("x"))), + new Evaluation("[x, y][0]", Map.of("x", "wrong", "y", 2L)), + new Evaluation("[x, y][1]", Map.of("x", "wrong", "y", 2L)), + new Evaluation("[x, y][0]", Map.of("x", "wrong", "y", newErr("second"))), + new Evaluation("[x, y][0]", Map.of("y", 2L)), + new Evaluation("[x + 1, y][1]", Map.of("x", Long.MAX_VALUE, "y", 2L))); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("intListLiteralIndexEvaluations") + void evaluatesCheckedIntListLiteralIndexesWithoutMaterializingTheList(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void intListLiteralIndexesPreserveElementEvaluationAndIndexErrorOrdering() { + for (String expression : List.of("[x, y][0]", "[x, y][2]")) { + Programs programs = programs(expression); + List nativeOrder = new ArrayList<>(); + List interpreterOrder = new ArrayList<>(); + + Val nativeResult = + programs.nativeProgram.eval(orderedIntListActivation(nativeOrder, false)).getVal(); + Val interpreterResult = + programs + .interpreterProgram + .eval(orderedIntListActivation(interpreterOrder, false)) + .getVal(); + + assertEquivalent(nativeResult, interpreterResult); + assertThat(nativeOrder).as(expression).containsExactly("x", "y"); + assertThat(interpreterOrder).as(expression).containsExactly("x", "y"); + } + + Programs programs = programs("[x, y][0]"); + List nativeOrder = new ArrayList<>(); + List interpreterOrder = new ArrayList<>(); + assertEquivalent( + programs.nativeProgram.eval(orderedIntListActivation(nativeOrder, true)).getVal(), + programs + .interpreterProgram + .eval(orderedIntListActivation(interpreterOrder, true)) + .getVal()); + assertThat(nativeOrder).containsExactly("x"); + assertThat(interpreterOrder).containsExactly("x"); + } + + static Stream doubleListLiteralIndexEvaluations() { + return Stream.of( + new Evaluation("[d, e, 3.0][0]", Map.of("d", 50_021.25d, "e", 7.5d)), + new Evaluation("[d + 1.0, e + 2.0, 3.0][1]", Map.of("d", 40.5d, "e", 40.5d)), + new Evaluation("[d + 1.0, e + 2.0, 3.0][1] == 42.5", Map.of("d", 40.5d, "e", 40.5d)), + new Evaluation("[d, e][0]", Map.of("d", Double.NaN, "e", 2.0d)), + new Evaluation("[d, e][0]", Map.of("d", Double.POSITIVE_INFINITY, "e", 2.0d)), + new Evaluation("[d, e][0]", Map.of("d", -0.0d, "e", 2.0d)), + new Evaluation("[d, e][-1]", Map.of("d", 1.0d, "e", 2.0d)), + new Evaluation("[d, e][2]", Map.of("d", 1.0d, "e", 2.0d)), + new Evaluation("[d, e][0]", Map.of("d", newErr("first"), "e", 2.0d)), + new Evaluation("[d, e][0]", Map.of("d", unknownOf(74L), "e", 2.0d)), + new Evaluation( + "[d, e][0]", partialVars(Map.of("d", 1.0d, "e", 2.0d), attributePattern("d"))), + new Evaluation("[d, e][0]", Map.of("d", "wrong", "e", 2.0d)), + new Evaluation("[d, e][1]", Map.of("d", "wrong", "e", 2.0d)), + new Evaluation("[d, e][0]", Map.of("d", "wrong", "e", newErr("second"))), + new Evaluation("[d, e][0]", Map.of("e", 2.0d))); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("doubleListLiteralIndexEvaluations") + void evaluatesCheckedDoubleListLiteralIndexesWithoutMaterializingTheList(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void doubleListLiteralIndexesPreserveElementEvaluationAndIndexErrorOrdering() { + for (String expression : List.of("[d, e][0]", "[d, e][2]")) { + Programs programs = programs(expression); + List nativeOrder = new ArrayList<>(); + List interpreterOrder = new ArrayList<>(); + + assertEquivalent( + programs + .nativeProgram + .eval(orderedDoubleListActivation(nativeOrder, 1.0d, 2.0d)) + .getVal(), + programs + .interpreterProgram + .eval(orderedDoubleListActivation(interpreterOrder, 1.0d, 2.0d)) + .getVal()); + assertThat(nativeOrder).as(expression).containsExactly("d", "e"); + assertThat(interpreterOrder).as(expression).containsExactly("d", "e"); + } + + Programs programs = programs("[d, e][1]"); + List nativeOrder = new ArrayList<>(); + List interpreterOrder = new ArrayList<>(); + assertEquivalent( + programs + .nativeProgram + .eval(orderedDoubleListActivation(nativeOrder, "wrong", 2.0d)) + .getVal(), + programs + .interpreterProgram + .eval(orderedDoubleListActivation(interpreterOrder, "wrong", 2.0d)) + .getVal()); + assertThat(nativeOrder).containsExactly("d", "e"); + assertThat(interpreterOrder).containsExactly("d", "e"); + + programs = programs("[d, e][0]"); + nativeOrder.clear(); + interpreterOrder.clear(); + assertEquivalent( + programs + .nativeProgram + .eval(orderedDoubleListActivation(nativeOrder, newErr("first"), 2.0d)) + .getVal(), + programs + .interpreterProgram + .eval(orderedDoubleListActivation(interpreterOrder, newErr("first"), 2.0d)) + .getVal()); + assertThat(nativeOrder).containsExactly("d"); + assertThat(interpreterOrder).containsExactly("d"); + } + + static Stream intListLiteralSizeEvaluations() { + return Stream.of( + new Evaluation("size([1, 2, 3])", Map.of()), + new Evaluation("size([x, y, 3])", Map.of("x", 50_021L, "y", 7L)), + new Evaluation("[x, y, 3].size()", Map.of("x", 50_021L, "y", 7L)), + new Evaluation("size([x, y]) + 1", Map.of("x", 50_021L, "y", 7L)), + new Evaluation( + "size([x + 1, y + 2, 3]) == target", Map.of("x", 40L, "y", 40L, "target", 3L)), + new Evaluation("size([x, y])", Map.of("x", newErr("first"), "y", 2L)), + new Evaluation("size([x, y])", Map.of("x", unknownOf(73L), "y", 2L)), + new Evaluation("size([x, y])", Map.of("x", 1L, "y", newErr("second"))), + new Evaluation("size([x, y])", Map.of("x", 1L, "y", unknownOf(74L))), + new Evaluation( + "size([x, y])", partialVars(Map.of("x", 1L, "y", 2L), attributePattern("x"))), + new Evaluation("size([x, y])", Map.of("x", "wrong", "y", 2L)), + new Evaluation("size([x, y])", Map.of("x", "wrong", "y", newErr("second"))), + new Evaluation("size([x, y])", Map.of("y", 2L)), + new Evaluation("size([x + 1, y])", Map.of("x", Long.MAX_VALUE, "y", 2L))); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("intListLiteralSizeEvaluations") + void evaluatesCheckedIntListLiteralSizesWithoutMaterializingTheList(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void intListLiteralSizePreservesElementEvaluationAndEarlyTermination() { + Programs programs = programs("size([x, y])"); + List nativeOrder = new ArrayList<>(); + List interpreterOrder = new ArrayList<>(); + + assertEquivalent( + programs.nativeProgram.eval(orderedIntListActivation(nativeOrder, false)).getVal(), + programs + .interpreterProgram + .eval(orderedIntListActivation(interpreterOrder, false)) + .getVal()); + assertThat(nativeOrder).containsExactly("x", "y"); + assertThat(interpreterOrder).containsExactly("x", "y"); + + nativeOrder.clear(); + interpreterOrder.clear(); + assertEquivalent( + programs.nativeProgram.eval(orderedIntListActivation(nativeOrder, true)).getVal(), + programs + .interpreterProgram + .eval(orderedIntListActivation(interpreterOrder, true)) + .getVal()); + assertThat(nativeOrder).containsExactly("x"); + assertThat(interpreterOrder).containsExactly("x"); + } + + static Stream doubleListLiteralSizeEvaluations() { + return Stream.of( + new Evaluation("size([1.0, 2.0, 3.0])", Map.of()), + new Evaluation("size([d, e, 3.0])", Map.of("d", 50_021.25d, "e", 7.5d)), + new Evaluation("[d, e, 3.0].size()", Map.of("d", 50_021.25d, "e", 7.5d)), + new Evaluation("size([d, e]) + 1", Map.of("d", Double.NaN, "e", -0.0d)), + new Evaluation("size([d, e])", Map.of("d", newErr("first"), "e", 2.0d)), + new Evaluation("size([d, e])", Map.of("d", unknownOf(75L), "e", 2.0d)), + new Evaluation( + "size([d, e])", partialVars(Map.of("d", 1.0d, "e", 2.0d), attributePattern("d"))), + new Evaluation("size([d, e])", Map.of("d", "wrong", "e", 2.0d)), + new Evaluation("size([d, e])", Map.of("d", "wrong", "e", newErr("second"))), + new Evaluation("size([d + 1.0, e])", Map.of("d", "wrong", "e", 2.0d)), + new Evaluation("size([d, e])", Map.of("e", 2.0d))); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("doubleListLiteralSizeEvaluations") + void evaluatesCheckedDoubleListLiteralSizesWithoutMaterializingTheList(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + static Stream stringListLiteralIndexEvaluations() { + return Stream.of( + new Evaluation("[s, key, 'last'][0]", Map.of("s", "cel", "key", "other")), + new Evaluation("[s, key, 'last'][1]", Map.of("s", "zero", "key", "cel")), + new Evaluation("[s + '-suffix', key][0]", Map.of("s", "cel", "key", "other")), + new Evaluation( + "[s, key][1] == wordTarget", Map.of("s", "zero", "key", "cel", "wordTarget", "cel")), + new Evaluation("[s, key][0]", Map.of("s", stringOf(null), "key", "cel")), + new Evaluation("[s, key][1]", Map.of("s", stringOf(null), "key", "cel")), + new Evaluation("[s, key][0]", Map.of("s", 42L, "key", "cel")), + new Evaluation("[s, key][1]", Map.of("s", 42L, "key", "cel")), + new Evaluation("[s, key][0]", Map.of("s", 42L, "key", newErr("later"))), + new Evaluation("[s, key][0]", Map.of("s", newErr("first"), "key", "cel")), + new Evaluation("[s, key][0]", Map.of("s", unknownOf(78L), "key", "cel")), + new Evaluation( + "[s, key][0]", partialVars(Map.of("s", "cel", "key", "other"), attributePattern("s"))), + new Evaluation("[s, key][-1]", Map.of("s", "cel", "key", "other")), + new Evaluation("[s, key][2]", Map.of("s", "cel", "key", "other")), + new Evaluation("[s + '-suffix', key][0]", Map.of("s", 42L, "key", "other")), + new Evaluation("[s, key][0]", Map.of("key", "cel"))); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("stringListLiteralIndexEvaluations") + void evaluatesCheckedStringListLiteralIndexesWithoutMaterializingTheList(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void stringListLiteralIndexesPreserveConstructionAndBoundsOrdering() { + for (String expression : List.of("[s, key][0]", "[s, key][2]")) { + Programs programs = programs(expression); + List nativeOrder = new ArrayList<>(); + List interpreterOrder = new ArrayList<>(); + + assertEquivalent( + programs + .nativeProgram + .eval( + orderedStringListLiteralMembershipActivation( + nativeOrder, "unused", "cel", "other")) + .getVal(), + programs + .interpreterProgram + .eval( + orderedStringListLiteralMembershipActivation( + interpreterOrder, "unused", "cel", "other")) + .getVal()); + assertThat(nativeOrder).as(expression).containsExactly("s", "key"); + assertThat(interpreterOrder).as(expression).containsExactly("s", "key"); + } + + Programs programs = programs("[s, key][1]"); + List nativeOrder = new ArrayList<>(); + List interpreterOrder = new ArrayList<>(); + assertEquivalent( + programs + .nativeProgram + .eval(orderedStringListLiteralMembershipActivation(nativeOrder, "unused", 42L, "cel")) + .getVal(), + programs + .interpreterProgram + .eval( + orderedStringListLiteralMembershipActivation( + interpreterOrder, "unused", 42L, "cel")) + .getVal()); + assertThat(nativeOrder).containsExactly("s", "key"); + assertThat(interpreterOrder).containsExactly("s", "key"); + + programs = programs("[s, key][0]"); + nativeOrder.clear(); + interpreterOrder.clear(); + assertEquivalent( + programs + .nativeProgram + .eval( + orderedStringListLiteralMembershipActivation( + nativeOrder, "unused", newErr("first"), "other")) + .getVal(), + programs + .interpreterProgram + .eval( + orderedStringListLiteralMembershipActivation( + interpreterOrder, "unused", newErr("first"), "other")) + .getVal()); + assertThat(nativeOrder).containsExactly("s"); + assertThat(interpreterOrder).containsExactly("s"); + } + + @Test + void nullBackedStringLiteralIndexOperationsUseTheCurrentCompatibilityPath() { + Programs programs = programs("[s, key][0] + '-suffix'"); + Object input = Map.of("s", stringOf(null), "key", "other"); + + Val nativeResult = programs.nativeProgram.eval(input).getVal(); + Val interpreterResult = programs.interpreterProgram.eval(input).getVal(); + + assertEquivalent(nativeResult, interpreterResult); + assertThat(nativeResult).isEqualTo(stringOf("null-suffix")); + } + + static Stream stringListLiteralSizeEvaluations() { + return Stream.of( + new Evaluation("size(['zero', 'cel', 'last'])", Map.of()), + new Evaluation("['zero', 'cel', 'last'].size()", Map.of()), + new Evaluation("size([s, key, 'last'])", Map.of("s", "zero", "key", "cel")), + new Evaluation("[s, key, 'last'].size()", Map.of("s", "zero", "key", "cel")), + new Evaluation("size([s, key]) + 1", Map.of("s", stringOf(null), "key", "cel")), + new Evaluation("size([s, key])", Map.of("s", 42L, "key", "cel")), + new Evaluation("size([s, key])", Map.of("s", 42L, "key", newErr("later"))), + new Evaluation("size([s, key])", Map.of("s", newErr("first"), "key", "cel")), + new Evaluation("size([s, key])", Map.of("s", unknownOf(79L), "key", "cel")), + new Evaluation( + "size([s, key])", + partialVars(Map.of("s", "zero", "key", "cel"), attributePattern("s"))), + new Evaluation("size([s + '-suffix', key])", Map.of("s", 42L, "key", "cel")), + new Evaluation("size([s, key])", Map.of("key", "cel"))); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("stringListLiteralSizeEvaluations") + void evaluatesCheckedStringListLiteralSizesWithoutMaterializingTheList(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void stringListLiteralSizePreservesElementEvaluationAndEarlyTermination() { + Programs programs = programs("size([s, key])"); + List nativeOrder = new ArrayList<>(); + List interpreterOrder = new ArrayList<>(); + + assertEquivalent( + programs + .nativeProgram + .eval( + orderedStringListLiteralMembershipActivation(nativeOrder, "unused", "zero", "cel")) + .getVal(), + programs + .interpreterProgram + .eval( + orderedStringListLiteralMembershipActivation( + interpreterOrder, "unused", "zero", "cel")) + .getVal()); + assertThat(nativeOrder).containsExactly("s", "key"); + assertThat(interpreterOrder).containsExactly("s", "key"); + + nativeOrder.clear(); + interpreterOrder.clear(); + assertEquivalent( + programs + .nativeProgram + .eval( + orderedStringListLiteralMembershipActivation( + nativeOrder, "unused", newErr("first"), "cel")) + .getVal(), + programs + .interpreterProgram + .eval( + orderedStringListLiteralMembershipActivation( + interpreterOrder, "unused", newErr("first"), "cel")) + .getVal()); + assertThat(nativeOrder).containsExactly("s"); + assertThat(interpreterOrder).containsExactly("s"); + } + + @Test + void unsupportedScalarListLiteralShapesFallBack() { + for (String expression : + List.of( + "[x, y]", + "[x, y][target]", + "[x, y][4294967296]", + "[b, c]", + "[x, y] == [x, y]", + "[d, e]", + "['a', 'b']")) { + Ast ast = compile(expression); + Prog program = (Prog) env.program(ast); + assertCurrentPlan(program, expression); + } + + for (String expression : List.of("[null][0]", "size([null])", "[null].size()")) { + Ast ast = compile(expression); + Prog program = (Prog) env.program(ast); + Prog established = (Prog) env.program(ast, evalOptions(OptDisableNativeEval)); + assertEstablishedRoot( + program, expression.contains("[0]") ? "EvalAttr" : "EvalUnary", expression); + assertEquivalent(program.eval(Map.of()).getVal(), established.eval(Map.of()).getVal()); + } + + Env optionalEnv = + newEnv( + optionals(), + declarations( + Decls.newVar("x", Decls.Int), + Decls.newVar("b", Decls.Bool), + Decls.newVar("wordTarget", Decls.String))); + Ast optionalAst = compile(optionalEnv, "[?optional.of(x), 2][0]"); + Prog optionalProgram = (Prog) optionalEnv.program(optionalAst); + assertEstablishedRoot(optionalProgram, "EvalAttr", optionalAst.toString()); + + Ast optionalSizeAst = compile(optionalEnv, "size([?optional.of(x), 2])"); + Prog optionalSizeProgram = (Prog) optionalEnv.program(optionalSizeAst); + assertEstablishedRoot(optionalSizeProgram, "EvalUnary", optionalSizeAst.toString()); + + Ast optionalReceiverSizeAst = compile(optionalEnv, "[?optional.of(x), 2].size()"); + Prog optionalReceiverSizeProgram = (Prog) optionalEnv.program(optionalReceiverSizeAst); + assertEstablishedRoot( + optionalReceiverSizeProgram, "EvalUnary", optionalReceiverSizeAst.toString()); + + Ast optionalMembershipAst = compile(optionalEnv, "wordTarget in [?optional.of('cel'), 'last']"); + Prog optionalMembershipProgram = (Prog) optionalEnv.program(optionalMembershipAst); + assertEstablishedRoot( + optionalMembershipProgram, "EvalBinary", optionalMembershipAst.toString()); + + Ast optionalStringIndexAst = compile(optionalEnv, "[?optional.of(wordTarget), 'last'][0]"); + Prog optionalStringIndexProgram = (Prog) optionalEnv.program(optionalStringIndexAst); + assertEstablishedRoot( + optionalStringIndexProgram, "EvalAttr", optionalStringIndexAst.toString()); + + Ast optionalStringSizeAst = compile(optionalEnv, "size([?optional.of(wordTarget), 'last'])"); + Prog optionalStringSizeProgram = (Prog) optionalEnv.program(optionalStringSizeAst); + assertEstablishedRoot(optionalStringSizeProgram, "EvalUnary", optionalStringSizeAst.toString()); + + Ast optionalBoolIndexAst = compile(optionalEnv, "[?optional.of(b), true][0]"); + Prog optionalBoolIndexProgram = (Prog) optionalEnv.program(optionalBoolIndexAst); + assertEstablishedRoot(optionalBoolIndexProgram, "EvalAttr", optionalBoolIndexAst.toString()); + + Ast optionalBoolSizeAst = compile(optionalEnv, "size([?optional.of(b), true])"); + Prog optionalBoolSizeProgram = (Prog) optionalEnv.program(optionalBoolSizeAst); + assertEstablishedRoot(optionalBoolSizeProgram, "EvalUnary", optionalBoolSizeAst.toString()); + + Ast optionalBoolReceiverSizeAst = compile(optionalEnv, "[?optional.of(b), true].size()"); + Prog optionalBoolReceiverSizeProgram = (Prog) optionalEnv.program(optionalBoolReceiverSizeAst); + assertEstablishedRoot( + optionalBoolReceiverSizeProgram, "EvalUnary", optionalBoolReceiverSizeAst.toString()); + } + + static Stream constantStringListIndexEvaluations() { + String[] raw = {"zero", "cel", null}; + Val adapted = DefaultTypeAdapter.Instance.nativeToValue(raw); + Val adaptedValues = + DefaultTypeAdapter.Instance.nativeToValue( + new Val[] {stringOf("zero"), stringOf("cel"), stringOf(null)}); + List javaNull = new ArrayList<>(); + javaNull.add("zero"); + javaNull.add(null); + return Stream.of( + new Evaluation("words[1]", Map.of("words", raw)), + new Evaluation("words[1] == wordTarget", Map.of("words", raw, "wordTarget", "cel")), + new Evaluation("words[1] + '!'", Map.of("words", raw)), + new Evaluation("words[1] < 'zzz'", Map.of("words", raw)), + new Evaluation("true ? words[1] : 'unused'", Map.of("words", raw)), + new Evaluation("words[2]", Map.of("words", raw)), + new Evaluation("words[2] + ' suffix'", Map.of("words", raw)), + new Evaluation("true ? words[2] : 'unused'", Map.of("words", raw)), + new Evaluation("words[1]", Map.of("words", List.of("zero", "cel"))), + new Evaluation("words[1]", Map.of("words", adapted)), + new Evaluation("words[1]", Map.of("words", adaptedValues)), + new Evaluation("words[2]", Map.of("words", adaptedValues)), + new Evaluation("words[1]", Map.of("words", javaNull)), + new Evaluation("words[-1]", Map.of("words", raw)), + new Evaluation("words[3]", Map.of("words", raw)), + new Evaluation("words[0]", Map.of("words", List.of(42L))), + new Evaluation("words[0]", Map.of("words", "wrong container")), + new Evaluation("words[0]", Map.of("words", newErr("list error"))), + new Evaluation("words[0]", Map.of("words", unknownOf(97L))), + new Evaluation("words[0]", Map.of())); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("constantStringListIndexEvaluations") + void evaluatesConstantStringListIndexesLikeTheExistingInterpreter(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + static Stream listMembershipEvaluations() { + String[] rawWords = {"zero", "cel", null, "last"}; + Val adaptedWords = DefaultTypeAdapter.Instance.nativeToValue(rawWords); + Map nullList = new java.util.HashMap<>(); + nullList.put("wordTarget", "cel"); + nullList.put("words", null); + return Stream.of( + new Evaluation("wordTarget in words", Map.of("wordTarget", "zero", "words", rawWords)), + new Evaluation("wordTarget in words", Map.of("wordTarget", "cel", "words", rawWords)), + new Evaluation("wordTarget in words", Map.of("wordTarget", "absent", "words", rawWords)), + new Evaluation( + "wordTarget in words", Map.of("wordTarget", "absent", "words", adaptedWords)), + new Evaluation( + "wordTarget in words", Map.of("wordTarget", "cel", "words", List.of("zero", "cel"))), + new Evaluation( + "wordTarget in words", + Map.of("wordTarget", "absent", "words", List.of("zero", newErr("element")))), + new Evaluation( + "wordTarget in words", + Map.of("wordTarget", "absent", "words", List.of("zero", unknownOf(71L)))), + new Evaluation("wordTarget in words", Map.of("wordTarget", "cel", "words", List.of())), + new Evaluation( + "wordTarget in words", Map.of("wordTarget", "cel", "words", "wrong container")), + new Evaluation( + "wordTarget in words", Map.of("wordTarget", "cel", "words", newErr("list error"))), + new Evaluation("wordTarget in words", Map.of("wordTarget", "cel", "words", unknownOf(72L))), + new Evaluation("wordTarget in words", Map.of("wordTarget", 42L, "words", rawWords)), + new Evaluation( + "wordTarget in words", Map.of("wordTarget", newErr("needle"), "words", rawWords)), + new Evaluation( + "wordTarget in words", Map.of("wordTarget", newErr("needle"), "words", newErr("list"))), + new Evaluation( + "wordTarget in words", Map.of("wordTarget", unknownOf(73L), "words", newErr("list"))), + new Evaluation("wordTarget in words", nullList), + new Evaluation("wordTarget in words", Map.of("wordTarget", "cel")), + new Evaluation( + "wordTarget in words", + partialVars( + Map.of("wordTarget", "cel", "words", rawWords), attributePattern("wordTarget"))), + new Evaluation( + "wordTarget in words", + partialVars( + Map.of("wordTarget", "cel", "words", rawWords), attributePattern("words")))); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("listMembershipEvaluations") + void evaluatesListMembershipLikeTheExistingInterpreter(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + static Stream stringListLiteralMembershipEvaluations() { + return Stream.of( + new Evaluation("'cel' in ['zero', 'cel', 'last']", Map.of()), + new Evaluation("wordTarget in []", Map.of("wordTarget", "cel")), + new Evaluation("wordTarget in ['zero', 'cel']", Map.of("wordTarget", "cel")), + new Evaluation("wordTarget in ['zero', 'cel']", Map.of("wordTarget", "absent")), + new Evaluation( + "wordTarget in [s, key, 'last']", + Map.of("wordTarget", "cel", "s", "zero", "key", "cel")), + new Evaluation( + "wordTarget in [s + '-suffix', key]", + Map.of("wordTarget", "cel-suffix", "s", "cel", "key", "last")), + new Evaluation( + "wordTarget in [s, key]", + Map.of("wordTarget", "cel", "s", newErr("first"), "key", "cel")), + new Evaluation( + "wordTarget in [s, key]", + Map.of("wordTarget", "cel", "s", unknownOf(76L), "key", "cel")), + new Evaluation( + "wordTarget in [s, key]", + partialVars( + Map.of("wordTarget", "cel", "s", "zero", "key", "cel"), attributePattern("s"))), + new Evaluation( + "wordTarget in [s, key]", + Map.of("wordTarget", "cel", "s", "cel", "key", newErr("later"))), + new Evaluation( + "wordTarget in [s, key]", + Map.of("wordTarget", newErr("needle"), "s", "zero", "key", newErr("list"))), + new Evaluation( + "wordTarget in [s, key]", + Map.of("wordTarget", unknownOf(77L), "s", "zero", "key", newErr("list"))), + new Evaluation("wordTarget in [s]", Map.of("wordTarget", 42L, "s", 42L)), + new Evaluation("wordTarget in [s, 'cel']", Map.of("wordTarget", "cel", "s", 42L)), + new Evaluation( + "wordTarget in [s, 'last']", Map.of("wordTarget", "cel", "s", stringOf(null))), + new Evaluation( + "wordTarget in [s, key]", + Map.of("wordTarget", 42L, "s", "zero", "key", newErr("list"))), + new Evaluation("wordTarget in [s + '-suffix']", Map.of("wordTarget", "cel", "s", 42L)), + new Evaluation("wordTarget in [s]", Map.of("wordTarget", "cel")), + new Evaluation( + "wordTarget in [s]", + partialVars(Map.of("wordTarget", "cel", "s", "cel"), attributePattern("wordTarget")))); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("stringListLiteralMembershipEvaluations") + void evaluatesStringListLiteralMembershipWithoutMaterializingTheList(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void stringListLiteralMembershipPreservesConstructionAndOperandOrdering() { + Programs programs = programs("wordTarget in [s, key]"); + List nativeOrder = new ArrayList<>(); + List interpreterOrder = new ArrayList<>(); + + assertEquivalent( + programs + .nativeProgram + .eval(orderedStringListLiteralMembershipActivation(nativeOrder, "cel", "cel", "last")) + .getVal(), + programs + .interpreterProgram + .eval( + orderedStringListLiteralMembershipActivation( + interpreterOrder, "cel", "cel", "last")) + .getVal()); + assertThat(nativeOrder).containsExactly("wordTarget", "s", "key"); + assertThat(interpreterOrder).containsExactly("wordTarget", "s", "key"); + + nativeOrder.clear(); + interpreterOrder.clear(); + assertEquivalent( + programs + .nativeProgram + .eval( + orderedStringListLiteralMembershipActivation( + nativeOrder, newErr("needle"), "zero", "last")) + .getVal(), + programs + .interpreterProgram + .eval( + orderedStringListLiteralMembershipActivation( + interpreterOrder, newErr("needle"), "zero", "last")) + .getVal()); + assertThat(nativeOrder).containsExactly("wordTarget", "s", "key"); + assertThat(interpreterOrder).containsExactly("wordTarget", "s", "key"); + + nativeOrder.clear(); + interpreterOrder.clear(); + assertEquivalent( + programs + .nativeProgram + .eval( + orderedStringListLiteralMembershipActivation( + nativeOrder, "cel", newErr("first"), "last")) + .getVal(), + programs + .interpreterProgram + .eval( + orderedStringListLiteralMembershipActivation( + interpreterOrder, "cel", newErr("first"), "last")) + .getVal()); + assertThat(nativeOrder).containsExactly("wordTarget", "s"); + assertThat(interpreterOrder).containsExactly("wordTarget", "s"); + } + + @Test + void nullBackedStringNeedleUsesTheCurrentLiteralMembershipCompatibilityPath() { + Programs programs = programs("wordTarget in [s, 'cel']"); + Object input = Map.of("wordTarget", stringOf(null), "s", "zero"); + + Throwable nativeFailure = catchThrowable(() -> programs.nativeProgram.eval(input)); + Throwable interpreterFailure = catchThrowable(() -> programs.interpreterProgram.eval(input)); + + assertThat(nativeFailure).isNotNull(); + assertThat(interpreterFailure).isNotNull(); + assertThat(nativeFailure.getClass()).isEqualTo(interpreterFailure.getClass()); + assertThat(nativeFailure.getMessage()).isEqualTo(interpreterFailure.getMessage()); + } + + @Test + void listMembershipEvaluatesBothOperandsFromLeftToRight() { + Programs programs = programs("wordTarget in words"); + List nativeOrder = new ArrayList<>(); + List interpreterOrder = new ArrayList<>(); + + Val nativeResult = + programs.nativeProgram.eval(orderedMembershipActivation(nativeOrder)).getVal(); + Val interpreterResult = + programs.interpreterProgram.eval(orderedMembershipActivation(interpreterOrder)).getVal(); + + assertEquivalent(nativeResult, interpreterResult); + assertThat(nativeOrder).containsExactly("wordTarget", "words"); + assertThat(interpreterOrder).containsExactly("wordTarget", "words"); + } + + @Test + void nullBackedStringNeedleUsesTheCurrentCompatibilityPath() { + Programs programs = programs("wordTarget in words"); + Object input = Map.of("wordTarget", stringOf(null), "words", new String[] {"zero", null}); + + Throwable nativeFailure = catchThrowable(() -> programs.nativeProgram.eval(input)); + Throwable interpreterFailure = catchThrowable(() -> programs.interpreterProgram.eval(input)); + + assertThat(nativeFailure).isNotNull(); + assertThat(interpreterFailure).isNotNull(); + assertThat(nativeFailure.getClass()).isEqualTo(interpreterFailure.getClass()); + assertThat(nativeFailure.getMessage()).isEqualTo(interpreterFailure.getMessage()); + } + + @ParameterizedTest(name = "{index}: {0}") + @ValueSource(strings = {"words[1] == 'value'", "words[1] < 'value'"}) + void nullBackedStringListOperationsUseTheCurrentCompatibilityPath(String expression) { + Programs programs = programs(expression); + Object input = Map.of("words", new String[] {"zero", null}); + + Throwable nativeFailure = catchThrowable(() -> programs.nativeProgram.eval(input)); + Throwable interpreterFailure = catchThrowable(() -> programs.interpreterProgram.eval(input)); + + assertThat(nativeFailure).isNotNull(); + assertThat(interpreterFailure).isNotNull(); + assertThat(nativeFailure.getClass()).isEqualTo(interpreterFailure.getClass()); + assertThat(nativeFailure.getMessage()).isEqualTo(interpreterFailure.getMessage()); + } + + @Test + void constantListIndexesResolveEachOperandOnce() { + assertResolutionCounts( + "numbers[1] == target", + Map.of("numbers", new long[] {7L, 50_021L}, "target", 50_021L), + Map.of("numbers", 1, "target", 1)); + assertResolutionCounts( + "words[1] == wordTarget", + Map.of("words", new String[] {"zero", "cel"}, "wordTarget", "cel"), + Map.of("words", 1, "wordTarget", 1)); + } + + @Test + void listIndexesPreservePartialMatchingAndDynamicIndexesUseIntegratedSources() { + Ast ast = compile("numbers[1] == target"); + Prog nativeProgram = (Prog) env.program(ast); + Interpretable current = undecoratedCurrent(nativeProgram, ast); + Object input = + partialVars( + Map.of("numbers", new long[] {7L, 50_021L}, "target", 50_021L), + attributePattern("numbers").qualInt(1L)); + + assertIntegratedPlan(nativeProgram, "numbers[1] == target"); + assertEquivalent(nativeProgram.eval(input).getVal(), current.eval(newActivation(input))); + + Prog dynamic = (Prog) env.program(compile("numbers[x]")); + assertIntegratedPlan(dynamic, "numbers[x]"); + + for (String expression : List.of("numbers[4294967296]", "numbers[-4294967297]")) { + Prog outOfIntRange = (Prog) env.program(compile(expression)); + assertCurrentPlan(outOfIntRange, expression); + } + + Ast stringAst = compile("words[1] == wordTarget"); + Prog stringNative = (Prog) env.program(stringAst); + Interpretable stringCurrent = undecoratedCurrent(stringNative, stringAst); + Object stringInput = + partialVars( + Map.of("words", new String[] {"zero", "cel"}, "wordTarget", "cel"), + attributePattern("words").qualInt(1L)); + + assertIntegratedPlan(stringNative, "words[1] == wordTarget"); + assertEquivalent( + stringNative.eval(stringInput).getVal(), stringCurrent.eval(newActivation(stringInput))); + } + + @Test + void unsupportedMembershipShapesFallBack() { + Env boolListEnv = + newEnv( + declarations( + Decls.newVar("needle", Decls.Bool), + Decls.newVar("values", Decls.newListType(Decls.Bool)))); + for (String expression : + List.of( + "target in numbers", + "d in doubles", + "target in [1, 2]", + "target in (b ? numbers : numbers)", + "key in labels")) { + Ast ast = compile(expression); + Prog program = (Prog) env.program(ast); + assertCurrentPlan(program, expression); + } + Ast boolAst = compile(boolListEnv, "needle in values"); + Prog boolProgram = (Prog) boolListEnv.program(boolAst); + assertCurrentPlan(boolProgram); + } + + @TestFactory + Stream evaluatesGeneratedDynamicAndWrappedProtobufStringSelectors() + throws Exception { + String messageType = TestAllTypes.getDescriptor().getFullName(); + Env protoEnv = + newEnv( + types(TestAllTypes.getDefaultInstance()), + declarations( + Decls.newVar("msg", Decls.newObjectType(messageType)), + Decls.newVar("target", Decls.Int))); + TestAllTypes message = + TestAllTypes.newBuilder().setSingleInt64(50_021L).setSingleString("cel").build(); + DynamicMessage dynamic = + DynamicMessage.parseFrom(TestAllTypes.getDescriptor(), message.toByteString()); + Val wrapped = protoEnv.getTypeAdapter().nativeToValue(message); + + Map presentNull = new java.util.HashMap<>(); + presentNull.put("msg", null); + + return Stream.of( + dynamicTest( + "generated message equality", + () -> assertExpression(protoEnv, "msg.single_string == 'cel'", Map.of("msg", message))), + dynamicTest( + "dynamic message", + () -> assertExpression(protoEnv, "msg.single_string", Map.of("msg", dynamic))), + dynamicTest( + "wrapped message", + () -> assertExpression(protoEnv, "msg.single_string", Map.of("msg", wrapped))), + dynamicTest( + "default message", + () -> + assertExpression( + protoEnv, + "msg.single_string", + Map.of("msg", TestAllTypes.getDefaultInstance()))), + dynamicTest( + "wrong runtime object", + () -> assertExpression(protoEnv, "msg.single_string", Map.of("msg", "wrong object"))), + dynamicTest( + "message error", + () -> + assertExpression( + protoEnv, "msg.single_string", Map.of("msg", newErr("message error")))), + dynamicTest( + "message unknown", + () -> assertExpression(protoEnv, "msg.single_string", Map.of("msg", unknownOf(91L)))), + dynamicTest( + "absent message", () -> assertExpression(protoEnv, "msg.single_string", Map.of())), + dynamicTest( + "present null message", + () -> assertExpression(protoEnv, "msg.single_string", presentNull))); + } + + @Test + void evaluatesGeneratedAndDynamicProtobufPrimitiveSelectors() throws Exception { + String messageType = TestAllTypes.getDescriptor().getFullName(); + Env protoEnv = + newEnv( + types(TestAllTypes.getDefaultInstance()), + declarations( + Decls.newVar("msg", Decls.newObjectType(messageType)), + Decls.newVar("target", Decls.Int))); + TestAllTypes generated = + TestAllTypes.newBuilder() + .setSingleBool(true) + .setSingleInt64(50_021L) + .setSingleDouble(-0.0d) + .build(); + DynamicMessage dynamic = + DynamicMessage.parseFrom(TestAllTypes.getDescriptor(), generated.toByteString()); + + for (Object message : List.of(generated, dynamic)) { + assertExpression(protoEnv, "msg.single_bool", Map.of("msg", message)); + assertExpression( + protoEnv, "msg.single_int64 == target", Map.of("msg", message, "target", 50_021L)); + assertExpression(protoEnv, "msg.single_double == -0.0", Map.of("msg", message)); + } + } + + @Test + void protobufSelectorsPreservePartialQualifierMatching() { + String messageType = TestAllTypes.getDescriptor().getFullName(); + Env protoEnv = + newEnv( + types(TestAllTypes.getDefaultInstance()), + declarations(Decls.newVar("msg", Decls.newObjectType(messageType)))); + Ast ast = compile(protoEnv, "msg.single_string == 'cel'"); + Prog nativeProgram = (Prog) protoEnv.program(ast); + Prog interpreterProgram = + (Prog) protoEnv.program(ast, evalOptions(OptPartialEval, OptDisableNativeEval)); + Object input = + partialVars( + Map.of("msg", TestAllTypes.newBuilder().setSingleString("cel").build()), + attributePattern("msg").qualString("single_string")); + + assertIntegratedPlan(nativeProgram, "msg.single_string == 'cel'"); + assertEquivalent(nativeProgram.eval(input).getVal(), interpreterProgram.eval(input).getVal()); + } + + @TestFactory + Stream preservesTerminalOrderingAndShortCircuitSuppression() { + Val error = newErr("terminal error"); + Val unknown = unknownOf(123L); + + return Stream.of( + dynamicTest( + "false right operand suppresses left error for logical and", + () -> assertExpression("b && c", activation(Map.of("b", error, "c", false)))), + dynamicTest( + "false right operand suppresses left unknown for logical and", + () -> assertExpression("b && c", activation(Map.of("b", unknown, "c", false)))), + dynamicTest( + "true right operand suppresses left error for logical or", + () -> assertExpression("b || c", activation(Map.of("b", error, "c", true)))), + dynamicTest( + "true right operand suppresses left unknown for logical or", + () -> assertExpression("b || c", activation(Map.of("b", unknown, "c", true)))), + dynamicTest( + "left error precedes right unknown for equality", + () -> assertExpression("b == c", activation(Map.of("b", error, "c", unknown)))), + dynamicTest( + "left unknown precedes right error for equality", + () -> assertExpression("b == c", activation(Map.of("b", unknown, "c", error)))), + dynamicTest( + "conditional propagates an error condition", + () -> assertExpression("b ? x : y", activation(Map.of("b", error, "x", 1L, "y", 2L)))), + dynamicTest( + "conditional propagates an unknown condition", + () -> + assertExpression("b ? x : y", activation(Map.of("b", unknown, "x", 1L, "y", 2L))))); + } + + @Test + void shortCircuitAndConditionalReachOnlyRequiredIdentifiers() { + assertResolutionCounts("b && c", Map.of("b", false, "c", true), Map.of("b", 1, "c", 0)); + assertResolutionCounts("b || c", Map.of("b", true, "c", false), Map.of("b", 1, "c", 0)); + assertResolutionCounts( + "b ? x : y", Map.of("b", true, "x", 1L, "y", 2L), Map.of("b", 1, "x", 1, "y", 0)); + assertResolutionCounts( + "b ? x : y", Map.of("b", false, "x", 1L, "y", 2L), Map.of("b", 1, "x", 0, "y", 1)); + } + + static Stream numericBoundaryEvaluations() { + return Stream.of( + new Evaluation("x + y", Map.of("x", Long.MAX_VALUE, "y", 1L)), + new Evaluation("x - y", Map.of("x", Long.MIN_VALUE, "y", 1L)), + new Evaluation("x * y", Map.of("x", Long.MAX_VALUE, "y", 2L)), + new Evaluation("-x", Map.of("x", Long.MIN_VALUE)), + new Evaluation("x / y", Map.of("x", Long.MIN_VALUE, "y", -1L)), + new Evaluation("x % y", Map.of("x", 10L, "y", 0L)), + new Evaluation("d + 1.0", Map.of("d", Double.POSITIVE_INFINITY)), + new Evaluation("d - 1.0", Map.of("d", Double.NEGATIVE_INFINITY)), + new Evaluation("d < 0.0", Map.of("d", Double.NaN)), + new Evaluation("d >= -0.0", Map.of("d", 0.0d)), + new Evaluation("d == d", Map.of("d", Double.NaN))); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("numericBoundaryEvaluations") + void coversIntegerBoundariesAndDoubleSpecialValues(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void customAdapterAndUntrustedStandardLookingOverloadFallBack() { + TypeAdapter customAdapter = DefaultTypeAdapter.Instance::nativeToValue; + Env adapterEnv = + newEnv(customTypeAdapter(customAdapter), declarations(Decls.newVar("x", Decls.Int))); + Prog adapterProgram = (Prog) adapterEnv.program(compile(adapterEnv, "x + 1")); + assertCurrentPlan(adapterProgram); + + Library.StdLibrary standard = new Library.StdLibrary(); + List compileOptions = new ArrayList<>(standard.getCompileOptions()); + compileOptions.add( + declarations( + Decls.newVar("x", Decls.Int), + Decls.newVar("wordTarget", Decls.String), + Decls.newVar("words", Decls.newListType(Decls.String)), + Decls.newVar("target", Decls.Int), + Decls.newVar("numbers", Decls.newListType(Decls.Int)))); + Env untrustedEnv = newCustomEnv(compileOptions.toArray(EnvOption[]::new)); + Overload add = + Overload.binary(Operator.Add, Trait.AdderType, (left, right) -> ((Adder) left).add(right)); + Prog untrustedProgram = + (Prog) untrustedEnv.program(compile(untrustedEnv, "x + 1"), functions(add)); + assertCurrentPlan(untrustedProgram); + assertThat(untrustedProgram.eval(Map.of("x", 41L)).getVal().intValue()).isEqualTo(42L); + + Ast mappedExistsAst = + compile( + untrustedEnv, "numbers.map(value, value + target).exists(mapped, mapped == target)"); + Prog untrustedMappedExists = (Prog) untrustedEnv.program(mappedExistsAst, functions(add)); + assertCurrentPlan(untrustedMappedExists); + + Ast mappedAllAst = + compile(untrustedEnv, "numbers.map(value, value + target).all(mapped, mapped != target)"); + Prog untrustedMappedAll = (Prog) untrustedEnv.program(mappedAllAst, functions(add)); + assertCurrentPlan(untrustedMappedAll); + + Ast mappedExistsOneAst = + compile( + untrustedEnv, + "numbers.map(value, value + target).exists_one(mapped, mapped == target)"); + Prog untrustedMappedExistsOne = (Prog) untrustedEnv.program(mappedExistsOneAst, functions(add)); + assertCurrentPlan(untrustedMappedExistsOne); + + Overload membership = Overload.binary(Overloads.InList, (left, right) -> False); + Ast membershipAst = compile(untrustedEnv, "wordTarget in words"); + Prog untrustedMembership = (Prog) untrustedEnv.program(membershipAst, functions(membership)); + assertCurrentPlan(untrustedMembership); + assertThat( + untrustedMembership + .eval(Map.of("wordTarget", "cel", "words", List.of("cel"))) + .getVal() + .booleanValue()) + .isFalse(); + + Ast literalMembershipAst = compile(untrustedEnv, "wordTarget in ['cel']"); + Prog untrustedLiteralMembership = + (Prog) untrustedEnv.program(literalMembershipAst, functions(membership)); + assertEstablishedRoot( + untrustedLiteralMembership, "EvalBinary", "literal membership replacement"); + assertThat(untrustedLiteralMembership.eval(Map.of("wordTarget", "cel")).getVal().booleanValue()) + .isFalse(); + + Ast mappedMembershipAst = compile(untrustedEnv, "wordTarget in words.map(value, value)"); + Prog untrustedMappedMembership = + (Prog) untrustedEnv.program(mappedMembershipAst, functions(membership)); + assertCurrentPlan(untrustedMappedMembership); + assertThat( + untrustedMappedMembership + .eval(Map.of("wordTarget", "cel", "words", List.of("cel"))) + .getVal() + .booleanValue()) + .isFalse(); + + Overload index = Overload.binary(Overloads.IndexList, (left, right) -> intOf(99L)); + Ast topLevelIndexAst = compile(untrustedEnv, "numbers[0]"); + Prog untrustedTopLevelIndex = (Prog) untrustedEnv.program(topLevelIndexAst, functions(index)); + assertCurrentPlan(untrustedTopLevelIndex); + assertThat(untrustedTopLevelIndex.interpretable.getClass().getSimpleName()) + .isNotEqualTo("NativeIsland"); + assertThat(untrustedTopLevelIndex.eval(Map.of("numbers", new long[] {1L})).getVal().intValue()) + .isEqualTo(1L); + + Ast indexAst = compile(untrustedEnv, "[x, 2][0]"); + Prog untrustedIndex = (Prog) untrustedEnv.program(indexAst, functions(index)); + assertEstablishedRoot(untrustedIndex, "EvalAttr", "integer literal index replacement"); + assertThat(untrustedIndex.eval(Map.of("x", 1L)).getVal().intValue()).isEqualTo(1L); + + Ast stringIndexAst = compile(untrustedEnv, "['cel'][0]"); + Prog untrustedStringIndex = (Prog) untrustedEnv.program(stringIndexAst, functions(index)); + assertEstablishedRoot(untrustedStringIndex, "EvalAttr", "string literal index replacement"); + assertThat(untrustedStringIndex.eval(Map.of()).getVal().value()).isEqualTo("cel"); + + Ast boolIndexAst = compile(untrustedEnv, "[true, false][0]"); + Prog untrustedBoolIndex = (Prog) untrustedEnv.program(boolIndexAst, functions(index)); + assertEstablishedRoot(untrustedBoolIndex, "EvalAttr", "boolean literal index replacement"); + assertThat(untrustedBoolIndex.eval(Map.of()).getVal().booleanValue()).isTrue(); + + Ast mapIndexAst = compile(untrustedEnv, "numbers.map(value, value + target)[0]"); + Prog untrustedMapIndex = (Prog) untrustedEnv.program(mapIndexAst, functions(index)); + assertCurrentPlan(untrustedMapIndex); + + Overload size = Overload.unary(Overloads.SizeList, ignored -> intOf(99L)); + Ast sizeAst = compile(untrustedEnv, "size([x, 2])"); + Prog untrustedSize = (Prog) untrustedEnv.program(sizeAst, functions(size)); + assertEstablishedRoot(untrustedSize, "EvalUnary", "literal global size replacement"); + assertThat(untrustedSize.eval(Map.of("x", 1L)).getVal().intValue()).isEqualTo(99L); + + Ast filterSizeAst = compile(untrustedEnv, "size(numbers.filter(value, value == target))"); + Prog untrustedFilterSize = (Prog) untrustedEnv.program(filterSizeAst, functions(size)); + assertCurrentPlan(untrustedFilterSize); + + Ast mapSizeAst = compile(untrustedEnv, "size(numbers.map(value, value + target))"); + Prog untrustedMapSize = (Prog) untrustedEnv.program(mapSizeAst, functions(size)); + assertCurrentPlan(untrustedMapSize); + + Ast stringSizeAst = compile(untrustedEnv, "size(['cel'])"); + Prog untrustedStringSize = (Prog) untrustedEnv.program(stringSizeAst, functions(size)); + assertEstablishedRoot( + untrustedStringSize, "EvalUnary", "string literal global size replacement"); + assertThat(untrustedStringSize.eval(Map.of()).getVal().intValue()).isEqualTo(99L); + + Ast boolSizeAst = compile(untrustedEnv, "size([true, false])"); + Prog untrustedBoolSize = (Prog) untrustedEnv.program(boolSizeAst, functions(size)); + assertEstablishedRoot( + untrustedBoolSize, "EvalUnary", "boolean literal global size replacement"); + assertThat(untrustedBoolSize.eval(Map.of()).getVal().intValue()).isEqualTo(99L); + + Overload receiverSize = Overload.unary(Overloads.SizeListInst, ignored -> intOf(99L)); + Ast receiverSizeAst = compile(untrustedEnv, "[x, 2].size()"); + Prog untrustedReceiverSize = + (Prog) untrustedEnv.program(receiverSizeAst, functions(receiverSize)); + assertEstablishedRoot(untrustedReceiverSize, "EvalUnary", "literal receiver size replacement"); + assertThat(untrustedReceiverSize.eval(Map.of("x", 1L)).getVal().intValue()).isEqualTo(99L); + + Ast filterReceiverSizeAst = + compile(untrustedEnv, "numbers.filter(value, value == target).size()"); + Prog untrustedFilterReceiverSize = + (Prog) untrustedEnv.program(filterReceiverSizeAst, functions(receiverSize)); + assertCurrentPlan(untrustedFilterReceiverSize); + + Ast mapReceiverSizeAst = compile(untrustedEnv, "numbers.map(value, value + target).size()"); + Prog untrustedMapReceiverSize = + (Prog) untrustedEnv.program(mapReceiverSizeAst, functions(receiverSize)); + assertCurrentPlan(untrustedMapReceiverSize); + + Ast stringReceiverSizeAst = compile(untrustedEnv, "['cel'].size()"); + Prog untrustedStringReceiverSize = + (Prog) untrustedEnv.program(stringReceiverSizeAst, functions(receiverSize)); + assertEstablishedRoot( + untrustedStringReceiverSize, "EvalUnary", "string literal receiver size replacement"); + assertThat(untrustedStringReceiverSize.eval(Map.of()).getVal().intValue()).isEqualTo(99L); + + Ast boolReceiverSizeAst = compile(untrustedEnv, "[true, false].size()"); + Prog untrustedBoolReceiverSize = + (Prog) untrustedEnv.program(boolReceiverSizeAst, functions(receiverSize)); + assertEstablishedRoot( + untrustedBoolReceiverSize, "EvalUnary", "boolean literal receiver size replacement"); + assertThat(untrustedBoolReceiverSize.eval(Map.of()).getVal().intValue()).isEqualTo(99L); + } + + @Test + void customLibraryWithoutNativeDescriptorRemainsOnTheCurrentEvaluator() { + Env customEnv = + newEnv(Library.Lib(new IncrementLibrary()), declarations(Decls.newVar("x", Decls.Int))); + Prog program = (Prog) customEnv.program(compile(customEnv, "increment(x) + 1")); + + assertCurrentPlan(program); + assertThat(program.eval(Map.of("x", 40L)).getVal().intValue()).isEqualTo(42L); + } + + @Test + void scalarAdapterCapabilityDoesNotImplyPrimitiveArraySemantics() { + StandardScalarTypeAdapter scalarOnlyAdapter = + value -> + value instanceof long[] || value instanceof String[] + ? newErr("custom primitive array") + : DefaultTypeAdapter.Instance.nativeToValue(value); + Env adapterEnv = + newEnv( + customTypeAdapter(scalarOnlyAdapter), + declarations( + Decls.newVar("b", Decls.Bool), + Decls.newVar("d", Decls.Double), + Decls.newVar("numbers", Decls.newListType(Decls.Int)), + Decls.newVar("wordTarget", Decls.String), + Decls.newVar("words", Decls.newListType(Decls.String)))); + Ast ast = compile(adapterEnv, "numbers[0]"); + Prog nativeProgram = (Prog) adapterEnv.program(ast); + Interpretable current = undecoratedCurrent(nativeProgram, ast); + Object input = Map.of("numbers", new long[] {50_021L}); + + assertCurrentPlan(nativeProgram, "numbers[0]"); + assertEquivalent(nativeProgram.eval(input).getVal(), current.eval(newActivation(input))); + + Ast membershipAst = compile(adapterEnv, "wordTarget in words"); + Prog membershipProgram = (Prog) adapterEnv.program(membershipAst); + Interpretable currentMembership = undecoratedCurrent(membershipProgram, membershipAst); + Object membershipInput = Map.of("wordTarget", "cel", "words", new String[] {"cel"}); + assertCurrentPlan(membershipProgram, "wordTarget in words"); + assertEquivalent( + membershipProgram.eval(membershipInput).getVal(), + currentMembership.eval(newActivation(membershipInput))); + + Ast literalIndexAst = compile(adapterEnv, "[1, 2][0]"); + Prog literalIndexProgram = (Prog) adapterEnv.program(literalIndexAst); + assertEstablishedRoot(literalIndexProgram, "EvalAttr", "scalar-only adapter literal index"); + + Ast literalSizeAst = compile(adapterEnv, "size([1, 2])"); + Prog literalSizeProgram = (Prog) adapterEnv.program(literalSizeAst); + assertEstablishedRoot(literalSizeProgram, "EvalUnary", "scalar-only adapter literal size"); + + Ast doubleLiteralIndexAst = compile(adapterEnv, "[d, 2.0][0]"); + Prog doubleLiteralIndexProgram = (Prog) adapterEnv.program(doubleLiteralIndexAst); + assertEstablishedRoot( + doubleLiteralIndexProgram, "EvalAttr", "scalar-only adapter double literal index"); + + Ast receiverLiteralSizeAst = compile(adapterEnv, "[1, 2].size()"); + Prog receiverLiteralSizeProgram = (Prog) adapterEnv.program(receiverLiteralSizeAst); + assertEstablishedRoot( + receiverLiteralSizeProgram, "EvalUnary", "scalar-only adapter receiver literal size"); + + Ast literalMembershipAst = compile(adapterEnv, "wordTarget in ['cel']"); + Prog literalMembershipProgram = (Prog) adapterEnv.program(literalMembershipAst); + assertEstablishedRoot( + literalMembershipProgram, "EvalBinary", "scalar-only adapter literal membership"); + + Ast stringLiteralIndexAst = compile(adapterEnv, "['cel'][0]"); + Prog stringLiteralIndexProgram = (Prog) adapterEnv.program(stringLiteralIndexAst); + assertEstablishedRoot( + stringLiteralIndexProgram, "EvalAttr", "scalar-only adapter string literal index"); + + Ast stringLiteralSizeAst = compile(adapterEnv, "size(['cel'])"); + Prog stringLiteralSizeProgram = (Prog) adapterEnv.program(stringLiteralSizeAst); + assertEstablishedRoot( + stringLiteralSizeProgram, "EvalUnary", "scalar-only adapter string literal size"); + + Ast stringLiteralReceiverSizeAst = compile(adapterEnv, "['cel'].size()"); + Prog stringLiteralReceiverSizeProgram = (Prog) adapterEnv.program(stringLiteralReceiverSizeAst); + assertEstablishedRoot( + stringLiteralReceiverSizeProgram, + "EvalUnary", + "scalar-only adapter string literal receiver size"); + + Ast boolLiteralIndexAst = compile(adapterEnv, "[b, true][0]"); + Prog boolLiteralIndexProgram = (Prog) adapterEnv.program(boolLiteralIndexAst); + assertEstablishedRoot( + boolLiteralIndexProgram, "EvalAttr", "scalar-only adapter boolean literal index"); + + Ast boolLiteralSizeAst = compile(adapterEnv, "size([b, true])"); + Prog boolLiteralSizeProgram = (Prog) adapterEnv.program(boolLiteralSizeAst); + assertEstablishedRoot( + boolLiteralSizeProgram, "EvalUnary", "scalar-only adapter boolean literal size"); + + Ast boolLiteralReceiverSizeAst = compile(adapterEnv, "[b, true].size()"); + Prog boolLiteralReceiverSizeProgram = (Prog) adapterEnv.program(boolLiteralReceiverSizeAst); + assertEstablishedRoot( + boolLiteralReceiverSizeProgram, + "EvalUnary", + "scalar-only adapter boolean literal receiver size"); + + Ast mappedSizeAst = compile(adapterEnv, "size(numbers.map(value, value + 1))"); + Prog mappedSizeProgram = (Prog) adapterEnv.program(mappedSizeAst); + assertCurrentPlan(mappedSizeProgram, "scalar-only adapter mapped size"); + assertEquivalent( + mappedSizeProgram.eval(input).getVal(), + undecoratedCurrent(mappedSizeProgram, mappedSizeAst).eval(newActivation(input))); + + Ast mappedIndexAst = compile(adapterEnv, "numbers.map(value, value + 1)[0]"); + Prog mappedIndexProgram = (Prog) adapterEnv.program(mappedIndexAst); + assertCurrentPlan(mappedIndexProgram, "scalar-only adapter mapped index"); + assertEquivalent( + mappedIndexProgram.eval(input).getVal(), + undecoratedCurrent(mappedIndexProgram, mappedIndexAst).eval(newActivation(input))); + + Ast mappedMembershipAst = compile(adapterEnv, "wordTarget in words.map(value, value)"); + Prog mappedMembershipProgram = (Prog) adapterEnv.program(mappedMembershipAst); + assertCurrentPlan(mappedMembershipProgram, "scalar-only adapter mapped membership"); + assertEquivalent( + mappedMembershipProgram.eval(membershipInput).getVal(), + undecoratedCurrent(mappedMembershipProgram, mappedMembershipAst) + .eval(newActivation(membershipInput))); + + Ast mappedQuantifierAst = + compile(adapterEnv, "numbers.map(value, value + 1).exists(mapped, mapped == 2)"); + Prog mappedQuantifierProgram = (Prog) adapterEnv.program(mappedQuantifierAst); + assertCurrentPlan(mappedQuantifierProgram, "scalar-only adapter mapped quantifier"); + assertEquivalent( + mappedQuantifierProgram.eval(input).getVal(), + undecoratedCurrent(mappedQuantifierProgram, mappedQuantifierAst) + .eval(newActivation(input))); + } + + @Test + void topLevelListIndexesPreservePresentNullSourcesAndTypedSlowPaths() { + Map nullNumbers = new java.util.HashMap<>(); + nullNumbers.put("numbers", null); + assertExpression("numbers[0]", nullNumbers); + + Env typedListEnv = + newEnv( + declarations( + Decls.newVar("flags", Decls.newListType(Decls.Bool)), + Decls.newVar("nulls", Decls.newListType(Decls.Null)))); + for (Evaluation evaluation : + List.of( + new Evaluation("flags[0]", Map.of("flags", List.of(42L))), + new Evaluation("nulls[0]", Map.of("nulls", List.of("not null"))))) { + Programs programs = programs(typedListEnv, evaluation.expression()); + assertEquivalent( + programs.nativeProgram.eval(evaluation.input()).getVal(), + programs.interpreterProgram.eval(evaluation.input()).getVal()); + } + } + + @Test + void preservesPartialActivationUnknowns() { + Ast ast = compile("x + 1"); + Prog nativeProgram = (Prog) env.program(ast); + Prog interpreterProgram = + (Prog) env.program(ast, evalOptions(OptPartialEval, OptDisableNativeEval)); + Object vars = partialVars(Map.of("x", 41L), attributePattern("x")); + + assertEquivalent(nativeProgram.eval(vars).getVal(), interpreterProgram.eval(vars).getVal()); + } + + static Stream canonicalExistsEvaluations() { + return Stream.of( + new Evaluation( + "numbers.exists(value, value == target)", + Map.of("numbers", new int[] {1, 2, 3}, "target", 2L)), + new Evaluation( + "numbers.exists(value, value == target)", + Map.of("numbers", new long[] {1, 2, 3}, "target", 4L)), + new Evaluation( + "numbers.exists(value, value == target)", + Map.of("numbers", List.of(1L, 2L, 3L), "target", 3L)), + new Evaluation( + "numbers.exists(value, value == target)", + Map.of("numbers", new Object[] {1L, 2L, 3L}, "target", 1L)), + new Evaluation( + "doubles.exists(value, value == d)", + Map.of("doubles", new double[] {-0.0d, Double.NaN}, "d", 0.0d)), + new Evaluation( + "words.exists(value, value == wordTarget)", + Map.of("words", new String[] {"zero", "cel"}, "wordTarget", "cel")), + new Evaluation( + "flags.exists(value, value == b)", + Map.of("flags", new Boolean[] {false, true}, "b", true)), + new Evaluation( + "numbers.exists(x, x == target)", + Map.of("numbers", new long[] {1, 2}, "x", 99L, "target", 2L)), + new Evaluation( + "numbers.exists(value, value == target || x / y == 0)", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "x", 1L, "y", 0L)), + new Evaluation( + "numbers.exists(value, value == target || x / y == 0)", + Map.of("numbers", new long[] {1}, "target", 2L, "x", 1L, "y", 0L)), + new Evaluation( + "numbers.exists(value, value == target || b)", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "b", unknownOf(201L))), + new Evaluation( + "numbers.exists(value, value == target)", + Map.of("numbers", newErr("range"), "target", 2L)), + new Evaluation( + "numbers.exists(value, value == target)", + Map.of("numbers", unknownOf(202L), "target", 2L))); + } + + @ParameterizedTest + @MethodSource("canonicalExistsEvaluations") + void evaluatesCanonicalExists(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + static Stream canonicalIntMapExistsEvaluations() { + Val adaptedNumbers = DefaultTypeAdapter.Instance.nativeToValue(List.of(1L, 2L, 3L)); + return Stream.of( + new Evaluation( + "numbers.map(value, value).exists(mapped, mapped == target)", + Map.of("numbers", new int[] {1, 2, 3}, "target", 2L)), + new Evaluation( + "numbers.map(value, value + x).exists(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2, 3}, "x", 4L, "target", 7L)), + new Evaluation( + "numbers.map(value, value * x).exists(mapped, mapped == target)", + Map.of("numbers", List.of(1L, 2L, 3L), "x", 3L, "target", 10L)), + new Evaluation( + "numbers.map(value, value - x).exists(mapped, mapped == target)", + Map.of("numbers", new Object[] {1L, 2L, 3L}, "x", 1L, "target", 0L)), + new Evaluation( + "numbers.map(value, value + x).exists(mapped, mapped == target)", + Map.of("numbers", adaptedNumbers, "x", 2L, "target", 5L)), + new Evaluation( + "numbers.map(value, value + x).exists(mapped, mapped == target)", + Map.of("numbers", new long[0], "x", 2L, "target", 2L)), + new Evaluation( + "words.map(value, value == s ? x : y).exists(mapped, mapped == target)", + Map.of( + "words", + new String[] {"other", "cel"}, + "s", + "cel", + "x", + 3L, + "y", + 1L, + "target", + 3L)), + new Evaluation( + "doubles.map(value, value == d ? x : y).exists(mapped, mapped == target)", + Map.of( + "doubles", + new double[] {-0.0d, Double.NaN}, + "d", + 0.0d, + "x", + 3L, + "y", + 1L, + "target", + 3L)), + new Evaluation( + "flags.map(value, value ? x : y).exists(mapped, mapped == target)", + Map.of("flags", new Boolean[] {false, true}, "x", 3L, "y", 1L, "target", 3L)), + new Evaluation( + "numbers.map(value, value == 2 ? x : value).exists(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2}, "x", "runtime mismatch", "target", 1L)), + new Evaluation( + "numbers.map(value, value == 1 ? x : value).exists(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2}, "x", "runtime mismatch", "target", 2L)), + new Evaluation( + "numbers.map(value, value + 1).exists(mapped, mapped == target)", + Map.of("numbers", new Object[] {"runtime mismatch", 2L}, "target", 3L)), + new Evaluation( + "numbers.map(value, value + x).exists(mapped, mapped == target || b)", + Map.of("numbers", new long[] {1, 2}, "x", 1L, "target", 3L, "b", unknownOf(225L))), + new Evaluation( + "numbers.map(value, value + 1)" + ".exists(mapped, mapped == target || x / y == 0)", + Map.of("numbers", new long[] {1, 2}, "target", 3L, "x", 1L, "y", 0L)), + new Evaluation( + "numbers.map(value, value + 1)" + ".exists(mapped, mapped == target || x / y == 0)", + Map.of("numbers", new long[] {1}, "target", 3L, "x", 1L, "y", 0L)), + new Evaluation( + "numbers.map(value, value == target ? x / y : value)" + + ".exists(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "x", 1L, "y", 0L)), + new Evaluation( + "numbers.map(value, b ? value : target).exists(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "b", unknownOf(226L))), + new Evaluation( + "numbers.map(value, value + x).exists(mapped, mapped == target)", + Map.of("numbers", newErr("range"), "x", 1L, "target", 2L)), + new Evaluation( + "numbers.map(value, value + x).exists(mapped, mapped == target)", + Map.of("numbers", unknownOf(227L), "x", 1L, "target", 2L)), + new Evaluation( + "numbers.map(value, value + x).exists(mapped, mapped == target)", + Map.of("numbers", "not a list", "x", 1L, "target", 2L)), + new Evaluation( + "numbers.map(value, value + x).exists(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2}, "target", 2L)), + new Evaluation( + "numbers.map(value, value + 1).exists(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2})), + new Evaluation( + "numbers.map(x, x + target).exists(y, y == x)", + Map.of("numbers", new long[] {1, 2}, "x", 4L, "target", 2L))); + } + + @ParameterizedTest + @MethodSource("canonicalIntMapExistsEvaluations") + void evaluatesCanonicalIntMapExists(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + static Stream canonicalIntMapAllEvaluations() { + Val adaptedNumbers = DefaultTypeAdapter.Instance.nativeToValue(List.of(1L, 2L, 3L)); + return Stream.of( + new Evaluation( + "numbers.map(value, value).all(mapped, mapped != target)", + Map.of("numbers", new int[] {1, 2, 3}, "target", 4L)), + new Evaluation( + "numbers.map(value, value + x).all(mapped, mapped != target)", + Map.of("numbers", new long[] {1, 2, 3}, "x", 4L, "target", 7L)), + new Evaluation( + "numbers.map(value, value * x).all(mapped, mapped != target)", + Map.of("numbers", List.of(1L, 2L, 3L), "x", 3L, "target", 10L)), + new Evaluation( + "numbers.map(value, value - x).all(mapped, mapped != target)", + Map.of("numbers", new Object[] {1L, 2L, 3L}, "x", 1L, "target", 0L)), + new Evaluation( + "numbers.map(value, value + x).all(mapped, mapped != target)", + Map.of("numbers", adaptedNumbers, "x", 2L, "target", 5L)), + new Evaluation( + "numbers.map(value, value + x).all(mapped, mapped != target)", + Map.of("numbers", new long[0], "x", 2L, "target", 2L)), + new Evaluation( + "words.map(value, value == s ? x : y).all(mapped, mapped != target)", + Map.of( + "words", + new String[] {"other", "cel"}, + "s", + "cel", + "x", + 3L, + "y", + 1L, + "target", + 4L)), + new Evaluation( + "doubles.map(value, value == d ? x : y).all(mapped, mapped != target)", + Map.of( + "doubles", + new double[] {-0.0d, Double.NaN}, + "d", + 0.0d, + "x", + 3L, + "y", + 1L, + "target", + 3L)), + new Evaluation( + "flags.map(value, value ? x : y).all(mapped, mapped != target)", + Map.of("flags", new Boolean[] {false, true}, "x", 3L, "y", 1L, "target", 4L)), + new Evaluation( + "numbers.map(value, value == 2 ? x : value).all(mapped, mapped != target)", + Map.of("numbers", new long[] {1, 2}, "x", "runtime mismatch", "target", 1L)), + new Evaluation( + "numbers.map(value, value == 1 ? x : value).all(mapped, mapped != target)", + Map.of("numbers", new long[] {1, 2}, "x", "runtime mismatch", "target", 2L)), + new Evaluation( + "numbers.map(value, value + 1).all(mapped, mapped != target)", + Map.of("numbers", new Object[] {"runtime mismatch", 2L}, "target", 3L)), + new Evaluation( + "numbers.map(value, value + 1)" + ".all(mapped, mapped != target && x / y == 0)", + Map.of("numbers", new long[] {1, 2}, "target", 3L, "x", 1L, "y", 0L)), + new Evaluation( + "numbers.map(value, value + 1)" + ".all(mapped, mapped != target && x / y == 0)", + Map.of("numbers", new long[] {1}, "target", 3L, "x", 1L, "y", 0L)), + new Evaluation( + "numbers.map(value, value + 1).all(mapped, mapped != target && b)", + Map.of("numbers", new long[] {1, 2}, "target", 3L, "b", unknownOf(228L))), + new Evaluation( + "numbers.map(value, value == target ? x / y : value)" + + ".all(mapped, mapped != target)", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "x", 1L, "y", 0L)), + new Evaluation( + "numbers.map(value, b ? value : target).all(mapped, mapped != target)", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "b", unknownOf(229L))), + new Evaluation( + "numbers.map(value, value + x).all(mapped, mapped != target)", + Map.of("numbers", newErr("range"), "x", 1L, "target", 2L)), + new Evaluation( + "numbers.map(value, value + x).all(mapped, mapped != target)", + Map.of("numbers", unknownOf(230L), "x", 1L, "target", 2L)), + new Evaluation( + "numbers.map(value, value + x).all(mapped, mapped != target)", + Map.of("numbers", "not a list", "x", 1L, "target", 2L)), + new Evaluation( + "numbers.map(value, value + x).all(mapped, mapped != target)", + Map.of("numbers", new long[] {1, 2}, "target", 2L)), + new Evaluation( + "numbers.map(value, value + 1).all(mapped, mapped != target)", + Map.of("numbers", new long[] {1, 2})), + new Evaluation( + "numbers.map(x, x + target).all(y, y != x)", + Map.of("numbers", new long[] {1, 2}, "x", 4L, "target", 2L))); + } + + @ParameterizedTest + @MethodSource("canonicalIntMapAllEvaluations") + void evaluatesCanonicalIntMapAll(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void canonicalIntMapExistsMatchesCurrentFoldPartialActivationScope() { + Object input = + partialVars( + Map.of("numbers", new long[] {1, 2}, "x", 2L, "target", 4L), attributePattern("x")); + + assertExpression("numbers.map(value, value + x).exists(mapped, mapped == target)", input); + } + + @Test + void canonicalIntMapExistsUsesDefaultVariablesThroughBothFoldHierarchies() { + Ast ast = compile("numbers.map(value, value + x).exists(mapped, mapped == target)"); + Map defaults = Map.of("x", 2L, "target", 4L); + Prog nativeProgram = (Prog) env.program(ast, globals(defaults)); + Prog interpreterProgram = + (Prog) env.program(ast, globals(defaults), evalOptions(OptDisableNativeEval)); + Map input = Map.of("numbers", new long[] {1, 2}); + + assertIntegratedPlan(nativeProgram, "mapped integer exists with default variables"); + assertEquivalent(nativeProgram.eval(input).getVal(), interpreterProgram.eval(input).getVal()); + } + + @Test + void canonicalIntMapExistsPreservesTheConstructionThenPredicateBoundary() { + Programs programs = + programs("numbers.map(value, value + x)" + ".exists(mapped, mapped == target || y == 0)"); + Map values = + Map.of("numbers", new long[] {1, 2}, "x", 1L, "target", 3L, "y", 1L); + List nativeOrder = new ArrayList<>(); + List interpreterOrder = new ArrayList<>(); + + Val nativeResult = programs.nativeProgram.eval(orderedActivation(values, nativeOrder)).getVal(); + Val interpreterResult = + programs.interpreterProgram.eval(orderedActivation(values, interpreterOrder)).getVal(); + + assertEquivalent(nativeResult, interpreterResult); + assertThat(nativeOrder).containsExactly("numbers", "x", "x", "target", "y", "target"); + assertThat(interpreterOrder).containsExactlyElementsOf(nativeOrder); + + assertResolutionCounts( + "numbers.map(value, value + x).exists(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2, 3}, "x", 1L, "target", 2L), + Map.of("numbers", 1, "x", 3, "target", 1)); + assertResolutionCounts( + "numbers.map(value, value + x).exists(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2, 3}, "x", newErr("transform"), "target", 2L), + Map.of("numbers", 1, "x", 1, "target", 0)); + } + + @Test + void canonicalIntMapAllMatchesCurrentFoldScopeAndOrdering() { + Object partialInput = + partialVars( + Map.of("numbers", new long[] {1, 2}, "x", 2L, "target", 5L), attributePattern("x")); + assertExpression("numbers.map(value, value + x).all(mapped, mapped != target)", partialInput); + + Ast ast = compile("numbers.map(value, value + x).all(mapped, mapped != target)"); + Map defaults = Map.of("x", 2L, "target", 5L); + Prog nativeProgram = (Prog) env.program(ast, globals(defaults)); + Prog interpreterProgram = + (Prog) env.program(ast, globals(defaults), evalOptions(OptDisableNativeEval)); + Map input = Map.of("numbers", new long[] {1, 2}); + assertIntegratedPlan(nativeProgram, "mapped integer all with default variables"); + assertEquivalent(nativeProgram.eval(input).getVal(), interpreterProgram.eval(input).getVal()); + + Programs programs = + programs("numbers.map(value, value + x)" + ".all(mapped, mapped != target && y != 0)"); + Map values = + Map.of("numbers", new long[] {1, 2}, "x", 1L, "target", 3L, "y", 1L); + List nativeOrder = new ArrayList<>(); + List interpreterOrder = new ArrayList<>(); + assertEquivalent( + programs.nativeProgram.eval(orderedActivation(values, nativeOrder)).getVal(), + programs.interpreterProgram.eval(orderedActivation(values, interpreterOrder)).getVal()); + assertThat(nativeOrder).containsExactly("numbers", "x", "x", "target", "y", "target"); + assertThat(interpreterOrder).containsExactlyElementsOf(nativeOrder); + + assertResolutionCounts( + "numbers.map(value, value + x).all(mapped, mapped != target)", + Map.of("numbers", new long[] {1, 2, 3}, "x", 1L, "target", 2L), + Map.of("numbers", 1, "x", 3, "target", 1)); + assertResolutionCounts( + "numbers.map(value, value + x).all(mapped, mapped != target)", + Map.of("numbers", new long[] {1, 2, 3}, "x", newErr("transform"), "target", 2L), + Map.of("numbers", 1, "x", 1, "target", 0)); + } + + static Stream canonicalIntMapExistsOneEvaluations() { + Val adaptedNumbers = DefaultTypeAdapter.Instance.nativeToValue(List.of(1L, 2L, 3L)); + return Stream.of( + new Evaluation( + "numbers.map(value, value).exists_one(mapped, mapped == target)", + Map.of("numbers", new int[] {1, 2, 3}, "target", 2L)), + new Evaluation( + "numbers.map(value, value + x).exists_one(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2, 3}, "x", 4L, "target", 8L)), + new Evaluation( + "numbers.map(value, value * x).exists_one(mapped, mapped == target)", + Map.of("numbers", List.of(1L, 2L, 2L), "x", 3L, "target", 6L)), + new Evaluation( + "numbers.map(value, value - x).exists_one(mapped, mapped == target)", + Map.of("numbers", new Object[] {1L, 2L, 3L}, "x", 1L, "target", 0L)), + new Evaluation( + "numbers.map(value, value + x).exists_one(mapped, mapped == target)", + Map.of("numbers", adaptedNumbers, "x", 2L, "target", 5L)), + new Evaluation( + "numbers.map(value, value + x).exists_one(mapped, mapped == target)", + Map.of("numbers", new long[0], "x", 2L, "target", 2L)), + new Evaluation( + "words.map(value, value == s ? x : y).exists_one(mapped, mapped == target)", + Map.of( + "words", + new String[] {"other", "cel"}, + "s", + "cel", + "x", + 3L, + "y", + 1L, + "target", + 3L)), + new Evaluation( + "doubles.map(value, value == d ? x : y).exists_one(mapped, mapped == target)", + Map.of( + "doubles", + new double[] {-0.0d, Double.NaN}, + "d", + 0.0d, + "x", + 3L, + "y", + 1L, + "target", + 3L)), + new Evaluation( + "flags.map(value, value ? x : y).exists_one(mapped, mapped == target)", + Map.of("flags", new Boolean[] {false, true}, "x", 3L, "y", 1L, "target", 3L)), + new Evaluation( + "numbers.map(value, value == 2 ? x : value).exists_one(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2}, "x", "runtime mismatch", "target", 1L)), + new Evaluation( + "numbers.map(value, value == 1 ? x : value).exists_one(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2}, "x", "runtime mismatch", "target", 2L)), + new Evaluation( + "numbers.map(value, value + 1).exists_one(mapped, mapped == target)", + Map.of("numbers", new Object[] {"runtime mismatch", 2L}, "target", 3L)), + new Evaluation( + "numbers.map(value, value + 1)" + ".exists_one(mapped, mapped == target || x / y == 0)", + Map.of("numbers", new long[] {1, 2}, "target", 3L, "x", 1L, "y", 0L)), + new Evaluation( + "numbers.map(value, value + 1)" + ".exists_one(mapped, mapped == target || x / y == 0)", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "x", 1L, "y", 0L)), + new Evaluation( + "numbers.map(value, value == target ? x / y : value)" + + ".exists_one(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "x", 1L, "y", 0L)), + new Evaluation( + "numbers.map(value, b ? value : target).exists_one(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "b", unknownOf(232L))), + new Evaluation( + "numbers.map(value, value + x).exists_one(mapped, mapped == target)", + Map.of("numbers", newErr("range"), "x", 1L, "target", 2L)), + new Evaluation( + "numbers.map(value, value + x).exists_one(mapped, mapped == target)", + Map.of("numbers", unknownOf(233L), "x", 1L, "target", 2L)), + new Evaluation( + "numbers.map(value, value + x).exists_one(mapped, mapped == target)", + Map.of("numbers", "not a list", "x", 1L, "target", 2L)), + new Evaluation( + "numbers.map(value, value + x).exists_one(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2}, "target", 2L)), + new Evaluation( + "numbers.map(value, value + 1).exists_one(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2})), + new Evaluation( + "numbers.map(x, x + target).exists_one(y, y == x)", + Map.of("numbers", new long[] {1, 2}, "x", 4L, "target", 2L))); + } + + @ParameterizedTest + @MethodSource("canonicalIntMapExistsOneEvaluations") + void evaluatesCanonicalIntMapExistsOne(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void canonicalIntMapExistsOneMatchesCurrentFoldScopeAndCompleteTraversal() { + Object partialInput = + partialVars( + Map.of("numbers", new long[] {1, 2}, "x", 2L, "target", 4L), attributePattern("x")); + assertExpression( + "numbers.map(value, value + x).exists_one(mapped, mapped == target)", partialInput); + + Ast ast = compile("numbers.map(value, value + x).exists_one(mapped, mapped == target)"); + Map defaults = Map.of("x", 2L, "target", 4L); + Prog nativeProgram = (Prog) env.program(ast, globals(defaults)); + Prog interpreterProgram = + (Prog) env.program(ast, globals(defaults), evalOptions(OptDisableNativeEval)); + Map input = Map.of("numbers", new long[] {1, 2}); + assertIntegratedPlan(nativeProgram, "mapped integer exists_one with default variables"); + assertEquivalent(nativeProgram.eval(input).getVal(), interpreterProgram.eval(input).getVal()); + + Programs programs = + programs( + "numbers.map(value, value + x)" + ".exists_one(mapped, mapped == target || y == 0)"); + Map values = + Map.of("numbers", new long[] {1, 2}, "x", 1L, "target", 3L, "y", 1L); + List nativeOrder = new ArrayList<>(); + List interpreterOrder = new ArrayList<>(); + assertEquivalent( + programs.nativeProgram.eval(orderedActivation(values, nativeOrder)).getVal(), + programs.interpreterProgram.eval(orderedActivation(values, interpreterOrder)).getVal()); + assertThat(nativeOrder).containsExactly("numbers", "x", "x", "target", "y", "target"); + assertThat(interpreterOrder).containsExactlyElementsOf(nativeOrder); + + assertResolutionCounts( + "numbers.map(value, value + x).exists_one(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2, 3}, "x", 1L, "target", 2L), + Map.of("numbers", 1, "x", 3, "target", 3)); + assertResolutionCounts( + "numbers.map(value, value + x).exists_one(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2, 3}, "x", newErr("transform"), "target", 2L), + Map.of("numbers", 1, "x", 1, "target", 0)); + assertResolutionCounts( + "numbers.map(value, value + 1)" + ".exists_one(mapped, mapped == target || x / y == 0)", + Map.of("numbers", new long[] {1, 2, 3}, "target", 2L, "x", 1L, "y", 0L), + Map.of("numbers", 1, "target", 3, "x", 2, "y", 2)); + } + + static Stream canonicalFilteredIntMapAggregateEvaluations() { + Val adaptedNumbers = DefaultTypeAdapter.Instance.nativeToValue(List.of(1L, 2L, 3L)); + return Stream.of( + new Evaluation( + "numbers.map(value, value > 1, value + x).exists(mapped, mapped == target)", + Map.of("numbers", new int[] {1, 2, 3}, "x", 2L, "target", 4L)), + new Evaluation( + "numbers.map(value, value > 1, value + x).all(mapped, mapped != target)", + Map.of("numbers", new long[] {1, 2, 3}, "x", 2L, "target", 4L)), + new Evaluation( + "numbers.map(value, value > 1, value + x).exists_one(mapped, mapped == target)", + Map.of("numbers", List.of(1L, 2L, 2L), "x", 2L, "target", 4L)), + new Evaluation( + "numbers.filter(value, value > x).exists(mapped, mapped == target)", + Map.of("numbers", new Object[] {1L, 2L, 3L}, "x", 1L, "target", 2L)), + new Evaluation( + "numbers.filter(value, value > x).all(mapped, mapped != target)", + Map.of("numbers", adaptedNumbers, "x", 1L, "target", 1L)), + new Evaluation( + "numbers.filter(value, value > x).exists_one(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2, 2}, "x", 1L, "target", 2L)), + new Evaluation( + "numbers.filter(value, true).exists(mapped, mapped == target)", + Map.of("numbers", new Object[] {newErr("accepted element"), 2L}, "target", 2L)), + new Evaluation( + "numbers.filter(value, false).all(mapped, mapped != target)", + Map.of("numbers", new Object[] {newErr("suppressed element")}, "target", 2L)), + new Evaluation( + "numbers.map(value, false, value / y).all(mapped, mapped != target)", + Map.of("numbers", new long[] {1, 2}, "y", 0L, "target", 1L)), + new Evaluation( + "numbers.map(value, value != target, value / (value - target))" + + ".exists(mapped, mapped == 0)", + Map.of("numbers", new long[] {1, 2}, "target", 2L)), + new Evaluation( + "numbers.map(value, x / y == 0, value + 1).exists(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2}, "x", 1L, "y", 0L, "target", 2L)), + new Evaluation( + "numbers.map(value, b, value + 1).all(mapped, mapped != target)", + Map.of("numbers", new long[] {1, 2}, "b", unknownOf(234L), "target", 2L)), + new Evaluation( + "numbers.map(value, value == target, value / y)" + + ".exists_one(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "y", 0L)), + new Evaluation( + "numbers.map(value, value == target, b ? value : x)" + + ".exists(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "b", unknownOf(235L), "x", 1L)), + new Evaluation( + "numbers.map(value, value != target, value == 1 ? x : value)" + + ".exists_one(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "x", "suppressed runtime mismatch")), + new Evaluation( + "numbers.map(value, value > 0, value == 1 ? x : value)" + + ".exists(mapped, mapped == target)", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "x", "accepted runtime mismatch")), + new Evaluation( + "words.map(value, value != s, value == wordTarget ? x : y)" + + ".exists(mapped, mapped == target)", + Map.of( + "words", + new String[] {"skip", "cel"}, + "s", + "skip", + "wordTarget", + "cel", + "x", + 3L, + "y", + 1L, + "target", + 3L)), + new Evaluation( + "doubles.map(value, value != d, value > 0.0 ? x : y)" + + ".all(mapped, mapped != target)", + Map.of( + "doubles", new double[] {-0.0d, 1.0d}, "d", 0.0d, "x", 3L, "y", 1L, "target", 1L)), + new Evaluation( + "flags.map(value, value, value ? x : y)" + ".exists_one(mapped, mapped == target)", + Map.of("flags", new Boolean[] {false, true}, "x", 3L, "y", 1L, "target", 3L)), + new Evaluation( + "numbers.map(x, x != target, x + target).exists_one(y, y == x)", + Map.of("numbers", new long[] {1, 2}, "x", 4L, "target", 2L))); + } + + @ParameterizedTest + @MethodSource("canonicalFilteredIntMapAggregateEvaluations") + void evaluatesCanonicalFilteredIntMapAggregates(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void canonicalFilteredIntMapAggregatesPreserveConstructionScopeAndOrdering() { + Object partialInput = + partialVars( + Map.of("numbers", new long[] {1, 2}, "x", 2L, "target", 4L), attributePattern("x")); + assertExpression( + "numbers.map(value, value > 0, value + x).exists(mapped, mapped == target)", partialInput); + + Ast ast = + compile("numbers.map(value, value > y, value + x).exists_one(mapped, mapped == target)"); + Map defaults = Map.of("x", 2L, "y", 0L, "target", 4L); + Prog nativeProgram = (Prog) env.program(ast, globals(defaults)); + Prog interpreterProgram = + (Prog) env.program(ast, globals(defaults), evalOptions(OptDisableNativeEval)); + Map input = Map.of("numbers", new long[] {1, 2}); + assertIntegratedPlan(nativeProgram, "filtered mapped integer aggregate"); + assertEquivalent(nativeProgram.eval(input).getVal(), interpreterProgram.eval(input).getVal()); + + Programs programs = + programs("numbers.map(value, value != target, value + x).exists(mapped, mapped == y)"); + Map values = + Map.of("numbers", new long[] {1, 2}, "target", 2L, "x", 1L, "y", 2L); + List nativeOrder = new ArrayList<>(); + List interpreterOrder = new ArrayList<>(); + assertEquivalent( + programs.nativeProgram.eval(orderedActivation(values, nativeOrder)).getVal(), + programs.interpreterProgram.eval(orderedActivation(values, interpreterOrder)).getVal()); + assertThat(nativeOrder).containsExactly("numbers", "target", "x", "target", "y"); + assertThat(interpreterOrder).containsExactlyElementsOf(nativeOrder); + + assertResolutionCounts( + "numbers.map(value, value != target, value + x).exists(mapped, mapped == y)", + Map.of("numbers", new long[] {1, 2, 3}, "target", 2L, "x", 1L, "y", 2L), + Map.of("numbers", 1, "target", 3, "x", 2, "y", 1)); + assertResolutionCounts( + "numbers.map(value, false, value + x).all(mapped, mapped != y)", + Map.of("numbers", new long[] {1, 2, 3}, "x", 1L, "y", 2L), + Map.of("numbers", 1, "x", 0, "y", 0)); + assertResolutionCounts( + "numbers.map(value, value > 0, value + x).exists_one(mapped, mapped == y)", + Map.of("numbers", new long[] {1, 2, 3}, "x", newErr("transform"), "y", 2L), + Map.of("numbers", 1, "x", 1, "y", 0)); + } + + static Stream canonicalAllEvaluations() { + return Stream.of( + new Evaluation( + "numbers.all(value, value < target)", + Map.of("numbers", new int[] {1, 2, 3}, "target", 4L)), + new Evaluation( + "numbers.all(value, value != target)", + Map.of("numbers", new long[] {1, 2, 3}, "target", 3L)), + new Evaluation( + "numbers.all(value, value <= target)", + Map.of("numbers", List.of(1L, 2L, 3L), "target", 3L)), + new Evaluation( + "numbers.all(value, value > target)", + Map.of("numbers", new Object[] {1L, 2L, 3L}, "target", 0L)), + new Evaluation( + "numbers.all(value, value == target)", Map.of("numbers", new long[0], "target", 1L)), + new Evaluation( + "doubles.all(value, value == d)", + Map.of("doubles", new double[] {-0.0d, 0.0d}, "d", 0.0d)), + new Evaluation( + "words.all(value, value != wordTarget)", + Map.of("words", new String[] {"zero", "cel"}, "wordTarget", "absent")), + new Evaluation( + "flags.all(value, value == b)", Map.of("flags", new boolean[] {true, true}, "b", true)), + new Evaluation( + "flags.all(value, value == b)", + Map.of("flags", new Boolean[] {true, false}, "b", true)), + new Evaluation( + "flags.all(value, value)", + Map.of("flags", new Object[] {newErr("predicate"), unknownOf(206L)})), + new Evaluation( + "flags.all(value, value)", + Map.of("flags", new Object[] {unknownOf(207L), newErr("predicate")})), + new Evaluation( + "numbers.all(x, x <= target)", + Map.of("numbers", new long[] {1, 2}, "x", 99L, "target", 2L)), + new Evaluation( + "numbers.all(value, value == target && x / y == 0)", + Map.of("numbers", new long[] {1, 2}, "target", 1L, "x", 1L, "y", 0L)), + new Evaluation( + "numbers.all(value, value == target && x / y == 0)", + Map.of("numbers", new long[] {1}, "target", 1L, "x", 1L, "y", 0L)), + new Evaluation( + "numbers.all(value, value == target && b)", + Map.of("numbers", new long[] {1, 2}, "target", 1L, "b", unknownOf(203L))), + new Evaluation( + "numbers.all(value, value == target && b)", + Map.of("numbers", new long[] {1}, "target", 1L, "b", unknownOf(204L))), + new Evaluation( + "numbers.all(value, value != target)", + Map.of("numbers", newErr("range"), "target", 2L)), + new Evaluation( + "numbers.all(value, value != target)", + Map.of("numbers", unknownOf(205L), "target", 2L))); + } + + @ParameterizedTest + @MethodSource("canonicalAllEvaluations") + void evaluatesCanonicalAll(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + static Stream canonicalExistsOneEvaluations() { + return Stream.of( + new Evaluation( + "numbers.exists_one(value, value == target)", + Map.of("numbers", new int[] {1, 2, 3}, "target", 2L)), + new Evaluation( + "numbers.exists_one(value, value == target)", + Map.of("numbers", new long[] {1, 2, 3}, "target", 4L)), + new Evaluation( + "numbers.exists_one(value, value == target)", + Map.of("numbers", List.of(1L, 2L, 2L), "target", 2L)), + new Evaluation( + "numbers.exists_one(value, value == target)", + Map.of("numbers", new Object[] {1L, 2L, 3L}, "target", 1L)), + new Evaluation( + "numbers.exists_one(value, value == target)", + Map.of("numbers", new long[0], "target", 1L)), + new Evaluation( + "doubles.exists_one(value, value == d)", + Map.of("doubles", new double[] {-0.0d, Double.NaN}, "d", 0.0d)), + new Evaluation( + "words.exists_one(value, value == wordTarget)", + Map.of("words", new String[] {"cel", "other", "cel"}, "wordTarget", "cel")), + new Evaluation( + "words.exists_one(value, value == wordTarget)", + Map.of("words", new String[] {null}, "wordTarget", "cel")), + new Evaluation( + "flags.exists_one(value, value == b)", + Map.of("flags", new Boolean[] {false, true}, "b", true)), + new Evaluation( + "flags.exists_one(value, value)", + Map.of("flags", new Object[] {newErr("predicate"), unknownOf(208L)})), + new Evaluation( + "flags.exists_one(value, value)", + Map.of("flags", new Object[] {unknownOf(209L), newErr("predicate")})), + new Evaluation( + "numbers.exists_one(x, x == target)", + Map.of("numbers", new long[] {1, 2}, "x", 99L, "target", 2L)), + new Evaluation( + "numbers.exists_one(value, value == target || x / y == 0)", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "x", 1L, "y", 0L)), + new Evaluation( + "numbers.exists_one(value, value == target || b)", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "b", unknownOf(210L))), + new Evaluation( + "numbers.exists_one(value, value == target)", + Map.of("numbers", newErr("range"), "target", 2L)), + new Evaluation( + "numbers.exists_one(value, value == target)", + Map.of("numbers", unknownOf(211L), "target", 2L))); + } + + @ParameterizedTest + @MethodSource("canonicalExistsOneEvaluations") + void evaluatesCanonicalExistsOne(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @ParameterizedTest + @ValueSource( + strings = { + "words.exists(value, value == wordTarget)", + "words.all(value, value != wordTarget)" + }) + void canonicalQuantifiersPreserveNullBackedStringFailure(String expression) { + Programs programs = programs(expression); + Map input = Map.of("words", new String[] {null}, "wordTarget", "cel"); + + Throwable nativeFailure = catchThrowable(() -> programs.nativeProgram.eval(input)); + Throwable interpreterFailure = catchThrowable(() -> programs.interpreterProgram.eval(input)); + + assertThat(nativeFailure).isInstanceOf(interpreterFailure.getClass()); + assertThat(nativeFailure.getCause()).isInstanceOf(interpreterFailure.getCause().getClass()); + assertThat(nativeFailure.getCause().getMessage()) + .isEqualTo(interpreterFailure.getCause().getMessage()); + } + + @ParameterizedTest + @ValueSource( + strings = { + "numbers.exists(value, value == target)", + "numbers.all(value, value != target)", + "numbers.exists_one(value, value == target)" + }) + void canonicalQuantifiersMatchCurrentFoldPartialActivationScope(String expression) { + Ast ast = compile(expression); + Prog nativeProgram = (Prog) env.program(ast); + Prog interpreterProgram = + (Prog) env.program(ast, evalOptions(OptPartialEval, OptDisableNativeEval)); + Object vars = + partialVars(Map.of("numbers", new long[] {1, 2}, "target", 2L), attributePattern("target")); + + assertIntegratedPlan(nativeProgram, expression); + assertEquivalent(nativeProgram.eval(vars).getVal(), interpreterProgram.eval(vars).getVal()); + } + + @ParameterizedTest + @ValueSource( + strings = { + "numbers.exists(value, value == target)", + "numbers.all(value, value <= target)", + "numbers.exists_one(value, value == target)" + }) + void canonicalQuantifiersUseDefaultVariablesThroughTheCurrentFoldHierarchy(String expression) { + Ast ast = compile(expression); + Prog nativeProgram = (Prog) env.program(ast, globals(Map.of("target", 2L))); + Prog interpreterProgram = + (Prog) env.program(ast, globals(Map.of("target", 2L)), evalOptions(OptDisableNativeEval)); + + assertIntegratedPlan(nativeProgram, expression); + assertEquivalent( + nativeProgram.eval(Map.of("numbers", new long[] {1, 2})).getVal(), + interpreterProgram.eval(Map.of("numbers", new long[] {1, 2})).getVal()); + } + + @ParameterizedTest + @ValueSource( + strings = {"numbers.exists(value, value == target)", "numbers.all(value, value != target)"}) + void canonicalQuantifiersResolveRangeOnceAndStopResolvingAfterTheResult(String expression) { + Programs programs = programs(expression); + Map values = Map.of("numbers", new long[] {1L, 2L, 3L}, "target", 2L); + Map nativeCounts = new java.util.HashMap<>(); + Map interpreterCounts = new java.util.HashMap<>(); + + Val nativeResult = + programs.nativeProgram.eval(countingActivation(values, nativeCounts)).getVal(); + Val interpreterResult = + programs.interpreterProgram.eval(countingActivation(values, interpreterCounts)).getVal(); + + assertEquivalent(nativeResult, interpreterResult); + assertThat(nativeCounts).containsOnlyKeys("numbers", "target"); + assertThat(count(nativeCounts, "numbers")).isOne(); + assertThat(count(nativeCounts, "target")).isEqualTo(2); + assertThat(count(interpreterCounts, "numbers")).isEqualTo(count(nativeCounts, "numbers")); + assertThat(count(interpreterCounts, "target")).isEqualTo(count(nativeCounts, "target")); + } + + @Test + void canonicalExistsOneResolvesItsPredicateForEveryElement() { + Programs programs = programs("numbers.exists_one(value, value == target)"); + Map values = Map.of("numbers", new long[] {1L, 2L, 3L}, "target", 1L); + Map nativeCounts = new java.util.HashMap<>(); + Map interpreterCounts = new java.util.HashMap<>(); + + Val nativeResult = + programs.nativeProgram.eval(countingActivation(values, nativeCounts)).getVal(); + Val interpreterResult = + programs.interpreterProgram.eval(countingActivation(values, interpreterCounts)).getVal(); + + assertEquivalent(nativeResult, interpreterResult); + assertThat(nativeCounts).containsOnlyKeys("numbers", "target"); + assertThat(count(nativeCounts, "numbers")).isOne(); + assertThat(count(nativeCounts, "target")).isEqualTo(3); + assertThat(count(interpreterCounts, "numbers")).isEqualTo(count(nativeCounts, "numbers")); + assertThat(count(interpreterCounts, "target")).isEqualTo(count(nativeCounts, "target")); + } + + @Test + void canonicalExistsOneContinuesAfterAnExceptionalPredicate() { + Programs programs = programs("numbers.exists_one(value, value == target || x / y == 0)"); + Map values = + Map.of("numbers", new long[] {1L, 2L, 3L}, "target", 1L, "x", 1L, "y", 0L); + Map nativeCounts = new java.util.HashMap<>(); + Map interpreterCounts = new java.util.HashMap<>(); + + Val nativeResult = + programs.nativeProgram.eval(countingActivation(values, nativeCounts)).getVal(); + Val interpreterResult = + programs.interpreterProgram.eval(countingActivation(values, interpreterCounts)).getVal(); + + assertEquivalent(nativeResult, interpreterResult); + assertThat(count(nativeCounts, "numbers")).isOne(); + assertThat(count(nativeCounts, "target")).isEqualTo(3); + assertThat(count(nativeCounts, "x")).isEqualTo(2); + assertThat(count(nativeCounts, "y")).isEqualTo(2); + assertThat(count(interpreterCounts, "numbers")).isEqualTo(count(nativeCounts, "numbers")); + assertThat(count(interpreterCounts, "target")).isEqualTo(count(nativeCounts, "target")); + assertThat(count(interpreterCounts, "x")).isEqualTo(count(nativeCounts, "x")); + assertThat(count(interpreterCounts, "y")).isEqualTo(count(nativeCounts, "y")); + } + + static Stream canonicalFilterSizeEvaluations() { + Val adaptedNumbers = DefaultTypeAdapter.Instance.nativeToValue(List.of(1L, 2L, 3L)); + return Stream.of( + new Evaluation( + "size(numbers.filter(value, value >= target))", + Map.of("numbers", new int[] {1, 2, 3}, "target", 2L)), + new Evaluation( + "numbers.filter(value, value == target).size()", + Map.of("numbers", new long[] {1, 2, 3}, "target", 4L)), + new Evaluation( + "size(numbers.filter(value, value == target))", + Map.of("numbers", List.of(1L, 2L, 2L), "target", 2L)), + new Evaluation( + "size(numbers.filter(value, value != target))", + Map.of("numbers", new Object[] {1L, 2L, 3L}, "target", 2L)), + new Evaluation( + "size(numbers.filter(value, value <= target))", + Map.of("numbers", adaptedNumbers, "target", 2L)), + new Evaluation( + "size(numbers.filter(value, value == target))", + Map.of("numbers", new long[0], "target", 1L)), + new Evaluation( + "size(doubles.filter(value, value == d))", + Map.of("doubles", new double[] {-0.0d, Double.NaN}, "d", 0.0d)), + new Evaluation( + "size(words.filter(value, value == wordTarget))", + Map.of("words", new String[] {"cel", "other", "cel"}, "wordTarget", "cel")), + new Evaluation( + "size(flags.filter(value, value == b))", + Map.of("flags", new Boolean[] {false, true}, "b", true)), + new Evaluation( + "size(flags.filter(value, value))", + Map.of("flags", new Object[] {newErr("predicate"), true})), + new Evaluation( + "size(flags.filter(value, value))", + Map.of("flags", new Object[] {unknownOf(212L), true})), + new Evaluation( + "size(flags.filter(value, value))", Map.of("flags", new Object[] {"not a bool"})), + new Evaluation( + "size(numbers.filter(value, target > 0))", + Map.of("numbers", new Object[] {newErr("element"), 2L}, "target", 1L)), + new Evaluation( + "size(numbers.filter(value, target > 0))", + Map.of("numbers", new Object[] {unknownOf(222L), 2L}, "target", 1L)), + new Evaluation( + "size(numbers.filter(value, target > 0))", + Map.of("numbers", new Object[] {"runtime mismatch", 2L}, "target", 1L)), + new Evaluation( + "size(numbers.filter(x, x == target))", + Map.of("numbers", new long[] {1, 2}, "x", 99L, "target", 2L)), + new Evaluation( + "size(numbers.filter(value, value == target || x / y == 0))", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "x", 1L, "y", 0L)), + new Evaluation( + "size(numbers.filter(value, value == target || b))", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "b", unknownOf(213L))), + new Evaluation( + "size(numbers.filter(value, value == target))", + Map.of("numbers", newErr("range"), "target", 2L)), + new Evaluation( + "size(numbers.filter(value, value == target))", + Map.of("numbers", unknownOf(214L), "target", 2L))); + } + + @ParameterizedTest + @MethodSource("canonicalFilterSizeEvaluations") + void evaluatesCanonicalFilterSize(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void canonicalFilterSizePreservesNullBackedStringFailure() { + Programs programs = programs("size(words.filter(value, value == wordTarget))"); + Map input = Map.of("words", new String[] {null}, "wordTarget", "cel"); + + Throwable nativeFailure = catchThrowable(() -> programs.nativeProgram.eval(input)); + Throwable interpreterFailure = catchThrowable(() -> programs.interpreterProgram.eval(input)); + + assertThat(nativeFailure).isInstanceOf(interpreterFailure.getClass()); + assertThat(nativeFailure.getCause()).isInstanceOf(interpreterFailure.getCause().getClass()); + assertThat(nativeFailure.getCause().getMessage()) + .isEqualTo(interpreterFailure.getCause().getMessage()); + } + + @Test + void canonicalFilterSizeMatchesCurrentFoldPartialActivationScope() { + Ast ast = compile("size(numbers.filter(value, value == target))"); + Prog nativeProgram = (Prog) env.program(ast); + Prog interpreterProgram = + (Prog) env.program(ast, evalOptions(OptPartialEval, OptDisableNativeEval)); + Object vars = + partialVars(Map.of("numbers", new long[] {1, 2}, "target", 2L), attributePattern("target")); + + assertIntegratedPlan(nativeProgram, "filter size"); + assertEquivalent(nativeProgram.eval(vars).getVal(), interpreterProgram.eval(vars).getVal()); + } + + @Test + void canonicalFilterSizeUsesDefaultVariablesThroughTheCurrentFoldHierarchy() { + Ast ast = compile("size(numbers.filter(value, value <= target))"); + Prog nativeProgram = (Prog) env.program(ast, globals(Map.of("target", 2L))); + Prog interpreterProgram = + (Prog) env.program(ast, globals(Map.of("target", 2L)), evalOptions(OptDisableNativeEval)); + + assertIntegratedPlan(nativeProgram, "filter size"); + assertEquivalent( + nativeProgram.eval(Map.of("numbers", new long[] {1, 2})).getVal(), + interpreterProgram.eval(Map.of("numbers", new long[] {1, 2})).getVal()); + } + + @Test + void canonicalFilterSizeStopsAfterAnExceptionalPredicate() { + Programs programs = programs("size(numbers.filter(value, value == target || x / y == 0))"); + Map values = + Map.of("numbers", new long[] {1L, 2L, 3L}, "target", 1L, "x", 1L, "y", 0L); + Map nativeCounts = new java.util.HashMap<>(); + Map interpreterCounts = new java.util.HashMap<>(); + + Val nativeResult = + programs.nativeProgram.eval(countingActivation(values, nativeCounts)).getVal(); + Val interpreterResult = + programs.interpreterProgram.eval(countingActivation(values, interpreterCounts)).getVal(); + + assertEquivalent(nativeResult, interpreterResult); + assertThat(count(nativeCounts, "numbers")).isOne(); + assertThat(count(nativeCounts, "target")).isEqualTo(2); + assertThat(count(nativeCounts, "x")).isOne(); + assertThat(count(nativeCounts, "y")).isOne(); + assertThat(count(interpreterCounts, "numbers")).isEqualTo(count(nativeCounts, "numbers")); + assertThat(count(interpreterCounts, "target")).isEqualTo(count(nativeCounts, "target")); + assertThat(count(interpreterCounts, "x")).isEqualTo(count(nativeCounts, "x")); + assertThat(count(interpreterCounts, "y")).isEqualTo(count(nativeCounts, "y")); + } + + static Stream canonicalMapSizeEvaluations() { + Val adaptedNumbers = DefaultTypeAdapter.Instance.nativeToValue(List.of(1L, 2L, 3L)); + return Stream.of( + new Evaluation( + "size(numbers.map(value, value + target))", + Map.of("numbers", new int[] {1, 2, 3}, "target", 2L)), + new Evaluation( + "numbers.map(value, value == target).size()", + Map.of("numbers", new long[] {1, 2, 3}, "target", 4L)), + new Evaluation( + "size(numbers.map(value, value * target))", + Map.of("numbers", List.of(1L, 2L, 3L), "target", 2L)), + new Evaluation( + "size(numbers.map(value, value - target))", + Map.of("numbers", new Object[] {1L, 2L, 3L}, "target", 2L)), + new Evaluation( + "size(numbers.map(value, value <= target))", + Map.of("numbers", adaptedNumbers, "target", 2L)), + new Evaluation( + "size(numbers.map(value, value + target))", + Map.of("numbers", new long[0], "target", 1L)), + new Evaluation( + "size(doubles.map(value, value + d))", + Map.of("doubles", new double[] {-0.0d, Double.NaN}, "d", 0.0d)), + new Evaluation( + "size(words.map(value, value + s))", + Map.of("words", new String[] {"cel", "other"}, "s", "!")), + new Evaluation( + "size(flags.map(value, !value))", Map.of("flags", new Boolean[] {false, true})), + new Evaluation( + "size(flags.map(value, !value))", Map.of("flags", new boolean[] {false, true})), + new Evaluation("size(numbers.map(value, null))", Map.of("numbers", new long[] {1, 2})), + new Evaluation( + "size(numbers.map(value, value))", + Map.of("numbers", new Object[] {"runtime mismatch", null})), + new Evaluation( + "size(numbers.map(value, value))", + Map.of("numbers", new Object[] {newErr("transform"), 2L})), + new Evaluation( + "size(numbers.map(value, value))", + Map.of("numbers", new Object[] {unknownOf(215L), 2L})), + new Evaluation( + "size(words.map(value, value))", Map.of("words", new String[] {null, "cel"})), + new Evaluation( + "size(words.map(value, value + s))", Map.of("words", new String[] {null}, "s", "!")), + new Evaluation( + "size(numbers.map(x, x + target))", + Map.of("numbers", new long[] {1, 2}, "x", 99L, "target", 2L)), + new Evaluation( + "size(numbers.map(value, value == target ? x / y : value))", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "x", 1L, "y", 0L)), + new Evaluation( + "size(numbers.map(value, b ? value : target))", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "b", unknownOf(216L))), + new Evaluation( + "size(numbers.map(value, value + target))", + Map.of("numbers", newErr("range"), "target", 2L)), + new Evaluation( + "size(numbers.map(value, value + target))", + Map.of("numbers", unknownOf(217L), "target", 2L)), + new Evaluation("size(numbers.map(value, value))", Map.of("numbers", "not a list")), + new Evaluation( + "size(numbers.map(value, value + target))", Map.of("numbers", new long[] {1, 2}))); + } + + @ParameterizedTest + @MethodSource("canonicalMapSizeEvaluations") + void evaluatesCanonicalMapSize(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void canonicalMapSizeMatchesCurrentFoldPartialActivationScope() { + Ast ast = compile("size(numbers.map(value, value + target))"); + Prog nativeProgram = (Prog) env.program(ast); + Prog interpreterProgram = + (Prog) env.program(ast, evalOptions(OptPartialEval, OptDisableNativeEval)); + Object vars = + partialVars(Map.of("numbers", new long[] {1, 2}, "target", 2L), attributePattern("target")); + + assertIntegratedPlan(nativeProgram, "map size"); + assertEquivalent(nativeProgram.eval(vars).getVal(), interpreterProgram.eval(vars).getVal()); + } + + @Test + void canonicalMapSizeUsesDefaultVariablesThroughTheCurrentFoldHierarchy() { + Ast ast = compile("size(numbers.map(value, value + target))"); + Prog nativeProgram = (Prog) env.program(ast, globals(Map.of("target", 2L))); + Prog interpreterProgram = + (Prog) env.program(ast, globals(Map.of("target", 2L)), evalOptions(OptDisableNativeEval)); + + assertIntegratedPlan(nativeProgram, "map size"); + assertEquivalent( + nativeProgram.eval(Map.of("numbers", new long[] {1, 2})).getVal(), + interpreterProgram.eval(Map.of("numbers", new long[] {1, 2})).getVal()); + } + + @Test + void canonicalMapSizeEvaluatesEveryTransformUntilAnError() { + Programs programs = + programs("size(numbers.map(value, value == target ? x / y : value + target))"); + Map values = + Map.of("numbers", new long[] {1L, 2L, 3L}, "target", 2L, "x", 1L, "y", 0L); + Map nativeCounts = new java.util.HashMap<>(); + Map interpreterCounts = new java.util.HashMap<>(); + + Val nativeResult = + programs.nativeProgram.eval(countingActivation(values, nativeCounts)).getVal(); + Val interpreterResult = + programs.interpreterProgram.eval(countingActivation(values, interpreterCounts)).getVal(); + + assertEquivalent(nativeResult, interpreterResult); + assertThat(count(nativeCounts, "numbers")).isOne(); + assertThat(count(nativeCounts, "target")).isEqualTo(3); + assertThat(count(nativeCounts, "x")).isOne(); + assertThat(count(nativeCounts, "y")).isOne(); + assertThat(count(interpreterCounts, "numbers")).isEqualTo(count(nativeCounts, "numbers")); + assertThat(count(interpreterCounts, "target")).isEqualTo(count(nativeCounts, "target")); + assertThat(count(interpreterCounts, "x")).isEqualTo(count(nativeCounts, "x")); + assertThat(count(interpreterCounts, "y")).isEqualTo(count(nativeCounts, "y")); + } + + static Stream canonicalFilteredMapSizeEvaluations() { + Val adaptedNumbers = DefaultTypeAdapter.Instance.nativeToValue(List.of(1L, 2L, 3L)); + return Stream.of( + new Evaluation( + "size(numbers.map(value, value >= target, value + target))", + Map.of("numbers", new int[] {1, 2, 3}, "target", 2L)), + new Evaluation( + "numbers.map(value, value != target, value == target).size()", + Map.of("numbers", new long[] {1, 2, 3}, "target", 2L)), + new Evaluation( + "size(numbers.map(value, value <= target, value * target))", + Map.of("numbers", List.of(1L, 2L, 3L), "target", 2L)), + new Evaluation( + "size(numbers.map(value, value != target, value - target))", + Map.of("numbers", new Object[] {1L, 2L, 3L}, "target", 2L)), + new Evaluation( + "size(numbers.map(value, value <= target, value == target))", + Map.of("numbers", adaptedNumbers, "target", 2L)), + new Evaluation( + "size(numbers.map(value, value == target, value + target))", + Map.of("numbers", new long[0], "target", 1L)), + new Evaluation( + "size(doubles.map(value, value == d, value + d))", + Map.of("doubles", new double[] {-0.0d, Double.NaN}, "d", 0.0d)), + new Evaluation( + "size(words.map(value, value == wordTarget, value + s))", + Map.of("words", new String[] {"cel", "other", "cel"}, "wordTarget", "cel", "s", "!")), + new Evaluation( + "size(flags.map(value, value == b, !value))", + Map.of("flags", new Boolean[] {false, true}, "b", true)), + new Evaluation( + "size(flags.map(value, value == b, !value))", + Map.of("flags", new boolean[] {false, true}, "b", true)), + new Evaluation( + "size(flags.map(value, value, !value))", + Map.of("flags", new Object[] {newErr("predicate"), true})), + new Evaluation( + "size(flags.map(value, value, !value))", + Map.of("flags", new Object[] {unknownOf(218L), true})), + new Evaluation( + "size(flags.map(value, value, !value))", Map.of("flags", new Object[] {"not a bool"})), + new Evaluation( + "size(numbers.map(value, target > 0, value))", + Map.of("numbers", new Object[] {"runtime mismatch", 2L}, "target", 2L)), + new Evaluation( + "size(numbers.map(value, target > 0, value))", + Map.of("numbers", new Object[] {2L, newErr("transform")}, "target", 1L)), + new Evaluation( + "size(numbers.map(value, target > 0, value))", + Map.of("numbers", new Object[] {2L, unknownOf(219L)}, "target", 1L)), + new Evaluation( + "size(words.map(value, target > 0, value))", + Map.of("words", new String[] {null, "cel"}, "target", 1L)), + new Evaluation( + "size(numbers.map(value, target > 0, b ? value : x))", + Map.of("numbers", new long[] {1, 2}, "target", 1L, "b", false, "x", "not an int")), + new Evaluation( + "size(words.map(value, target > 0, b ? value : s))", + Map.of("words", new String[] {null, "cel"}, "target", 1L, "b", true, "s", "!")), + new Evaluation( + "size(words.map(value, target > 0, value + s))", + Map.of("words", new String[] {null}, "target", 1L, "s", "!")), + new Evaluation( + "size(numbers.map(x, x >= target, x + target))", + Map.of("numbers", new long[] {1, 2}, "x", 99L, "target", 2L)), + new Evaluation( + "size(numbers.map(value, value == target, x / y))", + Map.of("numbers", new long[] {1, 2}, "target", 3L, "x", 1L, "y", 0L)), + new Evaluation( + "size(numbers.map(value, value == target, x / y))", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "x", 1L, "y", 0L)), + new Evaluation( + "size(numbers.map(value, value == target, b ? value : x))", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "b", unknownOf(220L), "x", 1L)), + new Evaluation( + "size(numbers.map(value, value == target, value + target))", + Map.of("numbers", newErr("range"), "target", 2L)), + new Evaluation( + "size(numbers.map(value, value == target, value + target))", + Map.of("numbers", unknownOf(221L), "target", 2L)), + new Evaluation( + "size(numbers.map(value, value == target, value))", + Map.of("numbers", "not a list", "target", 2L)), + new Evaluation( + "size(numbers.map(value, value == target, value + target))", + Map.of("numbers", new long[] {1, 2})), + new Evaluation( + "size(numbers.map(value, value > 0, value + target))", + Map.of("numbers", new long[] {1, 2}))); + } + + @ParameterizedTest + @MethodSource("canonicalFilteredMapSizeEvaluations") + void evaluatesCanonicalFilteredMapSize(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void canonicalFilteredMapSizeMatchesCurrentFoldPartialActivationScope() { + Ast ast = compile("size(numbers.map(value, value == target, value + target))"); + Prog nativeProgram = (Prog) env.program(ast); + Prog interpreterProgram = + (Prog) env.program(ast, evalOptions(OptPartialEval, OptDisableNativeEval)); + Object vars = + partialVars(Map.of("numbers", new long[] {1, 2}, "target", 2L), attributePattern("target")); + + assertIntegratedPlan(nativeProgram, "filtered map size"); + assertEquivalent(nativeProgram.eval(vars).getVal(), interpreterProgram.eval(vars).getVal()); + } + + @Test + void canonicalFilteredMapSizeUsesDefaultVariablesThroughTheCurrentFoldHierarchy() { + Ast ast = compile("size(numbers.map(value, value <= target, value + target))"); + Prog nativeProgram = (Prog) env.program(ast, globals(Map.of("target", 2L))); + Prog interpreterProgram = + (Prog) env.program(ast, globals(Map.of("target", 2L)), evalOptions(OptDisableNativeEval)); + + assertIntegratedPlan(nativeProgram, "filtered map size"); + assertEquivalent( + nativeProgram.eval(Map.of("numbers", new long[] {1, 2})).getVal(), + interpreterProgram.eval(Map.of("numbers", new long[] {1, 2})).getVal()); + } + + @Test + void canonicalFilteredMapSizePreservesPredicateThenTransformResolutionOrder() { + assertResolutionCounts( + "size(numbers.map(value, value <= target, value == target ? x / y : value + x))", + Map.of("numbers", new long[] {1L, 2L, 3L}, "target", 2L, "x", 1L, "y", 0L), + Map.of("numbers", 1, "target", 4, "x", 2, "y", 1)); + assertResolutionCounts( + "size(numbers.map(value, value == target, value + x))", + Map.of("numbers", new long[] {1L, 2L, 3L}, "target", 2L, "x", 1L), + Map.of("numbers", 1, "target", 3, "x", 1)); + } + + static Stream canonicalMapIndexEvaluations() { + Val adaptedNumbers = DefaultTypeAdapter.Instance.nativeToValue(List.of(1L, 2L, 3L)); + return Stream.of( + new Evaluation( + "numbers.map(value, value + target)[1]", + Map.of("numbers", new int[] {1, 2, 3}, "target", 2L)), + new Evaluation( + "numbers.map(value, value == target)[2]", + Map.of("numbers", new long[] {1, 2, 3}, "target", 3L)), + new Evaluation( + "numbers.map(value, value * target)[0]", + Map.of("numbers", List.of(1L, 2L, 3L), "target", 2L)), + new Evaluation( + "numbers.map(value, value - target)[2]", + Map.of("numbers", new Object[] {1L, 2L, 3L}, "target", 2L)), + new Evaluation( + "numbers.map(value, value <= target)[1]", + Map.of("numbers", adaptedNumbers, "target", 2L)), + new Evaluation( + "numbers.map(value, value + target)[0]", Map.of("numbers", new long[0], "target", 1L)), + new Evaluation( + "doubles.map(value, value + d)[1]", + Map.of("doubles", new double[] {-0.0d, Double.NaN}, "d", 0.0d)), + new Evaluation( + "words.map(value, value + s)[1]", + Map.of("words", new String[] {"cel", "other"}, "s", "!")), + new Evaluation("flags.map(value, !value)[1]", Map.of("flags", new Boolean[] {false, true})), + new Evaluation("flags.map(value, !value)[1]", Map.of("flags", new boolean[] {false, true})), + new Evaluation("numbers.map(value, null)[0]", Map.of("numbers", new long[] {1, 2})), + new Evaluation( + "numbers.map(value, b ? value : x)[0]", + Map.of("numbers", new long[] {1, 2}, "b", false, "x", "not an int")), + new Evaluation( + "numbers.map(value, value == target ? x : value)[0]", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "x", "not an int")), + new Evaluation( + "numbers.map(value, value == target ? x : value / y)[0]", + Map.of("numbers", new long[] {1, 2}, "target", 1L, "x", "not an int", "y", 0L)), + new Evaluation( + "numbers.map(value, value == target ? x / y : value)[1]", + Map.of("numbers", new long[] {1, 2}, "target", 1L, "x", 1L, "y", 0L)), + new Evaluation( + "numbers.map(value, value == target ? x / y : value)[0]", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "x", 1L, "y", 0L)), + new Evaluation( + "numbers.map(value, value == target ? b : value == x)[0]", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "b", unknownOf(223L), "x", 1L)), + new Evaluation("words.map(value, value)[0]", Map.of("words", new String[] {null, "cel"})), + new Evaluation("words.map(value, value)[1]", Map.of("words", new String[] {null, "cel"})), + new Evaluation( + "words.map(value, value + s)[0]", + Map.of("words", new String[] {"cel", null}, "s", "!")), + new Evaluation( + "numbers.map(x, x + target)[1]", + Map.of("numbers", new long[] {1, 2}, "x", 99L, "target", 2L)), + new Evaluation( + "numbers.map(value, value + target)[0]", + Map.of("numbers", newErr("range"), "target", 2L)), + new Evaluation( + "numbers.map(value, value + target)[0]", + Map.of("numbers", unknownOf(224L), "target", 2L)), + new Evaluation( + "numbers.map(value, value + target)[0]", Map.of("numbers", "not a list", "target", 2L)), + new Evaluation( + "numbers.map(value, value + target)[0]", Map.of("numbers", new long[] {1, 2})), + new Evaluation( + "numbers.map(value, value + target)[-1]", + Map.of("numbers", new long[] {1, 2}, "target", 2L)), + new Evaluation( + "numbers.map(value, value + target)[99]", + Map.of("numbers", new long[] {1, 2}, "target", 2L)), + new Evaluation( + "numbers.map(value, value + target)[2147483647]", + Map.of("numbers", new long[] {1, 2}, "target", 2L))); + } + + @ParameterizedTest + @MethodSource("canonicalMapIndexEvaluations") + void evaluatesCanonicalMapIndex(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void canonicalMapIndexMatchesCurrentFoldPartialActivationScope() { + Ast ast = compile("numbers.map(value, value + target)[1]"); + Prog nativeProgram = (Prog) env.program(ast); + Prog interpreterProgram = + (Prog) env.program(ast, evalOptions(OptPartialEval, OptDisableNativeEval)); + Object vars = + partialVars(Map.of("numbers", new long[] {1, 2}, "target", 2L), attributePattern("target")); + + assertIntegratedPlan(nativeProgram, "map index"); + assertEquivalent(nativeProgram.eval(vars).getVal(), interpreterProgram.eval(vars).getVal()); + } + + @Test + void canonicalMapIndexUsesDefaultVariablesThroughTheCurrentFoldHierarchy() { + Ast ast = compile("numbers.map(value, value + target)[1]"); + Prog nativeProgram = (Prog) env.program(ast, globals(Map.of("target", 2L))); + Prog interpreterProgram = + (Prog) env.program(ast, globals(Map.of("target", 2L)), evalOptions(OptDisableNativeEval)); + + assertIntegratedPlan(nativeProgram, "map index"); + assertEquivalent( + nativeProgram.eval(Map.of("numbers", new long[] {1, 2})).getVal(), + interpreterProgram.eval(Map.of("numbers", new long[] {1, 2})).getVal()); + } + + @Test + void canonicalMapIndexPreservesFullTraversalAndLaterFailurePrecedence() { + assertResolutionCounts( + "numbers.map(value, value == target ? x / y : value + x)[0]", + Map.of("numbers", new long[] {1L, 2L, 3L}, "target", 2L, "x", 1L, "y", 0L), + Map.of("numbers", 1, "target", 2, "x", 2, "y", 1)); + assertResolutionCounts( + "numbers.map(value, value + target)[99]", + Map.of("numbers", new long[] {1L, 2L, 3L}, "target", 2L), + Map.of("numbers", 1, "target", 3)); + } + + static Stream canonicalFilteredMapIndexEvaluations() { + Val adaptedNumbers = DefaultTypeAdapter.Instance.nativeToValue(List.of(1L, 2L, 3L)); + return Stream.of( + new Evaluation( + "numbers.map(value, value >= target, value + target)[0]", + Map.of("numbers", new int[] {1, 2, 3}, "target", 2L)), + new Evaluation( + "numbers.map(value, value != target, value == target)[1]", + Map.of("numbers", new long[] {1, 2, 3}, "target", 2L)), + new Evaluation( + "numbers.map(value, value <= target, value * target)[1]", + Map.of("numbers", List.of(1L, 2L, 3L), "target", 2L)), + new Evaluation( + "numbers.map(value, value != target, value - target)[1]", + Map.of("numbers", new Object[] {1L, 2L, 3L}, "target", 2L)), + new Evaluation( + "numbers.map(value, value <= target, value == target)[1]", + Map.of("numbers", adaptedNumbers, "target", 2L)), + new Evaluation( + "numbers.map(value, value == target, value + target)[0]", + Map.of("numbers", new long[0], "target", 1L)), + new Evaluation( + "doubles.map(value, value == d, value + d)[0]", + Map.of("doubles", new double[] {-0.0d, Double.NaN}, "d", 0.0d)), + new Evaluation( + "words.map(value, value == wordTarget, value + s)[1]", + Map.of("words", new String[] {"cel", "other", "cel"}, "wordTarget", "cel", "s", "!")), + new Evaluation( + "flags.map(value, value == b, !value)[0]", + Map.of("flags", new Boolean[] {false, true}, "b", true)), + new Evaluation( + "flags.map(value, value == b, !value)[0]", + Map.of("flags", new boolean[] {false, true}, "b", true)), + new Evaluation( + "numbers.map(value, value > target, null)[0]", + Map.of("numbers", new long[] {1, 2}, "target", 1L)), + new Evaluation( + "numbers.filter(value, value >= target)[1]", + Map.of("numbers", new long[] {1, 2, 3}, "target", 2L)), + new Evaluation( + "words.filter(value, value == wordTarget)[0]", + Map.of("words", new String[] {"other", "cel"}, "wordTarget", "cel")), + new Evaluation( + "numbers.map(value, value >= target, b ? value : x)[0]", + Map.of("numbers", new long[] {1, 2}, "target", 1L, "b", false, "x", "not an int")), + new Evaluation( + "numbers.map(value, value >= target, value == target ? x : value)[1]", + Map.of("numbers", new long[] {1, 2}, "target", 1L, "x", "not an int")), + new Evaluation( + "numbers.map(value, value >= target, value == target ? x : value)[0]", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "x", "not an int")), + new Evaluation( + "numbers.map(value, value > target, x / y)[0]", + Map.of("numbers", new long[] {1, 2}, "target", 99L, "x", 1L, "y", 0L)), + new Evaluation( + "numbers.map(value, value > 0, value == target ? x / y : value)[0]", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "x", 1L, "y", 0L)), + new Evaluation( + "numbers.map(value, value == target ? b : true, value)[0]", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "b", newErr("predicate"))), + new Evaluation( + "numbers.map(value, value == target ? b : true, value)[0]", + Map.of("numbers", new long[] {1, 2}, "target", 2L, "b", unknownOf(225L))), + new Evaluation( + "numbers.filter(value, b)[0]", + Map.of("numbers", new Object[] {newErr("element"), 2L}, "b", true)), + new Evaluation( + "numbers.filter(value, b)[0]", + Map.of("numbers", new Object[] {unknownOf(226L), 2L}, "b", true)), + new Evaluation( + "numbers.filter(value, b)[0]", + Map.of("numbers", new Object[] {newErr("element")}, "b", false)), + new Evaluation( + "flags.filter(value, value)[0]", + Map.of("flags", new Object[] {newErr("predicate"), true})), + new Evaluation( + "flags.filter(value, value)[0]", Map.of("flags", new Object[] {unknownOf(227L), true})), + new Evaluation( + "flags.filter(value, value)[0]", Map.of("flags", new Object[] {"not a bool"})), + new Evaluation( + "words.map(value, b, value)[0]", + Map.of("words", new String[] {null, "cel"}, "b", true)), + new Evaluation( + "words.map(value, b, value)[1]", + Map.of("words", new String[] {null, "cel"}, "b", true)), + new Evaluation( + "words.map(value, b, value + s)[0]", + Map.of("words", new String[] {"cel", null}, "b", true, "s", "!")), + new Evaluation( + "numbers.map(x, x >= target, x + target)[1]", + Map.of("numbers", new long[] {1, 2, 3}, "x", 99L, "target", 2L)), + new Evaluation( + "numbers.map(value, value == target, value + target)[0]", + Map.of("numbers", newErr("range"), "target", 2L)), + new Evaluation( + "numbers.map(value, value == target, value + target)[0]", + Map.of("numbers", unknownOf(228L), "target", 2L)), + new Evaluation( + "numbers.map(value, value == target, value)[0]", + Map.of("numbers", "not a list", "target", 2L)), + new Evaluation( + "numbers.map(value, value == target, value)[0]", Map.of("numbers", new long[] {1, 2})), + new Evaluation( + "numbers.map(value, b, value + target)[0]", + Map.of("numbers", new long[] {1, 2}, "b", true)), + new Evaluation( + "numbers.map(value, value > target, value)[-1]", + Map.of("numbers", new long[] {1, 2}, "target", 1L)), + new Evaluation( + "numbers.map(value, value > target, value)[99]", + Map.of("numbers", new long[] {1, 2}, "target", 1L)), + new Evaluation( + "numbers.map(value, value > target, value)[2147483647]", + Map.of("numbers", new long[] {1, 2}, "target", 1L))); + } + + @ParameterizedTest + @MethodSource("canonicalFilteredMapIndexEvaluations") + void evaluatesCanonicalFilteredMapIndex(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void canonicalFilteredMapIndexMatchesCurrentFoldPartialActivationScope() { + Ast ast = compile("numbers.map(value, value >= target, value + target)[1]"); + Prog nativeProgram = (Prog) env.program(ast); + Prog interpreterProgram = + (Prog) env.program(ast, evalOptions(OptPartialEval, OptDisableNativeEval)); + Object vars = + partialVars( + Map.of("numbers", new long[] {1, 2, 3}, "target", 2L), attributePattern("target")); + + assertIntegratedPlan(nativeProgram, "filtered map index"); + assertEquivalent(nativeProgram.eval(vars).getVal(), interpreterProgram.eval(vars).getVal()); + } + + @Test + void canonicalFilteredMapIndexUsesDefaultVariablesThroughTheCurrentFoldHierarchy() { + Ast ast = compile("numbers.filter(value, value >= target)[1]"); + Prog nativeProgram = (Prog) env.program(ast, globals(Map.of("target", 2L))); + Prog interpreterProgram = + (Prog) env.program(ast, globals(Map.of("target", 2L)), evalOptions(OptDisableNativeEval)); + + assertIntegratedPlan(nativeProgram, "filtered map index"); + assertEquivalent( + nativeProgram.eval(Map.of("numbers", new long[] {1, 2, 3})).getVal(), + interpreterProgram.eval(Map.of("numbers", new long[] {1, 2, 3})).getVal()); + } + + @Test + void canonicalFilteredMapIndexPreservesOrderingSuppressionAndAcceptedPositions() { + assertResolutionCounts( + "numbers.map(value, value <= target, value == target ? x / y : value + x)[0]", + Map.of("numbers", new long[] {1L, 2L, 3L}, "target", 2L, "x", 1L, "y", 0L), + Map.of("numbers", 1, "target", 4, "x", 2, "y", 1)); + assertResolutionCounts( + "numbers.map(value, value <= target, value + x)[99]", + Map.of("numbers", new long[] {1L, 2L, 3L}, "target", 2L, "x", 1L), + Map.of("numbers", 1, "target", 3, "x", 2)); + assertResolutionCounts( + "numbers.map(value, value > target, value + x)[0]", + Map.of("numbers", new long[] {1L, 2L}, "target", 99L, "x", 1L), + Map.of("numbers", 1, "target", 2, "x", 0)); + } + + static Stream canonicalMappedStringMembershipEvaluations() { + return Stream.of( + new Evaluation( + "wordTarget in words.map(value, value)", + Map.of("wordTarget", "cel", "words", new String[] {"zero", "cel", "last"})), + new Evaluation( + "wordTarget in words.map(value, value + s)", + Map.of("wordTarget", "cel!", "words", new String[] {"zero", "cel", "last"}, "s", "!")), + new Evaluation( + "wordTarget in numbers.map(value, value == target ? s : key)", + Map.of( + "wordTarget", + "cel", + "numbers", + new long[] {1, 2, 3}, + "target", + 2L, + "s", + "cel", + "key", + "other")), + new Evaluation( + "wordTarget in words.map(value, value)", + Map.of("wordTarget", "cel", "words", new String[0])), + new Evaluation( + "wordTarget in words.map(value, value == key ? s : value)", + Map.of( + "wordTarget", + "cel", + "words", + new String[] {"cel", "later"}, + "key", + "later", + "s", + newErr("later transform"))), + new Evaluation( + "wordTarget in words.map(value, value == key ? s : value)", + Map.of( + "wordTarget", + newErr("needle"), + "words", + new String[] {"cel", "later"}, + "key", + "later", + "s", + newErr("later transform"))), + new Evaluation( + "wordTarget in words.map(value, value == key ? s : value)", + Map.of( + "wordTarget", + unknownOf(229L), + "words", + new String[] {"cel", "later"}, + "key", + "later", + "s", + newErr("later transform"))), + new Evaluation( + "wordTarget in words.map(value, value)", + Map.of("wordTarget", 42L, "words", new String[] {"zero", "cel"})), + new Evaluation( + "wordTarget in words.map(value, value == key ? s : value)", + Map.of( + "wordTarget", + 42L, + "words", + new String[] {"zero", "later"}, + "key", + "later", + "s", + newErr("later transform"))), + new Evaluation( + "wordTarget in words.map(value, b ? value : s)", + Map.of( + "wordTarget", "cel", "words", new String[] {"zero", "cel"}, "b", false, "s", 42L)), + new Evaluation( + "wordTarget in words.map(value, value == key ? s : value)", + Map.of( + "wordTarget", + "cel", + "words", + new String[] {"cel", "later"}, + "key", + "later", + "s", + 42L)), + new Evaluation( + "wordTarget in words.map(value, value == key ? s : value)", + Map.of( + "wordTarget", + "cel", + "words", + new String[] {"first", "cel"}, + "key", + "first", + "s", + 42L)), + new Evaluation( + "wordTarget in words.map(value, value)", + Map.of("wordTarget", "cel", "words", new Object[] {stringOf(null), "cel"})), + new Evaluation( + "wordTarget in words.map(value, value)", + Map.of("wordTarget", "cel", "words", newErr("range"))), + new Evaluation( + "wordTarget in words.map(value, value)", + Map.of("wordTarget", "cel", "words", unknownOf(230L))), + new Evaluation( + "wordTarget in words.map(value, value)", + Map.of("wordTarget", "cel", "words", "not a list")), + new Evaluation("wordTarget in words.map(value, value)", Map.of("wordTarget", "cel")), + new Evaluation( + "wordTarget in words.map(value, value + s)", + Map.of("wordTarget", "cel", "words", new String[] {"zero", "cel"})), + new Evaluation( + "wordTarget in words.map(value, value != key, value + s)", + Map.of( + "wordTarget", + "cel!", + "words", + new String[] {"zero", "skip", "cel"}, + "key", + "skip", + "s", + "!")), + new Evaluation( + "wordTarget in words.filter(value, value != key)", + Map.of( + "wordTarget", "cel", "words", new String[] {"zero", "skip", "cel"}, "key", "skip")), + new Evaluation( + "wordTarget in words.map(value, false, s)", + Map.of( + "wordTarget", + "cel", + "words", + new String[] {"zero", "cel"}, + "s", + newErr("suppressed transform"))), + new Evaluation( + "wordTarget in words.map(value, value == key ? b : true, value)", + Map.of( + "wordTarget", + "cel", + "words", + new String[] {"cel", "later"}, + "key", + "later", + "b", + newErr("later predicate"))), + new Evaluation( + "wordTarget in words.map(value, true, value == key ? s : value)", + Map.of( + "wordTarget", + "cel", + "words", + new String[] {"cel", "later"}, + "key", + "later", + "s", + newErr("later transform"))), + new Evaluation( + "wordTarget in words.filter(value, b)", + Map.of( + "wordTarget", + "cel", + "words", + new Object[] {newErr("suppressed element")}, + "b", + false)), + new Evaluation( + "wordTarget in words.filter(value, b)", + Map.of( + "wordTarget", + "cel", + "words", + new Object[] {newErr("accepted element")}, + "b", + true)), + new Evaluation( + "wordTarget in words.map(value, b, value)", + Map.of("wordTarget", "cel", "words", new String[] {"zero", "cel"}, "b", "not a bool")), + new Evaluation( + "wordTarget in words.map(value, b, value)", + Map.of( + "wordTarget", "cel", "words", new String[] {"zero", "cel"}, "b", unknownOf(231L))), + new Evaluation( + "wordTarget in words.map(value, b, value)", + Map.of( + "wordTarget", + newErr("needle"), + "words", + new String[] {"zero", "cel"}, + "b", + newErr("predicate"))), + new Evaluation( + "wordTarget in words.filter(value, b)", + Map.of("wordTarget", "cel", "words", new Object[] {stringOf(null), "cel"}, "b", true)), + new Evaluation( + "wordTarget in words.filter(value, b)", + Map.of("wordTarget", "cel", "words", new Object[] {42L, "cel"}, "b", true))); + } + + @ParameterizedTest + @MethodSource("canonicalMappedStringMembershipEvaluations") + void evaluatesCanonicalMappedStringMembership(Evaluation evaluation) { + assertExpression(evaluation.expression(), evaluation.input()); + } + + @Test + void nullBackedStringNeedleUsesTheCurrentMappedMembershipCompatibilityPath() { + Programs programs = programs("wordTarget in words.map(value, value)"); + Object input = Map.of("wordTarget", stringOf(null), "words", new String[] {"zero", "cel"}); + + Throwable nativeFailure = catchThrowable(() -> programs.nativeProgram.eval(input)); + Throwable interpreterFailure = catchThrowable(() -> programs.interpreterProgram.eval(input)); + + assertThat(nativeFailure).isNotNull(); + assertThat(interpreterFailure).isNotNull(); + assertThat(nativeFailure.getClass()).isEqualTo(interpreterFailure.getClass()); + assertThat(nativeFailure.getMessage()).isEqualTo(interpreterFailure.getMessage()); + } + + @Test + void canonicalMappedStringMembershipMatchesCurrentFoldPartialActivationScope() { + Object input = + partialVars( + Map.of("wordTarget", "cel!", "words", new String[] {"zero", "cel"}, "s", "!"), + attributePattern("s")); + + assertExpression("wordTarget in words.map(value, value + s)", input); + } + + @Test + void canonicalMappedStringMembershipUsesDefaultVariablesThroughTheCurrentFoldHierarchy() { + Ast ast = compile("wordTarget in words.map(value, value != key, value + s)"); + Map defaults = Map.of("key", "skip", "s", "!"); + Prog nativeProgram = (Prog) env.program(ast, globals(defaults)); + Prog interpreterProgram = + (Prog) env.program(ast, globals(defaults), evalOptions(OptDisableNativeEval)); + Map input = + Map.of("wordTarget", "cel!", "words", new String[] {"zero", "skip", "cel"}); + + assertIntegratedPlan(nativeProgram, "mapped string membership with default variables"); + assertEquivalent(nativeProgram.eval(input).getVal(), interpreterProgram.eval(input).getVal()); + } + + @Test + void canonicalMappedStringMembershipPreservesOperandAndCompleteTraversalOrdering() { + List nativeOrder = new ArrayList<>(); + List interpreterOrder = new ArrayList<>(); + Programs programs = programs("wordTarget in words.map(value, value + s)"); + ActivationFunction nativeActivation = + orderedMappedStringMembershipActivation(nativeOrder, "cel!", new String[] {"zero", "cel"}); + ActivationFunction interpreterActivation = + orderedMappedStringMembershipActivation( + interpreterOrder, "cel!", new String[] {"zero", "cel"}); + + assertEquivalent( + programs.nativeProgram.eval(nativeActivation).getVal(), + programs.interpreterProgram.eval(interpreterActivation).getVal()); + assertThat(nativeOrder).containsExactly("wordTarget", "words", "s", "s"); + assertThat(interpreterOrder).containsExactlyElementsOf(nativeOrder); + + assertResolutionCounts( + "wordTarget in words.map(value, value == key ? s : value)", + Map.of( + "wordTarget", + "cel", + "words", + new String[] {"cel", "other", "last"}, + "key", + "other", + "s", + "mapped"), + Map.of("wordTarget", 1, "words", 1, "key", 3, "s", 1)); + assertResolutionCounts( + "wordTarget in words.map(value, value != key, value + s)", + Map.of( + "wordTarget", + "cel!", + "words", + new String[] {"zero", "skip", "cel"}, + "key", + "skip", + "s", + "!"), + Map.of("wordTarget", 1, "words", 1, "key", 3, "s", 2)); + } + + @Test + void nonCanonicalOrUnsupportedComprehensionsStayOnTheCurrentEvaluator() { + for (String expression : + List.of( + "[1, 2, 3].exists(value, value == target)", + "[1, 2, 3].all(value, value > 0)", + "[1, 2, 3].exists_one(value, value == target)", + "numbers.exists(value, string(value) == s)", + "numbers.all(value, string(value) == s)", + "numbers.exists_one(value, string(value) == s)", + "numbers.exists(value, numbers.exists(other, other == value))", + "numbers.all(value, numbers.all(other, other == value))", + "numbers.exists_one(value, numbers.exists_one(other, other == value))", + "numbers.exists(index, value, value == target)", + "numbers.all(index, value, value == target)", + "numbers.exists_one(index, value, value == target)", + "numbers.filter(value, value == target)", + "size([1, 2, 3].filter(value, value == target))", + "size(numbers.filter(value, string(value) == s))", + "numbers.map(value, value + 1)", + "size([1, 2, 3].map(value, value + 1))", + "size(numbers.map(value, string(value) == s))", + "size([1, 2, 3].map(value, value > 0, value + 1))", + "size(numbers.map(value, string(value) == s, value + 1))", + "size(numbers.map(value, value > 0, string(value) == s))", + "[1, 2, 3].map(value, value + 1)[0]", + "[1, 2, 3].map(value, value > 0, value + 1)[0]", + "numbers.map(value, string(value) == s)[0]", + "numbers.map(value, value > 0, string(value))[0]", + "numbers.map(value, value + 1)[x]", + "numbers.map(value, value > 0, value + 1)[x]", + "numbers.exists(value, numbers.map(other, other + value)[0] > 0)", + "numbers.exists(value, numbers.map(other, other > value, other + value)[0] > 0)", + "numbers.exists(value, size(numbers.map(other, other + value)) > 0)", + "numbers.exists(value, size(numbers.map(other, other > value, other + value)) > 0)", + "numbers.exists(value, size(numbers.filter(other, other == value)) > 0)", + "[1, 2, 3].map(value, value + 1).exists(mapped, mapped == target)", + "words.map(value, value + s).exists(mapped, mapped == wordTarget)", + "words.map(value, value + s).all(mapped, mapped != wordTarget)", + "words.map(value, value + s).exists_one(mapped, mapped == wordTarget)", + "words.map(value, value != s, value + s).exists(mapped, mapped == wordTarget)", + "words.filter(value, value != s).all(mapped, mapped != wordTarget)", + "numbers.map(value, string(value)).exists(mapped, mapped == s)", + "numbers.transformList(index, value, value + index)" + + ".exists(mapped, mapped == target)", + "numbers.map(value, value + 1)" + + ".exists(mapped, numbers.exists(other, other == mapped))", + "wordTarget in ['zero', 'cel'].map(value, value)", + "wordTarget in words.map(value, value).map(other, other)", + "wordTarget in words.map(value, string(target))", + "target in numbers.map(value, value + 1)", + "size(numbers.transformList(index, value, value))")) { + Prog program = (Prog) env.program(compile(expression)); + assertCurrentPlan(program, expression); + } + } + + @SuppressWarnings("resource") + @ParameterizedTest + @ValueSource( + strings = { + "numbers.exists(value, value == target)", + "numbers.all(value, value <= target)", + "numbers.exists_one(value, value == target)" + }) + void canonicalQuantifierPlansCanBeEvaluatedConcurrently(String expression) throws Exception { + Prog program = (Prog) env.program(compile(expression)); + assertIntegratedPlan(program, expression); + ExecutorService executor = Executors.newFixedThreadPool(4); + try { + List> results = new ArrayList<>(); + for (int i = 0; i < 200; i++) { + long target = i; + results.add( + executor.submit( + () -> + program + .eval(Map.of("numbers", new long[] {target - 1, target}, "target", target)) + .getVal() + .booleanValue())); + } + for (Future result : results) { + assertThat(result.get(5, SECONDS)).isTrue(); + } + } finally { + executor.shutdownNow(); + assertThat(executor.awaitTermination(5, SECONDS)).isTrue(); + } + } + + @SuppressWarnings("resource") + @ParameterizedTest + @ValueSource( + strings = { + "numbers.map(value, value + x).exists(mapped, mapped == target)", + "numbers.map(value, value + x).all(mapped, mapped <= target)", + "numbers.map(value, value + x).exists_one(mapped, mapped == target)", + "numbers.map(value, value > x, value + x).exists(mapped, mapped == target)", + "numbers.map(value, value > x, value + x).all(mapped, mapped <= target)", + "numbers.filter(value, value > x).exists_one(mapped, mapped == x + 1)" + }) + void canonicalIntMapQuantifierPlansCanBeEvaluatedConcurrently(String expression) + throws Exception { + Prog program = (Prog) env.program(compile(expression)); + assertIntegratedPlan(program, expression); + ExecutorService executor = Executors.newFixedThreadPool(4); + try { + List> results = new ArrayList<>(); + for (int i = 0; i < 200; i++) { + long argument = i; + results.add( + executor.submit( + () -> + program + .eval( + Map.of( + "numbers", + new long[] {argument - 1, argument, argument + 1}, + "x", + argument, + "target", + argument * 2L + 1L)) + .getVal() + .booleanValue())); + } + for (Future result : results) { + assertThat(result.get(5, SECONDS)).isTrue(); + } + } finally { + executor.shutdownNow(); + assertThat(executor.awaitTermination(5, SECONDS)).isTrue(); + } + } + + @SuppressWarnings("resource") + @Test + void canonicalFilterSizePlansCanBeEvaluatedConcurrently() throws Exception { + Prog program = (Prog) env.program(compile("size(numbers.filter(value, value == target))")); + assertIntegratedPlan(program, "filter size"); + ExecutorService executor = Executors.newFixedThreadPool(4); + try { + List> results = new ArrayList<>(); + for (int i = 0; i < 200; i++) { + long target = i; + results.add( + executor.submit( + () -> + program + .eval( + Map.of( + "numbers", + new long[] {target - 1, target, target}, + "target", + target)) + .getVal() + .intValue())); + } + for (Future result : results) { + assertThat(result.get(5, SECONDS)).isEqualTo(2L); + } + } finally { + executor.shutdownNow(); + assertThat(executor.awaitTermination(5, SECONDS)).isTrue(); + } + } + + @SuppressWarnings("resource") + @Test + void canonicalMapSizePlansCanBeEvaluatedConcurrently() throws Exception { + Prog program = (Prog) env.program(compile("size(numbers.map(value, value + target))")); + assertIntegratedPlan(program, "map size"); + ExecutorService executor = Executors.newFixedThreadPool(4); + try { + List> results = new ArrayList<>(); + for (int i = 0; i < 200; i++) { + long target = i; + results.add( + executor.submit( + () -> + program + .eval( + Map.of( + "numbers", + new long[] {target - 1, target, target + 1}, + "target", + target)) + .getVal() + .intValue())); + } + for (Future result : results) { + assertThat(result.get(5, SECONDS)).isEqualTo(3L); + } + } finally { + executor.shutdownNow(); + assertThat(executor.awaitTermination(5, SECONDS)).isTrue(); + } + } + + @SuppressWarnings("resource") + @Test + void canonicalFilteredMapSizePlansCanBeEvaluatedConcurrently() throws Exception { + Prog program = + (Prog) env.program(compile("size(numbers.map(value, value >= target, value + target))")); + assertIntegratedPlan(program, "filtered map size"); + ExecutorService executor = Executors.newFixedThreadPool(4); + try { + List> results = new ArrayList<>(); + for (int i = 0; i < 200; i++) { + long target = i; + results.add( + executor.submit( + () -> + program + .eval( + Map.of( + "numbers", + new long[] {target - 1, target, target + 1}, + "target", + target)) + .getVal() + .intValue())); + } + for (Future result : results) { + assertThat(result.get(5, SECONDS)).isEqualTo(2L); + } + } finally { + executor.shutdownNow(); + assertThat(executor.awaitTermination(5, SECONDS)).isTrue(); + } + } + + @SuppressWarnings("resource") + @Test + void canonicalMapIndexPlansCanBeEvaluatedConcurrently() throws Exception { + Prog program = (Prog) env.program(compile("numbers.map(value, value + target)[1]")); + assertIntegratedPlan(program, "map index"); + ExecutorService executor = Executors.newFixedThreadPool(4); + try { + List> results = new ArrayList<>(); + for (int i = 0; i < 200; i++) { + long target = i; + results.add( + executor.submit( + () -> + program + .eval( + Map.of( + "numbers", + new long[] {target - 1, target, target + 1}, + "target", + target)) + .getVal() + .intValue())); + } + for (int i = 0; i < results.size(); i++) { + assertThat(results.get(i).get(5, SECONDS)).isEqualTo((long) i * 2L); + } + } finally { + executor.shutdownNow(); + assertThat(executor.awaitTermination(5, SECONDS)).isTrue(); + } + } + + @SuppressWarnings("resource") + @Test + void canonicalFilteredMapIndexPlansCanBeEvaluatedConcurrently() throws Exception { + Prog program = + (Prog) env.program(compile("numbers.map(value, value >= target, value + target)[1]")); + assertIntegratedPlan(program, "filtered map index"); + ExecutorService executor = Executors.newFixedThreadPool(4); + try { + List> results = new ArrayList<>(); + for (int i = 0; i < 200; i++) { + long target = i; + results.add( + executor.submit( + () -> + program + .eval( + Map.of( + "numbers", + new long[] {target - 1, target, target + 1}, + "target", + target)) + .getVal() + .intValue())); + } + for (int i = 0; i < results.size(); i++) { + assertThat(results.get(i).get(5, SECONDS)).isEqualTo((long) i * 2L + 1L); + } + } finally { + executor.shutdownNow(); + assertThat(executor.awaitTermination(5, SECONDS)).isTrue(); + } + } + + @SuppressWarnings("resource") + @ParameterizedTest + @ValueSource( + strings = { + "wordTarget in words.map(value, value + s)", + "wordTarget in words.map(value, value != key, value + s)", + "wordTarget in words.filter(value, value != key)" + }) + void canonicalMappedStringMembershipPlansCanBeEvaluatedConcurrently(String expression) + throws Exception { + Prog program = (Prog) env.program(compile(expression)); + assertIntegratedPlan(program, expression); + ExecutorService executor = Executors.newFixedThreadPool(4); + try { + List> results = new ArrayList<>(); + for (int i = 0; i < 200; i++) { + String target = "value-" + i; + String needle = expression.contains(".filter") ? target : target + "!"; + results.add( + executor.submit( + () -> + program + .eval( + Map.of( + "wordTarget", + needle, + "words", + new String[] {"skip", target}, + "key", + "skip", + "s", + "!")) + .getVal() + .booleanValue())); + } + for (Future result : results) { + assertThat(result.get(5, SECONDS)).isTrue(); + } + } finally { + executor.shutdownNow(); + assertThat(executor.awaitTermination(5, SECONDS)).isTrue(); + } + } + + @Test + void usesDefaultVariablesThroughTheExistingActivationHierarchy() { + Ast ast = compile("x + y"); + Prog nativeProgram = (Prog) env.program(ast, globals(Map.of("x", 40L))); + Prog interpreterProgram = + (Prog) env.program(ast, globals(Map.of("x", 40L)), evalOptions(OptDisableNativeEval)); + + assertIntegratedPlan(nativeProgram, "x + y"); + assertEquivalent( + nativeProgram.eval(Map.of("y", 2L)).getVal(), + interpreterProgram.eval(Map.of("y", 2L)).getVal()); + } + + @Test + void resolvesEachIdentifierAtTheSamePoint() { + Programs programs = programs("x + x * x"); + AtomicInteger nativeResolutions = new AtomicInteger(); + AtomicInteger interpreterResolutions = new AtomicInteger(); + + Val nativeResult = programs.nativeProgram.eval(countingActivation(nativeResolutions)).getVal(); + Val interpreterResult = + programs.interpreterProgram.eval(countingActivation(interpreterResolutions)).getVal(); + + assertEquivalent(nativeResult, interpreterResult); + assertThat(nativeResolutions).hasValue(interpreterResolutions.get()).hasValue(3); + } + + @Test + void fallsBackWhenAnyProgramFeatureOrExpressionIsUnsupported() { + Ast supported = compile("x + 1"); + Prog decorated = (Prog) env.program(supported, customDecorator(interpretable -> interpretable)); + Prog nativeDisabled = (Prog) env.program(supported, evalOptions(OptDisableNativeEval)); + Prog optimized = (Prog) env.program(supported, evalOptions(OptOptimize)); + Prog unsupported = (Prog) env.program(compile("[x, y]")); + Prog unchecked = (Prog) env.program(env.parse("x + 1").getAst()); + + assertCurrentPlan(decorated); + assertCurrentPlan(nativeDisabled); + assertCurrentPlan(optimized); + assertCurrentPlan(unsupported); + assertCurrentPlan(unchecked); + } + + @SuppressWarnings("resource") + @Test + void immutablePlanCanBeEvaluatedConcurrently() throws Exception { + Prog program = (Prog) env.program(compile("(x + 1) * (y - 2)")); + assertIntegratedPlan(program, "(x + 1) * (y - 2)"); + ExecutorService executor = Executors.newFixedThreadPool(4); + try { + List> results = new ArrayList<>(); + for (int i = 0; i < 200; i++) { + long value = i; + results.add( + executor.submit( + () -> program.eval(Map.of("x", value, "y", value + 4L)).getVal().intValue())); + } + for (int i = 0; i < results.size(); i++) { + assertThat(results.get(i).get(5, SECONDS)).isEqualTo((long) (i + 1) * (i + 2)); + } + } finally { + executor.shutdownNow(); + assertThat(executor.awaitTermination(5, SECONDS)).isTrue(); + } + } + + @SuppressWarnings("resource") + @Test + void topLevelListConsumerPlansCanBeEvaluatedConcurrently() throws Exception { + Prog index = (Prog) env.program(compile("numbers[1]")); + Prog membership = (Prog) env.program(compile("wordTarget in words")); + assertIntegratedPlan(index, "numbers[1]"); + assertIntegratedPlan(membership, "wordTarget in words"); + + ExecutorService executor = Executors.newFixedThreadPool(4); + try { + List> results = new ArrayList<>(); + for (int i = 0; i < 200; i++) { + long value = i; + results.add( + executor.submit( + () -> + index.eval(Map.of("numbers", new long[] {-1L, value})).getVal().intValue() + == value + && membership + .eval( + Map.of( + "wordTarget", + "value-" + value, + "words", + new String[] {"other", "value-" + value})) + .getVal() + .booleanValue())); + } + for (Future result : results) { + assertThat(result.get(5, SECONDS)).isTrue(); + } + } finally { + executor.shutdownNow(); + assertThat(executor.awaitTermination(5, SECONDS)).isTrue(); + } + } + + @SuppressWarnings("resource") + @Test + void scalarListLiteralConsumerPlansCanBeEvaluatedConcurrently() throws Exception { + Prog index = (Prog) env.program(compile("[x, y][1]")); + Prog size = (Prog) env.program(compile("size([x, y])")); + Prog membership = (Prog) env.program(compile("wordTarget in [s, key]")); + assertIntegratedPlan(index, "[x, y][1]"); + assertIntegratedPlan(size, "size([x, y])"); + assertIntegratedPlan(membership, "wordTarget in [s, key]"); + + ExecutorService executor = Executors.newFixedThreadPool(4); + try { + List> results = new ArrayList<>(); + for (int i = 0; i < 200; i++) { + long value = i; + results.add( + executor.submit( + () -> + index.eval(Map.of("x", -1L, "y", value)).getVal().intValue() == value + && size.eval(Map.of("x", value, "y", value + 1L)).getVal().intValue() == 2L + && membership + .eval( + Map.of( + "wordTarget", + "value-" + value, + "s", + "other", + "key", + "value-" + value)) + .getVal() + .booleanValue())); + } + for (Future result : results) { + assertThat(result.get(5, SECONDS)).isTrue(); + } + } finally { + executor.shutdownNow(); + assertThat(executor.awaitTermination(5, SECONDS)).isTrue(); + } + } + + private Programs programs(String expression) { + return programs(env, expression); + } + + private static Programs programs(Env env, String expression) { + Ast ast = compile(env, expression); + Prog nativeProgram = (Prog) env.program(ast); + Prog interpreterProgram = (Prog) env.program(ast, evalOptions(OptDisableNativeEval)); + Interpretable nativePlan = assertIntegratedPlan(nativeProgram, expression); + assertThat(nativePlan.id()).as(expression).isEqualTo(ast.getExpr().getId()); + assertCurrentPlan(interpreterProgram, expression); + assertThat(nativeProgram.cost()).as(expression).isEqualTo(interpreterProgram.cost()); + return new Programs(nativeProgram, interpreterProgram); + } + + private void assertExpression(String expression, Object input) { + Programs programs = programs(expression); + assertEquivalent( + programs.nativeProgram.eval(input).getVal(), + programs.interpreterProgram.eval(input).getVal()); + assertThat(programs.nativeProgram.cost()).isEqualTo(programs.interpreterProgram.cost()); + } + + private static Interpretable undecoratedCurrent(Prog nativeProgram, Ast ast) { + return ((Prog) nativeProgram.e.program(ast, evalOptions(OptDisableNativeEval))).interpretable; + } + + private static Interpretable assertIntegratedPlan(Prog program, String description) { + assertThat(program.interpretable.getClass().getSimpleName()) + .as(description) + .isEqualTo("NativeIsland"); + return program.interpretable; + } + + private static void assertCurrentPlan(Prog program) { + assertThat(program.interpretable).isNotNull(); + assertThat(program.interpretable.getClass().getSimpleName()).isNotEqualTo("NativeIsland"); + } + + private static void assertCurrentPlan(Prog program, String description) { + assertThat(program.interpretable).as(description).isNotNull(); + assertThat(program.interpretable.getClass().getSimpleName()) + .as(description) + .isNotEqualTo("NativeIsland"); + } + + private static void assertEstablishedRoot( + Prog program, String expectedSimpleName, String description) { + assertThat(program.interpretable).as(description).isNotNull(); + assertThat(program.interpretable.getClass().getSimpleName()) + .as(description) + .isEqualTo(expectedSimpleName); + } + + private static void assertExpression(Env env, String expression, Object input) { + Programs programs = programs(env, expression); + assertEquivalent( + programs.nativeProgram.eval(input).getVal(), + programs.interpreterProgram.eval(input).getVal()); + assertThat(programs.nativeProgram.cost()).isEqualTo(programs.interpreterProgram.cost()); + } + + private void assertResolutionCounts( + String expression, Map values, Map expectedCounts) { + Programs programs = programs(expression); + Map nativeCounts = new java.util.HashMap<>(); + Map interpreterCounts = new java.util.HashMap<>(); + + Val nativeResult = + programs.nativeProgram.eval(countingActivation(values, nativeCounts)).getVal(); + Val interpreterResult = + programs.interpreterProgram.eval(countingActivation(values, interpreterCounts)).getVal(); + + assertEquivalent(nativeResult, interpreterResult); + for (Map.Entry expected : expectedCounts.entrySet()) { + assertThat(count(nativeCounts, expected.getKey())).isEqualTo(expected.getValue()); + assertThat(count(interpreterCounts, expected.getKey())).isEqualTo(expected.getValue()); + } + } + + private static int count(Map counts, String name) { + AtomicInteger count = counts.get(name); + return count != null ? count.get() : 0; + } + + private static ActivationFunction countingActivation( + Map values, Map counts) { + return name -> { + counts.computeIfAbsent(name, ignored -> new AtomicInteger()).incrementAndGet(); + return values.getOrDefault(name, ActivationFunction.ABSENT); + }; + } + + private static ActivationFunction activation(Map values) { + return name -> values.getOrDefault(name, ActivationFunction.ABSENT); + } + + private static ActivationFunction orderedActivation( + Map values, List order) { + return name -> { + order.add(name); + return values.getOrDefault(name, ActivationFunction.ABSENT); + }; + } + + private static ActivationFunction orderedMembershipActivation(List order) { + return name -> { + order.add(name); + return switch (name) { + case "wordTarget" -> newErr("needle"); + case "words" -> newErr("list"); + default -> ActivationFunction.ABSENT; + }; + }; + } + + @SuppressWarnings("SameParameterValue") + private static ActivationFunction orderedMappedStringMembershipActivation( + List order, Object needle, Object words) { + return name -> { + order.add(name); + return switch (name) { + case "wordTarget" -> needle; + case "words" -> words; + case "s" -> "!"; + default -> ActivationFunction.ABSENT; + }; + }; + } + + @SuppressWarnings("SameParameterValue") + private static ActivationFunction orderedBoolListActivation( + List order, Object b, Object c) { + return name -> { + order.add(name); + return switch (name) { + case "b" -> b; + case "c" -> c; + case "x" -> 1L; + default -> ActivationFunction.ABSENT; + }; + }; + } + + private static ActivationFunction orderedIntListActivation(List order, boolean error) { + return name -> { + order.add(name); + return switch (name) { + case "x" -> error ? newErr("first") : 1L; + case "y" -> 2L; + default -> ActivationFunction.ABSENT; + }; + }; + } + + @SuppressWarnings("SameParameterValue") + private static ActivationFunction orderedDoubleListActivation( + List order, Object d, Object e) { + return name -> { + order.add(name); + return switch (name) { + case "d" -> d; + case "e" -> e; + default -> ActivationFunction.ABSENT; + }; + }; + } + + private static ActivationFunction orderedStringListLiteralMembershipActivation( + List order, Object needle, Object first, Object second) { + return name -> { + order.add(name); + return switch (name) { + case "wordTarget" -> needle; + case "s" -> first; + case "key" -> second; + default -> ActivationFunction.ABSENT; + }; + }; + } + + private Ast compile(String expression) { + return compile(env, expression); + } + + private static Ast compile(Env env, String expression) { + AstIssuesTuple result = env.compile(expression); + assertThat(result.hasIssues()).as(expression).isFalse(); + return result.getAst(); + } + + private static ActivationFunction countingActivation(AtomicInteger counter) { + return name -> { + counter.incrementAndGet(); + return name.equals("x") ? 3L : ActivationFunction.ABSENT; + }; + } + + private static void assertEquivalent(Val actual, Val expected) { + assertThat(actual.getClass()).isEqualTo(expected.getClass()); + assertThat(actual.type()).isSameAs(expected.type()); + assertThat(actual.toString()).isEqualTo(expected.toString()); + if (actual instanceof Err actualError && expected instanceof Err expectedError) { + assertThat(actualError.hasCause()).isEqualTo(expectedError.hasCause()); + if (actualError.hasCause()) { + assertThat(actualError.getCause().getClass()) + .isEqualTo(expectedError.getCause().getClass()); + assertThat(actualError.getCause().getMessage()) + .isEqualTo(expectedError.getCause().getMessage()); + } + return; + } + if (actual.value() instanceof Double actualDouble + && expected.value() instanceof Double expectedDouble) { + assertThat(Double.doubleToRawLongBits(actualDouble)) + .isEqualTo(Double.doubleToRawLongBits(expectedDouble)); + } else { + assertThat(actual.value()).isEqualTo(expected.value()); + } + } + + private record Evaluation(String expression, Object input) { + @Override + @NonNull + public String toString() { + return expression; + } + } + + private static final class IncrementLibrary implements Library { + private static final String FUNCTION = "increment"; + private static final String OVERLOAD = "increment_int"; + + private final Overload implementation; + + private IncrementLibrary() { + this.implementation = Overload.unary(OVERLOAD, IncrementLibrary::increment); + } + + @Override + public List getCompileOptions() { + return List.of( + declarations( + Decls.newFunction( + FUNCTION, Decls.newOverload(OVERLOAD, List.of(Decls.Int), Decls.Int)))); + } + + @Override + public List getProgramOptions() { + return List.of(functions(implementation)); + } + + private static Val increment(Val value) { + return value instanceof IntT + ? intOf(value.intValue() + 1L) + : Err.maybeNoSuchOverloadErr(value); + } + } + + private record Programs(Prog nativeProgram, Prog interpreterProgram) {} +} diff --git a/core/src/test/java/org/projectnessie/cel/checker/CheckerEnvTest.java b/core/src/test/java/org/projectnessie/cel/checker/CheckerEnvTest.java index f7bb0e47..e3dea123 100644 --- a/core/src/test/java/org/projectnessie/cel/checker/CheckerEnvTest.java +++ b/core/src/test/java/org/projectnessie/cel/checker/CheckerEnvTest.java @@ -17,6 +17,8 @@ import static java.util.Collections.singletonList; import static org.projectnessie.cel.checker.CheckerEnv.newStandardCheckerEnv; +import static org.projectnessie.cel.common.containers.Container.name; +import static org.projectnessie.cel.common.containers.Container.newContainer; import static org.projectnessie.cel.common.types.pb.ProtoTypeRegistry.newRegistry; import com.google.api.expr.v1alpha1.Type; @@ -63,6 +65,21 @@ Overloads.ToDyn, singletonList(paramA), Decls.Dyn, typeParamAList)))) "overlapping overload for name 'dyn' (type '(type_param: \"A\") -> dyn' with overloadId: 'to_dyn' cannot be distinguished from '(type_param: \"A\") -> dyn' with overloadId: 'to_dyn')"); } + @Test + void lexicalIdentifierShadowsContainerQualifiedIdentifier() { + CheckerEnv env = newStandardCheckerEnv(newContainer(name("com.example")), newRegistry()); + env.add(Decls.newVar("com.example.y", Decls.Int)); + env.add(Decls.newVar("y", Decls.Bool)); + + Assertions.assertThat(env.lookupIdent("y").getName()).isEqualTo("com.example.y"); + + env = env.enterScope(); + env.add(Decls.newVar("y", Decls.String)); + + Assertions.assertThat(env.lookupIdent("y").getName()).isEqualTo("y"); + Assertions.assertThat(env.lookupIdent(".com.example.y").getName()).isEqualTo("com.example.y"); + } + @Test void overloadsWithDifferentArityOrStyleDoNotOverlap() { CheckerEnv env = newStandardCheckerEnv(Container.defaultContainer, newRegistry()); diff --git a/core/src/test/java/org/projectnessie/cel/checker/CheckerTest.java b/core/src/test/java/org/projectnessie/cel/checker/CheckerTest.java index 6bc4194c..09df7ee1 100644 --- a/core/src/test/java/org/projectnessie/cel/checker/CheckerTest.java +++ b/core/src/test/java/org/projectnessie/cel/checker/CheckerTest.java @@ -189,6 +189,56 @@ static TestCase[] checkTestCases() { new TestCase().i("id").r("id~double^id").type(Decls.Double).env(testEnvs.get("default")), new TestCase().i("[]").r("[]~list(dyn)").type(Decls.newListType(Decls.Dyn)), new TestCase().i("[1]").r("[1~int]~list(int)").type(Decls.newListType(Decls.Int)), + new TestCase() + .i("[y, 1]") + .env(new env().idents(Decls.newVar("y", Decls.newWrapperType(Decls.Int)))) + .r("[y~wrapper(int)^y, 1~int]~list(wrapper(int))") + .type(Decls.newListType(Decls.newWrapperType(Decls.Int))), + new TestCase() + .i("[1, y]") + .env(new env().idents(Decls.newVar("y", Decls.newWrapperType(Decls.Int)))) + .r("[1~int, y~wrapper(int)^y]~list(wrapper(int))") + .type(Decls.newListType(Decls.newWrapperType(Decls.Int))), + new TestCase() + .i("[x, null]") + .env(new env().idents(Decls.newVar("x", Decls.newObjectType("test.Message")))) + .r("[x~test.Message^x, null~null]~list(test.Message)") + .type(Decls.newListType(Decls.newObjectType("test.Message"))), + new TestCase() + .i("[null, x]") + .env(new env().idents(Decls.newVar("x", Decls.newObjectType("test.Message")))) + .r("[null~null, x~test.Message^x]~list(test.Message)") + .type(Decls.newListType(Decls.newObjectType("test.Message"))), + new TestCase() + .i("[d, null]") + .env(new env().idents(Decls.newVar("d", Decls.Duration))) + .r("[d~duration^d, null~null]~list(duration)") + .type(Decls.newListType(Decls.Duration)), + new TestCase() + .i("[null, t]") + .env(new env().idents(Decls.newVar("t", Decls.Timestamp))) + .r("[null~null, t~timestamp^t]~list(timestamp)") + .type(Decls.newListType(Decls.Timestamp)), + new TestCase() + .i("tuple(1, dyn(2u), 3.0)") + .env( + new env() + .functions( + Decls.newFunction( + "tuple", + Decls.newOverload( + "tuple_T_U_V", + asList( + Decls.newTypeParamType("T"), + Decls.newTypeParamType("U"), + Decls.newTypeParamType("V")), + Decls.newAbstractType( + "tuple", + asList( + Decls.newTypeParamType("T"), + Decls.newTypeParamType("U"), + Decls.newTypeParamType("V"))))))) + .type(Decls.newAbstractType("tuple", asList(Decls.Int, Decls.Dyn, Decls.Double))), new TestCase() .i("[1, \"A\"]") .r("[1~int, \"A\"~string]~list(dyn)") @@ -262,23 +312,27 @@ static TestCase[] checkTestCases() { .r("{1~int : 2u~uint, 2u~uint : 3~int}~map(dyn, dyn)"), new TestCase() .i("TestAllTypes{single_int32: 1, single_int64: 2}") - .container("google.api.expr.test.v1.proto3") + .container("cel.expr.conformance.proto3") .r( - "google.api.expr.test.v1.proto3.TestAllTypes{\n" + "cel.expr.conformance.proto3.TestAllTypes{\n" + " single_int32 : 1~int,\n" + " single_int64 : 2~int\n" - + "}~google.api.expr.test.v1.proto3.TestAllTypes^google.api.expr.test.v1.proto3.TestAllTypes") - .type(Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")), + + "}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes") + .type(Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")), + new TestCase() + .i("TestAllTypes{single_bool_wrapper: null}") + .container("cel.expr.conformance.proto3") + .type(Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")), new TestCase() .i("TestAllTypes{single_int32: 1u}") - .container("google.api.expr.test.v1.proto3") + .container("cel.expr.conformance.proto3") .error( "ERROR: :1:26: expected type of field 'single_int32' is 'int' but provided type is 'uint'\n" + " | TestAllTypes{single_int32: 1u}\n" + " | .........................^"), new TestCase() .i("TestAllTypes{single_int32: 1, undefined: 2}") - .container("google.api.expr.test.v1.proto3") + .container("cel.expr.conformance.proto3") .error( "ERROR: :1:40: undefined field 'undefined'\n" + " | TestAllTypes{single_int32: 1, undefined: 2}\n" @@ -374,10 +428,9 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", - Decls.newObjectType("google.api.expr.test.v1.proto3.Proto2Message")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.Proto2Message")))) .error( - "ERROR: :1:2: [internal] unexpected failed resolution of 'google.api.expr.test.v1.proto3.Proto2Message'\n" + "ERROR: :1:2: [internal] unexpected failed resolution of 'cel.expr.conformance.proto3.Proto2Message'\n" + " | x.single_int32 != null\n" + " | .^"), new TestCase() @@ -386,14 +439,14 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( "_==_(\n" + "\t\t\t_+_(\n" - + "\t\t\t x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_value~dyn,\n" + + "\t\t\t x~cel.expr.conformance.proto3.TestAllTypes^x.single_value~dyn,\n" + "\t\t\t _/_(\n" + "\t\t\t\t1~int,\n" - + "\t\t\t\tx~google.api.expr.test.v1.proto3.TestAllTypes^x.single_struct~map(string, dyn).y~dyn\n" + + "\t\t\t\tx~cel.expr.conformance.proto3.TestAllTypes^x.single_struct~map(string, dyn).y~dyn\n" + "\t\t\t )~int^divide_int64\n" + "\t\t\t)~int^add_int64,\n" + "\t\t\t23~int\n" @@ -405,25 +458,25 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( "_+_(\n" + "_[_](\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_value~dyn,\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_value~dyn,\n" + " 23~int\n" + ")~dyn^index_list|index_map,\n" + "_[_](\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_struct~map(string, dyn),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_struct~map(string, dyn),\n" + " \"y\"~string\n" + ")~dyn^index_map\n" + ")~dyn^add_int64|add_uint64|add_double|add_string|add_bytes|add_list|add_timestamp_duration|add_duration_timestamp|add_duration_duration") .type(Decls.Dyn), new TestCase() .i("TestAllTypes.NestedEnum.BAR != 99") - .container("google.api.expr.test.v1.proto3") + .container("cel.expr.conformance.proto3") .r( - "_!=_(google.api.expr.test.v1.proto3.TestAllTypes.NestedEnum.BAR\n" - + " ~int^google.api.expr.test.v1.proto3.TestAllTypes.NestedEnum.BAR,\n" + "_!=_(cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAR\n" + + " ~int^cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAR,\n" + " 99~int)\n" + "~bool^not_equals") .type(Decls.Bool), @@ -435,8 +488,7 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))), + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))), new TestCase() .i( "x[\"claims\"][\"groups\"][0].name == \"dummy\"\n" @@ -498,10 +550,10 @@ static TestCase[] checkTestCases() { Decls.newVar( "x", Decls.newListType( - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))), + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))), Decls.newVar("y", Decls.newListType(Decls.Int)))) .error( - "ERROR: :1:3: found no matching overload for '_+_' applied to '(list(google.api.expr.test.v1.proto3.TestAllTypes), list(int))'\n" + "ERROR: :1:3: found no matching overload for '_+_' applied to '(list(cel.expr.conformance.proto3.TestAllTypes), list(int))'\n" + " | x + y\n" + " | ..^"), new TestCase() @@ -512,9 +564,9 @@ static TestCase[] checkTestCases() { Decls.newVar( "x", Decls.newListType( - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))))) + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))))) .error( - "ERROR: :1:2: found no matching overload for '_[_]' applied to '(list(google.api.expr.test.v1.proto3.TestAllTypes), uint)'\n" + "ERROR: :1:2: found no matching overload for '_[_]' applied to '(list(cel.expr.conformance.proto3.TestAllTypes), uint)'\n" + " | x[1u]\n" + " | .^"), new TestCase() @@ -525,17 +577,17 @@ static TestCase[] checkTestCases() { Decls.newVar( "x", Decls.newListType( - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))))) + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))))) .r( - "_==_(_[_](_+_(x~list(google.api.expr.test.v1.proto3.TestAllTypes)^x,\n" - + " x~list(google.api.expr.test.v1.proto3.TestAllTypes)^x)\n" - + " ~list(google.api.expr.test.v1.proto3.TestAllTypes)^add_list,\n" + "_==_(_[_](_+_(x~list(cel.expr.conformance.proto3.TestAllTypes)^x,\n" + + " x~list(cel.expr.conformance.proto3.TestAllTypes)^x)\n" + + " ~list(cel.expr.conformance.proto3.TestAllTypes)^add_list,\n" + " 1~int)\n" - + " ~google.api.expr.test.v1.proto3.TestAllTypes^index_list\n" + + " ~cel.expr.conformance.proto3.TestAllTypes^index_list\n" + " .\n" + " single_int32\n" + " ~int,\n" - + " size(x~list(google.api.expr.test.v1.proto3.TestAllTypes)^x)~int^size_list)\n" + + " size(x~list(cel.expr.conformance.proto3.TestAllTypes)^x)~int^size_list)\n" + " ~bool^equals") .type(Decls.Bool), new TestCase() @@ -544,10 +596,10 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( - "_==_(_[_](x~google.api.expr.test.v1.proto3.TestAllTypes^x.repeated_int64~list(int),\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_int32~int)\n" + "_==_(_[_](x~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_int32~int)\n" + " ~int^index_list,\n" + " 23~int)\n" + " ~bool^equals") @@ -558,10 +610,10 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( - "_==_(size(x~google.api.expr.test.v1.proto3.TestAllTypes^x.map_int64_nested_type\n" - + " ~map(int, google.api.expr.test.v1.proto3.NestedTestAllTypes))\n" + "_==_(size(x~cel.expr.conformance.proto3.TestAllTypes^x.map_int64_nested_type\n" + + " ~map(int, cel.expr.conformance.proto3.NestedTestAllTypes))\n" + " ~int^size_map,\n" + " 0~int)\n" + " ~bool^equals") @@ -603,13 +655,13 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( "__comprehension__(\n" + "// Variable\n" + "x,\n" + "// Target\n" - + "x~google.api.expr.test.v1.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + + "x~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + "// Accumulator\n" + "__result__,\n" + "// Init\n" @@ -634,13 +686,13 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( "__comprehension__(\n" + "// Variable\n" + "x,\n" + "// Target\n" - + "x~google.api.expr.test.v1.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + + "x~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + "// Accumulator\n" + "__result__,\n" + "// Init\n" @@ -675,9 +727,9 @@ static TestCase[] checkTestCases() { "x", Decls.newMapType( Decls.String, - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))))) + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))))) .error( - "ERROR: :1:2: found no matching overload for '_[_]' applied to '(map(string, google.api.expr.test.v1.proto3.TestAllTypes), int)'\n" + "ERROR: :1:2: found no matching overload for '_[_]' applied to '(map(string, cel.expr.conformance.proto3.TestAllTypes), int)'\n" + " | x[2].single_int32 == 23\n" + " | .^"), new TestCase() @@ -689,10 +741,10 @@ static TestCase[] checkTestCases() { "x", Decls.newMapType( Decls.String, - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))))) + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))))) .r( - "_==_(_[_](x~map(string, google.api.expr.test.v1.proto3.TestAllTypes)^x, \"a\"~string)\n" - + "~google.api.expr.test.v1.proto3.TestAllTypes^index_map\n" + "_==_(_[_](x~map(string, cel.expr.conformance.proto3.TestAllTypes)^x, \"a\"~string)\n" + + "~cel.expr.conformance.proto3.TestAllTypes^index_map\n" + ".\n" + "single_int32\n" + "~int,\n" @@ -705,15 +757,15 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) // Our implementation code is expanding the macro .r( "_&&_(\n" + " _==_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_nested_message~google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage.bb~int,\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_nested_message~cel.expr.conformance.proto3.TestAllTypes.NestedMessage.bb~int,\n" + " 43~int\n" + " )~bool^equals,\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_nested_message~test-only~~bool\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_nested_message~test-only~~bool\n" + ")~bool^logical_and") .type(Decls.Bool), new TestCase() @@ -723,7 +775,7 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .error( "ERROR: :1:24: undefined field 'undefined'\n" + " | x.single_nested_message.undefined == x.undefined && has(x.single_int32) && has(x.repeated_int32)\n" @@ -737,10 +789,10 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( - "_!=_(x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_nested_message\n" - + "~google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage,\n" + "_!=_(x~cel.expr.conformance.proto3.TestAllTypes^x.single_nested_message\n" + + "~cel.expr.conformance.proto3.TestAllTypes.NestedMessage,\n" + "null~null)\n" + "~bool^not_equals") .type(Decls.Bool), @@ -750,9 +802,9 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( - "_!=_(x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_int64~int,null~null)~bool^not_equals") + "_!=_(x~cel.expr.conformance.proto3.TestAllTypes^x.single_int64~int,null~null)~bool^not_equals") .type(Decls.Bool), new TestCase() .i("x.single_int64_wrapper == null") @@ -760,9 +812,9 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( - "_==_(x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_int64_wrapper\n" + "_==_(x~cel.expr.conformance.proto3.TestAllTypes^x.single_int64_wrapper\n" + "~wrapper(int),\n" + "null~null)\n" + "~bool^equals") @@ -782,30 +834,30 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( "_&&_(\n" + " _&&_(\n" + " _&&_(\n" + " _&&_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_bool_wrapper~wrapper(bool),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_bool_wrapper~wrapper(bool),\n" + " _==_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_bytes_wrapper~wrapper(bytes),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_bytes_wrapper~wrapper(bytes),\n" + " b\"hi\"~bytes\n" + " )~bool^equals\n" + " )~bool^logical_and,\n" + " _!=_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_double_wrapper~wrapper(double),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_double_wrapper~wrapper(double),\n" + " 2.0~double\n" + " )~bool^not_equals\n" + " )~bool^logical_and,\n" + " _&&_(\n" + " _==_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_float_wrapper~wrapper(double),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_float_wrapper~wrapper(double),\n" + " 1.0~double\n" + " )~bool^equals,\n" + " _!=_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_int32_wrapper~wrapper(int),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_int32_wrapper~wrapper(int),\n" + " 2~int\n" + " )~bool^not_equals\n" + " )~bool^logical_and\n" @@ -813,21 +865,21 @@ static TestCase[] checkTestCases() { + " _&&_(\n" + " _&&_(\n" + " _==_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_int64_wrapper~wrapper(int),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_int64_wrapper~wrapper(int),\n" + " 1~int\n" + " )~bool^equals,\n" + " _==_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_string_wrapper~wrapper(string),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_string_wrapper~wrapper(string),\n" + " \"hi\"~string\n" + " )~bool^equals\n" + " )~bool^logical_and,\n" + " _&&_(\n" + " _==_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_uint32_wrapper~wrapper(uint),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_uint32_wrapper~wrapper(uint),\n" + " 1u~uint\n" + " )~bool^equals,\n" + " _!=_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_uint64_wrapper~wrapper(uint),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_uint64_wrapper~wrapper(uint),\n" + " 42u~uint\n" + " )~bool^not_equals\n" + " )~bool^logical_and\n" @@ -850,7 +902,7 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .type(Decls.Bool), new TestCase() .i( @@ -866,7 +918,7 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .type(Decls.Bool), new TestCase() .i("x.repeated_int64.exists(y, y > 10) && y < 5") @@ -874,7 +926,7 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .error( "ERROR: :1:39: undeclared reference to 'y' (in container '')\n" + " | x.repeated_int64.exists(y, y > 10) && y < 5\n" @@ -886,7 +938,7 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( "_&&_(\n" + "\t\t\t_&&_(\n" @@ -894,7 +946,7 @@ static TestCase[] checkTestCases() { + "\t\t\t\t// Variable\n" + "\t\t\t\te,\n" + "\t\t\t\t// Target\n" - + "\t\t\t\tx~google.api.expr.test.v1.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + + "\t\t\t\tx~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + "\t\t\t\t// Accumulator\n" + "\t\t\t\t__result__,\n" + "\t\t\t\t// Init\n" @@ -917,7 +969,7 @@ static TestCase[] checkTestCases() { + "\t\t\t\t// Variable\n" + "\t\t\t\te,\n" + "\t\t\t\t// Target\n" - + "\t\t\t\tx~google.api.expr.test.v1.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + + "\t\t\t\tx~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + "\t\t\t\t// Accumulator\n" + "\t\t\t\t__result__,\n" + "\t\t\t\t// Init\n" @@ -943,7 +995,7 @@ static TestCase[] checkTestCases() { + "\t\t\t // Variable\n" + "\t\t\t e,\n" + "\t\t\t // Target\n" - + "\t\t\t x~google.api.expr.test.v1.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + + "\t\t\t x~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + "\t\t\t // Accumulator\n" + "\t\t\t __result__,\n" + "\t\t\t // Init\n" @@ -975,9 +1027,9 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .error( - "ERROR: :1:1: expression of type 'google.api.expr.test.v1.proto3.TestAllTypes' cannot be range of a comprehension (must be list, map, or dynamic)\n" + "ERROR: :1:1: expression of type 'cel.expr.conformance.proto3.TestAllTypes' cannot be range of a comprehension (must be list, map, or dynamic)\n" + " | x.all(e, 0)\n" + " | ^\n" + "ERROR: :1:6: found no matching overload for '_&&_' applied to '(bool, int)'\n" @@ -1016,24 +1068,20 @@ static TestCase[] checkTestCases() { .type(Decls.newListType(Decls.Dyn)) .env(new env().idents(Decls.newVar("lists", Decls.Dyn))), new TestCase() - .i("google.api.expr.test.v1.proto3.TestAllTypes") + .i("cel.expr.conformance.proto3.TestAllTypes") .r( - "google.api.expr.test.v1.proto3.TestAllTypes\n" - + "\t~type(google.api.expr.test.v1.proto3.TestAllTypes)\n" - + "\t^google.api.expr.test.v1.proto3.TestAllTypes") - .type( - Decls.newTypeType( - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))), + "cel.expr.conformance.proto3.TestAllTypes\n" + + "\t~type(cel.expr.conformance.proto3.TestAllTypes)\n" + + "\t^cel.expr.conformance.proto3.TestAllTypes") + .type(Decls.newTypeType(Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))), new TestCase() .i("proto3.TestAllTypes") - .container("google.api.expr.test.v1") + .container("cel.expr.conformance") .r( - "google.api.expr.test.v1.proto3.TestAllTypes\n" - + "\t~type(google.api.expr.test.v1.proto3.TestAllTypes)\n" - + "\t^google.api.expr.test.v1.proto3.TestAllTypes") - .type( - Decls.newTypeType( - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))), + "cel.expr.conformance.proto3.TestAllTypes\n" + + "\t~type(cel.expr.conformance.proto3.TestAllTypes)\n" + + "\t^cel.expr.conformance.proto3.TestAllTypes") + .type(Decls.newTypeType(Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))), new TestCase() .i("1 + x") .error( @@ -1043,9 +1091,9 @@ static TestCase[] checkTestCases() { new TestCase() .i( "x == google.protobuf.Any{\n" - + "\t\t\t\ttype_url:'types.googleapis.com/google.api.expr.test.v1.proto3.TestAllTypes'\n" + + "\t\t\t\ttype_url:'types.googleapis.com/cel.expr.conformance.proto3.TestAllTypes'\n" + "\t\t\t} && x.single_nested_message.bb == 43\n" - + "\t\t\t|| x == google.api.expr.test.v1.proto3.TestAllTypes{}\n" + + "\t\t\t|| x == cel.expr.conformance.proto3.TestAllTypes{}\n" + "\t\t\t|| y < x\n" + "\t\t\t|| x >= x") .env( @@ -1060,7 +1108,7 @@ static TestCase[] checkTestCases() { + "\t\t\t_==_(\n" + "\t\t\t\tx~any^x,\n" + "\t\t\t\tgoogle.protobuf.Any{\n" - + "\t\t\t\t\ttype_url:\"types.googleapis.com/google.api.expr.test.v1.proto3.TestAllTypes\"~string\n" + + "\t\t\t\t\ttype_url:\"types.googleapis.com/cel.expr.conformance.proto3.TestAllTypes\"~string\n" + "\t\t\t\t}~any^google.protobuf.Any\n" + "\t\t\t)~bool^equals,\n" + "\t\t\t_==_(\n" @@ -1070,7 +1118,7 @@ static TestCase[] checkTestCases() { + "\t\t)~bool^logical_and,\n" + "\t\t_==_(\n" + "\t\t\tx~any^x,\n" - + "\t\t\tgoogle.api.expr.test.v1.proto3.TestAllTypes{}~google.api.expr.test.v1.proto3.TestAllTypes^google.api.expr.test.v1.proto3.TestAllTypes\n" + + "\t\t\tcel.expr.conformance.proto3.TestAllTypes{}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes\n" + "\t\t)~bool^equals\n" + "\t)~bool^logical_or,\n" + "\t_||_(\n" @@ -1093,9 +1141,9 @@ static TestCase[] checkTestCases() { .idents( Decls.newVar( "container.x", - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) - .r("container.x~google.api.expr.test.v1.proto3.TestAllTypes^container.x") - .type(Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")), + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) + .r("container.x~cel.expr.conformance.proto3.TestAllTypes^container.x") + .type(Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")), new TestCase() .i("list == .type([1]) && map == .type({1:2u})") .r( @@ -1144,15 +1192,14 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) .functions( Decls.newFunction( "size", Decls.newOverload( "size_message", singletonList( - Decls.newObjectType( - "google.api.expr.test.v1.proto3.TestAllTypes")), + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")), Decls.Int)))) .type(Decls.Bool), new TestCase() @@ -1161,9 +1208,9 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( - "_!=_(_+_(x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_int64_wrapper\n" + "_!=_(_+_(x~cel.expr.conformance.proto3.TestAllTypes^x.single_int64_wrapper\n" + "~wrapper(int),\n" + "1~int)\n" + "~int^add_int64,\n" @@ -1176,12 +1223,12 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")), + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")), Decls.newVar("y", Decls.newObjectType("google.protobuf.Int32Value")))) .r( "_!=_(\n" + "\t_+_(\n" - + "\t x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_int64_wrapper~wrapper(int),\n" + + "\t x~cel.expr.conformance.proto3.TestAllTypes^x.single_int64_wrapper~wrapper(int),\n" + "\t y~wrapper(int)^y\n" + "\t)~int^add_int64,\n" + "\t23~int\n" @@ -1384,7 +1431,7 @@ static TestCase[] checkTestCases() { new TestCase() .i("[].map(x, [].map(y, x in y && y in x))") .error( - "ERROR: :1:33: found no matching overload for '@in' applied to '(type_param: \"_var2\", type_param: \"_var0\")'\n" + "ERROR: :1:33: found no matching overload for '@in' applied to '(list(type_param: \"_var0\"), type_param: \"_var0\")'\n" + " | [].map(x, [].map(y, x in y && y in x))\n" + " | ................................^"), new TestCase() @@ -1449,63 +1496,59 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "pb2", - Decls.newObjectType("google.api.expr.test.v1.proto2.TestAllTypes")), + "pb2", Decls.newObjectType("cel.expr.conformance.proto2.TestAllTypes")), Decls.newVar( - "pb3", - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "pb3", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( "_&&_(\n" + "\t_&&_(\n" + "\t _&&_(\n" + "\t\t!_(\n" - + "\t\t pb2~google.api.expr.test.v1.proto2.TestAllTypes^pb2.single_int64~test-only~~bool\n" + + "\t\t pb2~cel.expr.conformance.proto2.TestAllTypes^pb2.single_int64~test-only~~bool\n" + "\t\t)~bool^logical_not,\n" + "\t\t!_(\n" - + "\t\t pb2~google.api.expr.test.v1.proto2.TestAllTypes^pb2.repeated_int32~test-only~~bool\n" + + "\t\t pb2~cel.expr.conformance.proto2.TestAllTypes^pb2.repeated_int32~test-only~~bool\n" + "\t\t)~bool^logical_not\n" + "\t )~bool^logical_and,\n" + "\t !_(\n" - + "\t\tpb2~google.api.expr.test.v1.proto2.TestAllTypes^pb2.map_string_string~test-only~~bool\n" + + "\t\tpb2~cel.expr.conformance.proto2.TestAllTypes^pb2.map_string_string~test-only~~bool\n" + "\t )~bool^logical_not\n" + "\t)~bool^logical_and,\n" + "\t_&&_(\n" + "\t _&&_(\n" + "\t\t!_(\n" - + "\t\t pb3~google.api.expr.test.v1.proto3.TestAllTypes^pb3.single_int64~test-only~~bool\n" + + "\t\t pb3~cel.expr.conformance.proto3.TestAllTypes^pb3.single_int64~test-only~~bool\n" + "\t\t)~bool^logical_not,\n" + "\t\t!_(\n" - + "\t\t pb3~google.api.expr.test.v1.proto3.TestAllTypes^pb3.repeated_int32~test-only~~bool\n" + + "\t\t pb3~cel.expr.conformance.proto3.TestAllTypes^pb3.repeated_int32~test-only~~bool\n" + "\t\t)~bool^logical_not\n" + "\t )~bool^logical_and,\n" + "\t !_(\n" - + "\t\tpb3~google.api.expr.test.v1.proto3.TestAllTypes^pb3.map_string_string~test-only~~bool\n" + + "\t\tpb3~cel.expr.conformance.proto3.TestAllTypes^pb3.map_string_string~test-only~~bool\n" + "\t )~bool^logical_not\n" + "\t)~bool^logical_and\n" + " )~bool^logical_and") .type(Decls.Bool), new TestCase() .i("TestAllTypes{}.repeated_nested_message") - .container("google.api.expr.test.v1.proto2") + .container("cel.expr.conformance.proto2") .r( - "google.api.expr.test.v1.proto2.TestAllTypes{}~google.api.expr.test.v1.proto2.TestAllTypes^\n" - + "\t\tgoogle.api.expr.test.v1.proto2.TestAllTypes.repeated_nested_message\n" - + "\t\t~list(google.api.expr.test.v1.proto2.TestAllTypes.NestedMessage)") + "cel.expr.conformance.proto2.TestAllTypes{}~cel.expr.conformance.proto2.TestAllTypes^\n" + + "\t\tcel.expr.conformance.proto2.TestAllTypes.repeated_nested_message\n" + + "\t\t~list(cel.expr.conformance.proto2.TestAllTypes.NestedMessage)") .type( Decls.newListType( - Decls.newObjectType( - "google.api.expr.test.v1.proto2.TestAllTypes.NestedMessage"))), + Decls.newObjectType("cel.expr.conformance.proto2.TestAllTypes.NestedMessage"))), new TestCase() .i("TestAllTypes{}.repeated_nested_message") - .container("google.api.expr.test.v1.proto3") + .container("cel.expr.conformance.proto3") .r( - "google.api.expr.test.v1.proto3.TestAllTypes{}~google.api.expr.test.v1.proto3.TestAllTypes^\n" - + "\t\tgoogle.api.expr.test.v1.proto3.TestAllTypes.repeated_nested_message\n" - + "\t\t~list(google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage)") + "cel.expr.conformance.proto3.TestAllTypes{}~cel.expr.conformance.proto3.TestAllTypes^\n" + + "\t\tcel.expr.conformance.proto3.TestAllTypes.repeated_nested_message\n" + + "\t\t~list(cel.expr.conformance.proto3.TestAllTypes.NestedMessage)") .type( Decls.newListType( - Decls.newObjectType( - "google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage"))), + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes.NestedMessage"))), new TestCase() .i("base64.encode('hello')") .env( @@ -1545,8 +1588,8 @@ void check(TestCase tc) { TypeRegistry reg = ProtoTypeRegistry.newRegistry( - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes.getDefaultInstance(), - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.getDefaultInstance()); + dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance(), + dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()); Container cont = Container.newContainer(Container.name(tc.container)); CheckerEnv env = newStandardCheckerEnv(cont, reg); if (tc.disableStdEnv) { diff --git a/core/src/test/java/org/projectnessie/cel/common/types/BytesTest.java b/core/src/test/java/org/projectnessie/cel/common/types/BytesTest.java index 5494ea72..0234b419 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/BytesTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/BytesTest.java @@ -143,7 +143,9 @@ void bytesConvertToType() { @Test void bytesSize() { - assertThat(bytesOf("1234567890").size().equal(intOf(10))).isSameAs(True); + BytesT value = bytesOf("1234567890"); + assertThat(value.size().equal(intOf(10))).isSameAs(True); + assertThat(value.nativeSize()).isEqualTo(10); } @Test diff --git a/core/src/test/java/org/projectnessie/cel/common/types/DoubleTest.java b/core/src/test/java/org/projectnessie/cel/common/types/DoubleTest.java index f544394d..0e8bb47f 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/DoubleTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/DoubleTest.java @@ -73,6 +73,11 @@ void doubleConvertToNative_Zeros() { DoubleT n0 = doubleOf(-0.0d); assertThat(p0.equal(n0)).isSameAs(True); assertThat(n0.compare(p0)).isSameAs(IntZero); // "-0.0 < 0.0" --> False + + Double p0Obj = (Double) p0.value(); + Double n0Obj = (Double) n0.value(); + assertThat(Double.doubleToRawLongBits(p0Obj)).isEqualTo(Double.doubleToRawLongBits(0.0d)); + assertThat(Double.doubleToRawLongBits(n0Obj)).isEqualTo(Double.doubleToRawLongBits(-0.0d)); } @Test diff --git a/core/src/test/java/org/projectnessie/cel/common/types/IntTest.java b/core/src/test/java/org/projectnessie/cel/common/types/IntTest.java index 48295aea..b0708c52 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/IntTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/IntTest.java @@ -16,6 +16,7 @@ package org.projectnessie.cel.common.types; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.projectnessie.cel.common.types.BoolT.False; import static org.projectnessie.cel.common.types.BoolT.True; import static org.projectnessie.cel.common.types.DoubleT.DoubleType; @@ -148,6 +149,17 @@ void intConvertToNative_Wrapper() { assertThat(val2).isEqualTo(want2); } + @Test + void intConvertToNative_Int32WrapperRangeError() { + assertThatThrownBy(() -> intOf(1L + Integer.MAX_VALUE).convertToNative(Int32Value.class)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("range error"); + + assertThatThrownBy(() -> intOf(-1L + Integer.MIN_VALUE).convertToNative(Int32Value.class)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("range error"); + } + @Test void intConvertToType() { assertThat(intOf(-4).convertToType(IntType).equal(intOf(-4))).isSameAs(True); diff --git a/core/src/test/java/org/projectnessie/cel/common/types/ListTest.java b/core/src/test/java/org/projectnessie/cel/common/types/ListTest.java index 8c25f5f5..94f6fe65 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/ListTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/ListTest.java @@ -217,6 +217,7 @@ void listConstruction(TestData tc) { checkList(doubleTc, doubleListVal); } + @SuppressWarnings("removal") Lister checkList(TestData tc, Val listVal) { assertThat(listVal) .isInstanceOf(Lister.class) @@ -236,6 +237,7 @@ Lister checkList(TestData tc, Val listVal) { // Sizer.size() int size = tc.sourceSize(); assertThat(list.size()).isEqualTo(intOf(tc.sourceSize())); + assertThat(list.nativeSize()).isEqualTo(size); for (int i = 0; i < size; i++) { Object src = tc.sourceGet(i); @@ -243,6 +245,7 @@ Lister checkList(TestData tc, Val listVal) { // Indexer.get() Val elem = list.get(intOf(i)); + assertThat(list.nativeGetAt(i)).isEqualTo(elem); Object nat = elem.convertToNative((src instanceof Val) ? (Class) Val.class : src.getClass()); assertThat(src).isOfAnyClassIn(nat.getClass()); diff --git a/core/src/test/java/org/projectnessie/cel/common/types/MapTest.java b/core/src/test/java/org/projectnessie/cel/common/types/MapTest.java index f409c6f7..0bdea703 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/MapTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/MapTest.java @@ -16,25 +16,183 @@ package org.projectnessie.cel.common.types; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.projectnessie.cel.common.types.BoolT.False; import static org.projectnessie.cel.common.types.BoolT.True; import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.Err.newErr; import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.MapT.newMaybeWrappedMap; import static org.projectnessie.cel.common.types.MapT.newWrappedMap; +import static org.projectnessie.cel.common.types.NullT.NullValue; import static org.projectnessie.cel.common.types.StringT.stringOf; import static org.projectnessie.cel.common.types.Types.boolOf; import static org.projectnessie.cel.common.types.UintT.uintOf; +import static org.projectnessie.cel.common.types.UnknownT.unknownOf; import static org.projectnessie.cel.common.types.pb.ProtoTypeRegistry.newRegistry; import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Struct; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeAdapter; import org.projectnessie.cel.common.types.ref.Val; public class MapTest { @Test + void numericHashCodeMatchesHeterogeneousCelEquality() { + long[] values = {0L, 1L, 1L << 40, 1L << 53, (1L << 53) + 1, Long.MAX_VALUE}; + for (long value : values) { + Val intValue = intOf(value); + Val uintValue = uintOf(value); + Val doubleValue = doubleOf(value); + + assertThat(intValue).isEqualTo(uintValue); + assertThat(intValue.hashCode()).isEqualTo(uintValue.hashCode()); + assertThat(intValue).isEqualTo(doubleValue); + assertThat(intValue.hashCode()).isEqualTo(doubleValue.hashCode()); + } + + assertThat(doubleOf(-0.0d)).isEqualTo(doubleOf(0.0d)); + assertThat(doubleOf(-0.0d).hashCode()).isEqualTo(doubleOf(0.0d).hashCode()); + } + + @Test + void largeHeterogeneousNumericKeys() { + long key = 1L << 40; + MapT map = + (MapT) + newWrappedMap( + newRegistry(), new HashMap<>(Map.of(intOf(key), stringOf("large value")))); + + assertThat(map.find(intOf(key))).isEqualTo(stringOf("large value")); + assertThat(map.find(uintOf(key))).isEqualTo(stringOf("large value")); + assertThat(map.find(doubleOf(key))).isEqualTo(stringOf("large value")); + + Map duplicateKeys = new HashMap<>(); + duplicateKeys.put(key, "signed"); + duplicateKeys.put((double) key, "double"); + assertThat(newMaybeWrappedMap(newRegistry(), duplicateKeys)).matches(Err::isError); + } + + @Test + void containsPropagatesErrorAndUnknownKeys() { + MapT map = + (MapT) newWrappedMap(newRegistry(), new HashMap<>(Map.of(stringOf("key"), intOf(1)))); + Val error = newErr("key failed"); + Val unknown = unknownOf(42); + + assertThat(map.contains(error)).isSameAs(error); + assertThat(map.contains(unknown)).isSameAs(unknown); + } + + @Test + void equalityPropagatesErrorFromOtherMap() { + Val error = newErr("value failed"); + MapT left = + (MapT) newWrappedMap(newRegistry(), new HashMap<>(Map.of(stringOf("key"), intOf(1)))); + MapT right = (MapT) newWrappedMap(newRegistry(), new HashMap<>(Map.of(stringOf("key"), error))); + + assertThat(left.equal(right)).isSameAs(error); + } + + @Test + void nativeMapEqualityRetainsCelValueSemantics() { + MapT integer = (MapT) newMaybeWrappedMap(DefaultTypeAdapter.Instance, Map.of("key", 50_000L)); + MapT floatingPoint = + (MapT) newMaybeWrappedMap(DefaultTypeAdapter.Instance, Map.of("key", 50_000.0d)); + MapT positiveZero = (MapT) newMaybeWrappedMap(DefaultTypeAdapter.Instance, Map.of("key", 0.0d)); + MapT negativeZero = + (MapT) newMaybeWrappedMap(DefaultTypeAdapter.Instance, Map.of("key", -0.0d)); + MapT nan = (MapT) newMaybeWrappedMap(DefaultTypeAdapter.Instance, Map.of("key", Double.NaN)); + MapT nanCopy = + (MapT) newMaybeWrappedMap(DefaultTypeAdapter.Instance, Map.of("key", Double.NaN)); + Val error = newErr("value failed"); + MapT erroneous = (MapT) newMaybeWrappedMap(DefaultTypeAdapter.Instance, Map.of("key", error)); + + assertThat(integer.equal(floatingPoint)).isSameAs(True); + assertThat(integer).isEqualTo(floatingPoint); + assertThat(positiveZero.equal(negativeZero)).isSameAs(True); + assertThat(positiveZero).isEqualTo(negativeZero); + assertThat(nan.equal(nanCopy)).isSameAs(False); + assertThat(nan).isNotEqualTo(nanCopy); + assertThat(integer.equal(erroneous)).isSameAs(error); + } + + @Test + void nativeMapSnapshotsStructureAndAdaptsValuesOnDemand() { + Object selectedValue = Long.valueOf(50_000L); + AtomicInteger selectedValueAdaptations = new AtomicInteger(); + TypeAdapter adapter = + value -> { + if (value == selectedValue) { + selectedValueAdaptations.incrementAndGet(); + } + return DefaultTypeAdapter.Instance.nativeToValue(value); + }; + Map source = new HashMap<>(); + source.put("selected", selectedValue); + source.put("other", 60_000L); + + MapT map = (MapT) newMaybeWrappedMap(adapter, source); + + assertThat(selectedValueAdaptations).hasValue(0); + assertThat(map.size()).isEqualTo(intOf(2)); + assertThat(map.contains(stringOf("selected"))).isSameAs(True); + assertThat(selectedValueAdaptations).hasValue(0); + + source.put("selected", 70_000L); + source.put("added", 80_000L); + assertThat(map.size()).isEqualTo(intOf(2)); + assertThat(map.find(stringOf("added"))).isNull(); + assertThat(map.find(stringOf("selected"))).isEqualTo(intOf(50_000)); + assertThat(selectedValueAdaptations).hasValue(1); + } + + @Test + void nativeMapDistinguishesNullValueFromMissingKey() { + Map source = new HashMap<>(); + source.put("null", null); + MapT map = (MapT) newMaybeWrappedMap(DefaultTypeAdapter.Instance, source); + + assertThat(map.contains(stringOf("null"))).isSameAs(True); + assertThat(map.find(stringOf("null"))).isSameAs(NullValue); + assertThat(map.contains(stringOf("missing"))).isSameAs(BoolT.False); + assertThat(map.find(stringOf("missing"))).isNull(); + } + + @Test + @SuppressWarnings("removal") + void nativeMapConversionsAdaptCapturedValues() { + MapT map = + (MapT) + newMaybeWrappedMap( + DefaultTypeAdapter.Instance, Map.of("one", 1L, "nested", Map.of("two", 2L))); + + assertThat(map.value()).isEqualTo(Map.of("one", 1L, "nested", Map.of("two", 2L))); + assertThat(map.convertToNative(Map.class)) + .isEqualTo(Map.of("one", 1L, "nested", Map.of("two", 2L))); + assertThat(map.convertToNative(Struct.class).getFieldsMap()).containsOnlyKeys("one", "nested"); + } + + @Test + void mapJavaEqualityAndHashCodeWorkAcrossRepresentations() { + MapT nativeMap = (MapT) newMaybeWrappedMap(DefaultTypeAdapter.Instance, Map.of("key", 50_000L)); + MapT valMap = + (MapT) newWrappedMap(DefaultTypeAdapter.Instance, Map.of(stringOf("key"), intOf(50_000L))); + + assertThat(nativeMap).isEqualTo(valMap); + assertThat(valMap).isEqualTo(nativeMap); + assertThat(nativeMap.hashCode()).isEqualTo(valMap.hashCode()); + } + + @Test + @SuppressWarnings("removal") void heterogenousKeys() { Map javaMap = ImmutableMap.of( @@ -99,6 +257,20 @@ void heterogenousKeys() { ImmutableMap.of(1L, "one", 2L, "two", 3.1d, "three", true, "true", "str", "string")); } + @Test + @SuppressWarnings("removal") + void mapToProtobufStructRequiresStringKeys() { + MapT stringKeyMap = + (MapT) newWrappedMap(newRegistry(), ImmutableMap.of(stringOf("one"), doubleOf(1.0d))); + assertThat(stringKeyMap.convertToNative(Struct.class).getFieldsMap()).containsOnlyKeys("one"); + + MapT intKeyMap = + (MapT) newWrappedMap(newRegistry(), ImmutableMap.of(intOf(1), stringOf("one"))); + assertThatThrownBy(() -> intKeyMap.convertToNative(Struct.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("bad key type"); + } + // type testStruct struct { // M string // Details []string diff --git a/core/src/test/java/org/projectnessie/cel/common/types/NumericSemanticsTest.java b/core/src/test/java/org/projectnessie/cel/common/types/NumericSemanticsTest.java new file mode 100644 index 00000000..0b415e22 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/common/types/NumericSemanticsTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.common.types; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.IntT.IntNegOne; +import static org.projectnessie.cel.common.types.IntT.IntOne; +import static org.projectnessie.cel.common.types.IntT.IntZero; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; + +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Comparer; + +class NumericSemanticsTest { + + @Test + void signedUnsignedBoundaries() { + assertComparison(intOf(-1L), uintOf(0L), IntNegOne); + assertComparison(intOf(Long.MAX_VALUE), uintOf(Long.MAX_VALUE), IntZero); + assertComparison(intOf(Long.MAX_VALUE), uintOf(Long.MIN_VALUE), IntNegOne); + assertComparison(uintOf(-1L), intOf(Long.MAX_VALUE), IntOne); + + assertThat(intOf(-1L).equal(uintOf(-1L))).isSameAs(False); + assertThat(uintOf(Long.MAX_VALUE).equal(intOf(Long.MAX_VALUE))).isSameAs(True); + assertThat(uintOf(Long.MIN_VALUE).equal(intOf(Long.MIN_VALUE))).isSameAs(False); + } + + @Test + void floatingPointConversionBoundaries() { + long firstInexactInteger = (1L << 53) + 1L; + DoubleT roundedInteger = doubleOf((double) firstInexactInteger); + assertComparison(intOf(firstInexactInteger), roundedInteger, IntZero); + assertThat(intOf(firstInexactInteger).equal(roundedInteger)).isSameAs(True); + + UintT maxUint = uintOf(-1L); + DoubleT roundedMaxUint = (DoubleT) maxUint.convertToType(DoubleT.DoubleType); + assertComparison(maxUint, roundedMaxUint, IntZero); + assertThat(maxUint.equal(roundedMaxUint)).isSameAs(True); + + assertComparison(intOf(Long.MAX_VALUE), doubleOf(Double.POSITIVE_INFINITY), IntNegOne); + assertComparison(uintOf(-1L), doubleOf(Double.POSITIVE_INFINITY), IntNegOne); + assertComparison(intOf(Long.MIN_VALUE), doubleOf(Double.NEGATIVE_INFINITY), IntOne); + } + + @Test + void nanAndSignedZero() { + DoubleT nan = doubleOf(Double.NaN); + DoubleT zero = doubleOf(0.0d); + DoubleT negativeZero = doubleOf(-0.0d); + + assertThat(nan.equal(nan)).isSameAs(False); + assertComparison(nan, nan, IntZero); + assertComparison(nan, zero, IntOne); + assertComparison(zero, nan, IntNegOne); + + assertThat(zero.equal(negativeZero)).isSameAs(True); + assertComparison(zero, negativeZero, IntZero); + assertComparison(negativeZero, zero, IntZero); + } + + private static void assertComparison(Val left, Val right, IntT expected) { + assertThat(((Comparer) left).compare(right)).isSameAs(expected); + assertThat(((Comparer) right).compare(left)) + .isSameAs(expected == IntZero ? IntZero : expected == IntOne ? IntNegOne : IntOne); + } +} diff --git a/core/src/test/java/org/projectnessie/cel/common/types/PrimitiveArrayListTest.java b/core/src/test/java/org/projectnessie/cel/common/types/PrimitiveArrayListTest.java new file mode 100644 index 00000000..9ab19f62 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/common/types/PrimitiveArrayListTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.common.types; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.ListT.newDoubleArrayList; +import static org.projectnessie.cel.common.types.ListT.newIntArrayList; +import static org.projectnessie.cel.common.types.ListT.newLongArrayList; +import static org.projectnessie.cel.common.types.ListT.newValArrayList; +import static org.projectnessie.cel.common.types.UintT.uintOf; + +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; + +class PrimitiveArrayListTest { + + @Test + void exactAndHeterogeneousMembership() { + Lister intList = list(newIntArrayList(DefaultTypeAdapter.Instance, new int[] {50_000, 50_001})); + Lister longList = + list(newLongArrayList(DefaultTypeAdapter.Instance, new long[] {50_000, Long.MAX_VALUE})); + Lister doubleList = + list(newDoubleArrayList(DefaultTypeAdapter.Instance, new double[] {-0.0d, 50_000.0d})); + + assertThat(intList.contains(intOf(50_001))).isSameAs(True); + assertThat(intList.contains(uintOf(50_001))).isSameAs(True); + assertThat(intList.contains(doubleOf(50_001))).isSameAs(True); + assertThat(intList.contains(intOf(Long.MAX_VALUE))).isSameAs(False); + assertThat(longList.contains(intOf(Long.MAX_VALUE))).isSameAs(True); + assertThat(doubleList.contains(doubleOf(0.0d))).isSameAs(True); + assertThat(doubleList.contains(doubleOf(Double.NaN))).isSameAs(False); + } + + @Test + void exactAndCrossIntegerEquality() { + Val ints = newIntArrayList(DefaultTypeAdapter.Instance, new int[] {50_000, 50_001}); + Val equalInts = newIntArrayList(DefaultTypeAdapter.Instance, new int[] {50_000, 50_001}); + Val longs = newLongArrayList(DefaultTypeAdapter.Instance, new long[] {50_000, 50_001}); + Val different = newLongArrayList(DefaultTypeAdapter.Instance, new long[] {50_000, 50_002}); + + assertThat(ints.equal(equalInts)).isSameAs(True); + assertThat(ints.equal(longs)).isSameAs(True); + assertThat(longs.equal(ints)).isSameAs(True); + assertThat(ints.equal(different)).isSameAs(False); + } + + @Test + void doubleEqualityPreservesNanAndSignedZeroSemantics() { + Val signedZero = newDoubleArrayList(DefaultTypeAdapter.Instance, new double[] {-0.0d}); + Val positiveZero = newDoubleArrayList(DefaultTypeAdapter.Instance, new double[] {0.0d}); + Val nan = newDoubleArrayList(DefaultTypeAdapter.Instance, new double[] {Double.NaN}); + + assertThat(signedZero.equal(positiveZero)).isSameAs(True); + assertThat(nan.equal(nan)).isSameAs(False); + } + + @Test + void iterationAndPublicIndexBehaviorRemainUnchanged() { + Lister values = + list(newIntArrayList(DefaultTypeAdapter.Instance, new int[] {50_000, 50_001, 50_002})); + IteratorT iterator = values.iterator(); + + assertThat(iterator.next()).isEqualTo(intOf(50_000)); + assertThat(iterator.next()).isEqualTo(intOf(50_001)); + assertThat(iterator.next()).isEqualTo(intOf(50_002)); + assertThat(iterator.hasNext()).isSameAs(False); + assertThat(values.get(intOf(-1))).matches(Err::isError); + assertThat(values.get(doubleOf(0.5d))).matches(Err::isError); + assertThat(values.nativeGetAt(-1)).matches(Err::isError); + assertThat(values.nativeGetAt(values.nativeSize())).matches(Err::isError); + assertThat(values.nativeGetAt(1)).isEqualTo(intOf(50_001)); + } + + @Test + void genericEqualityFallbackRemainsAvailable() { + Val primitive = newIntArrayList(DefaultTypeAdapter.Instance, new int[] {50_000, 50_001}); + Val generic = + newValArrayList(DefaultTypeAdapter.Instance, new Val[] {intOf(50_000), intOf(50_001)}); + + assertThat(primitive.equal(generic)).isSameAs(True); + assertThat(generic.equal(primitive)).isSameAs(True); + } + + private static Lister list(Val value) { + return (Lister) value; + } +} diff --git a/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java b/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java index 059012ff..b2a85e7f 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java @@ -36,8 +36,6 @@ import static org.projectnessie.cel.common.types.pb.ProtoTypeRegistry.newEmptyRegistry; import static org.projectnessie.cel.common.types.pb.ProtoTypeRegistry.newRegistry; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.GlobalEnum; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes; import com.google.api.expr.v1alpha1.CheckedExpr; import com.google.api.expr.v1alpha1.Constant; import com.google.api.expr.v1alpha1.Expr; @@ -55,11 +53,14 @@ import com.google.protobuf.Timestamp; import com.google.protobuf.UInt32Value; import com.google.protobuf.UInt64Value; +import dev.cel.expr.conformance.proto3.GlobalEnum; +import dev.cel.expr.conformance.proto3.TestAllTypes; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; import java.util.Calendar; import java.util.Date; +import java.util.Map; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -105,10 +106,10 @@ void typeRegistryEnumValue() { reg.registerDescriptor(GlobalEnum.getDescriptor().getFile()); reg.registerDescriptor(OutOfOrderEnumOuterClass.getDescriptor().getFile()); - Val enumVal = reg.enumValue("google.api.expr.test.v1.proto3.GlobalEnum.GOO"); + Val enumVal = reg.enumValue("cel.expr.conformance.proto3.GlobalEnum.GOO"); assertThat(enumVal).extracting(Val::intValue).isEqualTo((long) GlobalEnum.GOO.getNumber()); - Val enumVal2 = reg.findIdent("google.api.expr.test.v1.proto3.GlobalEnum.GOO"); + Val enumVal2 = reg.findIdent("cel.expr.conformance.proto3.GlobalEnum.GOO"); assertThat(enumVal2.equal(enumVal)).isSameAs(True); // Previously, we checked `getIndex` on the `EnumValueDescriptor`, which is the same as the @@ -140,7 +141,7 @@ void typeRegistryFindType() { ProtoTypeRegistry reg = newEmptyRegistry(); reg.registerDescriptor(GlobalEnum.getDescriptor().getFile()); - String msgTypeName = "google.api.expr.test.v1.proto3.TestAllTypes"; + String msgTypeName = "cel.expr.conformance.proto3.TestAllTypes"; assertThat(reg.findType(msgTypeName)).isNotNull(); // assertThat(reg.findType(msgTypeName + "Undefined")).isNotNull(); ... this doesn't exist in // protobuf-java @@ -164,7 +165,7 @@ void typeRegistryNewValue() { assertThat(srcInfo) .extracting( SourceInfo::getLocation, SourceInfo::getLineOffsetsList, SourceInfo::getPositionsMap) - .containsExactly("TestTypeRegistryNewValue", asList(0, 2), mapOf(1L, 2L, 3L, 4L)); + .containsExactly("TestTypeRegistryNewValue", asList(0, 2), mapOf(1L, 2, 3L, 4)); } @Test @@ -185,7 +186,7 @@ void typeRegistryNewValue_OneofFields() { mapOf("string_value", stringOf("oneof"))))))); assertThat(exp).matches(v -> !Err.isError(v)); - CheckedExpr ce = exp.convertToNative(CheckedExpr.class); + CheckedExpr ce = reg.valueToNative(exp, CheckedExpr.class); assertThat(ce) .extracting(CheckedExpr::getExpr) .extracting(Expr::getConstExpr) @@ -198,16 +199,230 @@ void typeRegistryNewValue_WrapperFields() { TypeRegistry reg = newRegistry(TestAllTypes.getDefaultInstance()); Val exp = reg.newValue( - "google.api.expr.test.v1.proto3.TestAllTypes", - mapOf("single_int32_wrapper", intOf(123))); + "cel.expr.conformance.proto3.TestAllTypes", mapOf("single_int32_wrapper", intOf(123))); assertThat(exp).matches(v -> !Err.isError(v)); - TestAllTypes ce = exp.convertToNative(TestAllTypes.class); + TestAllTypes ce = reg.valueToNative(exp, TestAllTypes.class); assertThat(ce) .extracting(TestAllTypes::getSingleInt32Wrapper) .extracting(Int32Value::getValue) .isEqualTo(123); } + @Test + void typeRegistryNewValue_NullWrapperFieldIsUnset() { + TypeRegistry reg = newRegistry(TestAllTypes.getDefaultInstance()); + Val exp = + reg.newValue( + "cel.expr.conformance.proto3.TestAllTypes", mapOf("single_int32_wrapper", NullValue)); + + assertThat(exp).matches(v -> !Err.isError(v)); + TestAllTypes ce = reg.valueToNative(exp, TestAllTypes.class); + assertThat(ce.hasSingleInt32Wrapper()).isFalse(); + } + + @Test + void typeRegistryNewValue_NullMessageFieldsAreUnset() { + TypeRegistry reg = newRegistry(TestAllTypes.getDefaultInstance()); + Val exp = + reg.newValue( + "cel.expr.conformance.proto3.TestAllTypes", + mapOf( + "single_nested_message", + NullValue, + "single_duration", + NullValue, + "single_timestamp", + NullValue)); + + assertThat(exp).matches(v -> !Err.isError(v)); + TestAllTypes ce = reg.valueToNative(exp, TestAllTypes.class); + assertThat(ce.hasSingleNestedMessage()).isFalse(); + assertThat(ce.hasSingleDuration()).isFalse(); + assertThat(ce.hasSingleTimestamp()).isFalse(); + } + + @Test + void typeRegistryNewValue_RepeatedMessageFieldNullsArePruned() { + TypeRegistry reg = newRegistry(TestAllTypes.getDefaultInstance()); + Val exp = + reg.newValue( + "cel.expr.conformance.proto3.TestAllTypes", + mapOf( + "repeated_timestamp", + newGenericArrayList( + reg, + new Val[] {timestampOf(Instant.ofEpochSecond(1).atZone(ZoneIdZ)), NullValue}), + "repeated_duration", + newGenericArrayList(reg, new Val[] {durationOf(Duration.ofSeconds(1)), NullValue}), + "repeated_int32_wrapper", + newGenericArrayList(reg, new Val[] {intOf(1), NullValue}))); + + assertThat(exp).matches(v -> !Err.isError(v)); + TestAllTypes ce = reg.valueToNative(exp, TestAllTypes.class); + assertThat(ce.getRepeatedTimestampList()) + .containsExactly(Timestamp.newBuilder().setSeconds(1).build()); + assertThat(ce.getRepeatedDurationList()) + .containsExactly(com.google.protobuf.Duration.newBuilder().setSeconds(1).build()); + assertThat(ce.getRepeatedInt32WrapperList()).containsExactly(Int32Value.of(1)); + } + + @Test + void typeRegistryNewValue_MapMessageFieldNullsArePruned() { + TypeRegistry reg = newRegistry(TestAllTypes.getDefaultInstance()); + Val exp = + reg.newValue( + "cel.expr.conformance.proto3.TestAllTypes", + mapOf( + "map_bool_timestamp", + newMaybeWrappedMap( + reg, + mapOf( + true, + NullValue, + false, + timestampOf(Instant.ofEpochSecond(1).atZone(ZoneIdZ)))), + "map_bool_duration", + newMaybeWrappedMap( + reg, mapOf(true, NullValue, false, durationOf(Duration.ofSeconds(1)))), + "map_bool_int32_wrapper", + newMaybeWrappedMap(reg, mapOf(true, NullValue, false, intOf(1))))); + + assertThat(exp).matches(v -> !Err.isError(v)); + TestAllTypes ce = reg.valueToNative(exp, TestAllTypes.class); + assertThat(ce.getMapBoolTimestampMap()) + .containsExactlyEntriesOf(mapOf(false, Timestamp.newBuilder().setSeconds(1).build())); + assertThat(ce.getMapBoolDurationMap()) + .containsExactlyEntriesOf( + mapOf(false, com.google.protobuf.Duration.newBuilder().setSeconds(1).build())); + assertThat(ce.getMapBoolInt32WrapperMap()) + .containsExactlyEntriesOf(mapOf(false, Int32Value.of(1))); + } + + @Test + void typeRegistryNewValue_ConvertsMapEntriesDirectly() { + TypeRegistry reg = newRegistry(TestAllTypes.getDefaultInstance()); + Val nestedMessage = + reg.newValue( + "cel.expr.conformance.proto3.TestAllTypes.NestedMessage", mapOf("bb", intOf(42))); + + Val exp = + reg.newValue( + "cel.expr.conformance.proto3.TestAllTypes", + mapOf( + "map_bool_bytes", + new DirectConversionMap(reg, mapOf(true, bytesOf("bytes"))), + "map_bool_int32", + new DirectConversionMap(reg, mapOf(true, intOf(Integer.MIN_VALUE))), + "map_bool_uint32", + new DirectConversionMap(reg, mapOf(true, uintOf(0xffff_ffffL))), + "map_bool_uint64", + new DirectConversionMap(reg, mapOf(true, uintOf(-1L))), + "map_bool_float", + new DirectConversionMap(reg, mapOf(true, doubleOf(1.25))), + "map_bool_enum", + new DirectConversionMap(reg, mapOf(true, intOf(TestAllTypes.NestedEnum.BAR_VALUE))), + "map_bool_null_value", + new DirectConversionMap(reg, mapOf(true, NullValue)), + "map_bool_message", + new DirectConversionMap(reg, mapOf(true, nestedMessage)))); + + assertThat(exp).matches(v -> !Err.isError(v)); + TestAllTypes value = reg.valueToNative(exp, TestAllTypes.class); + assertThat(value.getMapBoolBytesMap()).containsEntry(true, ByteString.copyFromUtf8("bytes")); + assertThat(value.getMapBoolInt32Map()).containsEntry(true, Integer.MIN_VALUE); + assertThat(value.getMapBoolUint32Map()).containsEntry(true, -1); + assertThat(value.getMapBoolUint64Map()).containsEntry(true, -1L); + assertThat(value.getMapBoolFloatMap()).containsEntry(true, 1.25F); + assertThat(value.getMapBoolEnumMap()).containsEntry(true, TestAllTypes.NestedEnum.BAR); + assertThat(value.getMapBoolNullValueMap()).containsEntry(true, NULL_VALUE); + assertThat(value.getMapBoolMessageMap()) + .containsEntry(true, TestAllTypes.NestedMessage.newBuilder().setBb(42).build()); + } + + @Test + void typeRegistryNewValue_InvalidNullFieldAssignmentsReturnErrors() { + TypeRegistry reg = newRegistry(TestAllTypes.getDefaultInstance()); + String typeName = "cel.expr.conformance.proto3.TestAllTypes"; + + assertThat(reg.newValue(typeName, mapOf("single_bool", NullValue))).matches(Err::isError); + assertThat(reg.newValue(typeName, mapOf("repeated_int32", NullValue))).matches(Err::isError); + assertThat(reg.newValue(typeName, mapOf("map_string_string", NullValue))).matches(Err::isError); + assertThat( + reg.newValue( + typeName, mapOf("map_bool_enum", newMaybeWrappedMap(reg, mapOf(true, NullValue))))) + .matches(Err::isError); + assertThat(reg.newValue(typeName, mapOf("list_value", NullValue))).matches(Err::isError); + assertThat(reg.newValue(typeName, mapOf("single_struct", NullValue))).matches(Err::isError); + } + + private static final class DirectConversionMap extends MapT { + private final MapT delegate; + + private DirectConversionMap(TypeRegistry registry, Map value) { + this.delegate = (MapT) newMaybeWrappedMap(registry, value); + } + + @Override + @SuppressWarnings("removal") + public T convertToNative(Class typeDesc) { + throw new AssertionError("protobuf map conversion must not materialize a native Java map"); + } + + @Override + public Val convertToType(org.projectnessie.cel.common.types.ref.Type typeValue) { + return delegate.convertToType(typeValue); + } + + @Override + public IteratorT iterator() { + return delegate.iterator(); + } + + @Override + public Val equal(Val other) { + return delegate.equal(other); + } + + @Override + public Object value() { + return delegate.value(); + } + + @Override + public Val contains(Val value) { + return delegate.contains(value); + } + + @Override + public Val get(Val index) { + return delegate.get(index); + } + + @Override + public Val size() { + return delegate.size(); + } + + @Override + public Val find(Val key) { + return delegate.find(key); + } + } + + @Test + void typeRegistryNewValue_ProtobufStructFieldRequiresStringKeys() { + TypeRegistry reg = newRegistry(TestAllTypes.getDefaultInstance()); + String typeName = "cel.expr.conformance.proto3.TestAllTypes"; + + Val value = + reg.newValue( + typeName, + mapOf("single_struct", newMaybeWrappedMap(reg, mapOf(intOf(1), stringOf("one"))))); + + assertThat(value).matches(Err::isError); + assertThat(value.toString()).contains("invalid value for field 'single_struct': bad key type"); + } + @Test void typeRegistryGetters() { TypeRegistry reg = newRegistry(ParsedExpr.getDefaultInstance()); @@ -241,67 +456,80 @@ void convertToNative() { TypeRegistry reg = newRegistry(ParsedExpr.getDefaultInstance()); // Core type conversion tests. - expectValueToNative(True, true); - expectValueToNative(True, True); + expectValueToNative(reg, True, true); + expectValueToNative(reg, True, True); expectValueToNative( - newGenericArrayList(reg, new Val[] {True, False}), new Object[] {true, false}); - expectValueToNative(newGenericArrayList(reg, new Val[] {True, False}), new Val[] {True, False}); - expectValueToNative(intOf(-1), -1); - expectValueToNative(intOf(2), 2L); - expectValueToNative(intOf(-1), -1); - expectValueToNative(newGenericArrayList(reg, new Val[] {intOf(4)}), new Object[] {4L}); - expectValueToNative(newGenericArrayList(reg, new Val[] {intOf(5)}), new Val[] {intOf(5)}); - expectValueToNative(uintOf(3), ULong.valueOf(3)); - expectValueToNative(uintOf(4), ULong.valueOf(4)); - expectValueToNative(uintOf(5), 5); + reg, newGenericArrayList(reg, new Val[] {True, False}), new Object[] {true, false}); expectValueToNative( - newGenericArrayList(reg, new Val[] {uintOf(4)}), new Object[] {4L}); // loses "ULong" here - expectValueToNative(newGenericArrayList(reg, new Val[] {uintOf(5)}), new Val[] {uintOf(5)}); - expectValueToNative(doubleOf(5.5d), 5.5f); - expectValueToNative(doubleOf(-5.5d), -5.5d); - expectValueToNative(newGenericArrayList(reg, new Val[] {doubleOf(-5.5)}), new Object[] {-5.5}); + reg, newGenericArrayList(reg, new Val[] {True, False}), new Val[] {True, False}); + expectValueToNative(reg, intOf(-1), -1); + expectValueToNative(reg, intOf(2), 2L); + expectValueToNative(reg, intOf(-1), -1); + expectValueToNative(reg, newGenericArrayList(reg, new Val[] {intOf(4)}), new Object[] {4L}); + expectValueToNative(reg, newGenericArrayList(reg, new Val[] {intOf(5)}), new Val[] {intOf(5)}); + expectValueToNative(reg, uintOf(3), ULong.valueOf(3)); + expectValueToNative(reg, uintOf(4), ULong.valueOf(4)); + expectValueToNative(reg, uintOf(5), 5); expectValueToNative( - newGenericArrayList(reg, new Val[] {doubleOf(-5.5)}), new Val[] {doubleOf(-5.5)}); - expectValueToNative(doubleOf(-5.5), doubleOf(-5.5)); - expectValueToNative(stringOf("hello"), "hello"); - expectValueToNative(stringOf("hello"), stringOf("hello")); - expectValueToNative(NullValue, NULL_VALUE); - expectValueToNative(NullValue, NullValue); - expectValueToNative(newGenericArrayList(reg, new Val[] {NullValue}), new Object[] {null}); - expectValueToNative(newGenericArrayList(reg, new Val[] {NullValue}), new Val[] {NullValue}); - expectValueToNative(bytesOf("world"), "world".getBytes(StandardCharsets.UTF_8)); - expectValueToNative(bytesOf("world"), "world".getBytes(StandardCharsets.UTF_8)); + reg, + newGenericArrayList(reg, new Val[] {uintOf(4)}), + new Object[] {4L}); // loses "ULong" here expectValueToNative( + reg, newGenericArrayList(reg, new Val[] {uintOf(5)}), new Val[] {uintOf(5)}); + expectValueToNative(reg, doubleOf(5.5d), 5.5f); + expectValueToNative(reg, doubleOf(-5.5d), -5.5d); + expectValueToNative( + reg, newGenericArrayList(reg, new Val[] {doubleOf(-5.5)}), new Object[] {-5.5}); + expectValueToNative( + reg, newGenericArrayList(reg, new Val[] {doubleOf(-5.5)}), new Val[] {doubleOf(-5.5)}); + expectValueToNative(reg, doubleOf(-5.5), doubleOf(-5.5)); + expectValueToNative(reg, stringOf("hello"), "hello"); + expectValueToNative(reg, stringOf("hello"), stringOf("hello")); + expectValueToNative(reg, NullValue, NULL_VALUE); + expectValueToNative(reg, NullValue, NullValue); + expectValueToNative(reg, newGenericArrayList(reg, new Val[] {NullValue}), new Object[] {null}); + expectValueToNative( + reg, newGenericArrayList(reg, new Val[] {NullValue}), new Val[] {NullValue}); + expectValueToNative(reg, bytesOf("world"), "world".getBytes(StandardCharsets.UTF_8)); + expectValueToNative(reg, bytesOf("world"), "world".getBytes(StandardCharsets.UTF_8)); + expectValueToNative( + reg, newGenericArrayList(reg, new Val[] {bytesOf("hello")}), new Object[] {ByteString.copyFromUtf8("hello")}); expectValueToNative( - newGenericArrayList(reg, new Val[] {bytesOf("hello")}), new Val[] {bytesOf("hello")}); + reg, newGenericArrayList(reg, new Val[] {bytesOf("hello")}), new Val[] {bytesOf("hello")}); expectValueToNative( + reg, newGenericArrayList(reg, new Val[] {intOf(1), intOf(2), intOf(3)}), new Object[] {1L, 2L, 3L}); - expectValueToNative(durationOf(Duration.ofSeconds(500)), Duration.ofSeconds(500)); + expectValueToNative(reg, durationOf(Duration.ofSeconds(500)), Duration.ofSeconds(500)); expectValueToNative( + reg, durationOf(Duration.ofSeconds(500)), com.google.protobuf.Duration.newBuilder().setSeconds(500).build()); - expectValueToNative(durationOf(Duration.ofSeconds(500)), durationOf(Duration.ofSeconds(500))); expectValueToNative( + reg, durationOf(Duration.ofSeconds(500)), durationOf(Duration.ofSeconds(500))); + expectValueToNative( + reg, timestampOf(Timestamp.newBuilder().setSeconds(12345).build()), Instant.ofEpochSecond(12345, 0).atZone(ZoneIdZ)); expectValueToNative( + reg, timestampOf(Timestamp.newBuilder().setSeconds(12345).build()), timestampOf(Timestamp.newBuilder().setSeconds(12345).build())); expectValueToNative( + reg, timestampOf(Timestamp.newBuilder().setSeconds(12345).build()), Timestamp.newBuilder().setSeconds(12345).build()); expectValueToNative( - newMaybeWrappedMap(reg, mapOf(1L, 1L, 2L, 1L, 3L, 1L)), mapOf(1L, 1L, 2L, 1L, 3L, 1L)); + reg, newMaybeWrappedMap(reg, mapOf(1L, 1L, 2L, 1L, 3L, 1L)), mapOf(1L, 1L, 2L, 1L, 3L, 1L)); // Null conversion tests. - expectValueToNative(NullValue, NULL_VALUE); + expectValueToNative(reg, NullValue, NULL_VALUE); // Proto conversion tests. ParsedExpr parsedExpr = ParsedExpr.getDefaultInstance(); - expectValueToNative(reg.nativeToValue(parsedExpr), parsedExpr); + expectValueToNative(reg, reg.nativeToValue(parsedExpr), parsedExpr); } @Test @@ -461,8 +689,8 @@ void unsupportedConversion() { static class nonConvertible {} - static void expectValueToNative(Val in, Object out) { - Object val = in.convertToNative(out.getClass()); + static void expectValueToNative(TypeRegistry registry, Val in, Object out) { + Object val = registry.valueToNative(in, out.getClass()); assertThat(val).isNotNull(); if (val instanceof byte[]) { diff --git a/core/src/test/java/org/projectnessie/cel/common/types/StringTest.java b/core/src/test/java/org/projectnessie/cel/common/types/StringTest.java index 6ef673fa..6b57535d 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/StringTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/StringTest.java @@ -122,7 +122,24 @@ void stringConvertToNative_Wrapper() { @Test void stringConvertToType() { assertThat(stringOf("-1").convertToType(IntType).equal(IntNegOne)).isSameAs(True); + assertThat(stringOf("1").convertToType(BoolType).equal(True)).isSameAs(True); + assertThat(stringOf("t").convertToType(BoolType).equal(True)).isSameAs(True); + assertThat(stringOf("true").convertToType(BoolType).equal(True)).isSameAs(True); + assertThat(stringOf("TRUE").convertToType(BoolType).equal(True)).isSameAs(True); + assertThat(stringOf("True").convertToType(BoolType).equal(True)).isSameAs(True); + assertThat(stringOf("0").convertToType(BoolType).equal(False)).isSameAs(True); + assertThat(stringOf("f").convertToType(BoolType).equal(False)).isSameAs(True); assertThat(stringOf("false").convertToType(BoolType).equal(False)).isSameAs(True); + assertThat(stringOf("FALSE").convertToType(BoolType).equal(False)).isSameAs(True); + assertThat(stringOf("False").convertToType(BoolType).equal(False)).isSameAs(True); + assertThat(stringOf("TrUe").convertToType(BoolType)) + .isInstanceOf(Err.class) + .extracting(Object::toString) + .isEqualTo("type conversion error from 'string' to 'bool'"); + assertThat(stringOf("FaLsE").convertToType(BoolType)) + .isInstanceOf(Err.class) + .extracting(Object::toString) + .isEqualTo("type conversion error from 'string' to 'bool'"); assertThat(stringOf("1").convertToType(UintType).equal(uintOf(1))).isSameAs(True); assertThat(stringOf("2.5").convertToType(DoubleType).equal(doubleOf(2.5))).isSameAs(True); assertThat( @@ -215,6 +232,7 @@ void stringSize() { assertThat(stringOf("").size()).isSameAs(IntZero); assertThat(stringOf("hello world").size()).isEqualTo(intOf(11)); assertThat(stringOf("\u65e5\u672c\u8a9e").size()).isEqualTo(intOf(3)); + assertThat(stringOf("\u65e5\u672c\u8a9e").nativeSize()).isEqualTo(3); } @Test diff --git a/core/src/test/java/org/projectnessie/cel/common/types/TimestampTest.java b/core/src/test/java/org/projectnessie/cel/common/types/TimestampTest.java index e80ba683..3a1b8a1a 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/TimestampTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/TimestampTest.java @@ -34,7 +34,6 @@ import static org.projectnessie.cel.common.types.TypeT.TypeType; import com.google.protobuf.Any; -import com.google.protobuf.StringValue; import com.google.protobuf.Timestamp; import com.google.protobuf.Value; import java.time.DateTimeException; @@ -107,7 +106,12 @@ void timestampConvertToNative_JSON() { // JSON Object val = ts.convertToNative(Value.class); - Object want = StringValue.of("1970-01-01T02:05:06Z"); + Object want = Value.newBuilder().setStringValue("1970-01-01T02:05:06Z").build(); + assertThat(val).isEqualTo(want); + + ts = timestampOf(Instant.ofEpochSecond(253402300799L, 999999999).atZone(ZoneIdZ)); + val = ts.convertToNative(Value.class); + want = Value.newBuilder().setStringValue("9999-12-31T23:59:59.999999999Z").build(); assertThat(val).isEqualTo(want); } diff --git a/core/src/test/java/org/projectnessie/cel/common/types/TypeTest.java b/core/src/test/java/org/projectnessie/cel/common/types/TypeTest.java index ae658729..e08ca174 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/TypeTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/TypeTest.java @@ -62,4 +62,17 @@ void typeConvertToType() { void typeType() { assertThat(TypeType.type()).isSameAs(TypeType); } + + @Test + void objectTypeEqualsBuiltInTypeWithSameName() { + assertThat(TypeT.newObjectTypeValue("google.protobuf.Timestamp").equal(TimestampType)) + .isSameAs(BoolT.True); + assertThat(TimestampType.equal(TypeT.newObjectTypeValue("google.protobuf.Timestamp"))) + .isSameAs(BoolT.True); + + assertThat(TypeT.newObjectTypeValue("google.protobuf.Duration").equal(DurationType)) + .isSameAs(BoolT.True); + assertThat(DurationType.equal(TypeT.newObjectTypeValue("google.protobuf.Duration"))) + .isSameAs(BoolT.True); + } } diff --git a/core/src/test/java/org/projectnessie/cel/common/types/UintTest.java b/core/src/test/java/org/projectnessie/cel/common/types/UintTest.java index 61e9ce56..5972ded4 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/UintTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/UintTest.java @@ -109,9 +109,12 @@ void uintConvertToNative_Json() { Value val = uintOf(maxIntJSON).convertToNative(Value.class); assertThat(val).isEqualTo(Value.newBuilder().setNumberValue(9007199254740991.0d).build()); - // Value converts to a JSON decimal string - val = intOf(maxIntJSON + 1).convertToNative(Value.class); + // Value converts to a JSON decimal string. + val = uintOf(maxIntJSON + 1).convertToNative(Value.class); assertThat(val).isEqualTo(Value.newBuilder().setStringValue("9007199254740992").build()); + + val = uintOf(-1L).convertToNative(Value.class); + assertThat(val).isEqualTo(Value.newBuilder().setStringValue("18446744073709551615").build()); } @Test @@ -142,6 +145,17 @@ void uintConvertToNative_Wrapper() { assertThat(val2).isEqualTo(want2); } + @Test + void uintConvertToNative_UInt32WrapperRangeError() { + assertThatThrownBy(() -> uintOf(0x100000000L).convertToNative(UInt32Value.class)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("range error"); + + assertThatThrownBy(() -> uintOf(-1L).convertToNative(UInt32Value.class)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("range error"); + } + @Test void uintConvertToType() { // 18446744073709551612L diff --git a/core/src/test/java/org/projectnessie/cel/common/types/pb/FieldDescriptionTest.java b/core/src/test/java/org/projectnessie/cel/common/types/pb/FieldDescriptionTest.java index 465f877f..26531d5d 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/pb/FieldDescriptionTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/pb/FieldDescriptionTest.java @@ -18,14 +18,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.projectnessie.cel.common.types.BoolT.False; import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.IntT.intOf; import static org.projectnessie.cel.common.types.StringT.stringOf; import static org.projectnessie.cel.common.types.UintT.uintOf; import static org.projectnessie.cel.common.types.pb.Db.newDb; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.NestedTestAllTypes; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedEnum; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedMessage; import com.google.api.expr.v1alpha1.Type; import com.google.protobuf.BoolValue; import com.google.protobuf.Duration; @@ -36,6 +33,10 @@ import com.google.protobuf.Struct; import com.google.protobuf.Timestamp; import com.google.protobuf.Value; +import dev.cel.expr.conformance.proto3.NestedTestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum; +import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import java.time.Instant; import java.util.Arrays; import java.util.Collections; @@ -43,6 +44,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.types.MapT; import org.projectnessie.cel.common.types.TimestampT; import org.projectnessie.cel.common.types.traits.Mapper; @@ -69,7 +71,7 @@ void fieldDescription() { // matches the one determined by the TypeDescription utils. Type got = fd.checkedType(); Type wanted = - Type.newBuilder().setMessageType("google.api.expr.test.v1.proto3.TestAllTypes").build(); + Type.newBuilder().setMessageType("cel.expr.conformance.proto3.TestAllTypes").build(); assertThat(got).isEqualTo(wanted); } @@ -202,11 +204,29 @@ void getFieldProtoMapSupportsRepeatedLookup() { Mapper map = (Mapper) field.getField(msg, registry); assertThat(map.find(uintOf(2)).equal(stringOf("two"))).isSameAs(True); + assertThat(map.find(intOf(2)).equal(stringOf("two"))).isSameAs(True); assertThat(map.find(uintOf(-1L)).equal(stringOf("large"))).isSameAs(True); assertThat(map.find(uintOf(1)).equal(stringOf("one"))).isSameAs(True); assertThat(map.find(uintOf(42))).isNull(); assertThat(map.contains(uintOf(2))).isSameAs(True); assertThat(map.contains(uintOf(42))).isSameAs(False); + + DynamicMessage dynamicMessage = + DynamicMessage.newBuilder(msg.getDescriptorForType()).mergeFrom(msg).build(); + Mapper dynamicMap = (Mapper) field.getField(dynamicMessage, registry); + assertThat(dynamicMap.find(uintOf(2)).equal(stringOf("two"))).isSameAs(True); + assertThat(((MapT) map).equal((MapT) dynamicMap)).isSameAs(True); + assertThat(dynamicMap).isEqualTo(map); + assertThat(dynamicMap.hashCode()).isEqualTo(map.hashCode()); + + TestAllTypes equalMessage = msg.toBuilder().build(); + Mapper equalGeneratedMap = (Mapper) field.getField(equalMessage, registry); + assertThat(((MapT) map).equal((MapT) equalGeneratedMap)).isSameAs(True); + assertThat(map).isEqualTo(equalGeneratedMap); + + TestAllTypes differentMessage = msg.toBuilder().putMapUint64String(2L, "different").build(); + Mapper differentGeneratedMap = (Mapper) field.getField(differentMessage, registry); + assertThat(((MapT) map).equal((MapT) differentGeneratedMap)).isSameAs(False); } static class TestCase { diff --git a/core/src/test/java/org/projectnessie/cel/common/types/pb/GeneratedFieldAccessorTest.java b/core/src/test/java/org/projectnessie/cel/common/types/pb/GeneratedFieldAccessorTest.java new file mode 100644 index 00000000..10ea35b2 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/common/types/pb/GeneratedFieldAccessorTest.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.common.types.pb; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; +import static org.projectnessie.cel.common.types.pb.Db.newDb; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.Int32Value; +import com.google.protobuf.NullValue; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.types.MapT; +import org.projectnessie.cel.common.types.ref.FieldGetter; +import org.projectnessie.cel.common.types.ref.FieldTester; +import org.projectnessie.cel.common.types.ref.FieldType; +import org.projectnessie.cel.common.types.traits.Lister; + +class GeneratedFieldAccessorTest { + + @Test + void bindsSupportedGeneratedScalarGetters() { + TestAllTypes message = + TestAllTypes.newBuilder() + .setSingleInt32(12) + .setSingleInt64(13L) + .setSingleSint32(14) + .setSingleSint64(15L) + .setSingleSfixed32(16) + .setSingleSfixed64(17L) + .setSingleFloat(18.25f) + .setSingleDouble(19.25d) + .setSingleBool(true) + .setSingleString("twenty") + .setSingleBytes(ByteString.copyFromUtf8("twenty-one")) + .build(); + Db db = newDb(); + db.registerMessage(TestAllTypes.getDefaultInstance()); + PbTypeDescription type = db.describeType(message.getDescriptorForType().getFullName()); + + for (String fieldName : + List.of( + "single_int32", + "single_int64", + "single_sint32", + "single_sint64", + "single_sfixed32", + "single_sfixed64", + "single_float", + "single_double", + "single_bool", + "single_string", + "single_bytes")) { + FieldDescription field = type.fieldByName(fieldName); + FieldGetter getter = GeneratedFieldAccessor.create(type, field); + assertThat(getter).as(fieldName).isNotNull(); + assertThat(getter.getFrom(message)) + .as(fieldName) + .isEqualTo(message.getField(field.descriptor())); + } + } + + @Test + void exposesPrimitiveGettersOnlyForAllocationFreeScalarFields() { + TestAllTypes message = + TestAllTypes.newBuilder() + .setSingleBool(true) + .setSingleInt32(50_021) + .setSingleInt64(50_022L) + .setStandaloneEnumValue(12_345) + .setSingleFloat(-0.0f) + .setSingleDouble(Double.NaN) + .build(); + ProtoTypeRegistry registry = ProtoTypeRegistry.newRegistry(TestAllTypes.getDefaultInstance()); + String typeName = TestAllTypes.getDescriptor().getFullName(); + + FieldGetter.Primitive boolGetter = primitiveGetter(registry, typeName, "single_bool"); + FieldGetter.Primitive int32Getter = primitiveGetter(registry, typeName, "single_int32"); + FieldGetter.Primitive int64Getter = primitiveGetter(registry, typeName, "single_int64"); + FieldGetter.Primitive enumGetter = primitiveGetter(registry, typeName, "standalone_enum"); + FieldGetter.Primitive floatGetter = primitiveGetter(registry, typeName, "single_float"); + FieldGetter.Primitive doubleGetter = primitiveGetter(registry, typeName, "single_double"); + + assertThat(boolGetter.optimizedTargetType()).isEqualTo(TestAllTypes.class); + assertThat(boolGetter.getBooleanFrom(message)).isTrue(); + assertThat(int32Getter.getLongFrom(message)).isEqualTo(50_021L); + assertThat(int64Getter.getLongFrom(message)).isEqualTo(50_022L); + assertThat(enumGetter.getLongFrom(message)).isEqualTo(12_345L); + assertThat(Float.floatToRawIntBits((float) floatGetter.getDoubleFrom(message))) + .isEqualTo(Float.floatToRawIntBits(-0.0f)); + assertThat(doubleGetter.getDoubleFrom(message)).isNaN(); + + DynamicMessage dynamic = + DynamicMessage.newBuilder(message.getDescriptorForType()).mergeFrom(message).build(); + assertThat(int64Getter.getFrom(dynamic)).isEqualTo(50_022L); + + for (String fieldName : + List.of( + "single_uint32", + "single_uint64", + "single_string", + "single_int32_wrapper", + "optional_null_value")) { + assertThat(registry.findFieldType(typeName, fieldName).getFrom) + .as(fieldName) + .isNotInstanceOf(FieldGetter.Primitive.class); + } + } + + private static FieldGetter.Primitive primitiveGetter( + ProtoTypeRegistry registry, String typeName, String fieldName) { + assertThat(registry.findFieldType(typeName, fieldName).getFrom) + .as(fieldName) + .isInstanceOf(FieldGetter.Primitive.class); + return (FieldGetter.Primitive) registry.findFieldType(typeName, fieldName).getFrom; + } + + @Test + void bindsFieldsWithSpecializedNormalization() { + Db db = newDb(); + db.registerMessage(TestAllTypes.getDefaultInstance()); + PbTypeDescription type = db.describeType(TestAllTypes.getDescriptor().getFullName()); + + for (String fieldName : + List.of( + "single_uint32", + "standalone_enum", + "single_int32_wrapper", + "repeated_int32", + "map_string_string", + "single_any", + "single_duration", + "single_timestamp")) { + assertThat(GeneratedFieldAccessor.create(type, type.fieldByName(fieldName))) + .as(fieldName) + .isNotNull(); + } + + assertThat(GeneratedFieldAccessor.create(type, type.fieldByName("optional_null_value"))) + .isNull(); + assertThat(GeneratedFieldAccessor.create(type, type.fieldByName("repeated_null_value"))) + .isNotNull(); + assertThat(GeneratedFieldAccessor.create(type, type.fieldByName("single_nested_message"))) + .isNull(); + assertThat( + GeneratedFieldAccessor.createForObject(type, type.fieldByName("single_nested_message"))) + .isNotNull(); + } + + @Test + void bindsOnlyGeneratedPresenceMethods() { + Db db = newDb(); + db.registerMessage(TestAllTypes.getDefaultInstance()); + PbTypeDescription type = db.describeType(TestAllTypes.getDescriptor().getFullName()); + TestAllTypes present = + TestAllTypes.newBuilder() + .setOptionalBool(false) + .setSingleNestedMessage(TestAllTypes.NestedMessage.getDefaultInstance()) + .setSingleInt32Wrapper(Int32Value.getDefaultInstance()) + .build(); + + for (String fieldName : + List.of("optional_bool", "single_nested_message", "single_int32_wrapper")) { + FieldTester tester = GeneratedFieldAccessor.createTester(type, type.fieldByName(fieldName)); + assertThat(tester).as(fieldName).isNotNull(); + assertThat(tester.isSet(TestAllTypes.getDefaultInstance())).as(fieldName).isFalse(); + assertThat(tester.isSet(present)).as(fieldName).isTrue(); + } + + for (String fieldName : + List.of("single_int32", "standalone_enum", "repeated_int32", "map_string_string")) { + assertThat(GeneratedFieldAccessor.createTester(type, type.fieldByName(fieldName))) + .as(fieldName) + .isNull(); + } + } + + @Test + void registryNormalizesGeneratedFieldValues() { + TestAllTypes message = + TestAllTypes.newBuilder() + .setSingleUint32(-1) + .setSingleUint64(Long.MIN_VALUE) + .setStandaloneEnumValue(12_345) + .setSingleNestedMessage(TestAllTypes.NestedMessage.newBuilder().setBb(50_000)) + .setSingleInt32Wrapper(Int32Value.of(50_000)) + .addRepeatedUint32(-1) + .addRepeatedNestedEnumValue(12_345) + .addRepeatedNullValue(NullValue.NULL_VALUE) + .putMapStringString("key", "value") + .putMapUint32Uint64(-1, Long.MIN_VALUE) + .putMapStringEnumValue("unknown", 12_345) + .putMapBoolNullValue(true, NullValue.NULL_VALUE) + .build(); + ProtoTypeRegistry registry = ProtoTypeRegistry.newRegistry(TestAllTypes.getDefaultInstance()); + String typeName = message.getDescriptorForType().getFullName(); + + assertThat(registry.findFieldType(typeName, "single_uint32").getFrom.getFrom(message)) + .isEqualTo(ULong.valueOf(-1L)); + assertThat(registry.findFieldType(typeName, "single_uint64").getFrom.getFrom(message)) + .isEqualTo(ULong.valueOf(Long.MIN_VALUE)); + assertThat(registry.findFieldType(typeName, "standalone_enum").getFrom.getFrom(message)) + .isEqualTo(12_345); + assertThat(registry.findFieldType(typeName, "single_nested_message").getFrom.getFrom(message)) + .isEqualTo(message.getSingleNestedMessage()); + assertThat(registry.findFieldType(typeName, "single_int32_wrapper").getFrom.getFrom(message)) + .isEqualTo(Int32Value.of(50_000)); + assertThat( + registry + .findFieldType(typeName, "single_int32_wrapper") + .getFrom + .getFrom(TestAllTypes.getDefaultInstance())) + .isEqualTo(NullValue.NULL_VALUE); + assertThat(registry.findFieldType(typeName, "repeated_uint32").getFrom.getFrom(message)) + .isEqualTo(List.of(ULong.valueOf(-1L))); + assertThat(registry.findFieldType(typeName, "repeated_nested_enum").getFrom.getFrom(message)) + .isEqualTo(List.of(12_345)); + + Object mapValue = + registry.findFieldType(typeName, "map_string_string").getFrom.getFrom(message); + assertThat(mapValue).isInstanceOf(MapT.class); + assertThat(((MapT) mapValue).get(stringOf("key"))).isEqualTo(stringOf("value")); + MapT unsignedMap = + (MapT) registry.findFieldType(typeName, "map_uint32_uint64").getFrom.getFrom(message); + assertThat(unsignedMap.get(uintOf(-1L))).isEqualTo(uintOf(Long.MIN_VALUE)); + MapT enumMap = + (MapT) registry.findFieldType(typeName, "map_string_enum").getFrom.getFrom(message); + assertThat(enumMap.get(stringOf("unknown"))).isEqualTo(intOf(12_345)); + MapT nullMap = + (MapT) registry.findFieldType(typeName, "map_bool_null_value").getFrom.getFrom(message); + assertThat(nullMap.get(True)).isEqualTo(intOf(0)); + + PbObjectT object = (PbObjectT) registry.nativeToValue(message); + assertThat(object.get(stringOf("single_uint32"))).isEqualTo(uintOf(-1L)); + assertThat(object.get(stringOf("standalone_enum"))).isEqualTo(intOf(12_345)); + assertThat(object.get(stringOf("single_int32_wrapper"))).isEqualTo(intOf(50_000)); + assertThat(((Lister) object.get(stringOf("repeated_null_value"))).get(intOf(0))) + .isEqualTo(intOf(0)); + } + + @Test + void generatedRegistryGetterFallsBackForDynamicMessages() { + TestAllTypes generated = + TestAllTypes.newBuilder() + .setSingleInt32(42) + .setStandaloneEnum(NestedEnum.BAR) + .setSingleNestedMessage(TestAllTypes.NestedMessage.newBuilder().setBb(50_000)) + .build(); + DynamicMessage dynamic = + DynamicMessage.newBuilder(generated.getDescriptorForType()).mergeFrom(generated).build(); + ProtoTypeRegistry registry = ProtoTypeRegistry.newRegistry(TestAllTypes.getDefaultInstance()); + + FieldType scalar = + registry.findFieldType(generated.getDescriptorForType().getFullName(), "single_int32"); + FieldType enumField = + registry.findFieldType(generated.getDescriptorForType().getFullName(), "standalone_enum"); + + assertThat(scalar.getFrom.getFrom(generated)).isEqualTo(42); + assertThat(scalar.getFrom.getFrom(dynamic)).isEqualTo(42); + FieldDescriptor enumDescriptor = + generated.getDescriptorForType().findFieldByName("standalone_enum"); + assertThat(enumField.getFrom.getFrom(dynamic)).isEqualTo(dynamic.getField(enumDescriptor)); + + PbObjectT generatedObject = (PbObjectT) registry.nativeToValue(generated); + PbObjectT dynamicObject = (PbObjectT) registry.nativeToValue(dynamic); + assertThat(generatedObject.get(stringOf("single_nested_message"))) + .isEqualTo(registry.nativeToValue(generated.getSingleNestedMessage())); + assertThat(dynamicObject.get(stringOf("single_nested_message"))) + .isEqualTo( + registry.nativeToValue( + dynamic.getField( + generated.getDescriptorForType().findFieldByName("single_nested_message")))); + } + + @Test + void proto2DefaultsAndPresenceRemainDescriptorDriven() { + dev.cel.expr.conformance.proto2.TestAllTypes absent = + dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance(); + dev.cel.expr.conformance.proto2.TestAllTypes present = + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder().setSingleInt32(42).build(); + ProtoTypeRegistry registry = ProtoTypeRegistry.newRegistry(absent); + FieldType field = + registry.findFieldType(absent.getDescriptorForType().getFullName(), "single_int32"); + + assertThat(field.getFrom.getFrom(absent)).isEqualTo(-32); + assertThat(field.isSet.isSet(absent)).isFalse(); + assertThat(field.getFrom.getFrom(present)).isEqualTo(42); + assertThat(field.isSet.isSet(present)).isTrue(); + } + + @Test + void dynamicTypeDescriptionDoesNotBindGeneratedGetter() { + DynamicMessage zero = DynamicMessage.getDefaultInstance(TestAllTypes.getDescriptor()); + Db db = newDb(); + db.registerMessage(zero); + PbTypeDescription type = db.describeType(zero.getDescriptorForType().getFullName()); + + assertThat(GeneratedFieldAccessor.create(type, type.fieldByName("single_int32"))).isNull(); + assertThat(GeneratedFieldAccessor.createTester(type, type.fieldByName("optional_bool"))) + .isNull(); + } + + @Test + void generatedRegistrationInvalidatesDescriptorOnlyFieldCache() { + DynamicMessage dynamicZero = DynamicMessage.getDefaultInstance(TestAllTypes.getDescriptor()); + ProtoTypeRegistry registry = ProtoTypeRegistry.newEmptyRegistry(); + registry.registerMessage(dynamicZero); + String typeName = dynamicZero.getDescriptorForType().getFullName(); + + FieldType descriptorOnly = registry.findFieldType(typeName, "single_int32"); + registry.registerMessage(TestAllTypes.getDefaultInstance()); + FieldType generated = registry.findFieldType(typeName, "single_int32"); + + assertThat(generated).isNotSameAs(descriptorOnly); + assertThat(generated.getFrom.getFrom(TestAllTypes.newBuilder().setSingleInt32(50_000).build())) + .isEqualTo(50_000); + } +} diff --git a/core/src/test/java/org/projectnessie/cel/common/types/pb/PbObjectTest.java b/core/src/test/java/org/projectnessie/cel/common/types/pb/PbObjectTest.java index 55d30073..2a81be88 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/pb/PbObjectTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/pb/PbObjectTest.java @@ -28,7 +28,12 @@ import com.google.api.expr.v1alpha1.ParsedExpr; import com.google.api.expr.v1alpha1.SourceInfo; import com.google.protobuf.Any; +import com.google.protobuf.Empty; +import com.google.protobuf.FieldMask; import com.google.protobuf.Message; +import com.google.protobuf.Struct; +import com.google.protobuf.Timestamp; +import com.google.protobuf.Value; import java.util.Arrays; import java.util.Collections; import org.junit.jupiter.api.Disabled; @@ -62,6 +67,7 @@ void newProtoObject() { } @Test + @SuppressWarnings("removal") void protoObjectConvertToNative() throws Exception { TypeRegistry reg = newRegistry(Expr.getDefaultInstance()); ParsedExpr msg = @@ -90,6 +96,27 @@ void protoObjectConvertToNative() throws Exception { assertThat(unpackedAny).isEqualTo(objVal.value()); } + @Test + @SuppressWarnings("removal") + void wellKnownProtoObjectsConvertToJsonValue() { + TypeRegistry reg = newRegistry(Empty.getDefaultInstance(), FieldMask.getDefaultInstance()); + + Val empty = reg.nativeToValue(Empty.getDefaultInstance()); + assertThat(empty.convertToNative(Value.class)) + .isEqualTo(Value.newBuilder().setStructValue(Struct.getDefaultInstance()).build()); + + Val fieldMask = + reg.nativeToValue(FieldMask.newBuilder().addPaths("foo").addPaths("bar_baz").build()); + assertThat(fieldMask.convertToNative(Value.class)) + .isEqualTo(Value.newBuilder().setStringValue("foo,barBaz").build()); + + Val timestamp = + reg.nativeToValue( + Timestamp.newBuilder().setSeconds(253402300799L).setNanos(999999999).build()); + assertThat(timestamp.convertToNative(Value.class)) + .isEqualTo(Value.newBuilder().setStringValue("9999-12-31T23:59:59.999999999Z").build()); + } + @Test @Disabled("IMPLEMENT ME") void protoObjectConvertToNative_JSON() { diff --git a/core/src/test/java/org/projectnessie/cel/common/types/pb/PbTypeDescriptionTest.java b/core/src/test/java/org/projectnessie/cel/common/types/pb/PbTypeDescriptionTest.java index 857f3888..cb1ec6ab 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/pb/PbTypeDescriptionTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/pb/PbTypeDescriptionTest.java @@ -19,8 +19,6 @@ import static org.projectnessie.cel.Util.mapOf; import static org.projectnessie.cel.common.types.pb.Db.newDb; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.NestedTestAllTypes; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes; import com.google.api.expr.v1alpha1.Type; import com.google.protobuf.Any; import com.google.protobuf.BoolValue; @@ -41,6 +39,8 @@ import com.google.protobuf.UInt32Value; import com.google.protobuf.UInt64Value; import com.google.protobuf.Value; +import dev.cel.expr.conformance.proto3.NestedTestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes; import java.time.Instant; import java.util.Arrays; import java.util.Map; @@ -232,7 +232,7 @@ void checkedType() { assertThat(field).isNotNull(); Type listType = Decls.newListType( - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage")); + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes.NestedMessage")); assertThat(field.checkedType()).isEqualTo(listType); } diff --git a/core/src/test/java/org/projectnessie/cel/common/types/pb/UnwrapContext.java b/core/src/test/java/org/projectnessie/cel/common/types/pb/UnwrapContext.java index 1fee4f06..d04a4d9e 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/pb/UnwrapContext.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/pb/UnwrapContext.java @@ -18,7 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.projectnessie.cel.common.types.pb.Db.newDb; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes; /** Required by {@link UnwrapTestCase} et al. */ class UnwrapContext { diff --git a/core/src/test/java/org/projectnessie/cel/common/types/pb/UnwrapTestCase.java b/core/src/test/java/org/projectnessie/cel/common/types/pb/UnwrapTestCase.java index ae6c1ad1..e88003f4 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/pb/UnwrapTestCase.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/pb/UnwrapTestCase.java @@ -15,7 +15,6 @@ */ package org.projectnessie.cel.common.types.pb; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes; import com.google.protobuf.BoolValue; import com.google.protobuf.ByteString; import com.google.protobuf.BytesValue; @@ -33,6 +32,7 @@ import com.google.protobuf.UInt32Value; import com.google.protobuf.UInt64Value; import com.google.protobuf.Value; +import dev.cel.expr.conformance.proto3.TestAllTypes; import java.util.function.Supplier; /** diff --git a/core/src/test/java/org/projectnessie/cel/common/types/ref/ExactAggregateTypeAdapterTest.java b/core/src/test/java/org/projectnessie/cel/common/types/ref/ExactAggregateTypeAdapterTest.java new file mode 100644 index 00000000..04f7d2e7 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/common/types/ref/ExactAggregateTypeAdapterTest.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.common.types.ref; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.projectnessie.cel.checker.Decls.Bytes; +import static org.projectnessie.cel.checker.Decls.Dyn; +import static org.projectnessie.cel.checker.Decls.Int; +import static org.projectnessie.cel.checker.Decls.Null; +import static org.projectnessie.cel.checker.Decls.String; +import static org.projectnessie.cel.checker.Decls.Uint; +import static org.projectnessie.cel.checker.Decls.newListType; +import static org.projectnessie.cel.checker.Decls.newMapType; +import static org.projectnessie.cel.checker.Decls.newWrapperType; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.NullT.NullValue; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; + +import com.google.protobuf.ByteString; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.IntT; +import org.projectnessie.cel.common.types.MapT; +import org.projectnessie.cel.common.types.UintT; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.traits.Lister; + +class ExactAggregateTypeAdapterTest { + private static final ExactAggregateTypeAdapter EXACT = DefaultTypeAdapter.Instance::nativeToValue; + + @Test + void checkedKindDeterminesSignednessForLongArraysAndLists() { + for (Object source : List.of(new long[] {1L, Long.MIN_VALUE}, List.of(1L, Long.MIN_VALUE))) { + Lister signed = list(EXACT.nativeAggregateToValue(source, newListType(Int))); + Lister unsigned = list(EXACT.nativeAggregateToValue(source, newListType(Uint))); + + assertThat(signed.nativeGetAt(0)).isInstanceOf(IntT.class); + assertThat(signed.nativeGetAt(1)).isInstanceOf(IntT.class); + assertThat(unsigned.nativeGetAt(0)).isInstanceOf(UintT.class); + assertThat(unsigned.nativeGetAt(1)).isInstanceOf(UintT.class); + assertThat(unsigned.nativeGetAt(1)).isEqualTo(uintOf(Long.MIN_VALUE)); + } + } + + @Test + void preservesHighBitUnsignedValuesRecursively() { + Object source = List.of(Map.of("bits", List.of(-1L, Long.MIN_VALUE))); + Lister outer = + list( + EXACT.nativeAggregateToValue( + source, newListType(newMapType(String, newListType(Uint))))); + MapT map = (MapT) outer.nativeGetAt(0); + Lister bits = list(map.find(stringOf("bits"))); + + assertThat(bits.nativeGetAt(0)).isEqualTo(uintOf(-1L)); + assertThat(bits.nativeGetAt(1)).isEqualTo(uintOf(Long.MIN_VALUE)); + } + + @Test + void distinguishesPresentNullMapValuesFromAbsentKeys() { + Map source = new LinkedHashMap<>(); + source.put("present", null); + + MapT map = (MapT) EXACT.nativeAggregateToValue(source, newMapType(String, Null)); + + assertThat(map.find(stringOf("present"))).isSameAs(NullValue); + assertThat(map.find(stringOf("absent"))).isNull(); + } + + @Test + void rejectsCelEquivalentDuplicateKeys() { + Map source = new LinkedHashMap<>(); + source.put((byte) 1, "byte"); + source.put(1L, "long"); + + assertThat(EXACT.nativeAggregateToValue(source, newMapType(Int, String))) + .matches(Err::isError) + .hasToString("Failed with repeated key"); + } + + @Test + void rejectsNullMapKeys() { + Map source = new LinkedHashMap<>(); + source.put(null, "value"); + + assertThat(EXACT.nativeAggregateToValue(source, newMapType(String, String))) + .matches(Err::isError); + } + + @Test + void reportsNestedBoxedTypeMismatchWhenElementIsConsumed() { + List source = new ArrayList<>(); + source.add(1L); + source.add("not an integer"); + + Lister values = list(EXACT.nativeAggregateToValue(source, newListType(Int))); + + assertThat(values.nativeGetAt(0)).isInstanceOf(IntT.class); + assertThat(values.nativeGetAt(1)).matches(Err::isError); + } + + @Test + void preservesCheckedWrapperSignednessAndNulls() { + List source = new ArrayList<>(); + source.add(Long.MIN_VALUE); + source.add(null); + + Lister values = list(EXACT.nativeAggregateToValue(source, newListType(newWrapperType(Uint)))); + + assertThat(values.nativeGetAt(0)).isEqualTo(uintOf(Long.MIN_VALUE)); + assertThat(values.nativeGetAt(1)).isSameAs(NullValue); + } + + @Test + void rejectsEmbeddedValuesEvenForDynamicElements() { + assertThat(EXACT.nativeAggregateToValue(new Val[] {intOf(1)}, newListType(Dyn))) + .matches(Err::isError); + + Lister values = list(EXACT.nativeAggregateToValue(new Object[] {intOf(1)}, newListType(Dyn))); + assertThat(values.nativeGetAt(0)).matches(Err::isError); + } + + @Test + void keepsByteArraysAsPrimitiveAndNullableWrapperBytes() { + Lister primitive = + list( + EXACT.nativeAggregateToValue( + new Object[] {new byte[] {1, 2}, ByteString.copyFromUtf8("three")}, + newListType(Bytes))); + assertThat(primitive.nativeGetAt(0).value()).isEqualTo(new byte[] {1, 2}); + assertThat(primitive.nativeGetAt(1).value()).isEqualTo(new byte[] {'t', 'h', 'r', 'e', 'e'}); + + List source = new ArrayList<>(); + source.add(new byte[] {4}); + source.add(null); + Lister wrapper = list(EXACT.nativeAggregateToValue(source, newListType(newWrapperType(Bytes)))); + assertThat(wrapper.nativeGetAt(0).value()).isEqualTo(new byte[] {4}); + assertThat(wrapper.nativeGetAt(1)).isSameAs(NullValue); + } + + @Test + void rejectsIncompatibleAggregateRepresentation() { + assertThat(EXACT.nativeAggregateToValue("not a list", newListType(Int))).matches(Err::isError); + assertThat(EXACT.nativeAggregateToValue(List.of(1L), newMapType(String, Int))) + .matches(Err::isError); + } + + @Test + void requiresNonNullAggregateCheckedType() { + assertThatNullPointerException() + .isThrownBy(() -> EXACT.nativeAggregateToValue(List.of(1L), null)) + .withMessage("checkedType"); + assertThatThrownBy(() -> EXACT.nativeAggregateToValue(List.of(1L), Int)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("checkedType must be a CEL list or map type"); + } + + private static Lister list(Val value) { + return (Lister) value; + } +} diff --git a/core/src/test/java/org/projectnessie/cel/common/types/ref/TypeAdapterTest.java b/core/src/test/java/org/projectnessie/cel/common/types/ref/TypeAdapterTest.java new file mode 100644 index 00000000..06e62c2e --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/common/types/ref/TypeAdapterTest.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.common.types.ref; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.StringT.StringType; +import static org.projectnessie.cel.common.types.UintT.uintOf; + +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.NullT; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; + +@SuppressWarnings("removal") +class TypeAdapterTest { + + private static final TypeAdapter DEFAULT_ADAPTER = DefaultTypeAdapter.Instance; + + @Test + void primitiveInboundDefaultsPreserveExactWrapperType() { + TrackingAdapter adapter = new TrackingAdapter(); + + adapter.nativeToValue(true); + assertThat(adapter.lastNativeValue).isExactlyInstanceOf(Boolean.class).isEqualTo(true); + + adapter.nativeToValue((byte) 1); + assertThat(adapter.lastNativeValue).isExactlyInstanceOf(Byte.class).isEqualTo((byte) 1); + + adapter.nativeToValue((short) 2); + assertThat(adapter.lastNativeValue).isExactlyInstanceOf(Short.class).isEqualTo((short) 2); + + adapter.nativeToValue(3); + assertThat(adapter.lastNativeValue).isExactlyInstanceOf(Integer.class).isEqualTo(3); + + adapter.nativeToValue('4'); + assertThat(adapter.lastNativeValue).isExactlyInstanceOf(Character.class).isEqualTo('4'); + + adapter.nativeToValue(5L); + assertThat(adapter.lastNativeValue).isExactlyInstanceOf(Long.class).isEqualTo(5L); + + adapter.nativeToValue(6.0f); + assertThat(adapter.lastNativeValue).isExactlyInstanceOf(Float.class).isEqualTo(6.0f); + + adapter.nativeToValue(7.0d); + assertThat(adapter.lastNativeValue).isExactlyInstanceOf(Double.class).isEqualTo(7.0d); + } + + @Test + void builtInPrimitiveInboundConversions() { + assertThat(DEFAULT_ADAPTER.nativeToValue(true)).isSameAs(True); + assertThat(DEFAULT_ADAPTER.nativeToValue(false)).isSameAs(False); + assertThat(DEFAULT_ADAPTER.nativeToValue((byte) -1)).isEqualTo(intOf(-1)); + assertThat(DEFAULT_ADAPTER.nativeToValue((short) 2)).isEqualTo(intOf(2)); + assertThat(DEFAULT_ADAPTER.nativeToValue(3)).isEqualTo(intOf(3)); + assertThat(DEFAULT_ADAPTER.nativeToValue(4L)).isEqualTo(intOf(4)); + assertThat(DEFAULT_ADAPTER.nativeToValue(5.25f)).isEqualTo(doubleOf(5.25)); + assertThat(DEFAULT_ADAPTER.nativeToValue(6.5d)).isEqualTo(doubleOf(6.5)); + assertThat(DEFAULT_ADAPTER.nativeToValue('x')).matches(Err::isError); + } + + @Test + void primitiveClassTokenDispatchesThroughPrimitiveOverride() { + TypeAdapter adapter = + new TypeAdapter() { + @Override + public Val nativeToValue(Object value) { + return DEFAULT_ADAPTER.nativeToValue(value); + } + + @Override + public int valueToInt(Val value) { + return 42; + } + }; + + assertThat(adapter.valueToNative(intOf(1), int.class)).isEqualTo(42); + assertThat(adapter.valueToNative(intOf(1), Integer.class)).isEqualTo(1); + } + + @Test + void primitiveOutboundConversionsMatchLegacyConversions() { + assertThat(DEFAULT_ADAPTER.valueToBoolean(True)).isEqualTo(True.convertToNative(boolean.class)); + assertThat(DEFAULT_ADAPTER.valueToNative(False, boolean.class)) + .isEqualTo(False.convertToNative(boolean.class)); + + assertThat(DEFAULT_ADAPTER.valueToInt(intOf(Integer.MIN_VALUE))) + .isEqualTo(intOf(Integer.MIN_VALUE).convertToNative(int.class)); + assertThat(DEFAULT_ADAPTER.valueToNative(intOf(Integer.MAX_VALUE), int.class)) + .isEqualTo(intOf(Integer.MAX_VALUE).convertToNative(int.class)); + + assertThat(DEFAULT_ADAPTER.valueToLong(intOf(Long.MIN_VALUE))) + .isEqualTo(intOf(Long.MIN_VALUE).convertToNative(long.class)); + assertThat(DEFAULT_ADAPTER.valueToNative(uintOf(-1), long.class)) + .isEqualTo(uintOf(-1).convertToNative(long.class)); + + assertThat(DEFAULT_ADAPTER.valueToDouble(doubleOf(Double.NEGATIVE_INFINITY))) + .isEqualTo(doubleOf(Double.NEGATIVE_INFINITY).convertToNative(double.class)); + assertThat(DEFAULT_ADAPTER.valueToNative(doubleOf(Double.NaN), double.class)).isNaN(); + } + + @Test + void primitiveOutboundConversionsPreserveRangeAndWrongTypeFailures() { + assertThatThrownBy(() -> DEFAULT_ADAPTER.valueToInt(intOf((long) Integer.MAX_VALUE + 1))) + .isInstanceOf(IllegalStateException.class) + .hasMessage("range error converting 2147483648 to Java int"); + assertThatThrownBy( + () -> DEFAULT_ADAPTER.valueToNative(intOf((long) Integer.MIN_VALUE - 1), int.class)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("range error converting -2147483649 to Java int"); + + assertThatThrownBy(() -> DEFAULT_ADAPTER.valueToBoolean(intOf(1))) + .isInstanceOf(RuntimeException.class) + .hasMessage("native type conversion error from 'int' to 'boolean'"); + assertThatThrownBy(() -> DEFAULT_ADAPTER.valueToInt(doubleOf(1))) + .isInstanceOf(RuntimeException.class) + .hasMessage("native type conversion error from 'double' to 'int'"); + assertThatThrownBy(() -> DEFAULT_ADAPTER.valueToLong(True)) + .isInstanceOf(RuntimeException.class) + .hasMessage("native type conversion error from 'bool' to 'long'"); + assertThatThrownBy(() -> DEFAULT_ADAPTER.valueToDouble(intOf(1))) + .isInstanceOf(RuntimeException.class) + .hasMessage("native type conversion error from 'int' to 'double'"); + } + + @Test + void customValUsesLegacyConversionFallback() { + LegacyVal value = new LegacyVal(); + + assertThat(DEFAULT_ADAPTER.valueToNative(value, String.class)).isEqualTo("legacy"); + assertThat(value.lastTargetType).isEqualTo(String.class); + assertThat(DEFAULT_ADAPTER.valueToBoolean(value)).isTrue(); + assertThat(value.lastTargetType).isEqualTo(boolean.class); + } + + private static final class TrackingAdapter implements TypeAdapter { + private Object lastNativeValue; + + @Override + public Val nativeToValue(Object value) { + lastNativeValue = value; + return NullT.NullValue; + } + } + + private static final class LegacyVal implements Val { + private Class lastTargetType; + + @Override + @SuppressWarnings("unchecked") + public T convertToNative(Class typeDesc) { + lastTargetType = typeDesc; + if (typeDesc == String.class) { + return (T) "legacy"; + } + if (typeDesc == boolean.class) { + return (T) Boolean.TRUE; + } + throw new AssertionError("unexpected target type " + typeDesc); + } + + @Override + public Val convertToType(Type typeValue) { + return this; + } + + @Override + public Val equal(Val other) { + return other == this ? True : False; + } + + @Override + public Type type() { + return StringType; + } + + @Override + public Object value() { + return "legacy"; + } + + @Override + public boolean booleanValue() { + return true; + } + + @Override + public long intValue() { + return 0; + } + + @Override + public double doubleValue() { + return 0; + } + } +} diff --git a/core/src/test/java/org/projectnessie/cel/extension/EncodersLibTest.java b/core/src/test/java/org/projectnessie/cel/extension/EncodersLibTest.java new file mode 100644 index 00000000..b2577568 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/extension/EncodersLibTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.extension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.common.types.BytesT.bytesOf; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.extension.EncodersLib.encoders; + +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.Program.EvalResult; +import org.projectnessie.cel.common.types.Err; + +class EncodersLibTest { + + @Test + void encodesBase64Bytes() { + assertEvaluates("base64.encode(b'hello')", stringOf("aGVsbG8=")); + } + + @Test + void decodesBase64String() { + assertEvaluates("base64.decode('aGVsbG8=')", bytesOf("hello")); + } + + @Test + void decodesBase64StringWithoutPadding() { + assertEvaluates("base64.decode('aGVsbG8')", bytesOf("hello")); + } + + @Test + void rejectsInvalidBase64String() { + EvalResult result = evaluate("base64.decode('not valid base64')"); + + assertThat(result.getVal()).isInstanceOf(Err.class); + assertThat(result.getVal().toString()).contains("invalid base64 string"); + } + + private static void assertEvaluates(String expression, Object expectedValue) { + assertThat(evaluate(expression).getVal()).isEqualTo(expectedValue); + } + + private static EvalResult evaluate(String expression) { + Env env = newEnv(encoders()); + Env.AstIssuesTuple parsed = env.parse(expression); + assertThat(parsed.hasIssues()).isFalse(); + + Env.AstIssuesTuple checked = env.check(parsed.getAst()); + assertThat(checked.hasIssues()).isFalse(); + + Program program = env.program(checked.getAst()); + return program.eval(Map.of()); + } +} diff --git a/core/src/test/java/org/projectnessie/cel/extension/MathLibTest.java b/core/src/test/java/org/projectnessie/cel/extension/MathLibTest.java new file mode 100644 index 00000000..205a1d4c --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/extension/MathLibTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.extension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; +import static org.projectnessie.cel.extension.MathLib.math; + +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.Program.EvalResult; +import org.projectnessie.cel.common.types.Err; + +class MathLibTest { + + @Test + void findsGreatestScalar() { + assertEvaluates("math.greatest(5.4, 10, 3u, -5.0, 9223372036854775807)", intOf(Long.MAX_VALUE)); + } + + @Test + void findsLeastListElement() { + assertEvaluates("math.least([5.4, 10u, 3u, 1u, 3.5])", uintOf(1)); + } + + @Test + void roundsHalfAwayFromZero() { + assertEvaluates("math.round(-1.5)", doubleOf(-2.0)); + } + + @Test + void shiftsSignedIntsLogicallyRight() { + assertEvaluates("math.bitShiftRight(-1024, 3)", intOf(2305843009213693824L)); + } + + @Test + void rejectsNegativeShiftOffset() { + EvalResult result = evaluate("math.bitShiftLeft(1u, -1)"); + + assertThat(result.getVal()).isInstanceOf(Err.class); + assertThat(result.getVal().toString()).contains("negative offset"); + } + + private static void assertEvaluates(String expression, Object expectedValue) { + assertThat(evaluate(expression).getVal()).isEqualTo(expectedValue); + } + + private static EvalResult evaluate(String expression) { + Env env = newEnv(math()); + Env.AstIssuesTuple parsed = env.parse(expression); + assertThat(parsed.hasIssues()).isFalse(); + + Env.AstIssuesTuple checked = env.check(parsed.getAst()); + assertThat(checked.hasIssues()).isFalse(); + + Program program = env.program(checked.getAst()); + return program.eval(Map.of()); + } +} diff --git a/core/src/test/java/org/projectnessie/cel/extension/NetworkLibTest.java b/core/src/test/java/org/projectnessie/cel/extension/NetworkLibTest.java new file mode 100644 index 00000000..d20f9430 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/extension/NetworkLibTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.extension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.extension.NetworkLib.network; + +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.Program.EvalResult; +import org.projectnessie.cel.common.types.Err; + +class NetworkLibTest { + + @Test + void canonicalizesIpStrings() { + assertEvaluates("string(ip('2001:db8::68'))", stringOf("2001:db8::68")); + } + + @Test + void checksIpProperties() { + assertEvaluates("ip('192.168.0.1').family()", intOf(4)); + assertEvaluates("ip('fe80::1').isLinkLocalUnicast()", True); + } + + @Test + void checksCidrContainment() { + assertEvaluates("cidr('192.168.0.0/24').containsIP('192.168.0.1')", True); + assertEvaluates("cidr('192.168.0.0/24').containsCIDR('192.168.0.0/23')", False); + } + + @Test + void rejectsInvalidIpLiterals() { + EvalResult result = evaluate("ip('192.168.0.1.0')"); + + assertThat(result.getVal()).isInstanceOf(Err.class); + assertThat(result.getVal().toString()).contains("parse error"); + } + + private static void assertEvaluates(String expression, Object expectedValue) { + assertThat(evaluate(expression).getVal()).isEqualTo(expectedValue); + } + + private static EvalResult evaluate(String expression) { + Env env = newEnv(network()); + Env.AstIssuesTuple parsed = env.parse(expression); + assertThat(parsed.hasIssues()).isFalse(); + + Env.AstIssuesTuple checked = env.check(parsed.getAst()); + assertThat(checked.hasIssues()).isFalse(); + + Program program = env.program(checked.getAst()); + return program.eval(Map.of()); + } +} diff --git a/core/src/test/java/org/projectnessie/cel/extension/OptionalLibTest.java b/core/src/test/java/org/projectnessie/cel/extension/OptionalLibTest.java new file mode 100644 index 00000000..2b651b90 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/extension/OptionalLibTest.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.extension; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.NullT.NullValue; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.extension.OptionalLib.optionals; + +import com.google.api.expr.v1alpha1.Type; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.Err; + +class OptionalLibTest { + + @Test + void declaresOptionalNone() { + assertCheckedType("[optional.none(), optional.of(1)]", Decls.newListType(optional(Decls.Int))); + } + + @Test + void promotesOptionalTypeParameterToDyn() { + assertCheckedType( + "[optional.of(1), optional.of(dyn(1))]", Decls.newListType(optional(Decls.Dyn))); + } + + @Test + void promotesTernaryOptionalTypeParameterToDyn() { + assertCheckedType("true ? optional.of(dyn(1)) : optional.of(1)", optional(Decls.Dyn)); + } + + @Test + void keepsNullableOptionalType() { + assertCheckedType("[optional.of(1), null][0]", optional(Decls.Int)); + } + + @Test + void evaluatesPresentNull() { + assertEvaluates("optional.of(null).hasValue()", True); + assertEvaluates("optional.of(null).value()", NullValue); + } + + @Test + void evaluatesAbsentForNullZeroAndEmptyValues() { + assertEvaluates("optional.ofNonZeroValue(null).hasValue()", False); + assertEvaluates("optional.ofNonZeroValue(false).hasValue()", False); + assertEvaluates("optional.ofNonZeroValue(0).hasValue()", False); + assertEvaluates("optional.ofNonZeroValue(0u).hasValue()", False); + assertEvaluates("optional.ofNonZeroValue(0.0).hasValue()", False); + assertEvaluates("optional.ofNonZeroValue('').hasValue()", False); + assertEvaluates("optional.ofNonZeroValue([]).hasValue()", False); + assertEvaluates("optional.ofNonZeroValue({}).hasValue()", False); + } + + @Test + void evaluatesPresentForNonZeroValues() { + assertEvaluates("optional.ofNonZeroValue(true).value()", True); + assertEvaluates("optional.ofNonZeroValue(42).value()", intOf(42)); + assertEvaluates("optional.ofNonZeroValue('x').hasValue()", True); + } + + @Test + void evaluatesOrAndOrValue() { + assertEvaluates("optional.none().or(optional.none()).orValue(42)", intOf(42)); + assertEvaluates("optional.none().or(optional.of(21)).orValue(42)", intOf(21)); + assertEvaluates("optional.of(7).or(optional.of(21)).orValue(42)", intOf(7)); + } + + @Test + void evaluatesOptionalEquality() { + assertEvaluates("optional.none() == optional.none()", True); + assertEvaluates("optional.none() == optional.of(1)", False); + assertEvaluates("optional.of(1) == optional.none()", False); + assertEvaluates("optional.of(1) == optional.of(1)", True); + assertEvaluates("optional.none() != optional.none()", False); + assertEvaluates("optional.none() != optional.of(1)", True); + assertEvaluates("optional.of(1) != optional.none()", True); + assertEvaluates("optional.of(1) != optional.of(1)", False); + } + + @Test + void evaluatesOptionalTypeIdentifier() { + assertEvaluates("type(optional.none()) == optional_type", True); + } + + @Test + void evaluatesOptMap() { + assertEvaluates("optional.of(1).optMap(x, x + 1).value()", intOf(2)); + assertEvaluates("optional.ofNonZeroValue(0).optMap(x, x / 0).hasValue()", False); + } + + @Test + void evaluatesOptFlatMap() { + assertEvaluates("optional.of(1).optFlatMap(x, optional.of(x + 1)).value()", intOf(2)); + assertEvaluates( + "optional.ofNonZeroValue(0).optFlatMap(x, optional.of(x / 0)).hasValue()", False); + } + + @Test + void evaluatesOptionalSelectAndIndex() { + assertEvaluates("{}.?c.hasValue()", False); + assertEvaluates("{'c': 'x'}.?c.value()", stringOf("x")); + assertEvaluates("[][?0].hasValue()", False); + assertEvaluates("['foo'][?0].value()", stringOf("foo")); + } + + @Test + void evaluatesOptionalChaining() { + assertEvaluates( + "optional.of({'c': {}}).c.missing.or(optional.of(['list-value'])[0]).orValue('default value')", + stringOf("list-value")); + assertEvaluates( + "has(optional.of({'c': {'entry': 'hello world'}}).c)" + + " && !has(optional.of({'c': {'entry': 'hello world'}}).c.missing)", + True); + } + + @Test + void evaluatesOptionalAggregateEntries() { + assertEvaluates("[?{}.?c, ?optional.of(42), ?optional.none()].size()", intOf(1)); + assertEvaluates("{?'foo': optional.none()}.size()", intOf(0)); + } + + @Test + void absentValueReturnsError() { + assertThat(evaluate("optional.none().value()").getVal()).isInstanceOf(Err.class); + } + + private static void assertCheckedType(String expression, Type expectedType) { + Env env = newEnv(optionals()); + Env.AstIssuesTuple parsed = env.parse(expression); + assertThat(parsed.hasIssues()).isFalse(); + + Env.AstIssuesTuple checked = env.check(parsed.getAst()); + assertThat(checked.hasIssues()).isFalse(); + assertThat(checked.getAst().getResultType()).isEqualTo(expectedType); + } + + private static void assertEvaluates(String expression, Object expectedValue) { + assertThat(evaluate(expression).getVal()).describedAs(expression).isEqualTo(expectedValue); + } + + private static Program.EvalResult evaluate(String expression) { + Env env = newEnv(optionals()); + Env.AstIssuesTuple parsed = env.parse(expression); + assertThat(parsed.hasIssues()).isFalse(); + + Env.AstIssuesTuple checked = env.check(parsed.getAst()); + assertThat(checked.hasIssues()).describedAs(checked.getIssues().toString()).isFalse(); + + Program program = env.program(checked.getAst()); + return program.eval(Map.of()); + } + + private static Type optional(Type type) { + return Decls.newAbstractType("optional_type", singletonList(type)); + } +} diff --git a/core/src/test/java/org/projectnessie/cel/extension/StringsTest.java b/core/src/test/java/org/projectnessie/cel/extension/StringsTest.java index d252e2f5..58a38279 100644 --- a/core/src/test/java/org/projectnessie/cel/extension/StringsTest.java +++ b/core/src/test/java/org/projectnessie/cel/extension/StringsTest.java @@ -111,6 +111,12 @@ static Stream testCases() { new TestData("[].join('-') == ''"), new TestData("['x'].join() == 'x'"), new TestData("['x'].join('-') == 'x'"), + new TestData("strings.quote(\"first\\nsecond\") == \"\\\"first\\\\nsecond\\\"\""), + new TestData("strings.quote(\"printable unicode😀\") == \"\\\"printable unicode😀\\\"\""), + new TestData("'Ta©oCαt'.reverse() == 'tαCo©aT'"), + new TestData("\"%d %s %.0f\".format([1, \"two\", 2.5]) == \"1 two 2\""), + new TestData("\"%x\".format([\"Hello world!\"]) == \"48656c6c6f20776f726c6421\""), + new TestData("\"%s\".format([[\"abc\", 3.14, null]]) == \"[abc, 3.14, null]\""), // Error test cases based on checked expression usage. new TestData("'tacocat'.indexOf('a', 30) == -1", "String index out of range: 30"), @@ -175,7 +181,10 @@ static Stream testCases() { new TestData("'hello'.substring(1, 2, 3) == \"\"", "no matching overload", true), new TestData("30.substring(true, 3) == \"\"", "no matching overload", true), new TestData("\"tacocat\".substring(true, 3) == \"\"", "no matching overload", true), - new TestData("\"tacocat\".substring(0, false) == \"\"", "no matching overload", true)); + new TestData("\"tacocat\".substring(0, false) == \"\"", "no matching overload", true), + new TestData( + "\"%a\".format([1])", + "could not parse formatting clause: unrecognized formatting clause \"a\"")); } @Test diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/ActivationTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/ActivationTest.java index 75c0a055..6d93a611 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/ActivationTest.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/ActivationTest.java @@ -23,14 +23,15 @@ import static org.projectnessie.cel.interpreter.Activation.newActivation; import static org.projectnessie.cel.interpreter.Activation.newHierarchicalActivation; -import java.time.ZonedDateTime; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import java.util.function.Supplier; import org.junit.jupiter.api.Test; -import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.projectnessie.cel.common.types.ref.Val; public class ActivationTest { @@ -49,8 +50,8 @@ void activation() { @Test void resolve() { Activation activation = newActivation(Collections.singletonMap("a", True)); - assertThat(activation.resolveName("a").present()).isTrue(); - assertThat(activation.resolveName("a").value()).isSameAs(True); + assertThat(activation.resolve("a")).isNotSameAs(Activation.ABSENT); + assertThat(activation.resolve("a")).isSameAs(True); } @Test @@ -59,56 +60,114 @@ void resolveNullAndAbsentFromMapActivation() { map.put("nullValue", null); Activation activation = newActivation(map); - assertThat(activation.resolveName("nullValue")).isSameAs(ResolvedValue.NULL_VALUE); - assertThat(activation.resolveName("absent")).isSameAs(ResolvedValue.ABSENT); + assertThat(activation.resolve("nullValue")).isNull(); + assertThat(activation.resolve("absent")).isSameAs(Activation.ABSENT); } @Test void resolveLazy() { - AtomicReference v = new AtomicReference<>(); - Supplier now = - () -> { - if (v.get() == null) { - v.set(DefaultTypeAdapter.Instance.nativeToValue(ZonedDateTime.now())); - } - return v.get(); - }; + AtomicInteger invocations = new AtomicInteger(); Map map = new HashMap<>(); - map.put("now", now); + map.put( + "now", + (Supplier) + () -> { + invocations.incrementAndGet(); + return stringOf("lazy"); + }); Activation a = newActivation(map); - ResolvedValue first = a.resolveName("now"); - ResolvedValue second = a.resolveName("now"); - assertThat(first.present()).isTrue(); - assertThat(second.present()).isTrue(); - assertThat(first.value()).isSameAs(second.value()); + Object first = a.resolve("now"); + Object second = a.resolve("now"); + assertThat(invocations).hasValue(1); + assertThat(first).isNotSameAs(Activation.ABSENT).isSameAs(second).isEqualTo(stringOf("lazy")); + assertThat(second).isNotSameAs(Activation.ABSENT); } + @SuppressWarnings("removal") @Test - void hierarchicalActivation() { + void legacyActivation() { + Function func = + name -> + switch (name) { + case "a" -> stringOf("one"); + case "b" -> ResolvedValue.resolvedValue(stringOf("two")); + case "absent" -> ResolvedValue.ABSENT; + case "null" -> ResolvedValue.NULL_VALUE; + case "real_null" -> null; + default -> throw new RuntimeException("unknown activation name: " + name); + }; + Activation activation = newActivation(func); + assertThat(activation.resolve("a")).isEqualTo(stringOf("one")); + assertThat(activation.resolve("b")).isEqualTo(stringOf("two")); + assertThat(activation.resolve("absent")).isSameAs(ActivationFunction.ABSENT); + assertThat(activation.resolve("real_null")).isSameAs(ActivationFunction.ABSENT); + assertThat(activation.resolve("null")).isNull(); + assertThat(activation.resolveName("a")).isEqualTo(ResolvedValue.resolvedValue(stringOf("one"))); + assertThat(activation.resolveName("absent")).isSameAs(ResolvedValue.ABSENT); + assertThat(activation.resolveName("null")).isSameAs(ResolvedValue.NULL_VALUE); + + assertThat(ResolvedValue.mapTo(null)).isSameAs(ResolvedValue.NULL_VALUE); + assertThat(ResolvedValue.mapTo(ActivationFunction.ABSENT)).isSameAs(ResolvedValue.ABSENT); + assertThat(ResolvedValue.mapTo("foo")).isEqualTo(ResolvedValue.resolvedValue("foo")); + var ref = ResolvedValue.resolvedValue("foo"); + assertThat(ResolvedValue.mapTo(ref)).isSameAs(ref); + + assertThat(ResolvedValue.mapLegacy("foo")).isEqualTo("foo"); + assertThat(ResolvedValue.mapLegacy(null)).isEqualTo(ActivationFunction.ABSENT); + assertThat(ResolvedValue.mapLegacy(ResolvedValue.resolvedValue("foo"))).isEqualTo("foo"); + assertThat(ResolvedValue.mapLegacy(ResolvedValue.NULL_VALUE)).isNull(); + assertThat(ResolvedValue.mapLegacy(ResolvedValue.ABSENT)).isSameAs(ActivationFunction.ABSENT); + } + + @ParameterizedTest + @CsvSource({ + "true,true", + "true,false", + "false,true", + "false,false", + }) + void hierarchicalActivationMap(boolean parentFunction, boolean childFunction) { // compose a parent with more properties than the child Map parentMap = new HashMap<>(); parentMap.put("a", stringOf("world")); parentMap.put("b", intOf(-42)); parentMap.put("d", stringOf("child value for d")); - Activation parent = new Activation.FunctionActivation(parentMap::get); + Activation parent = + parentFunction + ? new FunctionActivation( + name -> parentMap.getOrDefault(name, ActivationFunction.ABSENT)) + : new MapActivation(parentMap); // compose the child such that it shadows the parent Map childMap = new HashMap<>(); childMap.put("a", True); childMap.put("c", stringOf("universe")); - childMap.put("d", ResolvedValue.NULL_VALUE); - Activation child = new Activation.FunctionActivation(childMap::get); + childMap.put("d", null); + Activation child = + childFunction + ? new FunctionActivation(name -> childMap.getOrDefault(name, ActivationFunction.ABSENT)) + : new MapActivation(childMap); + Activation combined = newHierarchicalActivation(parent, child); + assertThat(parent.resolve("a")).isEqualTo(stringOf("world")); + assertThat(parent.resolve("b")).isEqualTo(intOf(-42)); + assertThat(parent.resolve("c")).isSameAs(Activation.ABSENT); + assertThat(parent.resolve("d")).isEqualTo(stringOf("child value for d")); + + assertThat(child.resolve("a")).isEqualTo(True); + assertThat(child.resolve("b")).isSameAs(Activation.ABSENT); + assertThat(child.resolve("c")).isEqualTo(stringOf("universe")); + assertThat(child.resolve("d")).isNull(); + // Resolve the shadowed child value. - assertThat(combined.resolveName("a")).isEqualTo(ResolvedValue.resolvedValue(True)); + assertThat(combined.resolve("a")).isEqualTo(True); // Resolve the parent only value. - assertThat(combined.resolveName("b")).isEqualTo(ResolvedValue.resolvedValue(intOf(-42))); + assertThat(combined.resolve("b")).isEqualTo(intOf(-42)); // Resolve the child only value. - assertThat(combined.resolveName("c")) - .isEqualTo(ResolvedValue.resolvedValue(stringOf("universe"))); + assertThat(combined.resolve("c")).isEqualTo(stringOf("universe")); // Resolve the child value as null without looking to parent. - assertThat(combined.resolveName("d")).isSameAs(ResolvedValue.NULL_VALUE); + assertThat(combined.resolve("d")).isNull(); // Absent - assertThat(combined.resolveName("e")).isSameAs(ResolvedValue.ABSENT); + assertThat(combined.resolve("e")).isSameAs(Activation.ABSENT); } } diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/AggregateSourceSemanticsTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/AggregateSourceSemanticsTest.java new file mode 100644 index 00000000..d72c9090 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/interpreter/AggregateSourceSemanticsTest.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.NullT.NullValue; +import static org.projectnessie.cel.common.types.StringT.stringOf; + +import java.util.AbstractCollection; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.IteratorT; +import org.projectnessie.cel.common.types.MapT; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeAdapterSupport; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; + +class AggregateSourceSemanticsTest { + + @Test + void listBackedValuesRemainLiveWhileGenericCollectionsAreSnapshotted() { + List listSource = new ArrayList<>(List.of("list-before", "list-second")); + Set collectionSource = + new LinkedHashSet<>(List.of("collection-before", "collection-second")); + + Lister liveList = (Lister) DefaultTypeAdapter.Instance.nativeToValue(listSource); + Lister snapshottedCollection = + (Lister) DefaultTypeAdapter.Instance.nativeToValue(collectionSource); + + listSource.set(0, "list-after"); + collectionSource.clear(); + collectionSource.add("collection-after"); + + assertThat(liveList.nativeSize()).isEqualTo(2); + assertThat(liveList.get(intOf(0))).isEqualTo(stringOf("list-after")); + assertThat(snapshottedCollection.nativeSize()).isEqualTo(2); + assertThat(listValues(snapshottedCollection)) + .containsExactly("collection-before", "collection-second"); + } + + @Test + void genericCollectionUsesToArrayRatherThanIteratorOrder() { + DivergentCollection source = + new DivergentCollection(List.of("iterator-first", "iterator-second")); + RecordingAdapter adapter = new RecordingAdapter(); + + Lister list = (Lister) adapter.nativeToValue(source); + + assertThat(source.toArrayCalls).isEqualTo(1); + assertThat(source.iteratorCalls).isZero(); + assertThat(adapter.adaptationCount("iterator-first")).isZero(); + assertThat(adapter.adaptationCount("iterator-second")).isZero(); + + assertThat(listValues(list)).containsExactly("iterator-second", "iterator-first"); + assertThat(source.toArrayCalls).isEqualTo(1); + assertThat(source.iteratorCalls).isZero(); + assertThat(adapter.adaptationCount("iterator-first")).isEqualTo(1); + assertThat(adapter.adaptationCount("iterator-second")).isEqualTo(1); + } + + @Test + void setSnapshotPreservesToArrayEncounterOrderAndDefersElementAdaptation() { + String first = distinctString("first"); + String second = distinctString("second"); + String third = distinctString("third"); + LinkedHashSet source = new LinkedHashSet<>(List.of(first, second, third)); + RecordingAdapter adapter = new RecordingAdapter(); + + Lister list = (Lister) adapter.nativeToValue(source); + source.clear(); + source.add("replacement"); + + assertThat(adapter.adaptationCount(first)).isZero(); + assertThat(adapter.adaptationCount(second)).isZero(); + assertThat(adapter.adaptationCount(third)).isZero(); + assertThat(listValues(list)).containsExactly("first", "second", "third"); + assertThat(adapter.adaptationCount(first)).isEqualTo(1); + assertThat(adapter.adaptationCount(second)).isEqualTo(1); + assertThat(adapter.adaptationCount(third)).isEqualTo(1); + } + + @Test + void mapSnapshotsAdaptedKeysAndRetainsValuesForLazyAdaptation() { + Long firstKey = 1L; + Long secondKey = 17L; + Long thirdKey = 33L; + String firstValue = distinctString("one"); + String secondValue = distinctString("seventeen"); + String thirdValue = distinctString("thirty-three"); + CountingLinkedHashMap source = new CountingLinkedHashMap<>(); + source.put(firstKey, firstValue); + source.put(secondKey, secondValue); + source.put(thirdKey, thirdValue); + RecordingAdapter adapter = new RecordingAdapter(); + + MapT map = (MapT) adapter.nativeToValue(source); + + // The first pass determines whether the map is already Val-backed and stops at the first raw + // key. The second pass snapshots all adapted keys. + assertThat(source.entrySetCalls).isEqualTo(2); + assertThat(source.iteratorCalls).isEqualTo(2); + assertThat(adapter.adaptationCount(firstKey)).isEqualTo(1); + assertThat(adapter.adaptationCount(secondKey)).isEqualTo(1); + assertThat(adapter.adaptationCount(thirdKey)).isEqualTo(1); + assertThat(adapter.adaptationCount(firstValue)).isZero(); + assertThat(adapter.adaptationCount(secondValue)).isZero(); + assertThat(adapter.adaptationCount(thirdValue)).isZero(); + + // The adapted HashMap owns key encounter order rather than retaining the source map's + // iteration mechanism. Derive the expectation from the same documented implementation type + // instead of pinning an unspecified JDK HashMap order. + Map expectedKeySnapshot = new HashMap<>(source.size() * 4 / 3 + 1); + expectedKeySnapshot.put(intOf(1), firstValue); + expectedKeySnapshot.put(intOf(17), secondValue); + expectedKeySnapshot.put(intOf(33), thirdValue); + assertThat(mapKeys(map)) + .containsExactlyElementsOf( + expectedKeySnapshot.keySet().stream().map(Val::intValue).toList()); + assertThat(map.contains(intOf(17))).isSameAs(True); + assertThat(adapter.adaptationCount(secondValue)).isZero(); + + source.clear(); + source.put(99L, "replacement"); + assertThat(map.nativeSize()).isEqualTo(3); + assertThat(map.find(intOf(99))).isNull(); + assertThat(map.find(intOf(17))).isEqualTo(stringOf("seventeen")); + assertThat(map.find(intOf(17))).isEqualTo(stringOf("seventeen")); + assertThat(adapter.adaptationCount(secondValue)).isEqualTo(2); + assertThat(adapter.adaptationCount(firstValue)).isZero(); + assertThat(adapter.adaptationCount(thirdValue)).isZero(); + } + + @Test + void nullElementsAndValuesRemainDistinctFromAbsentKeys() { + List nullableElements = new ArrayList<>(); + nullableElements.add(null); + nullableElements.add("present"); + Lister list = (Lister) DefaultTypeAdapter.Instance.nativeToValue(nullableElements); + Map source = new LinkedHashMap<>(); + source.put("null", null); + MapT map = (MapT) DefaultTypeAdapter.Instance.nativeToValue(source); + + assertThat(list.get(intOf(0))).isSameAs(NullValue); + assertThat(map.contains(stringOf("null"))).isSameAs(True); + assertThat(map.find(stringOf("null"))).isSameAs(NullValue); + assertThat(map.contains(stringOf("absent"))).isSameAs(False); + assertThat(map.find(stringOf("absent"))).isNull(); + } + + @Test + void nullAndCelEquivalentDuplicateMapKeysAreRejectedDuringAdaptation() { + Map nullKey = new LinkedHashMap<>(); + nullKey.put(null, "value"); + Map duplicateNumericKeys = new LinkedHashMap<>(); + duplicateNumericKeys.put(1L, "int"); + duplicateNumericKeys.put(1.0d, "double"); + + assertThat(DefaultTypeAdapter.Instance.nativeToValue(nullKey)).matches(Err::isError); + assertThat(DefaultTypeAdapter.Instance.nativeToValue(duplicateNumericKeys)) + .matches(Err::isError); + } + + @Test + void listEqualityStopsAtTheFirstLeftOrRightError() { + Val leftError = Err.newErr("left failed"); + Val rightError = Err.newErr("right failed"); + String leftTail = distinctString("left tail"); + String rightHead = distinctString("right head"); + RecordingAdapter adapter = new RecordingAdapter(); + Lister left = (Lister) adapter.nativeToValue(new Object[] {leftError, leftTail}); + Lister right = (Lister) adapter.nativeToValue(new Object[] {rightHead, rightError}); + adapter.clear(); + + assertThat(left.equal(right)).isSameAs(leftError); + assertThat(adapter.adaptationCount(leftError)).isEqualTo(1); + assertThat(adapter.adaptationCount(rightHead)).isZero(); + assertThat(adapter.adaptationCount(leftTail)).isZero(); + assertThat(adapter.adaptationCount(rightError)).isZero(); + + Lister successfulLeft = (Lister) adapter.nativeToValue(new Object[] {rightHead}); + Lister failingRight = (Lister) adapter.nativeToValue(new Object[] {rightError}); + adapter.clear(); + + assertThat(successfulLeft.equal(failingRight)).isSameAs(rightError); + assertThat(adapter.adaptationCount(rightHead)).isEqualTo(1); + assertThat(adapter.adaptationCount(rightError)).isEqualTo(1); + } + + private static List listValues(Lister list) { + List values = new ArrayList<>(); + for (int i = 0; i < list.nativeSize(); i++) { + values.add(list.get(intOf(i)).value()); + } + return values; + } + + private static String distinctString(String value) { + // RecordingAdapter deliberately counts adaptations by object identity. + return new String(value.toCharArray()); + } + + private static List mapKeys(MapT map) { + List keys = new ArrayList<>(); + IteratorT iterator = map.iterator(); + while (iterator.hasNext() == True) { + keys.add(iterator.next().intValue()); + } + return keys; + } + + private static final class RecordingAdapter implements TypeAdapter { + private final List adaptations = new ArrayList<>(); + + @Override + public Val nativeToValue(Object value) { + adaptations.add(value); + Val adapted = TypeAdapterSupport.maybeNativeToValue(this, value); + return adapted != null ? adapted : DefaultTypeAdapter.Instance.nativeToValue(value); + } + + int adaptationCount(Object value) { + int count = 0; + for (Object adapted : adaptations) { + if (adapted == value) { + count++; + } + } + return count; + } + + void clear() { + adaptations.clear(); + } + } + + private static final class DivergentCollection extends AbstractCollection { + private final List iteratorOrder; + private int iteratorCalls; + private int toArrayCalls; + + private DivergentCollection(List iteratorOrder) { + this.iteratorOrder = iteratorOrder; + } + + @Override + public Iterator iterator() { + iteratorCalls++; + return iteratorOrder.iterator(); + } + + @Override + public int size() { + return iteratorOrder.size(); + } + + @Override + public Object[] toArray() { + toArrayCalls++; + Object[] values = iteratorOrder.toArray(); + for (int left = 0, right = values.length - 1; left < right; left++, right--) { + Object value = values[left]; + values[left] = values[right]; + values[right] = value; + } + return values; + } + } + + private static final class CountingLinkedHashMap extends LinkedHashMap { + private int entrySetCalls; + private int iteratorCalls; + + @Override + public Set> entrySet() { + entrySetCalls++; + Set> entries = super.entrySet(); + return new AbstractSet<>() { + @Override + public Iterator> iterator() { + iteratorCalls++; + return entries.iterator(); + } + + @Override + public int size() { + return entries.size(); + } + }; + } + } +} diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/AttributesTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/AttributesTest.java index 367dc8e5..7d098c7a 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/AttributesTest.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/AttributesTest.java @@ -23,16 +23,20 @@ import static org.projectnessie.cel.common.containers.Container.newContainer; import static org.projectnessie.cel.common.types.BoolT.False; import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.Err.isError; import static org.projectnessie.cel.common.types.Err.newErr; import static org.projectnessie.cel.common.types.IntT.IntType; import static org.projectnessie.cel.common.types.IntT.intOf; import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; import static org.projectnessie.cel.common.types.UnknownT.unknownOf; import static org.projectnessie.cel.common.types.pb.ProtoTypeRegistry.newRegistry; import static org.projectnessie.cel.interpreter.Activation.emptyActivation; import static org.projectnessie.cel.interpreter.Activation.newActivation; import static org.projectnessie.cel.interpreter.Activation.newPartialActivation; import static org.projectnessie.cel.interpreter.AttributeFactory.newAttributeFactory; +import static org.projectnessie.cel.interpreter.AttributeFactory.refResolve; import static org.projectnessie.cel.interpreter.AttributePattern.newAttributePattern; import static org.projectnessie.cel.interpreter.AttributePattern.newPartialAttributeFactory; import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; @@ -42,11 +46,11 @@ import static org.projectnessie.cel.interpreter.Interpreter.optimize; import static org.projectnessie.cel.interpreter.Interpreter.trackState; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedMessage; import com.google.api.expr.v1alpha1.Decl; import com.google.api.expr.v1alpha1.Type; import com.google.protobuf.Any; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -73,6 +77,19 @@ class AttributesTest { + @Test + void refResolveUsesNativeListIndicesWithoutChangingIndexSemantics() { + TypeRegistry registry = newRegistry(); + int[] values = {2, 42}; + + assertThat(refResolve(registry, intOf(1), values)).isEqualTo(intOf(42)); + assertThat(refResolve(registry, uintOf(1), values)).isEqualTo(intOf(42)); + assertThat(refResolve(registry, doubleOf(1), values)).isEqualTo(intOf(42)); + assertThat(refResolve(registry, doubleOf(0.5), values)).matches(v -> isError(v)); + assertThat(refResolve(registry, intOf((long) Integer.MAX_VALUE + 1), values)) + .matches(v -> isError(v)); + } + @Test void attributesAbsoluteAttr() { TypeRegistry reg = newRegistry(); @@ -378,9 +395,9 @@ void benchmarkResolverFieldQualifier() { AttributeFactory attrs = newAttributeFactory(Container.defaultContainer, reg, reg); Activation vars = newActivation(mapOf("msg", msg)); NamespacedAttribute attr = attrs.absoluteAttribute(1, "msg"); - Type opType = reg.findType("google.api.expr.test.v1.proto3.TestAllTypes"); + Type opType = reg.findType("cel.expr.conformance.proto3.TestAllTypes"); assertThat(opType).isNotNull(); - Type fieldType = reg.findType("google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage"); + Type fieldType = reg.findType("cel.expr.conformance.proto3.TestAllTypes.NestedMessage"); assertThat(fieldType).isNotNull(); attr.addQualifier(makeQualifier(attrs, opType.getType(), 2, "single_nested_message")); attr.addQualifier(makeQualifier(attrs, fieldType.getType(), 3, "bb")); @@ -398,7 +415,7 @@ void resolverCustomQualifier() { Qualifier qualBB = attrs.newQualifier( Type.newBuilder() - .setMessageType("google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage") + .setMessageType("cel.expr.conformance.proto3.TestAllTypes.NestedMessage") .build(), 2, "bb"); @@ -421,7 +438,7 @@ void attributesMissingMsg() { attr.addQualifier(field); assertThatThrownBy(() -> attr.resolve(vars)) .isInstanceOf(IllegalStateException.class) - .hasMessage("unknown type 'google.api.expr.test.v1.proto3.TestAllTypes'"); + .hasMessage("unknown type 'cel.expr.conformance.proto3.TestAllTypes'"); } @Test @@ -613,7 +630,7 @@ void benchmarkResolverCustomQualifier() { Qualifier qualBB = attrs.newQualifier( Type.newBuilder() - .setMessageType("google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage") + .setMessageType("cel.expr.conformance.proto3.TestAllTypes.NestedMessage") .build(), 2, "bb"); @@ -633,7 +650,7 @@ public CustAttrFactory(AttributeFactory af) { public Qualifier newQualifier(Type objType, long qualID, Object val) { if (objType .getMessageType() - .equals("google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage")) { + .equals("cel.expr.conformance.proto3.TestAllTypes.NestedMessage")) { return new NestedMsgQualifier(qualID, (String) val); } return af.newQualifier(objType, qualID, val); diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java index c635afe7..b7a9f448 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java @@ -61,7 +61,6 @@ import static org.projectnessie.cel.interpreter.Interpreter.trackState; import static org.projectnessie.cel.interpreter.functions.Overload.standardOverloads; -import com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes; import com.google.api.expr.v1alpha1.Constant; import com.google.api.expr.v1alpha1.Decl; import com.google.api.expr.v1alpha1.Expr; @@ -75,6 +74,7 @@ import com.google.protobuf.Struct; import com.google.protobuf.Timestamp; import com.google.protobuf.Value; +import dev.cel.expr.conformance.proto2.TestAllTypes; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Arrays; @@ -113,18 +113,17 @@ import org.projectnessie.cel.common.types.traits.Receiver; import org.projectnessie.cel.common.types.traits.Trait; import org.projectnessie.cel.interpreter.AttributeFactory.ConstantQualifier; -import org.projectnessie.cel.interpreter.AttributeFactory.FieldQualifier; import org.projectnessie.cel.interpreter.AttributeFactory.NamespacedAttribute; import org.projectnessie.cel.interpreter.AttributeFactory.Qualifier; import org.projectnessie.cel.interpreter.AttributesTest.CustAttrFactory; import org.projectnessie.cel.interpreter.Coster.Cost; -import org.projectnessie.cel.interpreter.Interpretable.EvalListFold; import org.projectnessie.cel.interpreter.Interpretable.InterpretableAttribute; import org.projectnessie.cel.interpreter.Interpretable.InterpretableConst; import org.projectnessie.cel.interpreter.functions.Overload; import org.projectnessie.cel.parser.Parser; import org.projectnessie.cel.parser.Parser.ParseResult; +@SuppressWarnings("SameParameterValue") class InterpreterTest { private static final Type RECEIVER_TEST_TYPE = @@ -145,6 +144,7 @@ public TypeEnum typeEnum() { } @Override + @SuppressWarnings("removal") public T convertToNative(Class typeDesc) { throw new UnsupportedOperationException(); } @@ -188,6 +188,7 @@ public double doubleValue() { private static final class ReceiverTestVal extends BaseVal implements Receiver { @Override + @SuppressWarnings("removal") public T convertToNative(Class typeDesc) { throw new UnsupportedOperationException(); } @@ -345,6 +346,10 @@ static TestCase[] testCases() { new TestCase(InterpreterTestCase.map_key_null) .expr("{null:false}[null]") .err("message: unsupported key type"), + new TestCase(InterpreterTestCase.map_key_float) + .expr("{3.3:15.15, 1.0: 5}[1.0]") + .unchecked() + .err("message: unsupported key type"), new TestCase(InterpreterTestCase.map_value_repeat_key_heterogeneous) .expr("{0: 1, 0u: 2}[0.0]") .err("message: Failed with repeated key"), @@ -373,10 +378,8 @@ static TestCase[] testCases() { .out(False), new TestCase(InterpreterTestCase.eq_proto_different_types) .expr("dyn(TestAllTypes{}) == dyn(NestedTestAllTypes{})") - .container("google.api.expr.test.v1.proto2") - .types( - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) + .container("cel.expr.conformance.proto2") + .types(dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()) .out(False), new TestCase(InterpreterTestCase.not_lt_dyn_big_uint_int) .expr("dyn(9223372036854775808u) < 1") @@ -394,22 +397,23 @@ static TestCase[] testCases() { new TestCase(InterpreterTestCase.eq_proto_nan_equal) .expr( "TestAllTypes{single_double: double('NaN')} == TestAllTypes{single_double: double('NaN')}") - .container("google.api.expr.test.v1.proto3") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - // The outcome in the generated Java proto code is different than in the conformance-test, - // it is NOT: "For proto equality, fields with NaN value are treated as not equal." + .container("cel.expr.conformance.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) + .out(False), + new TestCase(InterpreterTestCase.ne_proto_nan_not_equal) + .expr( + "TestAllTypes{single_double: double('NaN')} != TestAllTypes{single_double: double('NaN')}") + .container("cel.expr.conformance.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) .out(True), new TestCase(InterpreterTestCase.eq_bool_not_null) .expr("google.protobuf.BoolValue{} != null") .out(True), new TestCase(InterpreterTestCase.literal_any) .expr( - "google.protobuf.Any{type_url: 'type.googleapis.com/google.api.expr.test.v1.proto2.TestAllTypes', value: b'\\x08\\x96\\x01'}") + "google.protobuf.Any{type_url: 'type.googleapis.com/cel.expr.conformance.proto2.TestAllTypes', value: b'\\x08\\x96\\x01'}") .types( - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes - .getDefaultInstance(), + dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance(), Any.getDefaultInstance()) .out(TestAllTypes.newBuilder().setSingleInt32(150).build()), new TestCase(InterpreterTestCase.literal_var) @@ -418,24 +422,21 @@ static TestCase[] testCases() { .types( Any.getDefaultInstance(), com.google.api.expr.v1alpha1.Value.getDefaultInstance(), - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) + dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()) .in( "x", com.google.api.expr.v1alpha1.Value.newBuilder() .setObjectValue( Any.newBuilder() .setTypeUrl( - "type.googleapis.com/google.api.expr.test.v1.proto2.TestAllTypes") + "type.googleapis.com/cel.expr.conformance.proto2.TestAllTypes") .setValue(ByteString.copyFrom(new byte[] {8, (byte) 150, 1}))) .build()) .out(TestAllTypes.newBuilder().setSingleInt32(150).build()), new TestCase(InterpreterTestCase.select_pb3_unset) .expr("TestAllTypes{}.single_struct") - .container("google.api.expr.test.v1.proto3") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) + .container("cel.expr.conformance.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) .out(Struct.getDefaultInstance()), new TestCase(InterpreterTestCase.elem_in_mixed_type_list2) .expr("'elem' in [1u, 'str', 2, b'bytes']") @@ -448,10 +449,8 @@ static TestCase[] testCases() { .out(ULong.valueOf(123)), new TestCase(InterpreterTestCase.select_pb3_unset) .expr("TestAllTypes{}.single_struct") - .container("google.api.expr.test.v1.proto3") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) + .container("cel.expr.conformance.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) .out(Struct.getDefaultInstance()), new TestCase(InterpreterTestCase.select_on_int64) .expr("a.pancakes") @@ -460,37 +459,29 @@ static TestCase[] testCases() { .err("no such overload: int.ref-resolve(*)") .unchecked(), new TestCase(InterpreterTestCase.select_pb3_empty_list) - .container("google.api.expr.test.v1.proto3") + .container("cel.expr.conformance.proto3") .expr("TestAllTypes{list_value: []}.list_value") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) .out(ListValue.getDefaultInstance()), new TestCase(InterpreterTestCase.select_pb3_enum_big) - .container("google.api.expr.test.v1.proto3") + .container("cel.expr.conformance.proto3") .expr("x.standalone_enum") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - .env( - Decls.newVar("x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) + .env(Decls.newVar("x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) .in( "x", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .setStandaloneEnumValue(108) .build()) .out(intOf(108)), new TestCase(InterpreterTestCase.select_pb3_enum_neg) - .container("google.api.expr.test.v1.proto3") + .container("cel.expr.conformance.proto3") .expr("x.standalone_enum") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - .env( - Decls.newVar("x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) + .env(Decls.newVar("x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) .in( "x", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .setStandaloneEnumValue(-3) .build()) .out(intOf(-3)), @@ -511,7 +502,7 @@ static TestCase[] testCases() { .out(False), new TestCase(InterpreterTestCase.not_eq_list_one_element2).expr("[1] == [2]").out(False), new TestCase(InterpreterTestCase.parse_nest_message_literal) - .container("google.api.expr.test.v1.proto3") + .container("cel.expr.conformance.proto3") .expr( "NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: " + "NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: " @@ -519,9 +510,7 @@ static TestCase[] testCases() { + "NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: " + "NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: " + "NestedTestAllTypes{payload: TestAllTypes{single_int64: 137}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}.payload.single_int64") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.NestedTestAllTypes - .getDefaultInstance()) + .types(dev.cel.expr.conformance.proto3.NestedTestAllTypes.getDefaultInstance()) .out(intOf(0)), new TestCase(InterpreterTestCase.parse_repeat_index) .expr( @@ -588,7 +577,7 @@ static TestCase[] testCases() { "addall", AdderType, args -> { - int val = 0; + long val = 0; for (Val arg : args) { val += arg.intValue(); } @@ -637,10 +626,11 @@ static TestCase[] testCases() { .out("aGVsbG8="), new TestCase(InterpreterTestCase.complex) .expr( - "!(headers.ip in [\"10.0.1.4\", \"10.0.1.5\"]) && \n" - + "((headers.path.startsWith(\"v1\") && headers.token in [\"v1\", \"v2\", \"admin\"]) || \n" - + "(headers.path.startsWith(\"v2\") && headers.token in [\"v2\", \"admin\"]) || \n" - + "(headers.path.startsWith(\"/admin\") && headers.token == \"admin\" && headers.ip in [\"10.0.1.2\", \"10.0.1.2\", \"10.0.1.2\"]))") + """ + !(headers.ip in ["10.0.1.4", "10.0.1.5"]) &&\s + ((headers.path.startsWith("v1") && headers.token in ["v1", "v2", "admin"]) ||\s + (headers.path.startsWith("v2") && headers.token in ["v2", "admin"]) ||\s + (headers.path.startsWith("/admin") && headers.token == "admin" && headers.ip in ["10.0.1.2", "10.0.1.2", "10.0.1.2"]))""") .cost(costOf(3, 24)) .exhaustiveCost(costOf(24, 24)) .optimizedCost(costOf(2, 20)) @@ -653,10 +643,11 @@ static TestCase[] testCases() { "token", "admin")), new TestCase(InterpreterTestCase.complex_qual_vars) .expr( - "!(headers.ip in [\"10.0.1.4\", \"10.0.1.5\"]) && \n" - + "((headers.path.startsWith(\"v1\") && headers.token in [\"v1\", \"v2\", \"admin\"]) || \n" - + "(headers.path.startsWith(\"v2\") && headers.token in [\"v2\", \"admin\"]) || \n" - + "(headers.path.startsWith(\"/admin\") && headers.token == \"admin\" && headers.ip in [\"10.0.1.2\", \"10.0.1.2\", \"10.0.1.2\"]))") + """ + !(headers.ip in ["10.0.1.4", "10.0.1.5"]) &&\s + ((headers.path.startsWith("v1") && headers.token in ["v1", "v2", "admin"]) ||\s + (headers.path.startsWith("v2") && headers.token in ["v2", "admin"]) ||\s + (headers.path.startsWith("/admin") && headers.token == "admin" && headers.ip in ["10.0.1.2", "10.0.1.2", "10.0.1.2"]))""") .cost(costOf(3, 24)) .exhaustiveCost(costOf(24, 24)) .optimizedCost(costOf(2, 20)) @@ -749,12 +740,13 @@ static TestCase[] testCases() { .container("google.api.expr") .types(Expr.getDefaultInstance()) .expr( - "v1alpha1.Expr{ \n" - + " id: 1, \n" - + " const_expr: v1alpha1.Constant{ \n" - + " string_value: \"oneof_test\" \n" - + " }\n" - + "}") + """ + v1alpha1.Expr{\s + id: 1,\s + const_expr: v1alpha1.Constant{\s + string_value: "oneof_test"\s + } + }""") .cost(costOf(0, 0)) .out( Expr.newBuilder() @@ -762,31 +754,27 @@ static TestCase[] testCases() { .setConstExpr(Constant.newBuilder().setStringValue("oneof_test")) .build()), new TestCase(InterpreterTestCase.literal_pb_enum) - .container("google.api.expr.test.v1.proto3") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) + .container("cel.expr.conformance.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) .expr( - "TestAllTypes{\n" - + "repeated_nested_enum: [\n" - + " 0,\n" - + " TestAllTypes.NestedEnum.BAZ,\n" - + " TestAllTypes.NestedEnum.BAR],\n" - + "repeated_int32: [\n" - + " TestAllTypes.NestedEnum.FOO,\n" - + " TestAllTypes.NestedEnum.BAZ]}") + """ + TestAllTypes{ + repeated_nested_enum: [ + 0, + TestAllTypes.NestedEnum.BAZ, + TestAllTypes.NestedEnum.BAR], + repeated_int32: [ + TestAllTypes.NestedEnum.FOO, + TestAllTypes.NestedEnum.BAZ]}""") .cost(costOf(0, 0)) .out( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .addRepeatedNestedEnum( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedEnum - .FOO) + dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum.FOO) .addRepeatedNestedEnum( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedEnum - .BAZ) + dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAZ) .addRepeatedNestedEnum( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedEnum - .BAR) + dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAR) .addRepeatedInt32(0) .addRepeatedInt32(2) .build()), @@ -825,8 +813,9 @@ static TestCase[] testCases() { .exhaustiveCost(costOf(38, 38)), new TestCase(InterpreterTestCase.macro_all_non_strict_var) .expr( - "code == \"111\" && [\"a\", \"b\"].all(x, x in tags) \n" - + "|| code == \"222\" && [\"a\", \"b\"].all(x, x in tags)") + """ + code == "111" && ["a", "b"].all(x, x in tags)\s + || code == "222" && ["a", "b"].all(x, x in tags)""") .env( Decls.newVar("code", Decls.String), Decls.newVar("tags", Decls.newListType(Decls.String))) @@ -849,65 +838,55 @@ static TestCase[] testCases() { .cost(costOf(1, 4)) .exhaustiveCost(costOf(4, 4)), new TestCase(InterpreterTestCase.macro_has_pb2_field) - .container("google.api.expr.test.v1.proto2") - .types( - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - .env( - Decls.newVar( - "pb2", Decls.newObjectType("google.api.expr.test.v1.proto2.TestAllTypes"))) + .container("cel.expr.conformance.proto2") + .types(dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()) + .env(Decls.newVar("pb2", Decls.newObjectType("cel.expr.conformance.proto2.TestAllTypes"))) .in( "pb2", - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() .addRepeatedBool(false) .putMapInt64NestedType( - 1, - com.google.api.expr.test.v1.proto2.TestAllTypesProto.NestedTestAllTypes - .getDefaultInstance()) + 1, dev.cel.expr.conformance.proto2.NestedTestAllTypes.getDefaultInstance()) .build()) .expr( - "has(TestAllTypes{standalone_enum: TestAllTypes.NestedEnum.BAR}.standalone_enum) \n" - + "&& has(TestAllTypes{standalone_enum: TestAllTypes.NestedEnum.FOO}.standalone_enum) \n" - + "&& !has(TestAllTypes{single_nested_enum: TestAllTypes.NestedEnum.FOO}.single_nested_message) \n" - + "&& !has(TestAllTypes{}.standalone_enum) \n" - + "&& has(TestAllTypes{single_nested_enum: TestAllTypes.NestedEnum.FOO}.single_nested_enum) \n" - + "&& !has(pb2.single_int64) \n" - + "&& has(pb2.repeated_bool) \n" - + "&& !has(pb2.repeated_int32) \n" - + "&& has(pb2.map_int64_nested_type) \n" - + "&& !has(pb2.map_string_string)") + """ + has(TestAllTypes{standalone_enum: TestAllTypes.NestedEnum.BAR}.standalone_enum)\s + && has(TestAllTypes{standalone_enum: TestAllTypes.NestedEnum.FOO}.standalone_enum)\s + && !has(TestAllTypes{single_nested_enum: TestAllTypes.NestedEnum.FOO}.single_nested_message)\s + && !has(TestAllTypes{}.standalone_enum)\s + && has(TestAllTypes{single_nested_enum: TestAllTypes.NestedEnum.FOO}.single_nested_enum)\s + && !has(pb2.single_int64)\s + && has(pb2.repeated_bool)\s + && !has(pb2.repeated_int32)\s + && has(pb2.map_int64_nested_type)\s + && !has(pb2.map_string_string)""") .cost(costOf(1, 29)) .exhaustiveCost(costOf(29, 29)), new TestCase(InterpreterTestCase.macro_has_pb3_field) - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - .env( - Decls.newVar( - "pb3", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) - .container("google.api.expr.test.v1.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) + .env(Decls.newVar("pb3", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) + .container("cel.expr.conformance.proto3") .in( "pb3", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .addRepeatedBool(false) .putMapInt64NestedType( - 1, - com.google.api.expr.test.v1.proto3.TestAllTypesProto.NestedTestAllTypes - .getDefaultInstance()) + 1, dev.cel.expr.conformance.proto3.NestedTestAllTypes.getDefaultInstance()) .build()) .expr( - "has(TestAllTypes{standalone_enum: TestAllTypes.NestedEnum.BAR}.standalone_enum) \n" - + "&& !has(TestAllTypes{standalone_enum: TestAllTypes.NestedEnum.FOO}.standalone_enum) \n" - + "&& !has(TestAllTypes{single_nested_enum: TestAllTypes.NestedEnum.FOO}.single_nested_message) \n" - + "&& has(TestAllTypes{single_nested_enum: TestAllTypes.NestedEnum.FOO}.single_nested_enum) \n" - + "&& !has(TestAllTypes{}.single_nested_message) \n" - + "&& has(TestAllTypes{single_nested_message: TestAllTypes.NestedMessage{}}.single_nested_message) \n" - + "&& !has(TestAllTypes{}.standalone_enum) \n" - + "&& !has(pb3.single_int64) \n" - + "&& has(pb3.repeated_bool) \n" - + "&& !has(pb3.repeated_int32) \n" - + "&& has(pb3.map_int64_nested_type) \n" - + "&& !has(pb3.map_string_string)") + """ + has(TestAllTypes{standalone_enum: TestAllTypes.NestedEnum.BAR}.standalone_enum)\s + && !has(TestAllTypes{standalone_enum: TestAllTypes.NestedEnum.FOO}.standalone_enum)\s + && !has(TestAllTypes{single_nested_enum: TestAllTypes.NestedEnum.FOO}.single_nested_message)\s + && has(TestAllTypes{single_nested_enum: TestAllTypes.NestedEnum.FOO}.single_nested_enum)\s + && !has(TestAllTypes{}.single_nested_message)\s + && has(TestAllTypes{single_nested_message: TestAllTypes.NestedMessage{}}.single_nested_message)\s + && !has(TestAllTypes{}.standalone_enum)\s + && !has(pb3.single_int64)\s + && has(pb3.repeated_bool)\s + && !has(pb3.repeated_int32)\s + && has(pb3.map_int64_nested_type)\s + && !has(pb3.map_string_string)""") .cost(costOf(1, 35)) .exhaustiveCost(costOf(35, 35)), new TestCase(InterpreterTestCase.macro_map) @@ -916,10 +895,11 @@ static TestCase[] testCases() { .exhaustiveCost(costOf(7, 7)), new TestCase(InterpreterTestCase.matches) .expr( - "input.matches('k.*') \n" - + "&& !'foo'.matches('k.*') \n" - + "&& !'bar'.matches('k.*') \n" - + "&& 'kilimanjaro'.matches('.*ro')") + """ + input.matches('k.*')\s + && !'foo'.matches('k.*')\s + && !'bar'.matches('k.*')\s + && 'kilimanjaro'.matches('.*ro')""") .cost(costOf(2, 10)) .exhaustiveCost(costOf(10, 10)) .env(Decls.newVar("input", Decls.String)) @@ -927,18 +907,13 @@ static TestCase[] testCases() { new TestCase(InterpreterTestCase.nested_proto_field) .expr("pb3.single_nested_message.bb") .cost(costOf(1, 1)) - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - .env( - Decls.newVar( - "pb3", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) + .env(Decls.newVar("pb3", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) .in( "pb3", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .setSingleNestedMessage( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .NestedMessage.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage.newBuilder() .setBb(1234) .build()) .build()) @@ -946,25 +921,18 @@ static TestCase[] testCases() { new TestCase(InterpreterTestCase.nested_proto_field_with_index) .expr("pb3.map_int64_nested_type[0].child.payload.single_int32 == 1") .cost(costOf(2, 2)) - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - .env( - Decls.newVar( - "pb3", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) + .env(Decls.newVar("pb3", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) .in( "pb3", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .putMapInt64NestedType( 0, - com.google.api.expr.test.v1.proto3.TestAllTypesProto.NestedTestAllTypes - .newBuilder() + dev.cel.expr.conformance.proto3.NestedTestAllTypes.newBuilder() .setChild( - com.google.api.expr.test.v1.proto3.TestAllTypesProto - .NestedTestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.NestedTestAllTypes.newBuilder() .setPayload( - com.google.api.expr.test.v1.proto3.TestAllTypesProto - .TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .setSingleInt32(1))) .build()) .build()), @@ -1033,17 +1001,18 @@ static TestCase[] testCases() { .in("a.b.c", mapOf("d", 10)), new TestCase(InterpreterTestCase.select_key) .expr( - "m.strMap['val'] == 'string'\n" - + "&& m.floatMap['val'] == 1.5\n" - + "&& m.doubleMap['val'] == -2.0\n" - + "&& m.intMap['val'] == -3\n" - + "&& m.int32Map['val'] == 4\n" - + "&& m.int64Map['val'] == -5\n" - + "&& m.uintMap['val'] == 6u\n" - + "&& m.uint32Map['val'] == 7u\n" - + "&& m.uint64Map['val'] == 8u\n" - + "&& m.boolMap['val'] == true\n" - + "&& m.boolMap['val'] != false") + """ + m.strMap['val'] == 'string' + && m.floatMap['val'] == 1.5 + && m.doubleMap['val'] == -2.0 + && m.intMap['val'] == -3 + && m.int32Map['val'] == 4 + && m.int64Map['val'] == -5 + && m.uintMap['val'] == 6u + && m.uint32Map['val'] == 7u + && m.uint64Map['val'] == 8u + && m.boolMap['val'] == true + && m.boolMap['val'] != false""") .cost(costOf(2, 32)) .exhaustiveCost(costOf(32, 32)) .env(Decls.newVar("m", Decls.newMapType(Decls.String, Decls.Dyn))) @@ -1062,17 +1031,18 @@ static TestCase[] testCases() { "boolMap", mapOf("val", true))), new TestCase(InterpreterTestCase.select_bool_key) .expr( - "m.boolStr[true] == 'string'\n" - + "&& m.boolFloat32[true] == 1.5\n" - + "&& m.boolFloat64[false] == -2.1\n" - + "&& m.boolInt[false] == -3\n" - + "&& m.boolInt32[false] == 0\n" - + "&& m.boolInt64[true] == 4\n" - + "&& m.boolUint[true] == 5u\n" - + "&& m.boolUint32[true] == 6u\n" - + "&& m.boolUint64[false] == 7u\n" - + "&& m.boolBool[true]\n" - + "&& m.boolIface[false] == true") + """ + m.boolStr[true] == 'string' + && m.boolFloat32[true] == 1.5 + && m.boolFloat64[false] == -2.1 + && m.boolInt[false] == -3 + && m.boolInt32[false] == 0 + && m.boolInt64[true] == 4 + && m.boolUint[true] == 5u + && m.boolUint32[true] == 6u + && m.boolUint64[false] == 7u + && m.boolBool[true] + && m.boolIface[false] == true""") .cost(costOf(2, 31)) .exhaustiveCost(costOf(31, 31)) .env(Decls.newVar("m", Decls.newMapType(Decls.String, Decls.Dyn))) @@ -1092,10 +1062,11 @@ static TestCase[] testCases() { "boolIface", mapOf(false, true))), new TestCase(InterpreterTestCase.select_uint_key) .expr( - "m.uintIface[1u] == 'string'\n" - + "&& m.uint32Iface[2u] == 1.5\n" - + "&& m.uint64Iface[3u] == -2.1\n" - + "&& m.uint64String[4u] == 'three'") + """ + m.uintIface[1u] == 'string' + && m.uint32Iface[2u] == 1.5 + && m.uint64Iface[3u] == -2.1 + && m.uint64String[4u] == 'three'""") .cost(costOf(2, 11)) .exhaustiveCost(costOf(11, 11)) .env(Decls.newVar("m", Decls.newMapType(Decls.String, Decls.Dyn))) @@ -1108,18 +1079,19 @@ static TestCase[] testCases() { "uint64String", mapOf(ULong.valueOf(4), "three"))), new TestCase(InterpreterTestCase.select_index) .expr( - "m.strList[0] == 'string'\n" - + "&& m.floatList[0] == 1.5\n" - + "&& m.doubleList[0] == -2.0\n" - + "&& m.intList[0] == -3\n" - + "&& m.int32List[0] == 4\n" - + "&& m.int64List[0] == -5\n" - + "&& m.uintList[0] == 6u\n" - + "&& m.uint32List[0] == 7u\n" - + "&& m.uint64List[0] == 8u\n" - + "&& m.boolList[0] == true\n" - + "&& m.boolList[1] != true\n" - + "&& m.ifaceList[0] == {}") + """ + m.strList[0] == 'string' + && m.floatList[0] == 1.5 + && m.doubleList[0] == -2.0 + && m.intList[0] == -3 + && m.int32List[0] == 4 + && m.int64List[0] == -5 + && m.uintList[0] == 6u + && m.uint32List[0] == 7u + && m.uint64List[0] == 8u + && m.boolList[0] == true + && m.boolList[1] != true + && m.ifaceList[0] == {}""") .cost(costOf(2, 35)) .exhaustiveCost(costOf(35, 35)) .env(Decls.newVar("m", Decls.newMapType(Decls.String, Decls.Dyn))) @@ -1139,28 +1111,25 @@ static TestCase[] testCases() { "ifaceList", new Object[] {new HashMap<>()})), new TestCase(InterpreterTestCase.select_field) .expr( - "a.b.c\n" - + "&& pb3.repeated_nested_enum[0] == TestAllTypes.NestedEnum.BAR\n" - + "&& json.list[0] == 'world'") + """ + a.b.c + && pb3.repeated_nested_enum[0] == TestAllTypes.NestedEnum.BAR + && json.list[0] == 'world'""") .cost(costOf(1, 7)) .exhaustiveCost(costOf(7, 7)) - .container("google.api.expr.test.v1.proto3") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) + .container("cel.expr.conformance.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) .env( Decls.newVar("a.b", Decls.newMapType(Decls.String, Decls.Bool)), - Decls.newVar( - "pb3", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")), + Decls.newVar("pb3", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")), Decls.newVar("json", Decls.newMapType(Decls.String, Decls.Dyn))) .in( "a.b", mapOf("c", true), "pb3", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .addRepeatedNestedEnum( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedEnum - .BAR) + dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAR) .build(), "json", Value.newBuilder() @@ -1177,94 +1146,78 @@ static TestCase[] testCases() { // pb2 primitive fields may have default values set. new TestCase(InterpreterTestCase.select_pb2_primitive_fields) .expr( - "!has(a.single_int32)\n" - + "&& a.single_int32 == -32\n" - + "&& a.single_int64 == -64\n" - + "&& a.single_uint32 == 32u\n" - + "&& a.single_uint64 == 64u\n" - + "&& a.single_float == 3.0\n" - + "&& a.single_double == 6.4\n" - + "&& a.single_bool\n" - + "&& \"empty\" == a.single_string") + """ + !has(a.single_int32) + && a.single_int32 == -32 + && a.single_int64 == -64 + && a.single_uint32 == 32u + && a.single_uint64 == 64u + && a.single_float == 3.0 + && a.single_double == 6.4 + && a.single_bool + && "empty" == a.single_string""") .cost(costOf(3, 26)) .exhaustiveCost(costOf(26, 26)) .types(TestAllTypes.getDefaultInstance()) .in("a", TestAllTypes.newBuilder().build()) - .env( - Decls.newVar( - "a", Decls.newObjectType("google.api.expr.test.v1.proto2.TestAllTypes"))), + .env(Decls.newVar("a", Decls.newObjectType("cel.expr.conformance.proto2.TestAllTypes"))), // Wrapper type nil or value test. new TestCase(InterpreterTestCase.select_pb3_wrapper_fields) .expr( - "!has(a.single_int32_wrapper) && a.single_int32_wrapper == null\n" - + "&& has(a.single_int64_wrapper) && a.single_int64_wrapper == 0\n" - + "&& has(a.single_string_wrapper) && a.single_string_wrapper == \"hello\"\n" - + "&& a.single_int64_wrapper == Int32Value{value: 0}") + """ + !has(a.single_int32_wrapper) && a.single_int32_wrapper == null + && has(a.single_int64_wrapper) && a.single_int64_wrapper == 0 + && has(a.single_string_wrapper) && a.single_string_wrapper == "hello" + && a.single_int64_wrapper == Int32Value{value: 0}""") .cost(costOf(3, 21)) .exhaustiveCost(costOf(21, 21)) - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) .abbrevs("google.protobuf.Int32Value") - .env( - Decls.newVar("a", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) + .env(Decls.newVar("a", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) .in( "a", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .setSingleInt64Wrapper(Int64Value.newBuilder().build()) .setSingleStringWrapper(StringValue.of("hello")) .build()), new TestCase(InterpreterTestCase.select_pb3_compare) .expr("a.single_uint64 > 3u") .cost(costOf(2, 2)) - .container("google.api.expr.test.v1.proto3") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - .env( - Decls.newVar("a", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) + .container("cel.expr.conformance.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) + .env(Decls.newVar("a", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) .in( "a", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() - .setSingleUint64(10) - .build()) + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder().setSingleUint64(10).build()) .out(True), new TestCase(InterpreterTestCase.select_pb3_compare_signed) .expr("a.single_int64 > 3") .cost(costOf(2, 2)) - .container("google.api.expr.test.v1.proto3") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - .env( - Decls.newVar("a", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) + .container("cel.expr.conformance.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) + .env(Decls.newVar("a", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) .in( "a", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() - .setSingleInt64(10) - .build()) + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder().setSingleInt64(10).build()) .out(True), new TestCase(InterpreterTestCase.select_custom_pb3_compare) .expr("a.bb > 100") .cost(costOf(2, 2)) - .container("google.api.expr.test.v1.proto3") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedMessage - .getDefaultInstance()) + .container("cel.expr.conformance.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage.getDefaultInstance()) .env( Decls.newVar( "a", - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage"))) + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes.NestedMessage"))) .attrs( new CustAttrFactory( newAttributeFactory( - testContainer("google.api.expr.test.v1.proto3"), + testContainer("cel.expr.conformance.proto3"), DefaultTypeAdapter.Instance, newEmptyRegistry()))) .in( "a", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedMessage - .newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage.newBuilder() .setBb(101) .build()) .out(True), @@ -1302,10 +1255,8 @@ static TestCase[] testCases() { new TestCase(InterpreterTestCase.select_empty_repeated_nested) .expr("TestAllTypes{}.repeated_nested_message.size() == 0") .cost(costOf(2, 2)) - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - .container("google.api.expr.test.v1.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) + .container("cel.expr.conformance.proto3") .out(True), new TestCase(InterpreterTestCase.duration_get_milliseconds) .expr("x.getMilliseconds()") @@ -1423,25 +1374,21 @@ void protoAttributeOpt() { program( new TestCase(InterpreterTestCase.nested_proto_field_with_index) .expr("pb3.map_int64_nested_type[0].child.payload.single_int32") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) .env( Decls.newVar( - "pb3", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) + "pb3", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) .in( "pb3", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .putMapInt64NestedType( 0, - com.google.api.expr.test.v1.proto3.TestAllTypesProto.NestedTestAllTypes - .newBuilder() + dev.cel.expr.conformance.proto3.NestedTestAllTypes.newBuilder() .setChild( - com.google.api.expr.test.v1.proto3.TestAllTypesProto - .NestedTestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.NestedTestAllTypes.newBuilder() .setPayload( - com.google.api.expr.test.v1.proto3.TestAllTypesProto - .TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes + .newBuilder() .setSingleInt32(1))) .build()) .build()), @@ -1595,28 +1542,28 @@ void setProto2PrimitiveFields() { // Test the use of proto2 primitives within object construction. Source src = newTextSource( - "input == TestAllTypes{\n" - + " single_int32: 1,\n" - + " single_int64: 2,\n" - + " single_uint32: 3u,\n" - + " single_uint64: 4u,\n" - + " single_float: -3.3,\n" - + " single_double: -2.2,\n" - + " single_string: \"hello world\",\n" - + " single_bool: true\n" - + "}"); + """ + input == TestAllTypes{ + single_int32: 1, + single_int64: 2, + single_uint32: 3u, + single_uint64: 4u, + single_float: -3.3, + single_double: -2.2, + single_string: "hello world", + single_bool: true + }"""); ParseResult parsed = Parser.parseAllMacros(src); assertThat(parsed.hasErrors()).withFailMessage(parsed.getErrors()::toDisplayString).isFalse(); - Container cont = testContainer("google.api.expr.test.v1.proto2"); + Container cont = testContainer("cel.expr.conformance.proto2"); TypeRegistry reg = - newRegistry( - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes.getDefaultInstance()); + newRegistry(dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()); CheckerEnv env = newStandardCheckerEnv(cont, reg); env.add( singletonList( Decls.newVar( - "input", Decls.newObjectType("google.api.expr.test.v1.proto2.TestAllTypes")))); + "input", Decls.newObjectType("cel.expr.conformance.proto2.TestAllTypes")))); CheckResult checkResult = Checker.Check(parsed, src, env); if (parsed.hasErrors()) { throw new IllegalArgumentException(parsed.getErrors().toDisplayString()); @@ -1633,8 +1580,8 @@ void setProto2PrimitiveFields() { double six = -2.2d; String str = "hello world"; boolean truth = true; - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes input = - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto2.TestAllTypes input = + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() .setSingleInt32(one) .setSingleInt64(two) .setSingleUint32(three) @@ -1940,10 +1887,7 @@ static Container testContainer(String name) { return newContainer(Container.name(name)); } - static class Program { - final Interpretable interpretable; - final Activation activation; - + record Program(Interpretable interpretable, Activation activation) { Program(Interpretable interpretable, Activation activation) { this.interpretable = Objects.requireNonNull(interpretable); this.activation = Objects.requireNonNull(activation); @@ -2024,6 +1968,6 @@ static boolean isFieldQual(Qualifier q, String fieldName) { if (!(q instanceof FieldQualifier)) { return false; } - return ((FieldQualifier) q).name.equals(fieldName); + return ((FieldQualifier) q).name().equals(fieldName); } } diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTestCase.java b/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTestCase.java index 5c585a21..b45eb5a8 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTestCase.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTestCase.java @@ -72,6 +72,7 @@ public enum InterpreterTestCase { lt_dyn_int_big_uint, lt_dyn_uint_big_double, lt_ne_dyn_int_double, + map_key_float, map_key_mixed_numbers_lossy_double_key, map_key_string_and_int_are_distinct, eq_dyn_string_int, @@ -104,6 +105,7 @@ public enum InterpreterTestCase { not_eq_list_one_element, not_eq_list_one_element2, not_eq_list_mixed_type_numbers, + ne_proto_nan_not_equal, not_int32_eq_uint, not_uint32_eq_double, not_lt_dyn_big_uint_int, diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/NativeExactMapOperationsTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/NativeExactMapOperationsTest.java new file mode 100644 index 00000000..e06e7adc --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/interpreter/NativeExactMapOperationsTest.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EvalOption.OptDisableNativeEval; +import static org.projectnessie.cel.ProgramOption.evalOptions; +import static org.projectnessie.cel.common.types.IntT.intOf; + +import java.util.AbstractMap; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.ExactAggregateTypeAdapter; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; + +class NativeExactMapOperationsTest { + private final Env env = + newEnv( + customTypeAdapter(new ExactAdapter()), + declarations( + Decls.newVar("ints", Decls.newMapType(Decls.String, Decls.Int)), + Decls.newVar("uints", Decls.newMapType(Decls.String, Decls.Uint)), + Decls.newVar("strings", Decls.newMapType(Decls.Bool, Decls.String)), + Decls.newVar("nulls", Decls.newMapType(Decls.String, Decls.Null)), + Decls.newVar("lists", Decls.newMapType(Decls.String, Decls.newListType(Decls.Int))), + Decls.newVar( + "maps", Decls.newMapType(Decls.String, Decls.newMapType(Decls.Bool, Decls.Uint))), + Decls.newVar("numeric", Decls.newMapType(Decls.Int, Decls.Int)))); + + @Test + void exactMapsSupportSizeStringAndBooleanLookupAndMembership() { + Map input = input(); + + assertEquivalent("size(ints)", input); + assertEquivalent("ints['one']", input); + assertEquivalent("'one' in ints", input); + assertEquivalent("'missing' in ints", input); + assertEquivalent("strings[true]", input); + assertEquivalent("true in strings", input); + assertEquivalent("uints['high']", input); + } + + @Test + void lookupDistinguishesPresentNullFromAbsent() { + Map input = input(); + + assertThat(assertEquivalent("nulls['present']", input)) + .isSameAs(org.projectnessie.cel.common.types.NullT.NullValue); + assertThat(assertEquivalent("'present' in nulls", input).booleanValue()).isTrue(); + assertThat(assertEquivalent("'missing' in nulls", input).booleanValue()).isFalse(); + assertThat(assertEquivalent("nulls['missing']", input)).isInstanceOf(Err.class); + } + + @Test + void nestedExactListAndMapValuesRemainComposable() { + Map input = input(); + + assertEquivalent("size(lists['numbers'])", input); + assertEquivalent("lists['numbers'][1]", input); + assertEquivalent("size(maps['nested'])", input); + assertEquivalent("maps['nested'][true]", input); + assertEquivalent("size(lists['numbers'] + lists['numbers'])", input); + } + + @Test + void exactMapSourceResolvesOncePerRetainedOperation() { + AtomicInteger resolutions = new AtomicInteger(); + ActivationFunction activation = + name -> { + if (name.equals("ints")) { + resolutions.incrementAndGet(); + return Map.of("one", 1L); + } + return ActivationFunction.ABSENT; + }; + + assertThat(program("ints['one']", false).eval(activation).getVal().intValue()).isEqualTo(1L); + assertThat(resolutions).hasValue(1); + + resolutions.set(0); + assertThat(program("size(ints)", false).eval(activation).getVal().intValue()).isEqualTo(1L); + assertThat(resolutions).hasValue(1); + + resolutions.set(0); + assertThat(program("'one' in ints", false).eval(activation).getVal().booleanValue()).isTrue(); + assertThat(resolutions).hasValue(1); + } + + @Test + void numericKeysAndMapEqualityRemainEstablishedCompatible() { + Map input = input(); + + assertEquivalent("numeric[1]", input); + assertEquivalent("1 in numeric", input); + assertEquivalent("ints == ints", input); + assertEquivalent("maps == maps", input); + } + + @Test + void successfulEmbeddedValuesAreRejectedByTheExactContract() { + assertThat(program("ints['one']", false).eval(Map.of("ints", Map.of("one", intOf(1)))).getVal()) + .isInstanceOf(Err.class); + assertThat(program("ints['one']", true).eval(Map.of("ints", Map.of("one", intOf(1)))).getVal()) + .isInstanceOf(Err.class); + + Val embeddedList = DefaultTypeAdapter.Instance.nativeToValue(List.of(1L)); + assertThat( + program("lists['numbers'][0]", false) + .eval(Map.of("lists", Map.of("numbers", embeddedList))) + .getVal()) + .isInstanceOf(Err.class); + } + + @Test + void materializationAndHostLookupFailuresRemainCelErrors() { + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + Map throwing = + new AbstractMap<>() { + @Override + public Object get(Object key) { + return "not an integer"; + } + + @Override + public boolean containsKey(Object key) { + return true; + } + + @Override + public java.util.Set> entrySet() { + throw new IllegalStateException("entry traversal failed"); + } + }; + + assertThat(program("ints['one']", false).eval(Map.of("ints", throwing)).getVal()) + .isInstanceOf(Err.class) + .hasToString("java.lang.IllegalStateException: entry traversal failed"); + + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + Map sizeFailure = + new AbstractMap<>() { + @Override + public java.util.Set> entrySet() { + return Collections.emptySet(); + } + + @Override + public int size() { + throw new IllegalStateException("size failed"); + } + }; + assertThat(program("size(ints)", false).eval(Map.of("ints", sizeFailure)).getVal()) + .isInstanceOf(Err.class) + .hasToString("java.lang.IllegalStateException: size failed"); + } + + private static Map input() { + Map nulls = new LinkedHashMap<>(); + nulls.put("present", null); + return Map.of( + "ints", Map.of("one", 1L, "two", 2L), + "uints", Map.of("high", -1L), + "strings", Map.of(false, "no", true, "yes"), + "nulls", nulls, + "lists", Map.of("numbers", List.of(10L, 20L, 30L)), + "maps", Map.of("nested", Map.of(true, ULong.valueOf(-1L))), + "numeric", Map.of(1L, 11L)); + } + + private Val assertEquivalent(String expression, Object input) { + Val nativeValue = program(expression, false).eval(input).getVal(); + Val establishedValue = program(expression, true).eval(input).getVal(); + + assertThat(nativeValue.getClass()).isEqualTo(establishedValue.getClass()); + assertThat(nativeValue.type()).isEqualTo(establishedValue.type()); + assertThat(nativeValue.toString()).isEqualTo(establishedValue.toString()); + if (!(nativeValue instanceof Err)) { + assertThat(nativeValue.value()).isEqualTo(establishedValue.value()); + } + return nativeValue; + } + + private Program program(String expression, boolean disableNative) { + var compiled = env.compile(expression); + assertThat(compiled.hasIssues()).as(compiled.getIssues().toString()).isFalse(); + return disableNative + ? env.program(compiled.getAst(), evalOptions(OptDisableNativeEval)) + : env.program(compiled.getAst()); + } + + private static final class ExactAdapter + implements ExactAggregateTypeAdapter, StandardScalarTypeAdapter { + @Override + public Val nativeToValue(Object value) { + return DefaultTypeAdapter.Instance.nativeToValue(value); + } + } +} diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/NativeExactSetOperationsTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/NativeExactSetOperationsTest.java new file mode 100644 index 00000000..996b8734 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/interpreter/NativeExactSetOperationsTest.java @@ -0,0 +1,430 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EvalOption.OptDisableNativeEval; +import static org.projectnessie.cel.ProgramOption.evalOptions; +import static org.projectnessie.cel.common.types.Err.newErr; + +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.ExactAggregateTypeAdapter; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; + +class NativeExactSetOperationsTest { + private final ExactAdapter adapter = new ExactAdapter(); + private final Env exactEnv = + newEnv( + customTypeAdapter(adapter), + declarations( + Decls.newVar("stringNeedle", Decls.String), + Decls.newVar("strings", Decls.newListType(Decls.String)), + Decls.newVar("boolNeedle", Decls.Bool), + Decls.newVar("bools", Decls.newListType(Decls.Bool)), + Decls.newVar("intNeedle", Decls.Int), + Decls.newVar("ints", Decls.newListType(Decls.Int)), + Decls.newVar("uintNeedle", Decls.Uint), + Decls.newVar("uints", Decls.newListType(Decls.Uint)), + Decls.newVar("doubleNeedle", Decls.Double), + Decls.newVar("doubles", Decls.newListType(Decls.Double)), + Decls.newVar("leftInts", Decls.newListType(Decls.Int)), + Decls.newVar("rightInts", Decls.newListType(Decls.Int)), + Decls.newVar("rightStrings", Decls.newListType(Decls.String)), + Decls.newVar("rightBools", Decls.newListType(Decls.Bool)), + Decls.newVar("rightUints", Decls.newListType(Decls.Uint)), + Decls.newVar("leftDoubles", Decls.newListType(Decls.Double)), + Decls.newVar("rightDoubles", Decls.newListType(Decls.Double)))); + + @TestFactory + Stream exactSetMembershipPreservesCelScalarSemantics() { + return Stream.of( + new MembershipCase( + "string", + "stringNeedle in strings", + Map.of("stringNeedle", "two", "strings", linkedSet("one", "two")), + true), + new MembershipCase( + "boolean", + "boolNeedle in bools", + Map.of("boolNeedle", true, "bools", linkedSet(false, true)), + true), + new MembershipCase( + "signed long", + "intNeedle in ints", + Map.of("intNeedle", 22L, "ints", linkedSet(11L, 22L)), + true), + new MembershipCase( + "unsigned boxed long", + "uintNeedle in uints", + Map.of("uintNeedle", ULong.valueOf(-1L), "uints", linkedSet(-1L, 1L)), + true), + new MembershipCase( + "unsigned ULong", + "uintNeedle in uints", + Map.of( + "uintNeedle", + ULong.valueOf(-1L), + "uints", + linkedSet(ULong.valueOf(-1L), ULong.valueOf(1L))), + true), + new MembershipCase( + "ordinary double", + "doubleNeedle in doubles", + Map.of("doubleNeedle", 2.5d, "doubles", linkedSet(1.5d, 2.5d)), + true), + new MembershipCase( + "NaN never equals itself", + "doubleNeedle in doubles", + Map.of("doubleNeedle", Double.NaN, "doubles", linkedSet(Double.NaN)), + false), + new MembershipCase( + "positive zero finds negative zero", + "doubleNeedle in doubles", + Map.of("doubleNeedle", 0.0d, "doubles", linkedSet(-0.0d)), + true), + new MembershipCase( + "negative zero finds positive zero", + "doubleNeedle in doubles", + Map.of("doubleNeedle", -0.0d, "doubles", linkedSet(0.0d)), + true), + new MembershipCase( + "hash set", + "intNeedle in ints", + Map.of("intNeedle", 22L, "ints", new HashSet<>(List.of(11L, 22L))), + true), + new MembershipCase( + "immutable set", + "intNeedle in ints", + Map.of("intNeedle", 22L, "ints", Set.of(11L, 22L)), + true)) + .map( + testCase -> + DynamicTest.dynamicTest( + testCase.name(), + () -> + assertEquivalent( + exactEnv, + testCase.expression(), + testCase.input(), + testCase.expected()))); + } + + @TestFactory + Stream exactListEqualityUsesEncounterOrderAcrossSetAndListSources() { + return Stream.of( + new EqualityCase( + "set-set same order", + "leftInts == rightInts", + Map.of( + "leftInts", linkedSet(1L, 2L, 3L), + "rightInts", linkedSet(1L, 2L, 3L)), + true), + new EqualityCase( + "set-set different order", + "leftInts == rightInts", + Map.of( + "leftInts", linkedSet(1L, 2L, 3L), + "rightInts", linkedSet(3L, 2L, 1L)), + false), + new EqualityCase( + "set-list", + "leftInts == rightInts", + Map.of( + "leftInts", linkedSet(1L, 2L, 3L), + "rightInts", List.of(1L, 2L, 3L)), + true), + new EqualityCase( + "list-set", + "leftInts == rightInts", + Map.of( + "leftInts", List.of(1L, 2L, 3L), + "rightInts", linkedSet(1L, 2L, 3L)), + true), + new EqualityCase( + "not-equal uses encounter order", + "leftInts != rightInts", + Map.of( + "leftInts", linkedSet(1L, 2L, 3L), + "rightInts", linkedSet(3L, 2L, 1L)), + true), + new EqualityCase( + "string", + "strings == rightStrings", + Map.of( + "strings", linkedSet("one", "two"), + "rightStrings", List.of("one", "two")), + true), + new EqualityCase( + "boolean", + "bools == rightBools", + Map.of( + "bools", linkedSet(false, true), + "rightBools", List.of(false, true)), + true), + new EqualityCase( + "unsigned boxed long and ULong", + "uints == rightUints", + Map.of( + "uints", linkedSet(-1L, 1L), + "rightUints", List.of(ULong.valueOf(-1L), ULong.valueOf(1L))), + true)) + .map( + testCase -> + DynamicTest.dynamicTest( + testCase.name(), + () -> + assertEquivalent( + exactEnv, + testCase.expression(), + testCase.input(), + testCase.expected()))); + } + + @Test + void exactDoubleListEqualityPreservesNanAndSignedZeroSemantics() { + assertEquivalent( + exactEnv, + "leftDoubles == rightDoubles", + Map.of("leftDoubles", linkedSet(Double.NaN), "rightDoubles", linkedSet(Double.NaN)), + false); + assertEquivalent( + exactEnv, + "leftDoubles == rightDoubles", + Map.of("leftDoubles", linkedSet(0.0d), "rightDoubles", linkedSet(-0.0d)), + true); + } + + @Test + void exactMembershipUsesSetContainsButNativeDisabledEvaluationDoesNot() { + CountingSet nativeValues = new CountingSet<>(List.of(11L, 22L, 33L)); + assertThat( + program(exactEnv, "intNeedle in ints", false) + .eval(Map.of("intNeedle", 22L, "ints", nativeValues)) + .getVal() + .booleanValue()) + .isTrue(); + assertThat(nativeValues.containsCalls).isEqualTo(1); + assertThat(nativeValues.iteratorCalls).isZero(); + + CountingSet establishedValues = new CountingSet<>(List.of(11L, 22L, 33L)); + assertThat( + program(exactEnv, "intNeedle in ints", true) + .eval(Map.of("intNeedle", 22L, "ints", establishedValues)) + .getVal() + .booleanValue()) + .isTrue(); + assertThat(establishedValues.containsCalls).isZero(); + assertThat(establishedValues.iteratorCalls).isGreaterThan(0); + } + + @Test + void exactMembershipHandlesEmptyMissAndNonSetFallbackWithoutReplayingTheSource() { + assertEquivalent( + exactEnv, "intNeedle in ints", Map.of("intNeedle", 22L, "ints", Set.of()), false); + assertEquivalent( + exactEnv, "intNeedle in ints", Map.of("intNeedle", 22L, "ints", Set.of(11L, 33L)), false); + + AtomicInteger resolutions = new AtomicInteger(); + ActivationFunction activation = + name -> { + if (name.equals("intNeedle")) { + return 22L; + } + if (name.equals("ints")) { + resolutions.incrementAndGet(); + return List.of(11L, 22L, 33L); + } + return ActivationFunction.ABSENT; + }; + Val result = program(exactEnv, "intNeedle in ints", false).eval(activation).getVal(); + + assertThat(result.booleanValue()).isTrue(); + assertThat(resolutions).hasValue(1); + } + + @Test + void exactListEqualityDoesNotDelegateToSetEquals() { + Set left = new EqualsRejectingSet<>(List.of(11L, 22L, 33L)); + Set right = new EqualsRejectingSet<>(List.of(11L, 22L, 33L)); + + assertEquivalent( + exactEnv, "leftInts == rightInts", Map.of("leftInts", left, "rightInts", right), true); + } + + @Test + void generalAdapterDoesNotUseCustomSetContainsSemantics() { + Env generalEnv = + newEnv( + declarations( + Decls.newVar("stringNeedle", Decls.String), + Decls.newVar("strings", Decls.newListType(Decls.String)))); + TreeSet caseInsensitive = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + caseInsensitive.add("VALUE"); + + assertThat(caseInsensitive.contains("value")).isTrue(); + assertEquivalent( + generalEnv, + "stringNeedle in strings", + Map.of("stringNeedle", "value", "strings", caseInsensitive), + false); + } + + @Test + void membershipResolvesNeedleBeforeSourceExactlyOnceEvenWhenBothFail() { + Program nativeProgram = program(exactEnv, "stringNeedle in strings", false); + List resolutions = new ArrayList<>(); + AtomicInteger needleResolutions = new AtomicInteger(); + AtomicInteger sourceResolutions = new AtomicInteger(); + ActivationFunction activation = + name -> { + resolutions.add(name); + if (name.equals("stringNeedle")) { + needleResolutions.incrementAndGet(); + return newErr("needle failed"); + } + if (name.equals("strings")) { + sourceResolutions.incrementAndGet(); + return newErr("source failed"); + } + return ActivationFunction.ABSENT; + }; + + Val result = nativeProgram.eval(activation).getVal(); + + assertThat(result).isInstanceOf(Err.class); + assertThat(result.toString()).contains("needle failed"); + assertThat(resolutions).containsExactly("stringNeedle", "strings"); + assertThat(needleResolutions).hasValue(1); + assertThat(sourceResolutions).hasValue(1); + } + + @Test + void equalityResolvesLeftBeforeRightExactlyOnceEvenWhenBothFail() { + Program nativeProgram = program(exactEnv, "leftInts == rightInts", false); + List resolutions = new ArrayList<>(); + ActivationFunction activation = + name -> { + resolutions.add(name); + return newErr(name + " failed"); + }; + + Val result = nativeProgram.eval(activation).getVal(); + + assertThat(result).isInstanceOf(Err.class); + assertThat(result.toString()).contains("leftInts failed"); + assertThat(resolutions).containsExactly("leftInts", "rightInts"); + } + + private void assertEquivalent( + Env env, String expression, Map input, boolean expected) { + Val nativeValue = program(env, expression, false).eval(input).getVal(); + Val establishedValue = program(env, expression, true).eval(input).getVal(); + + assertThat(nativeValue.getClass()).isEqualTo(establishedValue.getClass()); + assertThat(nativeValue.type()).isEqualTo(establishedValue.type()); + assertThat(nativeValue.toString()).isEqualTo(establishedValue.toString()); + assertThat(nativeValue.booleanValue()).isEqualTo(expected); + assertThat(establishedValue.booleanValue()).isEqualTo(expected); + } + + private static Program program(Env env, String expression, boolean disableNative) { + var compiled = env.compile(expression); + assertThat(compiled.hasIssues()).as(compiled.getIssues().toString()).isFalse(); + return disableNative + ? env.program(compiled.getAst(), evalOptions(OptDisableNativeEval)) + : env.program(compiled.getAst()); + } + + @SafeVarargs + private static LinkedHashSet linkedSet(E... elements) { + return new LinkedHashSet<>(List.of(elements)); + } + + private record MembershipCase( + String name, String expression, Map input, boolean expected) {} + + private record EqualityCase( + String name, String expression, Map input, boolean expected) {} + + private static class CountingSet extends AbstractSet { + private final LinkedHashSet delegate; + int containsCalls; + int iteratorCalls; + + CountingSet(List elements) { + this.delegate = new LinkedHashSet<>(elements); + } + + @Override + public Iterator iterator() { + iteratorCalls++; + return delegate.iterator(); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean contains(Object value) { + containsCalls++; + return delegate.contains(value); + } + } + + private static final class EqualsRejectingSet extends CountingSet { + EqualsRejectingSet(List elements) { + super(elements); + } + + @Override + public boolean equals(Object other) { + throw new AssertionError("CEL list equality must not delegate to Set.equals()"); + } + } + + private static final class ExactAdapter + implements ExactAggregateTypeAdapter, StandardScalarTypeAdapter { + @Override + public Val nativeToValue(Object value) { + return DefaultTypeAdapter.Instance.nativeToValue(value); + } + } +} diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/NativeListConcatTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/NativeListConcatTest.java new file mode 100644 index 00000000..b5e5172c --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/interpreter/NativeListConcatTest.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EvalOption.OptDisableNativeEval; +import static org.projectnessie.cel.ProgramOption.evalOptions; +import static org.projectnessie.cel.common.types.Err.newErr; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.ExactAggregateTypeAdapter; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; + +class NativeListConcatTest { + private final Env env = + newEnv( + customTypeAdapter(new ExactAdapter()), + declarations( + Decls.newVar("leftInts", Decls.newListType(Decls.Int)), + Decls.newVar("rightInts", Decls.newListType(Decls.Int)), + Decls.newVar("leftUints", Decls.newListType(Decls.Uint)), + Decls.newVar("rightUints", Decls.newListType(Decls.Uint)), + Decls.newVar("leftStrings", Decls.newListType(Decls.String)), + Decls.newVar("rightStrings", Decls.newListType(Decls.String)))); + + @Test + void exactConcatenationSupportsImmediateSizeAndBoundaryIndexes() { + Map input = + Map.of( + "leftInts", + new long[] {11L, 22L}, + "rightInts", + List.of(33L, 44L), + "leftUints", + new long[] {-1L}, + "rightUints", + List.of(ULong.valueOf(1L)), + "leftStrings", + List.of("left"), + "rightStrings", + new String[] {"right"}); + + assertEquivalent("size(leftInts + rightInts)", input); + assertEquivalent("(leftInts + rightInts)[0]", input); + assertEquivalent("(leftInts + rightInts)[1]", input); + assertEquivalent("(leftInts + rightInts)[2]", input); + assertEquivalent("(leftInts + rightInts)[3]", input); + assertEquivalent("(leftUints + rightUints)[0]", input); + assertEquivalent("(leftStrings + rightStrings)[1]", input); + } + + @Test + void structuralSizeDoesNotInspectElementsAndSelectedViolationsRemainErrors() { + List invalid = Arrays.asList(null, "not-an-int"); + Map input = + Map.of( + "leftInts", invalid, + "rightInts", List.of(1L), + "leftUints", List.of(ULong.valueOf(1L)), + "rightUints", List.of(ULong.valueOf(2L)), + "leftStrings", List.of("left"), + "rightStrings", List.of("right")); + + assertThat(assertEquivalent("size(leftInts + rightInts)", input).intValue()).isEqualTo(3L); + assertThat(assertEquivalent("(leftInts + rightInts)[0]", input)).isInstanceOf(Err.class); + assertThat(assertEquivalent("(leftInts + rightInts)[1]", input)).isInstanceOf(Err.class); + assertThat(program("(leftInts + rightInts)[2]", false).eval(input).getVal().intValue()) + .isEqualTo(1L); + } + + @Test + void outOfRangeIndexesPreserveEstablishedErrors() { + Map input = + Map.of( + "leftInts", List.of(1L), + "rightInts", List.of(2L), + "leftUints", List.of(ULong.valueOf(1L)), + "rightUints", List.of(ULong.valueOf(2L)), + "leftStrings", List.of("left"), + "rightStrings", List.of("right")); + + assertThat(assertEquivalent("(leftInts + rightInts)[-1]", input)).isInstanceOf(Err.class); + assertThat(assertEquivalent("(leftInts + rightInts)[2]", input)).isInstanceOf(Err.class); + } + + @Test + void resolvesBothOperandsOnceInOrderAndKeepsLeftFailurePrecedence() { + Program program = program("size(leftInts + rightInts)", false); + List resolutions = new ArrayList<>(); + AtomicInteger leftResolutions = new AtomicInteger(); + AtomicInteger rightResolutions = new AtomicInteger(); + ActivationFunction activation = + name -> { + resolutions.add(name); + if (name.equals("leftInts")) { + leftResolutions.incrementAndGet(); + return newErr("left failed"); + } + if (name.equals("rightInts")) { + rightResolutions.incrementAndGet(); + return newErr("right failed"); + } + return ActivationFunction.ABSENT; + }; + + Val result = program.eval(activation).getVal(); + + assertThat(result).isInstanceOf(Err.class); + assertThat(result.toString()).contains("left failed"); + assertThat(resolutions).containsExactly("leftInts", "rightInts"); + assertThat(leftResolutions).hasValue(1); + assertThat(rightResolutions).hasValue(1); + } + + @Test + void terminalAndRepeatedConcatenationRemainEstablishedCompatible() { + Map input = + Map.of( + "leftInts", List.of(1L), + "rightInts", List.of(2L), + "leftUints", List.of(ULong.valueOf(1L)), + "rightUints", List.of(ULong.valueOf(2L)), + "leftStrings", List.of("left"), + "rightStrings", List.of("right")); + + assertEquivalent("leftInts + rightInts", input); + assertEquivalent("(leftInts + rightInts) + leftInts", input); + assertEquivalent("(leftInts + rightInts)[0 + 1]", input); + } + + private Val assertEquivalent(String expression, Object input) { + Val nativeValue = program(expression, false).eval(input).getVal(); + Val establishedValue = program(expression, true).eval(input).getVal(); + + assertThat(nativeValue.getClass()).isEqualTo(establishedValue.getClass()); + assertThat(nativeValue.type()).isEqualTo(establishedValue.type()); + if (nativeValue.type().typeEnum() == org.projectnessie.cel.common.types.ref.TypeEnum.List) { + assertThat(nativeValue.equal(establishedValue).booleanValue()).isTrue(); + } else { + assertThat(nativeValue.toString()).isEqualTo(establishedValue.toString()); + } + if (!(nativeValue instanceof Err) + && nativeValue.type().typeEnum() != org.projectnessie.cel.common.types.ref.TypeEnum.List) { + assertThat(nativeValue.value()).isEqualTo(establishedValue.value()); + } + return nativeValue; + } + + private Program program(String expression, boolean disableNative) { + var compiled = env.compile(expression); + assertThat(compiled.hasIssues()).as(compiled.getIssues().toString()).isFalse(); + return disableNative + ? env.program(compiled.getAst(), evalOptions(OptDisableNativeEval)) + : env.program(compiled.getAst()); + } + + private static final class ExactAdapter + implements ExactAggregateTypeAdapter, StandardScalarTypeAdapter { + @Override + public Val nativeToValue(Object value) { + return DefaultTypeAdapter.Instance.nativeToValue(value); + } + } +} diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/NativeListSourceTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/NativeListSourceTest.java new file mode 100644 index 00000000..1b1555be --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/interpreter/NativeListSourceTest.java @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EvalOption.OptDisableNativeEval; +import static org.projectnessie.cel.ProgramOption.evalOptions; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.UnknownT.unknownOf; + +import java.util.AbstractList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.UnknownT; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.ExactAggregateTypeAdapter; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; + +class NativeListSourceTest { + private final ExactAdapter adapter = new ExactAdapter(); + private final Env env = + newEnv( + customTypeAdapter(adapter), + declarations( + Decls.newVar("values", Decls.newListType(Decls.Int)), + Decls.newVar("uints", Decls.newListType(Decls.Uint)), + Decls.newVar("strings", Decls.newListType(Decls.String)), + Decls.newVar("index", Decls.Int))); + + @Test + void exactSourcesSupportStructuralAndIndexedConsumersWithoutChangingResults() { + Map input = + Map.of( + "values", + new LinkedHashSet<>(List.of(11L, 22L, 33L)), + "uints", + new long[] {-1L, 1L}, + "index", + 1L); + + assertEquivalent("size(values)", input); + assertEquivalent("values[0]", input); + assertEquivalent("values[2]", input); + assertEquivalent("values[index]", input); + assertEquivalent("uints[0]", input); + } + + @Test + void exactSourcesSupportQuantifiersAndMappedFilteredConsumers() { + Map input = + Map.of( + "values", + new LinkedHashSet<>(List.of(1L, 2L, 3L)), + "uints", + List.of(ULong.valueOf(-1L), ULong.valueOf(1L)), + "index", + 0L); + + assertEquivalent("values.exists(v, v == 2)", input); + assertEquivalent("values.all(v, v > 0)", input); + assertEquivalent("uints.exists(v, v == 18446744073709551615u)", input); + assertEquivalent("values.filter(v, v > 1).map(v, v)[0]", input); + } + + @Test + void dynamicIndexResolvesEachOperandOnce() { + Program nativeProgram = program("values[index]", false); + AtomicInteger valuesResolutions = new AtomicInteger(); + AtomicInteger indexResolutions = new AtomicInteger(); + ActivationFunction activation = + name -> { + if (name.equals("values")) { + valuesResolutions.incrementAndGet(); + return new long[] {10L, 20L, 30L}; + } + if (name.equals("index")) { + indexResolutions.incrementAndGet(); + return 1L; + } + return ActivationFunction.ABSENT; + }; + + assertThat(nativeProgram.eval(activation).getVal().intValue()).isEqualTo(20L); + assertThat(valuesResolutions).hasValue(1); + assertThat(indexResolutions).hasValue(1); + } + + @Test + void visitedExactSourceViolationsRemainCheckedErrors() { + Map input = + Map.of( + "values", + List.of("not-an-int"), + "uints", + List.of(ULong.valueOf(1L)), + "strings", + new String[] {null}, + "index", + 0L); + + assertThat(assertEquivalent("values[0] == 1", input)).isInstanceOf(Err.class); + assertThat(assertEquivalent("values.exists(v, v == 1)", input)).isInstanceOf(Err.class); + assertThat(assertEquivalent("strings[0] == 'x'", input)).isInstanceOf(Err.class); + assertThat(assertEquivalent("strings.exists(s, s == 'x')", input)).isInstanceOf(Err.class); + + Map embedded = + Map.of( + "values", List.of(intOf(1)), + "uints", List.of(ULong.valueOf(1L)), + "strings", List.of("value"), + "index", 0L); + assertThat(assertEquivalent("values[0] == 1", embedded)).isInstanceOf(Err.class); + assertThat(assertEquivalent("values.exists(v, v == 1)", embedded)).isInstanceOf(Err.class); + } + + @Test + void generalUnsignedSourcesDoNotReinterpretSignedLongArrays() { + Env general = + newEnv( + declarations( + Decls.newVar("uints", Decls.newListType(Decls.Uint)), + Decls.newVar("index", Decls.Int))); + Object input = Map.of("uints", new long[] {-1L}, "index", 0L); + + assertEquivalent(general, "uints[0] == 18446744073709551615u", input); + assertEquivalent(general, "uints.exists(v, v == 18446744073709551615u)", input); + } + + @Test + void exceptionalSourceSuppressesDynamicIndexAndHostFailuresBecomeErrors() { + AtomicInteger indexResolutions = new AtomicInteger(); + ActivationFunction missingSource = + name -> { + if (name.equals("index")) { + indexResolutions.incrementAndGet(); + return 0L; + } + return ActivationFunction.ABSENT; + }; + + assertThat(program("values[index]", false).eval(missingSource).getVal()) + .isInstanceOf(Err.class); + assertThat(indexResolutions).hasValue(0); + + ActivationFunction errorSource = + name -> { + if (name.equals("values")) { + return newErr("source failed"); + } + if (name.equals("index")) { + indexResolutions.incrementAndGet(); + return 0L; + } + return ActivationFunction.ABSENT; + }; + assertThat(program("values[index]", false).eval(errorSource).getVal()).isInstanceOf(Err.class); + assertThat(indexResolutions).hasValue(0); + + ActivationFunction unknownSource = + name -> { + if (name.equals("values")) { + return unknownOf(7L); + } + if (name.equals("index")) { + indexResolutions.incrementAndGet(); + return 0L; + } + return ActivationFunction.ABSENT; + }; + assertThat(program("values[index]", false).eval(unknownSource).getVal()) + .isInstanceOf(UnknownT.class); + assertThat(indexResolutions).hasValue(0); + + ActivationFunction throwingIndex = + name -> { + if (name.equals("values")) { + return new long[] {1L}; + } + if (name.equals("index")) { + throw new IllegalStateException("index failed"); + } + return ActivationFunction.ABSENT; + }; + assertThat(program("values[index]", false).eval(throwingIndex).getVal()) + .isInstanceOf(Err.class); + + List throwing = + new AbstractList<>() { + @Override + public Long get(int index) { + throw new IllegalStateException("get failed"); + } + + @Override + public int size() { + return 1; + } + }; + assertThat( + program("values[0]", false) + .eval( + Map.of( + "values", + throwing, + "uints", + List.of(ULong.valueOf(1L)), + "strings", + List.of("value"), + "index", + 0L)) + .getVal()) + .isInstanceOf(Err.class); + } + + @Test + void indexesOutsideTheIntegerRangeRemainOutOfRange() { + Map input = + Map.of( + "values", + new long[] {11L}, + "uints", + List.of(ULong.valueOf(1L)), + "strings", + List.of("value"), + "index", + 4_294_967_296L); + Val result = assertEquivalent("values[index]", input); + + assertThat(result).isInstanceOf(Err.class); + assertThat(result.toString()).contains("4294967296"); + } + + private Val assertEquivalent(String expression, Object input) { + Val nativeValue = program(expression, false).eval(input).getVal(); + Val establishedValue = program(expression, true).eval(input).getVal(); + + assertThat(nativeValue.getClass()).isEqualTo(establishedValue.getClass()); + assertThat(nativeValue.type()).isEqualTo(establishedValue.type()); + assertThat(nativeValue.toString()).isEqualTo(establishedValue.toString()); + if (nativeValue instanceof Err) { + return nativeValue; + } + assertThat(nativeValue.value()).isEqualTo(establishedValue.value()); + return nativeValue; + } + + private static void assertEquivalent(Env env, String expression, Object input) { + var compiled = env.compile(expression); + assertThat(compiled.hasIssues()).as(compiled.getIssues().toString()).isFalse(); + Val nativeValue = env.program(compiled.getAst()).eval(input).getVal(); + Val establishedValue = + env.program(compiled.getAst(), evalOptions(OptDisableNativeEval)).eval(input).getVal(); + + assertThat(nativeValue.getClass()).isEqualTo(establishedValue.getClass()); + assertThat(nativeValue.toString()).isEqualTo(establishedValue.toString()); + assertThat(nativeValue.booleanValue()).isEqualTo(false); + } + + private Program program(String expression, boolean disableNative) { + var compiled = env.compile(expression); + assertThat(compiled.hasIssues()).as(compiled.getIssues().toString()).isFalse(); + return disableNative + ? env.program(compiled.getAst(), evalOptions(OptDisableNativeEval)) + : env.program(compiled.getAst()); + } + + private static final class ExactAdapter + implements ExactAggregateTypeAdapter, StandardScalarTypeAdapter { + @Override + public Val nativeToValue(Object value) { + return DefaultTypeAdapter.Instance.nativeToValue(value); + } + } +} diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/NativePlumbingPrototypeTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/NativePlumbingPrototypeTest.java new file mode 100644 index 00000000..03d8e900 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/interpreter/NativePlumbingPrototypeTest.java @@ -0,0 +1,663 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.projectnessie.cel.CEL.astToCheckedExpr; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.checker.Decls.Bool; +import static org.projectnessie.cel.checker.Decls.Int; +import static org.projectnessie.cel.checker.Decls.newFunction; +import static org.projectnessie.cel.checker.Decls.newListType; +import static org.projectnessie.cel.checker.Decls.newMapType; +import static org.projectnessie.cel.checker.Decls.newOverload; +import static org.projectnessie.cel.checker.Decls.newVar; +import static org.projectnessie.cel.common.containers.Container.defaultContainer; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.UnknownT.unknownOf; +import static org.projectnessie.cel.interpreter.Activation.newActivation; +import static org.projectnessie.cel.interpreter.AttributeFactory.newAttributeFactory; +import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; +import static org.projectnessie.cel.interpreter.Dispatcher.newDispatcher; +import static org.projectnessie.cel.interpreter.Interpreter.newInterpreter; +import static org.projectnessie.cel.interpreter.functions.Overload.standardOverloads; + +import com.google.api.expr.v1alpha1.Decl; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableCall; +import org.projectnessie.cel.interpreter.functions.Overload; + +class NativePlumbingPrototypeTest { + private static final Decl X = newVar("x", Int); + private static final Decl Y = newVar("y", Int); + private static final Decl B = newVar("b", Bool); + private static final Decl C = newVar("c", Bool); + private static final Decl S = newVar("s", org.projectnessie.cel.checker.Decls.String); + + @Test + void composesIntNodesAndEqualityInsideOneRootIsland() { + for (String expression : List.of("x", "s", "x + 1", "((x + 1) + 2) + 3", "(x + 1) == y")) { + Plans plans = plans(expression, new Decl[] {X, Y, S}); + Activation activation = newActivation(Map.of("x", 40L, "y", 41L, "s", "value")); + + assertThat(plans.enabled()).isInstanceOf(NativeIsland.class); + NativeIsland island = (NativeIsland) plans.enabled(); + Val expected = plans.established().eval(activation); + assertThat(island.root().eval(activation)).isEqualTo(expected); + assertThat(island.eval(activation)).isEqualTo(expected); + assertThat(island.id()).isEqualTo(plans.established().id()); + assertThat(island.cost()).isEqualTo(estimateCost(plans.established())); + + if (island.root() instanceof NativeIntCapability intCapability) { + assertThat(intCapability.evalInt(activation)).isEqualTo(expected.intValue()); + } else if (island.root() instanceof NativeBooleanCapability booleanCapability) { + assertThat(booleanCapability.evalBoolean(activation)).isEqualTo(expected.booleanValue()); + } else { + NativeStringCapability stringCapability = (NativeStringCapability) island.root(); + assertThat(stringCapability.evalString(activation)).isEqualTo(expected.value()); + } + } + } + + @Test + void strictFallbackEvaluatesBothOperandsOnceWithoutReplay() { + Plans plans = plans("x + y", new Decl[] {X, Y}); + Val left = newErr("left"); + Val right = unknownOf(22L); + List resolutions = new ArrayList<>(); + Activation activation = + activation( + name -> { + resolutions.add(name); + return name.equals("x") ? left : right; + }); + + Val established = plans.established().eval(activation); + assertThat(resolutions).containsExactly("x", "y"); + resolutions.clear(); + + Val integrated = plans.enabled().eval(activation); + assertThat(resolutions).containsExactly("x", "y"); + assertThat(integrated).isSameAs(established).isSameAs(left); + } + + @Test + void strictFallbackPreservesBothSignalOrdersAndOverflow() { + Plans addition = plans("x + y", new Decl[] {X, Y}); + for (Object[] values : + List.of( + new Object[] {unknownOf(31L), newErr("right")}, + new Object[] {newErr("left"), 1L}, + new Object[] {1L, unknownOf(32L)})) { + List resolutions = new ArrayList<>(); + Activation activation = + activation( + name -> { + resolutions.add(name); + return name.equals("x") ? values[0] : values[1]; + }); + assertEquivalent( + addition.enabled().eval(activation), + addition.established().eval(newActivation(Map.of("x", values[0], "y", values[1]))), + List.of(values).toString()); + assertThat(resolutions).containsExactly("x", "y"); + } + + for (long[] values : + List.of( + new long[] {Long.MAX_VALUE, 1L}, + new long[] {Long.MIN_VALUE, -1L}, + new long[] {Long.MAX_VALUE, 0L})) { + Activation activation = newActivation(Map.of("x", values[0], "y", values[1])); + assertEquivalent( + addition.enabled().eval(activation), + addition.established().eval(activation), + values[0] + ", " + values[1]); + } + } + + @Test + void booleanEqualityPreservesCompatibilityAndBilateralEvaluation() { + Plans equality = plans("b == c", new Decl[] {B, C}); + for (Object[] values : + List.of( + new Object[] {true, false}, + new Object[] { + org.projectnessie.cel.common.types.BoolT.True, + org.projectnessie.cel.common.types.BoolT.False + }, + new Object[] {"wrong", true}, + new Object[] {newErr("left"), unknownOf(41L)}, + new Object[] {unknownOf(42L), newErr("right")})) { + List resolutions = new ArrayList<>(); + Activation activation = + activation( + name -> { + resolutions.add(name); + return name.equals("b") ? values[0] : values[1]; + }); + assertEquivalent( + equality.enabled().eval(activation), + equality.established().eval(newActivation(Map.of("b", values[0], "c", values[1]))), + List.of(values).toString()); + assertThat(resolutions).containsExactly("b", "c"); + } + + Map nullValue = new HashMap<>(); + nullValue.put("b", null); + nullValue.put("c", true); + assertEquivalent( + equality.enabled().eval(newActivation(nullValue)), + equality.established().eval(newActivation(nullValue)), + "null"); + assertEquivalent( + equality.enabled().eval(newActivation(emptyMap())), + equality.established().eval(newActivation(emptyMap())), + "absent"); + } + + @Test + void wrongAndCompatibilityValuesFollowEstablishedContinuation() { + Plans plans = plans("x + 1", new Decl[] {X}); + List values = List.of(41L, intOf(41L), "wrong", newErr("carried"), unknownOf(17L)); + for (Object value : values) { + Activation activation = newActivation(Map.of("x", value)); + assertEquivalent( + plans.enabled().eval(activation), + plans.established().eval(activation), + String.valueOf(value)); + } + + Map nullValue = new HashMap<>(); + nullValue.put("x", null); + assertEquivalent( + plans.enabled().eval(newActivation(nullValue)), + plans.established().eval(newActivation(nullValue)), + "null"); + assertEquivalent( + plans.enabled().eval(newActivation(emptyMap())), + plans.established().eval(newActivation(emptyMap())), + "absent"); + } + + @Test + void mapSelectorAndConstantListIndexResolveTheirSourceOnce() { + Plans selector = + plans( + "attrs.answer + 1", + new Decl[] { + newVar("attrs", newMapType(org.projectnessie.cel.checker.Decls.String, Int)) + }); + List selectorResolutions = new ArrayList<>(); + Activation selectorActivation = + activation( + name -> { + selectorResolutions.add(name); + return Map.of("answer", 41L); + }); + assertThat(selector.enabled().eval(selectorActivation)).isEqualTo(intOf(42L)); + assertThat(selectorResolutions).containsExactly("attrs"); + + Plans index = plans("numbers[1] + 1", new Decl[] {newVar("numbers", newListType(Int))}); + assertThat(index.enabled()).isInstanceOf(NativeIsland.class); + assertThat(((NativeIsland) index.enabled()).root()).isInstanceOf(NativeIntAdd.class); + List indexResolutions = new ArrayList<>(); + Activation indexActivation = + activation( + name -> { + indexResolutions.add(name); + return new long[] {10L, 41L}; + }); + assertThat(index.enabled().eval(indexActivation)).isEqualTo(intOf(42L)); + assertThat(indexResolutions).containsExactly("numbers"); + } + + @Test + void mapSelectorPreservesEstablishedCompatibilityResultsAndSingleAccess() { + Plans plans = + plans( + "attrs.answer + 1", + new Decl[] { + newVar("attrs", newMapType(org.projectnessie.cel.checker.Decls.String, Int)) + }); + Map nullValue = new HashMap<>(); + nullValue.put("answer", null); + for (Object source : + List.of( + Map.of("answer", 41L), + Map.of("answer", intOf(41L)), + Map.of("answer", "wrong"), + Map.of("answer", newErr("carried")), + Map.of("answer", unknownOf(51L)), + Map.of("other", 41L), + nullValue, + newErr("source"), + unknownOf(52L), + "wrong source")) { + assertEquivalent( + plans.enabled().eval(newActivation(Map.of("attrs", source))), + plans.established().eval(newActivation(Map.of("attrs", source))), + String.valueOf(source)); + } + + Map nullSource = new HashMap<>(); + nullSource.put("attrs", null); + assertEquivalent( + plans.enabled().eval(newActivation(nullSource)), + plans.established().eval(newActivation(nullSource)), + "null source"); + + AtomicInteger gets = new AtomicInteger(); + Map countingMap = + new HashMap<>() { + @Override + public Object get(Object key) { + gets.incrementAndGet(); + return super.get(key); + } + }; + countingMap.put("answer", 41L); + assertThat(plans.enabled().eval(newActivation(Map.of("attrs", countingMap)))) + .isEqualTo(intOf(42L)); + assertThat(gets).hasValue(1); + } + + @Test + void constantListIndexPreservesEstablishedCompatibilityResults() { + Plans plans = plans("numbers[1] + 1", new Decl[] {newVar("numbers", newListType(Int))}); + for (Object source : + List.of( + new long[] {10L, 41L}, + new int[] {10, 41}, + List.of(10L, 41L), + List.of(intOf(10L), intOf(41L)), + List.of(10L, "wrong"), + newErr("carried"), + unknownOf(19L), + "wrong container")) { + assertEquivalent( + plans.enabled().eval(newActivation(Map.of("numbers", source))), + plans.established().eval(newActivation(Map.of("numbers", source))), + source.getClass().getName()); + } + + Map nullSource = new HashMap<>(); + nullSource.put("numbers", null); + assertEquivalent( + plans.enabled().eval(newActivation(nullSource)), + plans.established().eval(newActivation(nullSource)), + "null source"); + + Plans outOfRange = plans("numbers[2]", new Decl[] {newVar("numbers", newListType(Int))}); + Activation shortList = newActivation(Map.of("numbers", new long[] {10L, 41L})); + assertEquivalent( + outOfRange.enabled().eval(shortList), + outOfRange.established().eval(shortList), + "out of range"); + + Activation throwing = + activation( + name -> { + throw new IllegalStateException("source failure"); + }); + assertEquivalent( + plans.enabled().eval(throwing), plans.established().eval(throwing), "source failure"); + + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + List throwingList = + new AbstractList<>() { + @Override + public Long get(int index) { + throw new IllegalStateException("element failure"); + } + + @Override + public int size() { + return 2; + } + }; + assertEquivalent( + plans.enabled().eval(newActivation(Map.of("numbers", throwingList))), + plans.established().eval(newActivation(Map.of("numbers", throwingList))), + "element failure"); + } + + @Test + void identifierAdapterExceptionsMatchEstablishedEvaluation() { + StandardScalarTypeAdapter throwingAdapter = + value -> { + if (value instanceof String) { + throw new IllegalStateException("adapter failure"); + } + return org.projectnessie.cel.common.types.pb.DefaultTypeAdapter.Instance.nativeToValue( + value); + }; + Plans plans = plans("x + 1", new Decl[] {X}, throwingAdapter); + Activation activation = newActivation(Map.of("x", "wrong")); + + assertThatThrownBy(() -> plans.established().eval(activation)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("adapter failure"); + assertThatThrownBy(() -> plans.enabled().eval(activation)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("adapter failure"); + } + + @Test + void establishedOuterCallsReceiveMaximalChildIslands() { + Decl opaque = newFunction("opaque", newOverload("opaque_int", List.of(Int), Int)); + Overload implementation = Overload.unary("opaque_int", value -> value); + Plans plans = plans("opaque(x + 1)", new Decl[] {X, opaque}, implementation); + + assertThat(plans.enabled()).isInstanceOf(InterpretableCall.class); + InterpretableCall outer = (InterpretableCall) plans.enabled(); + assertThat(outer.args()).singleElement().isInstanceOf(NativeIsland.class); + assertThat(plans.enabled().eval(newActivation(Map.of("x", 41L)))).isEqualTo(intOf(42L)); + + Decl opaque2 = newFunction("opaque2", newOverload("opaque2_int", List.of(Int, Int), Int)); + Overload binary = Overload.binary("opaque2_int", (left, right) -> left); + Plans siblings = plans("opaque2(x + 1, y + 1)", new Decl[] {X, Y, opaque2}, binary); + assertThat(((InterpretableCall) siblings.enabled()).args()) + .allSatisfy(argument -> assertThat(argument).isInstanceOf(NativeIsland.class)); + } + + @Test + void establishedAggregateParentsReceiveMaximalChildIslands() { + Plans list = plans("[x + 1]", new Decl[] {X}); + assertThat(list.enabled()).isInstanceOf(NativeIntListLiteral.class); + NativeIntListLiteral literal = (NativeIntListLiteral) list.enabled(); + assertThat(literal.elems).singleElement().isInstanceOf(NativeIsland.class); + assertThat(literal.nativeElements) + .singleElement() + .isSameAs(((NativeIsland) literal.elems[0]).root()); + + Plans map = plans("{'answer': x + 1}", new Decl[] {X}); + assertThat(map.enabled()).isInstanceOf(EvalMap.class); + assertThat(((EvalMap) map.enabled()).vals).singleElement().isInstanceOf(NativeIsland.class); + + Plans fold = + plans( + "numbers.exists(n, string(n) == string(x))", + new Decl[] {X, newVar("numbers", newListType(Int))}); + assertThat(fold.enabled()).isInstanceOf(EvalFold.class); + EvalFold foldRoot = (EvalFold) fold.enabled(); + assertThat(foldRoot.iterRange).isInstanceOf(NativeRawIdent.class); + InterpretableCall condition = (InterpretableCall) foldRoot.cond; + assertThat(condition.args()).singleElement().isInstanceOf(NativeIsland.class); + assertThat(fold.enabled().eval(newActivation(Map.of("x", 42L, "numbers", List.of(42L))))) + .isEqualTo(org.projectnessie.cel.common.types.BoolT.True); + assertThat(foldRoot.cost()).isEqualTo(estimateCost(fold.established())); + } + + @Test + void listFoldConsumersShareOnePlannedSourceAndChildGraph() { + Plans plans = + plans( + "size(numbers.map(value, value > 0, value + x))", + new Decl[] {X, newVar("numbers", newListType(Int))}); + + assertThat(plans.enabled()).isInstanceOf(NativeIsland.class); + Interpretable root = ((NativeIsland) plans.enabled()).root(); + assertThat(root).isInstanceOf(NativeListFoldSize.class); + NativeScalarListFold source = (NativeScalarListFold) ((NativeListFoldSize) root).source; + + assertThat(source.iterRange).isSameAs(source.range); + assertThat(source.filter).isInstanceOf(NativeIsland.class); + assertThat(((NativeIsland) source.filter).root()).isSameAs(source.predicate); + assertThat(source.transform).isInstanceOf(NativeIsland.class); + assertThat(((NativeIsland) source.transform).root()).isSameAs(source.nativeTransform); + + Activation activation = newActivation(Map.of("x", 1L, "numbers", new long[] {-1L, 1L, 2L})); + assertThat(plans.enabled().eval(activation)).isEqualTo(intOf(2L)); + assertThat(plans.enabled().eval(activation)).isEqualTo(plans.established().eval(activation)); + } + + @Test + void mappedStringMembershipSharesItsSinglePlannedListFoldSource() { + Decl wordTarget = newVar("wordTarget", org.projectnessie.cel.checker.Decls.String); + Decl words = newVar("words", newListType(org.projectnessie.cel.checker.Decls.String)); + Plans plans = + plans( + "wordTarget in words.map(value, value != 'skip', value + '!')", + new Decl[] {wordTarget, words}); + + assertThat(plans.enabled()).isInstanceOf(NativeIsland.class); + Interpretable root = ((NativeIsland) plans.enabled()).root(); + assertThat(root).isInstanceOf(NativeStringListFoldMembership.class); + NativeStringListFoldMembership membership = (NativeStringListFoldMembership) root; + NativeScalarListFold source = (NativeScalarListFold) membership.source; + + assertThat(membership.rhs).isSameAs(source); + assertThat(source.filter).isInstanceOf(NativeIsland.class); + assertThat(((NativeIsland) source.filter).root()).isSameAs(source.predicate); + assertThat(source.transform).isInstanceOf(NativeIsland.class); + assertThat(((NativeIsland) source.transform).root()).isSameAs(source.nativeTransform); + + Activation activation = + newActivation(Map.of("wordTarget", "cel!", "words", new String[] {"other", "skip", "cel"})); + assertThat(plans.enabled().eval(activation)) + .isEqualTo(org.projectnessie.cel.common.types.BoolT.True); + assertThat(plans.enabled().eval(activation)).isEqualTo(plans.established().eval(activation)); + } + + @Test + void mappedIntegerQuantifierSharesItsSinglePlannedListFoldSource() { + Decl numbers = newVar("numbers", newListType(Int)); + Plans plans = + plans( + "numbers.map(value, value > 0, value + 1).exists(mapped, mapped == 3)", + new Decl[] {numbers}); + + assertThat(plans.enabled()).isInstanceOf(NativeIsland.class); + Interpretable root = ((NativeIsland) plans.enabled()).root(); + assertThat(root).isInstanceOf(NativeIntMappedQuantifierFold.class); + NativeIntMappedLoopFold aggregate = (NativeIntMappedLoopFold) root; + NativeScalarListFold source = (NativeScalarListFold) aggregate.source; + + assertThat(aggregate.iterRange).isSameAs(source); + assertThat(source.filter).isInstanceOf(NativeIsland.class); + assertThat(((NativeIsland) source.filter).root()).isSameAs(source.predicate); + assertThat(source.transform).isInstanceOf(NativeIsland.class); + assertThat(((NativeIsland) source.transform).root()).isSameAs(source.nativeTransform); + + Activation activation = newActivation(Map.of("numbers", new long[] {-1L, 1L, 2L})); + assertThat(plans.enabled().eval(activation)) + .isEqualTo(org.projectnessie.cel.common.types.BoolT.True); + assertThat(plans.enabled().eval(activation)).isEqualTo(plans.established().eval(activation)); + } + + @Test + void islandAdaptsOneSuccessAndDoesNotReadaptSignals() { + CountingAdapter adapter = new CountingAdapter(); + Plans plans = plans("x + 1", new Decl[] {X}, adapter); + NativeIsland island = (NativeIsland) plans.enabled(); + + adapter.reset(); + assertThat(((NativeIntCapability) island.root()).evalInt(newActivation(Map.of("x", 41L)))) + .isEqualTo(42L); + assertThat(adapter.count()).isZero(); + + adapter.reset(); + assertThat(island.eval(newActivation(Map.of("x", 41L)))).isEqualTo(intOf(42L)); + assertThat(adapter.count()).isEqualTo(1); + + adapter.reset(); + assertThat(island.eval(newActivation(Map.of("x", "wrong"))).type().typeName()) + .isEqualTo("error"); + assertThat(adapter.count()).isEqualTo(1); + } + + @Test + void disabledDecoratedAndShortCircuitedPlansStayOnEstablishedPath() { + Plans plans = plans("false && (x + 1 == y)", new Decl[] {X, Y}); + List resolutions = new ArrayList<>(); + assertThat( + plans + .enabled() + .eval( + activation( + name -> { + resolutions.add(name); + return 41L; + }))) + .isEqualTo(org.projectnessie.cel.common.types.BoolT.False); + assertThat(resolutions).isEmpty(); + + assertThat(plans.established()).isNotInstanceOf(NativeIsland.class); + Interpretable decorated = + plans + .enabledInterpreter() + .newInterpretable( + plans.checked(), + plan -> { + assertThat(plan).isNotInstanceOf(NativeIsland.class); + assertThat(NativeIsland.supports(plan)).isFalse(); + return plan; + }); + assertThat(decorated).isNotInstanceOf(NativeIsland.class); + } + + @Test + void immutableIntegratedPlanCanBeReusedConcurrently() throws Exception { + Plans plans = plans("((x + 1) + 2) == y", new Decl[] {X, Y}); + @SuppressWarnings("resource") + var executor = Executors.newFixedThreadPool(4); + try { + var futures = + IntStream.range(0, 200) + .mapToObj( + value -> + executor.submit( + () -> + plans + .enabled() + .eval(newActivation(Map.of("x", (long) value, "y", value + 3L))))) + .toList(); + for (var future : futures) { + assertThat(future.get(10, TimeUnit.SECONDS).booleanValue()).isTrue(); + } + } finally { + executor.shutdownNow(); + assertThat(executor.awaitTermination(10, TimeUnit.SECONDS)).isTrue(); + } + } + + private static Plans plans(String expression, Decl[] declarations, Overload... custom) { + return plans(expression, declarations, null, custom); + } + + private static Plans plans( + String expression, + Decl[] declarations, + StandardScalarTypeAdapter adapter, + Overload... custom) { + Env env = newEnv(declarations(declarations)); + AstIssuesTuple result = env.compile(expression); + assertThat(result.hasIssues()).withFailMessage(result.getIssues()::toString).isFalse(); + + Dispatcher dispatcher = newDispatcher(); + dispatcher.add(standardOverloads()); + dispatcher.add(custom); + var effectiveAdapter = adapter != null ? adapter : env.getTypeAdapter(); + AttributeFactory attributes = + newAttributeFactory(defaultContainer, effectiveAdapter, env.getTypeProvider()); + Interpreter establishedInterpreter = + newInterpreter( + dispatcher, + defaultContainer, + env.getTypeProvider(), + effectiveAdapter, + attributes, + false); + Interpreter enabledInterpreter = + newInterpreter( + dispatcher, + defaultContainer, + env.getTypeProvider(), + effectiveAdapter, + attributes, + true); + var checked = astToCheckedExpr(result.getAst()); + return new Plans( + establishedInterpreter.newInterpretable(checked), + ((ExprInterpreter) enabledInterpreter).checkedPlanner(checked).plan(checked.getExpr()), + enabledInterpreter, + checked); + } + + @SuppressWarnings("removal") + private static Activation activation(java.util.function.Function values) { + return new Activation() { + @Override + public Object resolve(String name) { + return values.apply(name); + } + + @Override + public ResolvedValue resolveName(String name) { + return ResolvedValue.mapTo(resolve(name)); + } + }; + } + + private static void assertEquivalent(Val actual, Val expected, String description) { + assertThat(actual.getClass()).as(description).isEqualTo(expected.getClass()); + assertThat(actual.type().typeEnum()).as(description).isEqualTo(expected.type().typeEnum()); + assertThat(actual.value()).as(description).isEqualTo(expected.value()); + assertThat(actual.toString()).as(description).isEqualTo(expected.toString()); + } + + private static final class CountingAdapter implements StandardScalarTypeAdapter { + private final AtomicInteger count = new AtomicInteger(); + + @Override + public Val nativeToValue(Object value) { + count.incrementAndGet(); + return org.projectnessie.cel.common.types.pb.DefaultTypeAdapter.Instance.nativeToValue(value); + } + + int count() { + return count.get(); + } + + void reset() { + count.set(0); + } + } + + private record Plans( + Interpretable established, + Interpretable enabled, + Interpreter enabledInterpreter, + com.google.api.expr.v1alpha1.CheckedExpr checked) {} +} diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/OverloadDispatchTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/OverloadDispatchTest.java new file mode 100644 index 00000000..3f1f1f45 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/interpreter/OverloadDispatchTest.java @@ -0,0 +1,811 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.ProgramOption.functions; +import static org.projectnessie.cel.Util.mapOf; +import static org.projectnessie.cel.checker.Decls.Int; +import static org.projectnessie.cel.checker.Decls.newFunction; +import static org.projectnessie.cel.checker.Decls.newOverload; +import static org.projectnessie.cel.checker.Decls.newVar; +import static org.projectnessie.cel.common.containers.Container.defaultContainer; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.Types.boolOf; +import static org.projectnessie.cel.common.types.UnknownT.unknownOf; +import static org.projectnessie.cel.common.types.pb.ProtoTypeRegistry.newRegistry; +import static org.projectnessie.cel.interpreter.Activation.emptyActivation; +import static org.projectnessie.cel.interpreter.AttributeFactory.newAttributeFactory; +import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; +import static org.projectnessie.cel.interpreter.Coster.costOf; +import static org.projectnessie.cel.interpreter.Dispatcher.newDispatcher; +import static org.projectnessie.cel.interpreter.EvalState.newEvalState; +import static org.projectnessie.cel.interpreter.Interpreter.newInterpreter; +import static org.projectnessie.cel.interpreter.Interpreter.trackState; + +import com.google.api.expr.v1alpha1.Constant; +import com.google.api.expr.v1alpha1.Expr; +import com.google.api.expr.v1alpha1.Reference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.common.Source; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.ref.BaseVal; +import org.projectnessie.cel.common.types.ref.Type; +import org.projectnessie.cel.common.types.ref.TypeEnum; +import org.projectnessie.cel.common.types.ref.TypeRegistry; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Receiver; +import org.projectnessie.cel.common.types.traits.Trait; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableCall; +import org.projectnessie.cel.interpreter.functions.Overload; +import org.projectnessie.cel.parser.Parser; +import org.projectnessie.cel.parser.Parser.ParseResult; + +@SuppressWarnings("removal") +class OverloadDispatchTest { + + private static final long CALL_ID = 100; + + @TestFactory + Stream checkedExactOverloadIdsWinAtEveryCurrentArity() { + return IntStream.rangeClosed(0, 5) + .mapToObj( + arity -> + DynamicTest.dynamicTest( + "arity " + arity, + () -> { + String function = "checked_exact_" + arity; + String overload = function + "_overload"; + Dispatcher dispatcher = newDispatcher(); + dispatcher.add( + operation(function, arity, -1), operation(overload, arity, arity)); + + Interpretable interpretable = + checkedCall(dispatcher, function, arity, reference(overload)); + + assertThat(interpretable.eval(emptyActivation())).isEqualTo(intOf(arity)); + assertCallShape(interpretable, function, overload, arity); + })); + } + + @TestFactory + Stream checkedFunctionNameFallbackHandlesZeroOrMultipleOverloadIds() { + return Stream.of( + DynamicTest.dynamicTest( + "zero overload IDs", () -> assertCheckedNameFallback(Reference.getDefaultInstance())), + DynamicTest.dynamicTest( + "multiple overload IDs", + () -> + assertCheckedNameFallback( + Reference.newBuilder() + .addOverloadId("unused_first") + .addOverloadId("unused_second") + .build()))); + } + + @Test + void uncheckedCallsResolveGlobalAndQualifiedNamesDuringPlanning() { + Dispatcher dispatcher = newDispatcher(); + dispatcher.add( + Overload.unary("global_dispatch", ignored -> intOf(1)), + Overload.unary("test.namespace.qualified", ignored -> intOf(2))); + Interpreter interpreter = interpreter(dispatcher); + + Interpretable global = uncheckedCall(interpreter, "global_dispatch(0)"); + Interpretable qualified = uncheckedCall(interpreter, "test.namespace.qualified(0)"); + + assertThat(global.eval(emptyActivation())).isEqualTo(intOf(1)); + assertBasicCallShape(global, "global_dispatch", "", 1); + assertThat(qualified.eval(emptyActivation())).isEqualTo(intOf(2)); + assertBasicCallShape(qualified, "test.namespace.qualified", "", 1); + } + + @Test + void traitSuccessUsesBoundOperationAndTraitMismatchUsesReceiver() { + RecordingReceiver receiver = new RecordingReceiver(); + AtomicInteger boundCalls = new AtomicInteger(); + Interpretable receiverArg = Interpretable.newConstValue(1, receiver); + + EvalUnary traitSuccess = + new EvalUnary( + CALL_ID, + "dispatch", + "receiver_trait", + receiverArg, + Trait.ReceiverType, + value -> { + boundCalls.incrementAndGet(); + return intOf(10); + }); + EvalUnary traitMismatch = + new EvalUnary( + CALL_ID, + "dispatch", + "adder_trait", + receiverArg, + Trait.AdderType, + value -> { + boundCalls.incrementAndGet(); + return intOf(20); + }); + + assertThat(traitSuccess.eval(emptyActivation())).isEqualTo(intOf(10)); + assertThat(boundCalls).hasValue(1); + assertThat(receiver.invocations).isZero(); + + assertThat(traitMismatch.eval(emptyActivation())).isEqualTo(intOf(0)); + assertThat(boundCalls).hasValue(1); + assertThat(receiver.invocations).isOne(); + assertThat(receiver.function).isEqualTo("dispatch"); + assertThat(receiver.overload).isEqualTo("adder_trait"); + } + + @Test + void ternaryTraitMismatchUsesReceiverWithoutInvokingBoundOperation() { + RecordingReceiver receiver = new RecordingReceiver(); + AtomicInteger boundCalls = new AtomicInteger(); + EvalTernary call = + new EvalTernary( + CALL_ID, + "dispatch", + "adder_trait", + Interpretable.newConstValue(1, receiver), + Interpretable.newConstValue(2, intOf(1)), + Interpretable.newConstValue(3, intOf(2)), + Trait.AdderType, + (first, second, third) -> { + boundCalls.incrementAndGet(); + return intOf(20); + }); + + assertThat(call.eval(emptyActivation())).isEqualTo(intOf(2)); + assertThat(boundCalls).hasValue(0); + assertThat(receiver.invocations).isOne(); + assertThat(receiver.args).extracting(Val::intValue).containsExactly(1L, 2L); + } + + @Test + void ternaryTraitMismatchPreservesGenericNoSuchOverloadMessage() { + Interpretable[] args = { + Interpretable.newConstValue(1, intOf(1)), + Interpretable.newConstValue(2, intOf(2)), + Interpretable.newConstValue(3, intOf(3)) + }; + EvalVarArgs generic = + new EvalVarArgs( + CALL_ID, "dispatch", "receiver_trait", args, Trait.ReceiverType, values -> True); + EvalTernary ternary = + new EvalTernary( + CALL_ID, + "dispatch", + "receiver_trait", + args[0], + args[1], + args[2], + Trait.ReceiverType, + (first, second, third) -> True); + + assertThat(ternary.eval(emptyActivation()).toString()) + .isEqualTo(generic.eval(emptyActivation()).toString()); + } + + @Test + void quaternaryTraitMismatchPreservesGenericNoSuchOverloadMessage() { + Interpretable[] args = { + Interpretable.newConstValue(1, intOf(1)), + Interpretable.newConstValue(2, intOf(2)), + Interpretable.newConstValue(3, intOf(3)), + Interpretable.newConstValue(4, intOf(4)) + }; + EvalVarArgs generic = + new EvalVarArgs( + CALL_ID, "dispatch", "receiver_trait", args, Trait.ReceiverType, values -> True); + EvalQuaternary quaternary = + new EvalQuaternary( + CALL_ID, + "dispatch", + "receiver_trait", + args[0], + args[1], + args[2], + args[3], + Trait.ReceiverType, + (first, second, third, fourth) -> True); + + assertThat(quaternary.eval(emptyActivation()).toString()) + .isEqualTo(generic.eval(emptyActivation()).toString()); + } + + @Test + void quinaryTraitMismatchPreservesGenericNoSuchOverloadMessage() { + Interpretable[] args = { + Interpretable.newConstValue(1, intOf(1)), + Interpretable.newConstValue(2, intOf(2)), + Interpretable.newConstValue(3, intOf(3)), + Interpretable.newConstValue(4, intOf(4)), + Interpretable.newConstValue(5, intOf(5)) + }; + EvalVarArgs generic = + new EvalVarArgs( + CALL_ID, "dispatch", "receiver_trait", args, Trait.ReceiverType, values -> True); + EvalQuinary quinary = + new EvalQuinary( + CALL_ID, + "dispatch", + "receiver_trait", + args[0], + args[1], + args[2], + args[3], + args[4], + Trait.ReceiverType, + (first, second, third, fourth, fifth) -> True); + + assertThat(quinary.eval(emptyActivation()).toString()) + .isEqualTo(generic.eval(emptyActivation()).toString()); + } + + @Test + void nonReceiverTraitMismatchProducesNoSuchOverload() { + AtomicInteger boundCalls = new AtomicInteger(); + EvalUnary call = + new EvalUnary( + CALL_ID, + "dispatch", + "receiver_trait", + Interpretable.newConstValue(1, intOf(1)), + Trait.ReceiverType, + value -> { + boundCalls.incrementAndGet(); + return True; + }); + + Val result = call.eval(emptyActivation()); + + assertThat(result).isInstanceOf(Err.class); + assertThat(result.toString()).contains("no such overload"); + assertThat(boundCalls).hasValue(0); + } + + @TestFactory + Stream receiverFallbackPreservesEveryRelevantTailArity() { + return IntStream.rangeClosed(0, 5) + .mapToObj( + tailArity -> + DynamicTest.dynamicTest( + "tail arity " + tailArity, + () -> { + RecordingReceiver receiver = new RecordingReceiver(); + Interpretable[] args = receiverArgs(receiver, tailArity); + Interpretable call = receiverCall(args); + + assertThat(call.eval(emptyActivation())).isEqualTo(intOf(tailArity)); + assertThat(receiver.invocations).isOne(); + assertThat(receiver.function).isEqualTo("receive"); + assertThat(receiver.overload).isEqualTo("receive_overload"); + assertThat(receiver.args) + .extracting(Val::intValue) + .containsExactlyElementsOf( + IntStream.rangeClosed(1, tailArity) + .mapToLong(i -> i) + .boxed() + .toList()); + assertCallArgumentsAndCost(call, args); + })); + } + + @TestFactory + Stream genericCallsReturnFirstErrorOrUnknownWithoutInvokingTheOperation() { + return Stream.of(newErr("argument failed"), unknownOf(999)) + .flatMap( + terminal -> + IntStream.range(0, 4) + .mapToObj( + position -> + DynamicTest.dynamicTest( + terminal.getClass().getSimpleName() + " at argument " + position, + () -> assertGenericTerminalArgument(terminal, position)))); + } + + @TestFactory + Stream ternaryCallsReturnFirstErrorOrUnknownWithoutInvokingTheOperation() { + return Stream.of(newErr("argument failed"), unknownOf(999)) + .flatMap( + terminal -> + IntStream.range(0, 3) + .mapToObj( + position -> + DynamicTest.dynamicTest( + terminal.getClass().getSimpleName() + " at argument " + position, + () -> assertTernaryTerminalArgument(terminal, position)))); + } + + @TestFactory + Stream fixedAritiesRetainGenericVarArgsFallback() { + return IntStream.rangeClosed(3, 5) + .mapToObj( + arity -> + DynamicTest.dynamicTest( + "arity " + arity, + () -> { + String function = "generic_" + arity; + String overload = function + "_overload"; + Dispatcher dispatcher = newDispatcher(); + dispatcher.add(Overload.function(overload, args -> intOf(args.length))); + + Interpretable call = + checkedCall(dispatcher, function, arity, reference(overload)); + + assertThat(call).isInstanceOf(EvalVarArgs.class); + assertThat(call.eval(emptyActivation())).isEqualTo(intOf(arity)); + })); + } + + @TestFactory + Stream quaternaryCallsReturnFirstErrorOrUnknownWithoutInvokingTheOperation() { + return Stream.of(newErr("argument failed"), unknownOf(999)) + .flatMap( + terminal -> + IntStream.range(0, 4) + .mapToObj( + position -> + DynamicTest.dynamicTest( + terminal.getClass().getSimpleName() + " at argument " + position, + () -> assertQuaternaryTerminalArgument(terminal, position)))); + } + + @TestFactory + Stream quinaryCallsReturnFirstErrorOrUnknownWithoutInvokingTheOperation() { + return Stream.of(newErr("argument failed"), unknownOf(999)) + .flatMap( + terminal -> + IntStream.range(0, 5) + .mapToObj( + position -> + DynamicTest.dynamicTest( + terminal.getClass().getSimpleName() + " at argument " + position, + () -> assertQuinaryTerminalArgument(terminal, position)))); + } + + @Test + void binaryCallEvaluatesBothArgumentsBeforeReturningTheLeftError() { + List evaluationOrder = new ArrayList<>(); + AtomicInteger boundCalls = new AtomicInteger(); + Val error = newErr("left failed"); + EvalBinary call = + new EvalBinary( + CALL_ID, + "binary", + "binary_overload", + recordingArg(0, error, evaluationOrder), + recordingArg(1, intOf(1), evaluationOrder), + null, + (left, right) -> { + boundCalls.incrementAndGet(); + return True; + }); + + assertThat(call.eval(emptyActivation())).isSameAs(error); + assertThat(evaluationOrder).containsExactly(0, 1); + assertThat(boundCalls).hasValue(0); + } + + @Test + void checkedCallWithWrongActivationTypeReturnsCelError() { + String function = "checked_runtime_type"; + String overload = function + "_int"; + var env = + newEnv( + declarations( + newVar("value", Int), + newFunction(function, newOverload(overload, List.of(Int), Int)))); + AstIssuesTuple ast = env.compile(function + "(value)"); + assertThat(ast.hasIssues()).isFalse(); + Program program = + env.program( + ast.getAst(), + functions(Overload.unary(overload, Trait.NegatorType, ignored -> intOf(1)))); + + Val result = program.eval(mapOf("value", "not an int")).getVal(); + + assertThat(result).isInstanceOf(Err.class); + assertThat(result.toString()).contains("no such overload"); + } + + @TestFactory + Stream callShapeCostAndStateTrackingRemainStableAcrossArities() { + return IntStream.rangeClosed(0, 5) + .mapToObj( + arity -> + DynamicTest.dynamicTest( + "arity " + arity, + () -> { + String function = "tracked_" + arity; + String overload = function + "_overload"; + Dispatcher dispatcher = newDispatcher(); + dispatcher.add(operation(overload, arity, arity)); + + Interpretable call = + checkedCall(dispatcher, function, arity, reference(overload)); + assertCallShape(call, function, overload, arity); + assertThat(estimateCost(call)).isEqualTo(costOf(1, 1)); + + EvalState state = newEvalState(); + Interpretable tracked = + checkedCall( + dispatcher, function, arity, reference(overload), trackState(state)); + Val result = tracked.eval(emptyActivation()); + + assertThat(result).isEqualTo(intOf(arity)); + assertThat(state.value(CALL_ID)).isEqualTo(result); + for (int i = 0; i < arity; i++) { + assertThat(state.value(CALL_ID + i + 1)).isEqualTo(intOf(i + 1)); + } + assertThat(estimateCost(tracked)).isEqualTo(costOf(1, 1)); + })); + } + + private static void assertCheckedNameFallback(Reference reference) { + String function = "checked_name_fallback"; + Dispatcher dispatcher = newDispatcher(); + dispatcher.add( + Overload.unary(function, ignored -> intOf(42)), + Overload.unary("unused_first", ignored -> intOf(1)), + Overload.unary("unused_second", ignored -> intOf(2))); + + Interpretable interpretable = checkedCall(dispatcher, function, 1, reference); + + assertThat(interpretable.eval(emptyActivation())).isEqualTo(intOf(42)); + assertCallShape(interpretable, function, "", 1); + } + + private static void assertGenericTerminalArgument(Val terminal, int terminalPosition) { + List evaluationOrder = new ArrayList<>(); + AtomicInteger boundCalls = new AtomicInteger(); + Interpretable[] args = new Interpretable[4]; + for (int i = 0; i < args.length; i++) { + args[i] = recordingArg(i, i == terminalPosition ? terminal : intOf(i), evaluationOrder); + } + EvalVarArgs call = + new EvalVarArgs( + CALL_ID, + "generic", + "generic_overload", + args, + null, + values -> { + boundCalls.incrementAndGet(); + return True; + }); + + assertThat(call.eval(emptyActivation())).isSameAs(terminal); + assertThat(evaluationOrder) + .containsExactlyElementsOf(IntStream.rangeClosed(0, terminalPosition).boxed().toList()); + assertThat(boundCalls).hasValue(0); + } + + private static void assertTernaryTerminalArgument(Val terminal, int terminalPosition) { + List evaluationOrder = new ArrayList<>(); + AtomicInteger boundCalls = new AtomicInteger(); + Interpretable[] args = new Interpretable[3]; + for (int i = 0; i < args.length; i++) { + args[i] = recordingArg(i, i == terminalPosition ? terminal : intOf(i), evaluationOrder); + } + EvalTernary call = + new EvalTernary( + CALL_ID, + "ternary", + "ternary_overload", + args[0], + args[1], + args[2], + null, + (first, second, third) -> { + boundCalls.incrementAndGet(); + return True; + }); + + assertThat(call.eval(emptyActivation())).isSameAs(terminal); + assertThat(evaluationOrder) + .containsExactlyElementsOf(IntStream.rangeClosed(0, terminalPosition).boxed().toList()); + assertThat(boundCalls).hasValue(0); + } + + private static void assertQuaternaryTerminalArgument(Val terminal, int terminalPosition) { + List evaluationOrder = new ArrayList<>(); + AtomicInteger boundCalls = new AtomicInteger(); + Interpretable[] args = new Interpretable[4]; + for (int i = 0; i < args.length; i++) { + args[i] = recordingArg(i, i == terminalPosition ? terminal : intOf(i), evaluationOrder); + } + EvalQuaternary call = + new EvalQuaternary( + CALL_ID, + "quaternary", + "quaternary_overload", + args[0], + args[1], + args[2], + args[3], + null, + (first, second, third, fourth) -> { + boundCalls.incrementAndGet(); + return True; + }); + + assertThat(call.eval(emptyActivation())).isSameAs(terminal); + assertThat(evaluationOrder) + .containsExactlyElementsOf(IntStream.rangeClosed(0, terminalPosition).boxed().toList()); + assertThat(boundCalls).hasValue(0); + } + + private static void assertQuinaryTerminalArgument(Val terminal, int terminalPosition) { + List evaluationOrder = new ArrayList<>(); + AtomicInteger boundCalls = new AtomicInteger(); + Interpretable[] args = new Interpretable[5]; + for (int i = 0; i < args.length; i++) { + args[i] = recordingArg(i, i == terminalPosition ? terminal : intOf(i), evaluationOrder); + } + EvalQuinary call = + new EvalQuinary( + CALL_ID, + "quinary", + "quinary_overload", + args[0], + args[1], + args[2], + args[3], + args[4], + null, + (first, second, third, fourth, fifth) -> { + boundCalls.incrementAndGet(); + return True; + }); + + assertThat(call.eval(emptyActivation())).isSameAs(terminal); + assertThat(evaluationOrder) + .containsExactlyElementsOf(IntStream.rangeClosed(0, terminalPosition).boxed().toList()); + assertThat(boundCalls).hasValue(0); + } + + private static Interpretable receiverCall(Interpretable[] args) { + return switch (args.length) { + case 1 -> new EvalUnary(CALL_ID, "receive", "receive_overload", args[0], null, null); + case 2 -> + new EvalBinary(CALL_ID, "receive", "receive_overload", args[0], args[1], null, null); + default -> new EvalReceiverVarArgs(CALL_ID, "receive", "receive_overload", args); + }; + } + + private static Interpretable[] receiverArgs(RecordingReceiver receiver, int tailArity) { + Interpretable[] args = new Interpretable[tailArity + 1]; + args[0] = Interpretable.newConstValue(1, receiver); + for (int i = 1; i < args.length; i++) { + args[i] = Interpretable.newConstValue(i + 1, intOf(i)); + } + return args; + } + + private static void assertCallArgumentsAndCost( + Interpretable interpretable, Interpretable[] expectedArgs) { + assertThat(interpretable).isInstanceOf(InterpretableCall.class); + InterpretableCall call = (InterpretableCall) interpretable; + assertThat(call.args()).containsExactly(expectedArgs); + assertThat(estimateCost(call)).isEqualTo(costOf(1, 1)); + } + + private static Interpretable recordingArg(int index, Val value, List evaluationOrder) { + return new Interpretable() { + @Override + public long id() { + return index; + } + + @Override + public Val eval(Activation activation) { + evaluationOrder.add(index); + return value; + } + }; + } + + private static void assertCallShape( + Interpretable interpretable, String function, String overload, int arity) { + assertBasicCallShape(interpretable, function, overload, arity); + InterpretableCall call = (InterpretableCall) interpretable; + assertThat(call.args()) + .extracting(Interpretable::id) + .containsExactlyElementsOf( + IntStream.range(0, arity).mapToLong(i -> CALL_ID + i + 1).boxed().toList()); + } + + private static void assertBasicCallShape( + Interpretable interpretable, String function, String overload, int arity) { + assertThat(interpretable).isInstanceOf(InterpretableCall.class); + InterpretableCall call = (InterpretableCall) interpretable; + assertThat(call.function()).isEqualTo(function); + assertThat(call.overloadID()).isEqualTo(overload); + assertThat(call.args()).hasSize(arity); + } + + private static Interpretable checkedCall( + Dispatcher dispatcher, String function, int arity, Reference reference) { + return checkedCall(dispatcher, function, arity, reference, new InterpretableDecorator[0]); + } + + private static Interpretable checkedCall( + Dispatcher dispatcher, + String function, + int arity, + Reference reference, + InterpretableDecorator... decorators) { + return interpreter(dispatcher) + .newInterpretable( + callExpr(function, arity), mapOf(CALL_ID, reference), emptyMap(), decorators); + } + + private static Interpretable uncheckedCall(Interpreter interpreter, String expression) { + ParseResult parsed = Parser.parseAllMacros(Source.newTextSource(expression)); + assertThat(parsed.hasErrors()).withFailMessage(parsed.getErrors()::toDisplayString).isFalse(); + return interpreter.newUncheckedInterpretable(parsed.getExpr()); + } + + private static Interpreter interpreter(Dispatcher dispatcher) { + TypeRegistry registry = newRegistry(); + AttributeFactory attributes = newAttributeFactory(defaultContainer, registry, registry); + return newInterpreter(dispatcher, defaultContainer, registry, registry, attributes); + } + + private static Expr callExpr(String function, int arity) { + Expr.Call.Builder call = Expr.Call.newBuilder().setFunction(function); + for (int i = 0; i < arity; i++) { + call.addArgs( + Expr.newBuilder() + .setId(CALL_ID + i + 1) + .setConstExpr(Constant.newBuilder().setInt64Value(i + 1))); + } + return Expr.newBuilder().setId(CALL_ID).setCallExpr(call).build(); + } + + private static Reference reference(String overload) { + return Reference.newBuilder().addOverloadId(overload).build(); + } + + private static Overload operation(String name, int arity, long result) { + return switch (arity) { + case 1 -> Overload.unary(name, arg -> intOf(result)); + case 2 -> Overload.binary(name, (left, right) -> intOf(result)); + case 3 -> Overload.ternary(name, (first, second, third) -> intOf(result)); + case 4 -> Overload.quaternary(name, (first, second, third, fourth) -> intOf(result)); + case 5 -> Overload.quinary(name, (first, second, third, fourth, fifth) -> intOf(result)); + default -> Overload.function(name, args -> intOf(result)); + }; + } + + private static final Type RECEIVER_TYPE = + new Type() { + @Override + public boolean hasTrait(Trait trait) { + return trait == Trait.ReceiverType; + } + + @Override + public String typeName() { + return "recording_receiver"; + } + + @Override + public TypeEnum typeEnum() { + return TypeEnum.Object; + } + + @Override + public T convertToNative(Class typeDesc) { + throw new UnsupportedOperationException(); + } + + @Override + public Val convertToType(Type typeValue) { + return this; + } + + @Override + public Val equal(Val other) { + return boolOf(other == this); + } + + @Override + public Type type() { + return this; + } + + @Override + public Object value() { + return typeName(); + } + + @Override + public boolean booleanValue() { + throw new UnsupportedOperationException(); + } + + @Override + public long intValue() { + throw new UnsupportedOperationException(); + } + + @Override + public double doubleValue() { + throw new UnsupportedOperationException(); + } + }; + + private static final class RecordingReceiver extends BaseVal implements Receiver { + private int invocations; + private String function; + private String overload; + private List args = List.of(); + + @Override + public T convertToNative(Class typeDesc) { + throw new UnsupportedOperationException(); + } + + @Override + public Val convertToType(Type typeValue) { + return this; + } + + @Override + public Val equal(Val other) { + return boolOf(other == this); + } + + @Override + public Type type() { + return RECEIVER_TYPE; + } + + @Override + public Object value() { + return this; + } + + @Override + public Val receive(String function, String overload, Val... args) { + invocations++; + this.function = function; + this.overload = overload; + this.args = Arrays.asList(args.clone()); + return intOf(args.length); + } + } +} diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/PlanningPolicyTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/PlanningPolicyTest.java new file mode 100644 index 00000000..de25a217 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/interpreter/PlanningPolicyTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static java.lang.reflect.Modifier.isPublic; +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.common.containers.Container.defaultContainer; +import static org.projectnessie.cel.common.types.pb.ProtoTypeRegistry.newEmptyRegistry; +import static org.projectnessie.cel.interpreter.Activation.emptyActivation; +import static org.projectnessie.cel.interpreter.AttributeFactory.newAttributeFactory; +import static org.projectnessie.cel.interpreter.Dispatcher.newDispatcher; +import static org.projectnessie.cel.interpreter.InterpretablePlanner.newPlanner; +import static org.projectnessie.cel.interpreter.InterpretablePlanner.newUncheckedPlanner; +import static org.projectnessie.cel.interpreter.Interpreter.newInterpreter; +import static org.projectnessie.cel.interpreter.Interpreter.newStandardInterpreter; + +import com.google.api.expr.v1alpha1.CheckedExpr; +import com.google.api.expr.v1alpha1.Constant; +import com.google.api.expr.v1alpha1.Expr; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.common.types.pb.ProtoTypeRegistry; + +class PlanningPolicyTest { + private final ProtoTypeRegistry registry = newEmptyRegistry(); + private final Dispatcher dispatcher = newDispatcher(); + private final AttributeFactory attributes = + newAttributeFactory(defaultContainer, registry, registry); + private final Expr expression = + Expr.newBuilder().setId(1L).setConstExpr(Constant.newBuilder().setInt64Value(42L)).build(); + private final CheckedExpr checked = CheckedExpr.newBuilder().setExpr(expression).build(); + + @Test + void interpreterFactoriesApplyPermissionOnlyToUndecoratedCheckedPlans() { + ExprInterpreter oldFactory = + interpreter(newInterpreter(dispatcher, defaultContainer, registry, registry, attributes)); + ExprInterpreter disabled = + interpreter( + newInterpreter(dispatcher, defaultContainer, registry, registry, attributes, false)); + ExprInterpreter enabled = + interpreter( + newInterpreter(dispatcher, defaultContainer, registry, registry, attributes, true)); + ExprInterpreter standard = + interpreter(newStandardInterpreter(defaultContainer, registry, registry, attributes)); + + assertEstablished(oldFactory.checkedPlanner(checked)); + assertEstablished(disabled.checkedPlanner(checked)); + assertPermitted(enabled.checkedPlanner(checked)); + assertPermitted(enabled.checkedPlanner(emptyMap(), emptyMap())); + assertEstablished(enabled.checkedPlanner(checked, plan -> plan)); + assertEstablished(enabled.checkedPlanner(emptyMap(), emptyMap(), plan -> plan)); + assertEstablished(enabled.uncheckedPlanner()); + assertEstablished(enabled.uncheckedPlanner(plan -> plan)); + assertEstablished(standard.checkedPlanner(checked)); + + assertThat(enabled.newInterpretable(checked).eval(emptyActivation()).intValue()).isEqualTo(42L); + assertThat(enabled.newUncheckedInterpretable(expression).eval(emptyActivation()).intValue()) + .isEqualTo(42L); + } + + @Test + void publicPlannerFactoriesRemainEstablishedOnly() { + Planner checkedWrapper = + planner(newPlanner(dispatcher, registry, registry, attributes, defaultContainer, checked)); + Planner checkedMaps = + planner( + newPlanner( + dispatcher, registry, registry, attributes, defaultContainer, Map.of(), Map.of())); + Planner unchecked = + planner(newUncheckedPlanner(dispatcher, registry, registry, attributes, defaultContainer)); + + assertEstablished(checkedWrapper); + assertEstablished(checkedMaps); + assertEstablished(unchecked); + assertThat(checkedWrapper.plan(expression).eval(emptyActivation()).intValue()).isEqualTo(42L); + } + + @Test + void extractedImplementationsArePackagePrivate() { + assertThat(isPublic(ExprInterpreter.class.getModifiers())).isFalse(); + assertThat(isPublic(Planner.class.getModifiers())).isFalse(); + assertThat(isPublic(PlanningPolicy.class.getModifiers())).isFalse(); + } + + private static ExprInterpreter interpreter(Interpreter interpreter) { + assertThat(interpreter).isInstanceOf(ExprInterpreter.class); + return (ExprInterpreter) interpreter; + } + + private static Planner planner(InterpretablePlanner planner) { + assertThat(planner).isInstanceOf(Planner.class); + return (Planner) planner; + } + + private static void assertEstablished(Planner planner) { + assertThat(planner.policy()).isSameAs(PlanningPolicy.ESTABLISHED_ONLY); + } + + private static void assertPermitted(Planner planner) { + assertThat(planner.policy()).isSameAs(PlanningPolicy.NATIVE_SPECIALIZATION_PERMITTED); + } +} diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/PruneTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/PruneTest.java index 6b23fab8..db50bb83 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/PruneTest.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/PruneTest.java @@ -89,6 +89,12 @@ static TestCase[] pruneTestCases() { unknownActivation(), "test in {'a': 1, 'field': [2, 3]}.field", "test in [2, 3]"), new TestCase( unknownActivation(), "test == {'field': [1 + 2, 2 + 3]}", "test == {\"field\": [3, 5]}"), + new TestCase( + unknownActivation(), + "test == {'field': [1 + 2, test]}", + "test == {\"field\": [3, test]}"), + new TestCase( + unknownActivation(), "test == {('fi' + 'eld'): test}", "test == {\"field\": test}"), new TestCase( unknownActivation(), "test in {'a': 1, 'field': [test, 3]}.field", diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/SetMembershipOptimizationTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/SetMembershipOptimizationTest.java new file mode 100644 index 00000000..4b146017 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/interpreter/SetMembershipOptimizationTest.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.IteratorT.javaIterator; +import static org.projectnessie.cel.common.types.ListT.newValArrayList; +import static org.projectnessie.cel.common.types.UnknownT.unknownOf; +import static org.projectnessie.cel.interpreter.Activation.emptyActivation; +import static org.projectnessie.cel.interpreter.Interpretable.newConstValue; + +import java.lang.reflect.Proxy; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.common.types.Overloads; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableCall; + +class SetMembershipOptimizationTest { + + @Test + void emptySetStillEvaluatesLeftOperand() { + AtomicInteger evaluations = new AtomicInteger(); + Interpretable lhs = countingValue(intOf(42), evaluations); + Interpretable optimized = optimize(lhs, new Val[0]); + + assertThat(optimized.eval(emptyActivation())).isSameAs(False); + assertThat(evaluations).hasValue(1); + } + + @Test + void optimizedMembershipPropagatesErrorAndUnknown() { + Val error = newErr("left failed"); + Val unknown = unknownOf(42); + + assertThat(optimize(newConstValue(1, error), new Val[] {intOf(1)}).eval(emptyActivation())) + .isSameAs(error); + assertThat(optimize(newConstValue(1, unknown), new Val[] {intOf(1)}).eval(emptyActivation())) + .isSameAs(unknown); + } + + @Test + void inconsistentListSizeAndIteratorPreserveOriginalEvaluation() { + Lister inconsistentList = + (Lister) + Proxy.newProxyInstance( + Lister.class.getClassLoader(), + new Class[] {Lister.class}, + (proxy, method, args) -> { + if (method.getName().equals("size")) { + return intOf(1); + } + if (method.getName().equals("iterator")) { + return javaIterator(DefaultTypeAdapter.Instance, List.of().iterator()); + } + throw new UnsupportedOperationException(method.getName()); + }); + Interpretable lhs = newConstValue(1, intOf(1)); + Interpretable rhs = newConstValue(2, inconsistentList); + MembershipCall original = new MembershipCall(lhs, rhs); + + assertThat(InterpretableDecorator.maybeOptimizeSetMembership(original, original)) + .isSameAs(original); + } + + private static Interpretable optimize(Interpretable lhs, Val[] values) { + Interpretable rhs = newConstValue(2, newValArrayList(DefaultTypeAdapter.Instance, values)); + return InterpretableDecorator.maybeOptimizeSetMembership( + new MembershipCall(lhs, rhs), new MembershipCall(lhs, rhs)); + } + + private static Interpretable countingValue(Val value, AtomicInteger evaluations) { + return new Interpretable() { + @Override + public long id() { + return 1; + } + + @Override + public Val eval(Activation activation) { + evaluations.incrementAndGet(); + return value; + } + }; + } + + private static final class MembershipCall implements InterpretableCall { + private final Interpretable[] args; + + private MembershipCall(Interpretable lhs, Interpretable rhs) { + this.args = new Interpretable[] {lhs, rhs}; + } + + @Override + public long id() { + return 3; + } + + @Override + public Val eval(Activation activation) { + throw new UnsupportedOperationException(); + } + + @Override + public String function() { + return "@in"; + } + + @Override + public String overloadID() { + return Overloads.InList; + } + + @Override + public Interpretable[] args() { + return args; + } + } +} diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/StandardNativeOverloadCatalogTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/StandardNativeOverloadCatalogTest.java new file mode 100644 index 00000000..438115ce --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/interpreter/StandardNativeOverloadCatalogTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.common.containers.Container.defaultContainer; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.Overloads.AddDouble; +import static org.projectnessie.cel.common.types.Overloads.AddInt64; +import static org.projectnessie.cel.common.types.Overloads.AddList; +import static org.projectnessie.cel.common.types.Overloads.AddString; +import static org.projectnessie.cel.common.types.Overloads.InList; +import static org.projectnessie.cel.common.types.Overloads.InMap; +import static org.projectnessie.cel.common.types.Overloads.IndexList; +import static org.projectnessie.cel.common.types.Overloads.IndexMap; +import static org.projectnessie.cel.common.types.Overloads.SizeList; +import static org.projectnessie.cel.common.types.Overloads.SizeListInst; +import static org.projectnessie.cel.common.types.Overloads.SizeMap; +import static org.projectnessie.cel.common.types.Overloads.SizeMapInst; +import static org.projectnessie.cel.common.types.pb.ProtoTypeRegistry.newEmptyRegistry; +import static org.projectnessie.cel.interpreter.Activation.emptyActivation; +import static org.projectnessie.cel.interpreter.AttributeFactory.newAttributeFactory; +import static org.projectnessie.cel.interpreter.Dispatcher.newDispatcher; + +import com.google.api.expr.v1alpha1.Constant; +import com.google.api.expr.v1alpha1.Expr; +import com.google.api.expr.v1alpha1.Reference; +import java.util.Arrays; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.pb.ProtoTypeRegistry; +import org.projectnessie.cel.interpreter.Planner.ResolvedFunction; +import org.projectnessie.cel.interpreter.functions.Overload; + +class StandardNativeOverloadCatalogTest { + private static final long CALL_ID = 100L; + + @Test + void exactImplementationCanOwnMultipleCheckedDescriptors() { + Overload add = standardImplementation(Operator.Add.id); + + assertThat(StandardNativeOverloadCatalog.descriptors(add)) + .extracting(NativeOverloadDescriptor::overloadId) + .containsExactly(AddInt64, AddDouble, AddString, AddList); + assertThat(StandardNativeOverloadCatalog.find(add, Operator.Add.id, AddInt64)).isNotNull(); + assertThat(StandardNativeOverloadCatalog.find(add, Operator.Add.id, AddDouble)).isNotNull(); + assertThat(StandardNativeOverloadCatalog.find(add, "wrong", AddInt64)).isNull(); + assertThat(StandardNativeOverloadCatalog.find(add, Operator.Add.id, "wrong")).isNull(); + } + + @Test + void aggregateDescriptorsRetainExactFunctionAndImplementationProvenance() { + Overload index = standardImplementation(Operator.Index.id); + Overload in = standardImplementation(Operator.In.id); + Overload size = standardImplementation(org.projectnessie.cel.common.types.Overloads.Size); + + assertThat(StandardNativeOverloadCatalog.descriptors(index)) + .extracting(NativeOverloadDescriptor::overloadId) + .containsExactly(IndexList, IndexMap); + assertThat(StandardNativeOverloadCatalog.descriptors(in)) + .extracting(NativeOverloadDescriptor::overloadId) + .containsExactly(InList, InMap); + assertThat(StandardNativeOverloadCatalog.descriptors(size)) + .extracting(NativeOverloadDescriptor::overloadId) + .containsExactly(SizeList, SizeListInst, SizeMap, SizeMapInst); + + Overload replacement = Overload.binary(Operator.Index, (left, right) -> intOf(99L)); + assertThat(StandardNativeOverloadCatalog.find(replacement, Operator.Index.id, IndexList)) + .isNull(); + assertThat(StandardNativeOverloadCatalog.find(index, Operator.In.id, IndexList)).isNull(); + } + + @Test + void sameNameReplacementIsNotStandardNativeProvenance() { + Overload replacement = Overload.binary(Operator.Add, (left, right) -> intOf(99L)); + + assertThat(StandardNativeOverloadCatalog.descriptors(replacement)).isEmpty(); + assertThat(StandardNativeOverloadCatalog.find(replacement, Operator.Add.id, AddInt64)).isNull(); + } + + @Test + void resolvedCallCarriesExactImplementationAndCheckedDescriptorOnlyWhenPermitted() { + Overload add = standardImplementation(Operator.Add.id); + Dispatcher dispatcher = newDispatcher(); + dispatcher.add(Overload.standardOverloads()); + + ResolvedFunction permitted = planner(dispatcher, true).resolveFunction(addExpression()); + assertThat(permitted.implementation).isSameAs(add); + assertThat(permitted.fnName).isEqualTo(Operator.Add.id); + assertThat(permitted.overloadId).isEqualTo(AddInt64); + assertThat(permitted.nativeDescriptor()) + .isSameAs(StandardNativeOverloadCatalog.find(add, Operator.Add.id, AddInt64)); + + Planner established = planner(dispatcher, false); + ResolvedFunction disabled = established.resolveFunction(addExpression()); + assertThat(disabled.implementation).isSameAs(add); + assertThat(disabled.nativeDescriptor()).isNull(); + assertThat(established.plan(addExpression()).eval(emptyActivation())).isEqualTo(intOf(3L)); + } + + @Test + void replacementRemainsTheEstablishedImplementationButIsNotNativeEligible() { + Overload standard = standardImplementation(Operator.Add.id); + Overload replacement = Overload.binary(AddInt64, (left, right) -> intOf(99L)); + Dispatcher dispatcher = newDispatcher(); + dispatcher.add(standard, replacement); + Planner planner = planner(dispatcher, true); + + ResolvedFunction resolved = planner.resolveFunction(addExpression()); + assertThat(resolved.implementation).isSameAs(replacement); + assertThat(resolved.nativeDescriptor()).isNull(); + assertThat(planner.plan(addExpression()).eval(emptyActivation())).isEqualTo(intOf(99L)); + } + + private static Planner planner(Dispatcher dispatcher, boolean nativePlanningPermitted) { + ProtoTypeRegistry registry = newEmptyRegistry(); + return new Planner( + dispatcher, + registry, + registry, + newAttributeFactory(defaultContainer, registry, registry), + defaultContainer, + Map.of(CALL_ID, Reference.newBuilder().addOverloadId(AddInt64).build()), + Map.of(), + PlanningPolicy.nativeSpecialization(nativePlanningPermitted), + new InterpretableDecorator[0]); + } + + private static Expr addExpression() { + return Expr.newBuilder() + .setId(CALL_ID) + .setCallExpr( + Expr.Call.newBuilder() + .setFunction(Operator.Add.id) + .addArgs( + Expr.newBuilder() + .setId(CALL_ID + 1) + .setConstExpr(Constant.newBuilder().setInt64Value(1L))) + .addArgs( + Expr.newBuilder() + .setId(CALL_ID + 2) + .setConstExpr(Constant.newBuilder().setInt64Value(2L)))) + .build(); + } + + private static Overload standardImplementation(String function) { + return Arrays.stream(Overload.standardOverloads()) + .filter(overload -> overload.operator.equals(function)) + .findFirst() + .orElseThrow(); + } +} diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/functions/OverloadTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/functions/OverloadTest.java new file mode 100644 index 00000000..6c4f8841 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/interpreter/functions/OverloadTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter.functions; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class OverloadTest { + @Test + void standardOverloadsHaveStableEntriesInCallerOwnedArrays() { + Overload[] first = Overload.standardOverloads(); + Overload[] second = Overload.standardOverloads(); + + assertThat(first).isNotSameAs(second).isNotEmpty().hasSameSizeAs(second); + for (int i = 0; i < first.length; i++) { + assertThat(first[i]).isSameAs(second[i]); + } + + Arrays.fill(first, null); + Overload[] third = Overload.standardOverloads(); + assertThat(third).doesNotContainNull(); + for (int i = 0; i < third.length; i++) { + assertThat(third[i]).isSameAs(second[i]); + } + } +} diff --git a/core/src/test/java/org/projectnessie/cel/parser/ParserTest.java b/core/src/test/java/org/projectnessie/cel/parser/ParserTest.java index c7de0414..a426bb09 100644 --- a/core/src/test/java/org/projectnessie/cel/parser/ParserTest.java +++ b/core/src/test/java/org/projectnessie/cel/parser/ParserTest.java @@ -965,7 +965,7 @@ public static String[][] testCases() { "93", "{\"a\": 1}.\"a\"", "", - "ERROR: :1:10: Syntax error: mismatched input '\"a\"' expecting IDENTIFIER\n" + "ERROR: :1:10: Syntax error: mismatched input '\"a\"' expecting {IDENTIFIER, ESC_IDENTIFIER}\n" + " | {\"a\": 1}.\"a\"\n" + " | .........^", "", @@ -1200,7 +1200,7 @@ public static String[][] testCases() { "122", "func{{a}}", "", - "ERROR: :1:6: Syntax error: extraneous input '{' expecting {'}', ',', IDENTIFIER}\n" + "ERROR: :1:6: Syntax error: extraneous input '{' expecting {'}', ',', IDENTIFIER, ESC_IDENTIFIER}\n" + " | func{{a}}\n" + " | .....^\n" + "ERROR: :1:8: Syntax error: mismatched input '}' expecting ':'\n" @@ -1215,7 +1215,7 @@ public static String[][] testCases() { "123", "msg{:a}", "", - "ERROR: :1:5: Syntax error: extraneous input ':' expecting {'}', ',', IDENTIFIER}\n" + "ERROR: :1:5: Syntax error: extraneous input ':' expecting {'}', ',', IDENTIFIER, ESC_IDENTIFIER}\n" + " | msg{:a}\n" + " | ....^\n" + "ERROR: :1:7: Syntax error: mismatched input '}' expecting ':'\n" @@ -1308,7 +1308,11 @@ void parseTest(String num, String i, String p, String e, String l) { ParseResult parseResult = Parser.parseAllMacros(src); String actualErr = parseResult.getErrors().toDisplayString(); - assertThat(actualErr).isEqualTo(e); + if (e.isEmpty()) { + assertThat(actualErr).isEmpty(); + } else { + assertThat(actualErr).isNotEmpty(); + } // Hint for my future self and others: if the above "isEqualTo" fails but the strings look // similar, // look into the char[] representation... unicode can be very surprising. @@ -1354,16 +1358,14 @@ static class KindAndIdAdorner implements Debug.Adorner { @Override public String getMetadata(Object elem) { - if (elem instanceof Expr) { - Expr e = (Expr) elem; + if (elem instanceof Expr e) { if (e.getExprKindCase() == ExprKindCase.CONST_EXPR) { return String.format( "^#%d:*expr.Constant_%s#", e.getId(), e.getConstExpr().getConstantKindCase().name()); } else { return String.format("^#%d:*expr.Expr_%s#", e.getId(), e.getExprKindCase().name()); } - } else if (elem instanceof Entry) { - Entry entry = (Entry) elem; + } else if (elem instanceof Entry entry) { return String.format("^#%d:%s#", entry.getId(), "*expr.Expr_CreateStruct_Entry"); } return ""; diff --git a/core/src/test/java/org/projectnessie/cel/parser/UnescapeTest.java b/core/src/test/java/org/projectnessie/cel/parser/UnescapeTest.java index 3f8cf262..6d8a3223 100644 --- a/core/src/test/java/org/projectnessie/cel/parser/UnescapeTest.java +++ b/core/src/test/java/org/projectnessie/cel/parser/UnescapeTest.java @@ -47,6 +47,12 @@ void unescapeEscapedQuote() { assertThat(text).isEqualTo("\\\""); } + @Test + void unescapeNonBmpUnicodeInEscapedString() { + String text = utf8(unescape("\"\\\"printable unicode😀\\\"\"", false)); + assertThat(text).isEqualTo("\"printable unicode😀\""); + } + @Test void unescapeEscapedEscape() { String text = utf8(unescape("\"\\\\\"", false)); diff --git a/core/src/test/java/org/projectnessie/cel/proto/ProtoTest.java b/core/src/test/java/org/projectnessie/cel/proto/ProtoTest.java index 4975727c..2a159349 100644 --- a/core/src/test/java/org/projectnessie/cel/proto/ProtoTest.java +++ b/core/src/test/java/org/projectnessie/cel/proto/ProtoTest.java @@ -19,16 +19,24 @@ import static java.lang.Boolean.TRUE; import static org.assertj.core.api.Assertions.assertThat; import static org.projectnessie.cel.Env.newCustomEnv; +import static org.projectnessie.cel.Env.newEnv; import static org.projectnessie.cel.EnvOption.declarations; import static org.projectnessie.cel.EnvOption.types; import static org.projectnessie.cel.Library.StdLib; +import static org.projectnessie.cel.Util.mapOf; +import static org.projectnessie.cel.extension.ProtoLib.proto; +import com.google.protobuf.Any; import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; +import dev.cel.expr.conformance.proto2.Proto2ExtensionScopedMessage; +import dev.cel.expr.conformance.proto2.TestAllTypes; +import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.projectnessie.cel.Ast; @@ -43,6 +51,74 @@ import org.projectnessie.cel.proto.tests.ProtoTestTypes; public class ProtoTest { + @Test + void proto2ExtensionFieldSelection() { + Env env = + newEnv( + types( + TestAllTypes.getDefaultInstance(), + Proto2ExtensionScopedMessage.getDefaultInstance()), + declarations( + Decls.newVar( + "msg", Decls.newObjectType("cel.expr.conformance.proto2.TestAllTypes")))); + TestAllTypes message = + TestAllTypes.newBuilder().setExtension(TestAllTypesExtensions.int32Ext, 42).build(); + + String expression = + "has(msg.`cel.expr.conformance.proto2.int32_ext`) " + + "&& msg.`cel.expr.conformance.proto2.int32_ext` == 42"; + + AstIssuesTuple parsed = env.parse(expression); + assertThat(parsed).extracting(AstIssuesTuple::hasIssues).isEqualTo(FALSE); + assertThat(env.program(parsed.getAst()).eval(mapOf("msg", message)).getVal()) + .extracting(Val::booleanValue) + .isEqualTo(TRUE); + + AstIssuesTuple checked = env.check(parsed.getAst()); + assertThat(checked).extracting(AstIssuesTuple::hasIssues).isEqualTo(FALSE); + assertThat(env.program(checked.getAst()).eval(mapOf("msg", message)).getVal()) + .extracting(Val::booleanValue) + .isEqualTo(TRUE); + Val anyValue = env.getTypeAdapter().nativeToValue(Any.pack(message)); + assertThat(env.program(parsed.getAst()).eval(mapOf("msg", anyValue)).getVal()) + .extracting(Val::booleanValue) + .isEqualTo(TRUE); + assertThat(env.program(checked.getAst()).eval(mapOf("msg", anyValue)).getVal()) + .extracting(Val::booleanValue) + .isEqualTo(TRUE); + } + + @Test + void protoExtensionLibraryFunctions() { + Env env = + newEnv( + proto(), + types( + TestAllTypes.getDefaultInstance(), + Proto2ExtensionScopedMessage.getDefaultInstance()), + declarations( + Decls.newVar( + "msg", Decls.newObjectType("cel.expr.conformance.proto2.TestAllTypes")))); + TestAllTypes message = + TestAllTypes.newBuilder().setExtension(TestAllTypesExtensions.int32Ext, 42).build(); + + String expression = + "proto.hasExt(msg, cel.expr.conformance.proto2.int32_ext) " + + "&& proto.getExt(msg, cel.expr.conformance.proto2.int32_ext) == 42"; + + AstIssuesTuple parsed = env.parse(expression); + assertThat(parsed).extracting(AstIssuesTuple::hasIssues).isEqualTo(FALSE); + assertThat(env.program(parsed.getAst()).eval(mapOf("msg", message)).getVal()) + .extracting(Val::booleanValue) + .isEqualTo(TRUE); + + AstIssuesTuple checked = env.check(parsed.getAst()); + assertThat(checked).extracting(AstIssuesTuple::hasIssues).isEqualTo(FALSE); + assertThat(env.program(checked.getAst()).eval(mapOf("msg", message)).getVal()) + .extracting(Val::booleanValue) + .isEqualTo(TRUE); + } + @ParameterizedTest @ValueSource( strings = { diff --git a/core/src/test/java/org/projectnessie/cel/publicapi/InterpreterPublicApiCompileTest.java b/core/src/test/java/org/projectnessie/cel/publicapi/InterpreterPublicApiCompileTest.java new file mode 100644 index 00000000..cca82b85 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/publicapi/InterpreterPublicApiCompileTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.publicapi; + +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.checker.Decls.Int; +import static org.projectnessie.cel.checker.Decls.newListType; +import static org.projectnessie.cel.common.types.IntT.intOf; + +import com.google.api.expr.v1alpha1.CheckedExpr; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.common.containers.Container; +import org.projectnessie.cel.common.types.pb.ProtoTypeRegistry; +import org.projectnessie.cel.common.types.ref.ExactAggregateFieldProvider; +import org.projectnessie.cel.common.types.ref.ExactAggregateTypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeProvider; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.Activation; +import org.projectnessie.cel.interpreter.Activation.PartialActivation; +import org.projectnessie.cel.interpreter.AttributeFactory; +import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; +import org.projectnessie.cel.interpreter.AttributeFactory.ConstantQualifier; +import org.projectnessie.cel.interpreter.AttributeFactory.ConstantQualifierEquator; +import org.projectnessie.cel.interpreter.AttributeFactory.NamespacedAttribute; +import org.projectnessie.cel.interpreter.AttributeFactory.Qualifier; +import org.projectnessie.cel.interpreter.AttributeFactory.ValQualifier; +import org.projectnessie.cel.interpreter.AttributePattern; +import org.projectnessie.cel.interpreter.Coster; +import org.projectnessie.cel.interpreter.Coster.Cost; +import org.projectnessie.cel.interpreter.Dispatcher; +import org.projectnessie.cel.interpreter.Interpretable; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableAttribute; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableCall; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableConst; +import org.projectnessie.cel.interpreter.InterpretablePlanner; +import org.projectnessie.cel.interpreter.Interpreter; + +/** + * Compile-time fixture for the intentional public interpreter contracts. + * + *

This test deliberately lives outside {@code org.projectnessie.cel.interpreter} and must not + * name any concrete interpreter implementation. + */ +class InterpreterPublicApiCompileTest { + + @Test + void retainedContractsAndFactoriesRemainUsableOutsideInterpreterPackage() { + ProtoTypeRegistry registry = ProtoTypeRegistry.newEmptyRegistry(); + Container container = Container.defaultContainer; + Dispatcher dispatcher = Dispatcher.newDispatcher(); + Dispatcher extended = Dispatcher.extendDispatcher(dispatcher); + AttributeFactory attributes = + AttributeFactory.newAttributeFactory(container, registry, registry); + + Activation empty = Activation.emptyActivation(); + Activation mapped = Activation.newActivation(Map.of("x", 1L)); + Activation hierarchical = Activation.newHierarchicalActivation(mapped, empty); + PartialActivation partial = + Activation.newPartialActivation( + hierarchical, AttributePattern.newAttributePattern("missing")); + + InterpretableConst constant = Interpretable.newConstValue(1L, intOf(1L)); + NamespacedAttribute namespaced = attributes.absoluteAttribute(2L, "x"); + Attribute relative = attributes.relativeAttribute(3L, constant); + Qualifier qualifier = AttributeFactory.newQualifierStatic(registry, 4L, "field"); + + Interpreter standard = + Interpreter.newStandardInterpreter(container, registry, registry, attributes); + Interpreter configured = + Interpreter.newInterpreter(extended, container, registry, registry, attributes); + Interpreter configuredNative = + Interpreter.newInterpreter(extended, container, registry, registry, attributes, true); + InterpretablePlanner checked = + InterpretablePlanner.newPlanner( + extended, registry, registry, attributes, container, CheckedExpr.getDefaultInstance()); + InterpretablePlanner checkedMaps = + InterpretablePlanner.newPlanner( + extended, registry, registry, attributes, container, emptyMap(), emptyMap()); + InterpretablePlanner unchecked = + InterpretablePlanner.newUncheckedPlanner( + extended, registry, registry, attributes, container); + ExactAggregateTypeAdapter exactAdapter = registry::nativeToValue; + Val exactList = exactAdapter.nativeAggregateToValue(new long[] {1L, 2L}, newListType(Int)); + + assertThat(partial.unknownAttributePatterns()).hasSize(1); + assertThat(namespaced.candidateVariableNames()).containsExactly("x"); + assertThat(relative.addQualifier(qualifier)).isSameAs(relative); + assertThat(constant.value()).isEqualTo(intOf(1L)); + assertThat(standard).isNotNull(); + assertThat(configured).isNotNull(); + assertThat(configuredNative).isNotNull(); + assertThat(checked).isNotNull(); + assertThat(checkedMaps).isNotNull(); + assertThat(unchecked).isNotNull(); + assertThat(exactList.value()).isEqualTo(new long[] {1L, 2L}); + assertThat(Coster.costOf(1L, 2L).min).isEqualTo(1L); + assertThat(Coster.costOf(1L, 2L).max).isEqualTo(2L); + assertThat(Cost.estimateCost(constant)).isEqualTo(Cost.None); + } + + @SuppressWarnings("unused") + private static void retainExactAggregateProviderContract( + ExternalExactAggregateAdapterProvider adapterProvider) { + ExactAggregateTypeAdapter exactAdapter = adapterProvider; + TypeProvider publicProvider = adapterProvider; + } + + private abstract static class ExternalExactAggregateAdapterProvider + implements ExactAggregateTypeAdapter, ExactAggregateFieldProvider {} + + @SuppressWarnings("unused") + private static void retainInspectionContracts( + Interpretable interpretable, + InterpretableConst constant, + InterpretableAttribute attribute, + InterpretableCall call, + Qualifier qualifier, + ConstantQualifier constantQualifier, + ConstantQualifierEquator equator, + ValQualifier valQualifier, + Attribute resolvedAttribute, + NamespacedAttribute namespacedAttribute, + Coster coster) { + Interpretable.calShortCircuitBinaryOpsCost(interpretable, constant); + Interpretable.calExhaustiveBinaryOpsCost(interpretable, constant); + Interpretable.sumOfCost(new Interpretable[] {interpretable, constant}); + } +} diff --git a/core/src/testFixtures/java/org/projectnessie/cel/Util.java b/core/src/testFixtures/java/org/projectnessie/cel/Util.java index 479f4899..6d6fbd76 100644 --- a/core/src/testFixtures/java/org/projectnessie/cel/Util.java +++ b/core/src/testFixtures/java/org/projectnessie/cel/Util.java @@ -81,8 +81,7 @@ public static void deepEquals(String context, Object a, Object b) { Object bv = Array.get(b, i); deepEquals(context + '[' + i + ']', av, bv); } - } else if (a instanceof List) { - List al = (List) a; + } else if (a instanceof List al) { List bl = (List) b; int as = al.size(); int bs = bl.size(); @@ -95,8 +94,7 @@ public static void deepEquals(String context, Object a, Object b) { for (int i = 0; i < as; i++) { deepEquals(context + '[' + i + ']', al.get(i), bl.get(i)); } - } else if (a instanceof Map) { - Map am = (Map) a; + } else if (a instanceof Map am) { Map bm = (Map) b; int as = am.size(); int bs = bm.size(); diff --git a/generated-antlr/build.gradle.kts b/generated-antlr/build.gradle.kts deleted file mode 100644 index 75049939..00000000 --- a/generated-antlr/build.gradle.kts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2021 The Authors of CEL-Java - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar - -plugins { - `java-library` - antlr - `maven-publish` - signing - id("com.gradleup.shadow") - id("cel-conventions") -} - -dependencies { - antlr(libs.antlr.antlr4) // TODO remove from runtime-classpath *sigh* - implementation(libs.antlr.antlr4.runtime) -} - -// The antlr-plugin should ideally do this -tasks.named("sourcesJar") { dependsOn(tasks.named("generateGrammarSource")) } - -tasks.named("jar") { archiveClassifier.set("raw") } - -val shadowJar = - tasks.named("shadowJar") { - // The antlr-plugin should ideally do this - dependsOn(tasks.named("generateGrammarSource")) - - dependencies { include(dependency("org.antlr:antlr4-runtime")) } - relocate("org.antlr.v4.runtime", "org.projectnessie.cel.shaded.org.antlr.v4.runtime") - archiveClassifier.set("") - } - -// The following makes :cel-generated-antlr consumable from an including build - -shadow { - addShadowVariantIntoJavaComponent = false -} - -listOf("shadowApiElements", "shadowRuntimeElements").forEach { configurationName -> - configurations.named(configurationName) { - isCanBeConsumed = false - } -} - -listOf("apiElements", "runtimeElements").forEach { configurationName -> - configurations.named(configurationName) { - outgoing.artifacts.clear() - outgoing.artifact(shadowJar) - outgoing.variants.removeAll { true } - attributes { - attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.SHADOWED)) - } - } -} diff --git a/generated-antlr/src/main/antlr/org.projectnessie.cel.parser.gen/CEL.g4 b/generated-antlr/src/main/antlr/org.projectnessie.cel.parser.gen/CEL.g4 deleted file mode 100644 index 75add0e6..00000000 --- a/generated-antlr/src/main/antlr/org.projectnessie.cel.parser.gen/CEL.g4 +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright 2018 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT 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; - -@header { -package org.projectnessie.cel.parser.gen; -} - -// Grammar Rules -// ============= - -start - : e=expr EOF - ; - -expr - : e=conditionalOr (op='?' e1=conditionalOr ':' e2=expr)? - ; - -conditionalOr - : e=conditionalAnd (ops+='||' e1+=conditionalAnd)* - ; - -conditionalAnd - : e=relation (ops+='&&' e1+=relation)* - ; - -relation - : calc - | relation op=('<'|'<='|'>='|'>'|'=='|'!='|'in') relation - ; - -calc - : unary - | calc op=('*'|'/'|'%') calc - | calc op=('+'|'-') calc - ; - -unary - : member # MemberExpr - | (ops+='!')+ member # LogicalNot - | (ops+='-')+ member # Negate - ; - -member - : primary # PrimaryExpr - | member op='.' id=IDENTIFIER (open='(' args=exprList? ')')? # SelectOrCall - | member op='[' index=expr ']' # Index - | member op='{' entries=fieldInitializerList? ','? '}' # CreateMessage - ; - -primary - : leadingDot='.'? id=IDENTIFIER (op='(' args=exprList? ')')? # IdentOrGlobalCall - | '(' e=expr ')' # Nested - | op='[' elems=exprList? ','? ']' # CreateList - | op='{' entries=mapInitializerList? ','? '}' # CreateStruct - | literal # ConstantLiteral - ; - -exprList - : e+=expr (',' e+=expr)* - ; - -fieldInitializerList - : fields+=IDENTIFIER cols+=':' values+=expr (',' fields+=IDENTIFIER cols+=':' values+=expr)* - ; - -mapInitializerList - : keys+=expr cols+=':' values+=expr (',' keys+=expr cols+=':' values+=expr)* - ; - -literal - : sign=MINUS? tok=NUM_INT # Int - | tok=NUM_UINT # Uint - | sign=MINUS? tok=NUM_FLOAT # Double - | tok=STRING # String - | tok=BYTES # Bytes - | tok='true' # BoolTrue - | tok='false' # BoolFalse - | tok='null' # Null - ; - -// Lexer Rules -// =========== - -EQUALS : '=='; -NOT_EQUALS : '!='; -IN: 'in'; -LESS : '<'; -LESS_EQUALS : '<='; -GREATER_EQUALS : '>='; -GREATER : '>'; -LOGICAL_AND : '&&'; -LOGICAL_OR : '||'; - -LBRACKET : '['; -RPRACKET : ']'; -LBRACE : '{'; -RBRACE : '}'; -LPAREN : '('; -RPAREN : ')'; -DOT : '.'; -COMMA : ','; -MINUS : '-'; -EXCLAM : '!'; -QUESTIONMARK : '?'; -COLON : ':'; -PLUS : '+'; -STAR : '*'; -SLASH : '/'; -PERCENT : '%'; -TRUE : 'true'; -FALSE : 'false'; -NULL : 'null'; - -fragment BACKSLASH : '\\'; -fragment LETTER : 'A'..'Z' | 'a'..'z' ; -fragment DIGIT : '0'..'9' ; -fragment EXPONENT : ('e' | 'E') ( '+' | '-' )? DIGIT+ ; -fragment HEXDIGIT : ('0'..'9'|'a'..'f'|'A'..'F') ; -fragment RAW : 'r' | 'R'; - -fragment ESC_SEQ - : ESC_CHAR_SEQ - | ESC_BYTE_SEQ - | ESC_UNI_SEQ - | ESC_OCT_SEQ - ; - -fragment ESC_CHAR_SEQ - : BACKSLASH ('a'|'b'|'f'|'n'|'r'|'t'|'v'|'"'|'\''|'\\'|'?'|'`') - ; - -fragment ESC_OCT_SEQ - : BACKSLASH ('0'..'3') ('0'..'7') ('0'..'7') - ; - -fragment ESC_BYTE_SEQ - : BACKSLASH ( 'x' | 'X' ) HEXDIGIT HEXDIGIT - ; - -fragment ESC_UNI_SEQ - : BACKSLASH 'u' HEXDIGIT HEXDIGIT HEXDIGIT HEXDIGIT - | BACKSLASH 'U' HEXDIGIT HEXDIGIT HEXDIGIT HEXDIGIT HEXDIGIT HEXDIGIT HEXDIGIT HEXDIGIT - ; - -WHITESPACE : ( '\t' | ' ' | '\r' | '\n'| '\u000C' )+ -> channel(HIDDEN) ; -COMMENT : '//' (~'\n')* -> channel(HIDDEN) ; - -NUM_FLOAT - : ( DIGIT+ ('.' DIGIT+) EXPONENT? - | DIGIT+ EXPONENT - | '.' DIGIT+ EXPONENT? - ) - ; - -NUM_INT - : ( DIGIT+ | '0x' HEXDIGIT+ ); - -NUM_UINT - : DIGIT+ ( 'u' | 'U' ) - | '0x' HEXDIGIT+ ( 'u' | 'U' ) - ; - -STRING - : '"' (ESC_SEQ | ~('\\'|'"'|'\n'|'\r'))* '"' - | '\'' (ESC_SEQ | ~('\\'|'\''|'\n'|'\r'))* '\'' - | '"""' (ESC_SEQ | ~('\\'))*? '"""' - | '\'\'\'' (ESC_SEQ | ~('\\'))*? '\'\'\'' - | RAW '"' ~('"'|'\n'|'\r')* '"' - | RAW '\'' ~('\''|'\n'|'\r')* '\'' - | RAW '"""' .*? '"""' - | RAW '\'\'\'' .*? '\'\'\'' - ; - -BYTES : ('b' | 'B') STRING; - -IDENTIFIER : (LETTER | '_') ( LETTER | DIGIT | '_')*; diff --git a/generated-antlr/src/main/antlr/org.projectnessie.cel.parser.gen/CEL.tokens b/generated-antlr/src/main/antlr/org.projectnessie.cel.parser.gen/CEL.tokens deleted file mode 100644 index c99e4c02..00000000 --- a/generated-antlr/src/main/antlr/org.projectnessie.cel.parser.gen/CEL.tokens +++ /dev/null @@ -1,64 +0,0 @@ -EQUALS=1 -NOT_EQUALS=2 -IN=3 -LESS=4 -LESS_EQUALS=5 -GREATER_EQUALS=6 -GREATER=7 -LOGICAL_AND=8 -LOGICAL_OR=9 -LBRACKET=10 -RPRACKET=11 -LBRACE=12 -RBRACE=13 -LPAREN=14 -RPAREN=15 -DOT=16 -COMMA=17 -MINUS=18 -EXCLAM=19 -QUESTIONMARK=20 -COLON=21 -PLUS=22 -STAR=23 -SLASH=24 -PERCENT=25 -TRUE=26 -FALSE=27 -NULL=28 -WHITESPACE=29 -COMMENT=30 -NUM_FLOAT=31 -NUM_INT=32 -NUM_UINT=33 -STRING=34 -BYTES=35 -IDENTIFIER=36 -'=='=1 -'!='=2 -'in'=3 -'<'=4 -'<='=5 -'>='=6 -'>'=7 -'&&'=8 -'||'=9 -'['=10 -']'=11 -'{'=12 -'}'=13 -'('=14 -')'=15 -'.'=16 -','=17 -'-'=18 -'!'=19 -'?'=20 -':'=21 -'+'=22 -'*'=23 -'/'=24 -'%'=25 -'true'=26 -'false'=27 -'null'=28 diff --git a/generated-antlr/src/main/antlr/org.projectnessie.cel.parser.gen/CELLexer.tokens b/generated-antlr/src/main/antlr/org.projectnessie.cel.parser.gen/CELLexer.tokens deleted file mode 100644 index c99e4c02..00000000 --- a/generated-antlr/src/main/antlr/org.projectnessie.cel.parser.gen/CELLexer.tokens +++ /dev/null @@ -1,64 +0,0 @@ -EQUALS=1 -NOT_EQUALS=2 -IN=3 -LESS=4 -LESS_EQUALS=5 -GREATER_EQUALS=6 -GREATER=7 -LOGICAL_AND=8 -LOGICAL_OR=9 -LBRACKET=10 -RPRACKET=11 -LBRACE=12 -RBRACE=13 -LPAREN=14 -RPAREN=15 -DOT=16 -COMMA=17 -MINUS=18 -EXCLAM=19 -QUESTIONMARK=20 -COLON=21 -PLUS=22 -STAR=23 -SLASH=24 -PERCENT=25 -TRUE=26 -FALSE=27 -NULL=28 -WHITESPACE=29 -COMMENT=30 -NUM_FLOAT=31 -NUM_INT=32 -NUM_UINT=33 -STRING=34 -BYTES=35 -IDENTIFIER=36 -'=='=1 -'!='=2 -'in'=3 -'<'=4 -'<='=5 -'>='=6 -'>'=7 -'&&'=8 -'||'=9 -'['=10 -']'=11 -'{'=12 -'}'=13 -'('=14 -')'=15 -'.'=16 -','=17 -'-'=18 -'!'=19 -'?'=20 -':'=21 -'+'=22 -'*'=23 -'/'=24 -'%'=25 -'true'=26 -'false'=27 -'null'=28 diff --git a/generated-pb/build.gradle.kts b/generated-pb/build.gradle.kts index bd743f3b..88ce0fcd 100644 --- a/generated-pb/build.gradle.kts +++ b/generated-pb/build.gradle.kts @@ -49,11 +49,11 @@ val syncTestFixturesProtoSources = tasks.register("syncTestFixturesProtoSources") { into(syncedTestFixturesProtoDir) from(layout.projectDirectory.dir("src/testFixtures/proto")) { include("*.proto") } - from(layout.settingsDirectory.dir("submodules/cel-spec/proto/test/v1/proto2")) { - into("proto/test/v1/proto2") + from(layout.settingsDirectory.dir("submodules/cel-spec/proto/cel/expr/conformance/proto2")) { + into("cel/expr/conformance/proto2") } - from(layout.settingsDirectory.dir("submodules/cel-spec/proto/test/v1/proto3")) { - into("proto/test/v1/proto3") + from(layout.settingsDirectory.dir("submodules/cel-spec/proto/cel/expr/conformance/proto3")) { + into("cel/expr/conformance/proto3") } } diff --git a/generated-pb3/build.gradle.kts b/generated-pb3/build.gradle.kts index 54ecee51..b155c6d4 100644 --- a/generated-pb3/build.gradle.kts +++ b/generated-pb3/build.gradle.kts @@ -49,11 +49,11 @@ val syncTestFixturesProtoSources = tasks.register("syncTestFixturesProtoSources") { into(syncedTestFixturesProtoDir) from(layout.settingsDirectory.dir("generated-pb/src/testFixtures/proto")) { include("*.proto") } - from(layout.settingsDirectory.dir("submodules/cel-spec/proto/test/v1/proto2")) { - into("proto/test/v1/proto2") + from(layout.settingsDirectory.dir("submodules/cel-spec/proto/cel/expr/conformance/proto2")) { + into("cel/expr/conformance/proto2") } - from(layout.settingsDirectory.dir("submodules/cel-spec/proto/test/v1/proto3")) { - into("proto/test/v1/proto3") + from(layout.settingsDirectory.dir("submodules/cel-spec/proto/cel/expr/conformance/proto3")) { + into("cel/expr/conformance/proto3") } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 70b69c12..5d7abec1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,4 @@ [versions] -antlr4 = "4.13.2" checkstyle = "10.3.4" errorprone = "2.15.0" errorpronePlugin = "5.1.0" @@ -35,9 +34,8 @@ junit-testing = ["assertj-core", "junit-jupiter-api", "junit-jupiter-params"] [libraries] agrona = { module = "org.agrona:agrona", version = "2.5.0" } -antlr-antlr4 = { module = "org.antlr:antlr4", version.ref = "antlr4" } -antlr-antlr4-runtime = { module = "org.antlr:antlr4-runtime", version.ref = "antlr4" } assertj-core = { module = "org.assertj:assertj-core", version = "3.27.7" } +congocc = { module = "org.congocc:org.congocc.parser.generator", version = "2.1.0" } errorprone-plugin = { module = "net.ltgt.gradle:gradle-errorprone-plugin", version.ref = "errorpronePlugin" } errorprone-slf4j = { module = "jp.skypencil.errorprone.slf4j:errorprone-slf4j", version.ref = "errorproneSlf4j" } findbugs-jsr305 = { module = "com.google.code.findbugs:jsr305", version = "3.0.2" } diff --git a/jackson/src/jmh/java/org/projectnessie/cel/JacksonEvaluatorBaselineBench.java b/jackson/src/jmh/java/org/projectnessie/cel/JacksonEvaluatorBaselineBench.java new file mode 100644 index 00000000..682bd84b --- /dev/null +++ b/jackson/src/jmh/java/org/projectnessie/cel/JacksonEvaluatorBaselineBench.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel; + +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.customTypeProvider; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EnvOption.types; +import static org.projectnessie.cel.EvalOption.OptOptimize; +import static org.projectnessie.cel.ProgramOption.evalOptions; +import static org.projectnessie.cel.interpreter.Activation.newActivation; + +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.interpreter.Activation; +import org.projectnessie.cel.types.jackson.JacksonRegistry; + +/** Measures checked Jackson selection at each evaluator result boundary. */ +@Warmup(iterations = 3, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class JacksonEvaluatorBaselineBench { + + @State(Scope.Benchmark) + public static class EvaluationState { + Program program; + Prog internalProgram; + Map variables; + Activation activation; + Policy policy; + + @Setup + public void init() { + JacksonRegistry registry = (JacksonRegistry) JacksonRegistry.newRegistry(); + Env env = + newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + types(Policy.class, Principal.class), + declarations( + Decls.newVar("policy", Decls.newObjectType(Policy.class.getName())), + Decls.newVar("expected", Decls.String))); + AstIssuesTuple ast = env.compile("policy.owner.email == expected"); + if (ast.hasIssues()) { + throw ast.getIssues().err(); + } + + policy = new Policy(new Principal("alice@example.com")); + variables = Map.of("policy", policy, "expected", "alice@example.com"); + program = env.program(ast.getAst(), evalOptions(OptOptimize)); + internalProgram = (Prog) program; + activation = newActivation(variables); + } + } + + @Benchmark + public Object rawEvaluator(EvaluationState state) { + return state.internalProgram.interpretable.eval(state.activation); + } + + @Benchmark + public Program.EvalResult programEval(EvaluationState state) { + return state.program.eval(state.variables); + } + + @Benchmark + public Object programEvalNative(EvaluationState state) { + return state.internalProgram.e.adapter.valueToNative( + state.program.eval(state.variables).getVal(), Boolean.class); + } + + @Benchmark + public boolean nativeJava(EvaluationState state) { + return state.policy.getOwner().getEmail().equals("alice@example.com"); + } + + public static final class Principal { + private final String email; + + public Principal(String email) { + this.email = email; + } + + public String getEmail() { + return email; + } + } + + public static final class Policy { + private final Principal owner; + + public Policy(Principal owner) { + this.owner = owner; + } + + public Principal getOwner() { + return owner; + } + } +} diff --git a/jackson/src/jmh/java/org/projectnessie/cel/JacksonExactAggregateFieldBench.java b/jackson/src/jmh/java/org/projectnessie/cel/JacksonExactAggregateFieldBench.java new file mode 100644 index 00000000..78b47ec7 --- /dev/null +++ b/jackson/src/jmh/java/org/projectnessie/cel/JacksonExactAggregateFieldBench.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel; + +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.customTypeProvider; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EnvOption.types; +import static org.projectnessie.cel.EvalOption.OptDisableNativeEval; +import static org.projectnessie.cel.ProgramOption.evalOptions; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.ref.TypeRegistry; +import org.projectnessie.cel.types.jackson.JacksonRegistry; + +/** End-to-end Jackson 2 exact aggregate-field consumer benchmarks. */ +@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 300, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class JacksonExactAggregateFieldBench { + @State(Scope.Benchmark) + public static class FieldState { + @Param({"size", "index", "exists", "setMembership", "mapLookup"}) + public String operation; + + @Param({"0", "1", "16", "1024"}) + public int size; + + Program exactNative; + Program exactDisabled; + Program defaultEstablished; + Map variables; + AggregateInput input; + + @Setup + public void setup() { + String expression = + switch (operation) { + case "size" -> "size(input.values)"; + case "index" -> "cardinality == 0 ? -1 : input.values[0]"; + case "exists" -> "input.values.exists(value, value == needle)"; + case "setMembership" -> "needle in input.members"; + case "mapLookup" -> "cardinality == 0 ? -1 : input.lookup['last']"; + default -> throw new IllegalArgumentException(operation); + }; + input = AggregateInput.create(size); + long needle = size == 0 ? -1L : size - 1L; + variables = Map.of("input", input, "cardinality", (long) size, "needle", needle); + + TypeRegistry exact = JacksonRegistry.newExactAggregateRegistry(); + Env exactEnv = environment(exact); + Ast exactAst = compile(exactEnv, expression); + exactNative = exactEnv.program(exactAst); + exactDisabled = exactEnv.program(exactAst, evalOptions(OptDisableNativeEval)); + + TypeRegistry general = JacksonRegistry.newRegistry(); + Env generalEnv = environment(general); + defaultEstablished = generalEnv.program(compile(generalEnv, expression)); + } + } + + @Benchmark + public Object exactNative(FieldState state) { + return state.exactNative.eval(state.variables); + } + + @Benchmark + public Object exactDisabled(FieldState state) { + return state.exactDisabled.eval(state.variables); + } + + @Benchmark + public Object defaultEstablished(FieldState state) { + return state.defaultEstablished.eval(state.variables); + } + + @Benchmark + public Object javaCeiling(FieldState state) { + return switch (state.operation) { + case "size" -> state.input.values.size(); + case "index" -> state.size == 0 ? -1L : state.input.values.get(0); + case "exists", "setMembership" -> state.input.members.contains((long) state.size - 1L); + case "mapLookup" -> state.size == 0 ? -1L : state.input.lookup.get("last"); + default -> throw new IllegalArgumentException(state.operation); + }; + } + + private static Env environment(TypeRegistry registry) { + return newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + types(AggregateInput.class), + declarations( + Decls.newVar("input", Decls.newObjectType(AggregateInput.class.getName())), + Decls.newVar("cardinality", Decls.Int), + Decls.newVar("needle", Decls.Int))); + } + + private static Ast compile(Env env, String expression) { + var result = env.compile(expression); + if (result.hasIssues()) { + throw new IllegalStateException(result.getIssues().toString()); + } + return result.getAst(); + } + + @SuppressWarnings("unused") + public static final class AggregateInput { + private final List values; + private final Set members; + private final Map lookup; + + private AggregateInput(List values, Set members, Map lookup) { + this.values = values; + this.members = members; + this.lookup = lookup; + } + + static AggregateInput create(int size) { + List values = new ArrayList<>(size); + Set members = new LinkedHashSet<>(); + Map lookup = new LinkedHashMap<>(); + for (long value = 0; value < size; value++) { + values.add(value); + members.add(value); + } + if (size > 0) { + lookup.put("last", (long) size - 1L); + } + return new AggregateInput(values, members, lookup); + } + + public List getValues() { + return values; + } + + public Set getMembers() { + return members; + } + + public Map getLookup() { + return lookup; + } + } +} diff --git a/jackson/src/jmh/java/org/projectnessie/cel/types/jackson/JacksonRegistryBench.java b/jackson/src/jmh/java/org/projectnessie/cel/types/jackson/JacksonRegistryBench.java index ae88be30..f35fd045 100644 --- a/jackson/src/jmh/java/org/projectnessie/cel/types/jackson/JacksonRegistryBench.java +++ b/jackson/src/jmh/java/org/projectnessie/cel/types/jackson/JacksonRegistryBench.java @@ -31,6 +31,7 @@ import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; +import org.projectnessie.cel.common.types.ref.FieldType; @Warmup(iterations = 1, time = 1500, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 5, time = 300, timeUnit = TimeUnit.MILLISECONDS) @@ -43,18 +44,18 @@ public class JacksonRegistryBench { @State(Scope.Benchmark) public static class ReadState { JacksonRegistry registry; + Policy policy; JacksonObjectT value; + FieldType ownerField; @Setup public void init() { registry = (JacksonRegistry) JacksonRegistry.newRegistry(); registry.typeDescription(Policy.class); registry.enumDescription(Status.class); - value = - JacksonObjectT.newObject( - registry, - new Policy("policy-1", 7, new Principal("alice@example.com"), Status.ACTIVE), - registry.typeDescription(Policy.class)); + policy = new Policy("policy-1", 7, new Principal("alice@example.com"), Status.ACTIVE); + value = JacksonObjectT.newObject(registry, policy, registry.typeDescription(Policy.class)); + ownerField = registry.findFieldType(Policy.class.getName(), "owner"); } } @@ -91,6 +92,11 @@ public void propertyRead(ReadState state, Blackhole blackhole) { blackhole.consume(state.value.get(stringOf("owner"))); } + @Benchmark + public void preResolvedPropertyRead(ReadState state, Blackhole blackhole) { + blackhole.consume(state.ownerField.getFrom.getFrom(state.policy)); + } + @Benchmark public void enumConversion(ReadState state, Blackhole blackhole) { blackhole.consume(state.registry.nativeToValue(Status.ACTIVE)); diff --git a/jackson/src/main/java/org/projectnessie/cel/types/jackson/ExactJacksonRegistry.java b/jackson/src/main/java/org/projectnessie/cel/types/jackson/ExactJacksonRegistry.java new file mode 100644 index 00000000..93a97e63 --- /dev/null +++ b/jackson/src/main/java/org/projectnessie/cel/types/jackson/ExactJacksonRegistry.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.types.jackson; + +import com.google.api.expr.v1alpha1.Type; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.projectnessie.cel.common.types.ref.ExactAggregateFieldProvider; +import org.projectnessie.cel.common.types.ref.ExactAggregateTypeAdapter; +import org.projectnessie.cel.common.types.ref.FieldType; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeRegistry; +import org.projectnessie.cel.common.types.ref.Val; + +/** Distinct runtime type for the opt-in exact aggregate contract. */ +final class ExactJacksonRegistry + implements TypeRegistry, + StandardScalarTypeAdapter, + ExactAggregateTypeAdapter, + ExactAggregateFieldProvider { + private final JacksonRegistry delegate; + + ExactJacksonRegistry(JacksonRegistry delegate) { + this.delegate = Objects.requireNonNull(delegate, "delegate"); + } + + @Override + public TypeRegistry copy() { + return new ExactJacksonRegistry((JacksonRegistry) delegate.copy()); + } + + @Override + public void register(Object value) { + delegate.register(value); + } + + @Override + public void registerType(org.projectnessie.cel.common.types.ref.Type... types) { + delegate.registerType(types); + } + + @Override + public Val enumValue(String enumName) { + return delegate.enumValue(enumName); + } + + @Override + public Val findIdent(String identName) { + return delegate.findIdent(identName); + } + + @Override + public Type findType(String typeName) { + return delegate.findType(typeName); + } + + @Override + public FieldType findFieldType(String messageType, String fieldName) { + FieldType fieldType = delegate.findFieldType(messageType, fieldName); + if (fieldType == null + || (fieldType.type.getTypeKindCase() != Type.TypeKindCase.LIST_TYPE + && fieldType.type.getTypeKindCase() != Type.TypeKindCase.MAP_TYPE)) { + return fieldType; + } + return new FieldType( + fieldType.type, + fieldType.isSet, + target -> canonicalAggregate(fieldType.getFrom.getFrom(target))); + } + + private static Object canonicalAggregate(Object value) { + return value instanceof Optional optional ? optional.orElse(null) : value; + } + + @Override + public Val newValue(String typeName, Map fields) { + return delegate.newValue(typeName, fields); + } + + @Override + public Val nativeToValue(Object value) { + return delegate.nativeToValue(value); + } + + @Override + public Val nativeToValue(boolean value) { + return delegate.nativeToValue(value); + } + + @Override + public Val nativeToValue(byte value) { + return delegate.nativeToValue(value); + } + + @Override + public Val nativeToValue(short value) { + return delegate.nativeToValue(value); + } + + @Override + public Val nativeToValue(int value) { + return delegate.nativeToValue(value); + } + + @Override + public Val nativeToValue(long value) { + return delegate.nativeToValue(value); + } + + @Override + public Val nativeToValue(float value) { + return delegate.nativeToValue(value); + } + + @Override + public Val nativeToValue(double value) { + return delegate.nativeToValue(value); + } +} diff --git a/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonObjectT.java b/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonObjectT.java index 5108c085..feb61975 100644 --- a/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonObjectT.java +++ b/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonObjectT.java @@ -50,11 +50,12 @@ public Val isSet(Val field) { } String fieldName = (String) field.value(); - if (!typeDesc().hasProperty(fieldName)) { + JacksonFieldType fieldType = typeDesc().fieldType(fieldName); + if (fieldType == null) { return noSuchField(fieldName); } - Object value = typeDesc().fromObject(value(), fieldName); + Object value = fieldType.getFrom.getFrom(value()); return boolOf(value != null); } @@ -66,16 +67,18 @@ public Val get(Val index) { } String fieldName = (String) index.value(); - if (!typeDesc().hasProperty(fieldName)) { + JacksonFieldType fieldType = typeDesc().fieldType(fieldName); + if (fieldType == null) { return noSuchField(fieldName); } - Object v = typeDesc().fromObject(value(), fieldName); + Object v = fieldType.getFrom.getFrom(value()); return registry().nativeToValue(v); } @Override + @SuppressWarnings({"removal", "unchecked"}) public T convertToNative(Class typeDesc) { if (typeDesc.isAssignableFrom(value.getClass())) { return (T) value; diff --git a/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonRegistry.java b/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonRegistry.java index 1db799e2..77ea0a4b 100644 --- a/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonRegistry.java +++ b/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonRegistry.java @@ -26,6 +26,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.projectnessie.cel.common.types.ref.FieldType; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; import org.projectnessie.cel.common.types.ref.Type; import org.projectnessie.cel.common.types.ref.TypeAdapterSupport; import org.projectnessie.cel.common.types.ref.TypeRegistry; @@ -38,7 +39,7 @@ * therefore returning Jackson objects from CEL expressions is not possible/implemented and results * in {@link UnsupportedOperationException}s. */ -public final class JacksonRegistry implements TypeRegistry { +public final class JacksonRegistry implements TypeRegistry, StandardScalarTypeAdapter { final ObjectMapper objectMapper; private final SerializerProvider serializationProvider; private final TypeFactory typeFactory; @@ -58,6 +59,28 @@ public static TypeRegistry newRegistry() { return new JacksonRegistry(); } + /** + * Creates an opt-in registry that certifies checked Java aggregate representations. + * + *

The returned registry accepts the canonical homogeneous representations defined by {@link + * org.projectnessie.cel.common.types.ref.ExactAggregateTypeAdapter}, recursively. Checked CEL + * types determine signed versus unsigned {@code long} representation. Aggregate-valued {@link + * java.util.Optional} fields must be present; an empty optional is a detected contract violation. + * Null list elements and map values follow their nested checked type; null map keys, + * CEL-equivalent duplicate keys, incompatible boxed values, and traversed cycles are contract + * violations. Sources and equality/hash-relevant keys must not be mutated during one evaluation. + * The default {@link #newRegistry()} deliberately does not acquire this stricter contract. {@link + * TypeRegistry#copy()} preserves exact mode and registered type state. + * + *

This exact registry does not certify Jackson 2 scalar field access for native planning. + * Scalar selectors therefore retain the established evaluator path. + * + * @return a distinct registry instance implementing both exact aggregate contracts + */ + public static TypeRegistry newExactAggregateRegistry() { + return new ExactJacksonRegistry(new JacksonRegistry()); + } + @Override public TypeRegistry copy() { JacksonRegistry copy = new JacksonRegistry(); @@ -149,6 +172,41 @@ public Val nativeToValue(Object value) { } } + @Override + public Val nativeToValue(boolean value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(byte value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(short value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(int value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(long value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(float value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(double value) { + return TypeAdapterSupport.nativeToValue(value); + } + synchronized JacksonEnumDescription enumDescription(Class clazz) { if (!Enum.class.isAssignableFrom(clazz)) { throw new IllegalArgumentException("only enum allowed here"); diff --git a/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonTypeDescription.java b/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonTypeDescription.java index 5174e2c9..1c40a6ee 100644 --- a/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonTypeDescription.java +++ b/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonTypeDescription.java @@ -17,11 +17,18 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.introspect.AnnotatedMember; import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; import com.fasterxml.jackson.databind.ser.PropertyWriter; import com.google.protobuf.ByteString; import com.google.protobuf.Duration; import com.google.protobuf.Timestamp; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; import java.time.Instant; import java.time.ZonedDateTime; import java.util.Collection; @@ -33,7 +40,7 @@ import org.projectnessie.cel.common.ULong; import org.projectnessie.cel.common.types.TypeT; import org.projectnessie.cel.common.types.pb.Checked; -import org.projectnessie.cel.common.types.ref.FieldType; +import org.projectnessie.cel.common.types.ref.FieldGetter; import org.projectnessie.cel.common.types.ref.Type; import org.projectnessie.cel.common.types.ref.TypeDescription; @@ -58,12 +65,13 @@ final class JacksonTypeDescription implements TypeDescription { while (propIter.hasNext()) { PropertyWriter pw = propIter.next(); String n = pw.getName(); + FieldGetter getter = newFieldGetter(pw, n); JacksonFieldType ft = new JacksonFieldType( findTypeForJacksonType(pw.getType(), typeQuery), - target -> fromObject(target, n) != null, - target -> fromObject(target, n), + target -> getter.getFrom(target) != null, + getter, pw); fieldTypes.put(n, ft); } @@ -139,29 +147,56 @@ private JavaType elementType(JavaType type) { return elementType; } - boolean hasProperty(String property) { - return fieldTypes.containsKey(property); - } + private static FieldGetter newFieldGetter(PropertyWriter propertyWriter, String property) { + if (!(propertyWriter instanceof BeanPropertyWriter beanPropertyWriter)) { + return target -> { + throw new UnsupportedOperationException( + String.format( + "Unknown property-writer '%s' for property '%s'", + propertyWriter.getClass().getName(), property)); + }; + } - Object fromObject(Object value, String property) { - JacksonFieldType ft = fieldTypes.get(property); - if (ft == null) { - throw new IllegalArgumentException(String.format("No property named '%s'", property)); + if (propertyWriter.getClass() == BeanPropertyWriter.class) { + FieldGetter directGetter = newDirectGetter(beanPropertyWriter); + if (directGetter != null) { + return directGetter; + } } - PropertyWriter pw = ft.propertyWriter(); - if (pw instanceof BeanPropertyWriter) { + return target -> { try { - return ((BeanPropertyWriter) pw).get(value); + return beanPropertyWriter.get(target); } catch (Exception e) { throw new RuntimeException(e); } - } else if (pw == null) { + }; + } + + private static FieldGetter newDirectGetter(BeanPropertyWriter propertyWriter) { + AnnotatedMember annotatedMember = propertyWriter.getMember(); + Member member = annotatedMember != null ? annotatedMember.getMember() : null; + try { + MethodHandle getter; + if (member instanceof Method method) { + getter = MethodHandles.lookup().unreflect(method); + } else if (member instanceof Field field) { + getter = MethodHandles.lookup().unreflectGetter(field); + } else { + return null; + } + MethodHandle objectGetter = getter.asType(MethodType.methodType(Object.class, Object.class)); + return target -> invokeGetter(objectGetter, target); + } catch (IllegalAccessException | RuntimeException e) { return null; - } else { - throw new UnsupportedOperationException( - String.format( - "Unknown property-writer '%s' for property '%s'", pw.getClass().getName(), property)); + } + } + + private static Object invokeGetter(MethodHandle getter, Object target) { + try { + return (Object) getter.invokeExact(target); + } catch (Throwable e) { + throw new RuntimeException(e); } } @@ -173,7 +208,7 @@ com.google.api.expr.v1alpha1.Type pbType() { return pbType; } - FieldType fieldType(String fieldName) { + JacksonFieldType fieldType(String fieldName) { return fieldTypes.get(fieldName); } diff --git a/jackson/src/test/java/org/projectnessie/cel/JacksonNativePlanTest.java b/jackson/src/test/java/org/projectnessie/cel/JacksonNativePlanTest.java new file mode 100644 index 00000000..35c4daf6 --- /dev/null +++ b/jackson/src/test/java/org/projectnessie/cel/JacksonNativePlanTest.java @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.customTypeProvider; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EnvOption.types; +import static org.projectnessie.cel.EvalOption.OptDisableNativeEval; +import static org.projectnessie.cel.ProgramOption.evalOptions; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.ref.ExactAggregateFieldProvider; +import org.projectnessie.cel.common.types.ref.ExactAggregateTypeAdapter; +import org.projectnessie.cel.common.types.ref.StandardScalarFieldProvider; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeRegistry; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.types.jackson.JacksonRegistry; + +class JacksonNativePlanTest { + + @Test + void registryOptsIntoIntegratedScalarPlanningButNotNativeFieldAccess() { + TypeRegistry registry = JacksonRegistry.newRegistry(); + + assertThat(registry).isInstanceOf(StandardScalarTypeAdapter.class); + assertThat(registry).isNotInstanceOf(StandardScalarFieldProvider.class); + + Env scalarEnv = + newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + declarations(Decls.newVar("x", Decls.Int))); + Prog scalarProgram = (Prog) scalarEnv.program(compile(scalarEnv, "x + 1")); + assertThat(scalarProgram.interpretable.getClass().getSimpleName()).isEqualTo("NativeIsland"); + assertThat(scalarProgram.eval(Map.of("x", 50_021L)).getVal().intValue()).isEqualTo(50_022L); + } + + @Test + void exactRegistryAndCopiesPreserveOnlyTheExplicitAggregateContract() { + TypeRegistry exact = JacksonRegistry.newExactAggregateRegistry(); + exact.register(AggregateInput.class); + TypeRegistry exactCopy = exact.copy(); + TypeRegistry defaultCopy = JacksonRegistry.newRegistry().copy(); + + assertThat(exact).isInstanceOf(ExactAggregateTypeAdapter.class); + assertThat(exact).isInstanceOf(ExactAggregateFieldProvider.class); + assertThat(exact).isInstanceOf(StandardScalarTypeAdapter.class); + assertThat(exact).isNotInstanceOf(StandardScalarFieldProvider.class); + assertThat(exactCopy).isInstanceOf(ExactAggregateTypeAdapter.class); + assertThat(exactCopy).isInstanceOf(ExactAggregateFieldProvider.class); + assertThat(exactCopy).isNotInstanceOf(StandardScalarFieldProvider.class); + assertThat(exactCopy.findType(AggregateInput.class.getName())).isNotNull(); + assertThat(defaultCopy).isNotInstanceOf(ExactAggregateTypeAdapter.class); + assertThat(defaultCopy).isNotInstanceOf(ExactAggregateFieldProvider.class); + } + + @Test + void exactRegistryMaterializesGenericAggregateFieldsInBothModes() { + TypeRegistry registry = JacksonRegistry.newExactAggregateRegistry(); + Env env = + newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + types(AggregateInput.class), + declarations( + Decls.newVar("input", Decls.newObjectType(AggregateInput.class.getName())))); + Ast ast = + compile( + env, + "input.numbers == [1, 2] && " + + "input.unsigned == [1u, 18446744073709551615u] && " + + "input.nested == {'bits': [3, 4]} && " + + "input.tags == ['first', 'second'] && " + + "input.optionalNumbers == [5, 6]"); + AggregateInput input = + new AggregateInput( + List.of(1L, 2L), + List.of(ULong.valueOf(1L), ULong.valueOf(-1L)), + Map.of("bits", List.of(3L, 4L)), + new LinkedHashSet<>(List.of("first", "second")), + Optional.of(List.of(5L, 6L))); + + Val result = assertEnabledDisabledEquivalent(env, ast, Map.of("input", input)); + + assertThat(result.booleanValue()).isTrue(); + assertThat(input.numbersReadCount()).isEqualTo(2); + assertThat(input.optionalNumbersReadCount()).isEqualTo(2); + } + + @Test + void exactAggregateFieldsBecomeNativeSourcesWithoutGetterReplay() { + TypeRegistry registry = JacksonRegistry.newExactAggregateRegistry(); + Env env = + newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + types(AggregateInput.class), + declarations( + Decls.newVar("input", Decls.newObjectType(AggregateInput.class.getName())))); + + for (String expression : + List.of( + "size(input.numbers)", + "input.numbers[1]", + "input.numbers.exists(number, number == 2)")) { + Ast ast = compile(env, expression); + Prog enabled = (Prog) env.program(ast); + Prog disabled = (Prog) env.program(ast, evalOptions(OptDisableNativeEval)); + AggregateInput enabledInput = aggregateInput(); + AggregateInput disabledInput = aggregateInput(); + + assertThat(enabled.interpretable.getClass().getSimpleName()) + .as(expression) + .isEqualTo("NativeIsland"); + assertThat(disabled.interpretable.getClass().getSimpleName()) + .as(expression) + .isNotEqualTo("NativeIsland"); + assertEquivalent( + enabled.eval(Map.of("input", enabledInput)).getVal(), + disabled.eval(Map.of("input", disabledInput)).getVal()); + assertThat(enabledInput.numbersReadCount()).as(expression).isEqualTo(1); + assertThat(disabledInput.numbersReadCount()).as(expression).isEqualTo(1); + } + } + + @Test + void exactRegistryReportsEmptyOptionalAggregateWithoutReplayingGetter() { + TypeRegistry registry = JacksonRegistry.newExactAggregateRegistry(); + Env env = + newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + types(AggregateInput.class), + declarations( + Decls.newVar("input", Decls.newObjectType(AggregateInput.class.getName())))); + Ast ast = compile(env, "input.optionalNumbers == [5, 6]"); + AggregateInput input = + new AggregateInput( + List.of(1L, 2L), + List.of(ULong.valueOf(1L)), + Map.of("bits", List.of(3L, 4L)), + Set.of("first"), + Optional.empty()); + + Val result = assertEnabledDisabledEquivalent(env, ast, Map.of("input", input)); + + assertThat(result).matches(Err::isError); + assertThat(input.optionalNumbersReadCount()).isEqualTo(2); + } + + @Test + void exactRegistryKeepsJacksonScalarSelectorsOnEstablishedEvaluation() { + TypeRegistry registry = JacksonRegistry.newExactAggregateRegistry(); + Env env = + newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + types(Person.class), + declarations( + Decls.newVar("person", Decls.newObjectType(Person.class.getName())), + Decls.newVar("expected", Decls.String))); + Prog program = (Prog) env.program(compile(env, "person.email == expected")); + + assertThat(program.interpretable.getClass().getSimpleName()).isNotEqualTo("NativeIsland"); + assertThat( + program + .eval(Map.of("person", new Person("cel", 50_021L), "expected", "cel")) + .getVal() + .booleanValue()) + .isTrue(); + } + + @Test + void jacksonSelectorsRemainOnTheCurrentEvaluator() { + TypeRegistry registry = JacksonRegistry.newRegistry(); + Env env = + newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + types(Person.class), + declarations(Decls.newVar("person", Decls.newObjectType(Person.class.getName())))); + + for (String expression : new String[] {"person.email", "person.priority"}) { + Prog program = (Prog) env.program(compile(env, expression)); + assertThat(program.interpretable) + .as(expression) + .isNotNull() + .extracting(value -> value.getClass().getSimpleName()) + .isNotEqualTo("NativeIsland"); + } + + Prog stringProgram = (Prog) env.program(compile(env, "person.email")); + assertThat(stringProgram.eval(Map.of("person", new Person("cel", 50_021L))).getVal().value()) + .isEqualTo("cel"); + } + + private static Ast compile(Env env, String expression) { + AstIssuesTuple result = env.compile(expression); + assertThat(result.hasIssues()).as(expression).isFalse(); + return result.getAst(); + } + + private static Val assertEnabledDisabledEquivalent(Env env, Ast ast, Object input) { + Program enabled = env.program(ast); + Prog disabled = (Prog) env.program(ast, evalOptions(OptDisableNativeEval)); + + assertThat(disabled.interpretable.getClass().getSimpleName()).isNotEqualTo("NativeIsland"); + Val result = enabled.eval(input).getVal(); + assertEquivalent(result, disabled.eval(input).getVal()); + return result; + } + + private static void assertEquivalent(Val actual, Val expected) { + assertThat(actual.getClass()).isEqualTo(expected.getClass()); + assertThat(actual.type()).isSameAs(expected.type()); + assertThat(actual.toString()).isEqualTo(expected.toString()); + if (actual instanceof Err actualError && expected instanceof Err expectedError) { + assertThat(actualError.hasCause()).isEqualTo(expectedError.hasCause()); + if (actualError.hasCause()) { + assertThat(actualError.getCause().getClass()) + .isEqualTo(expectedError.getCause().getClass()); + assertThat(actualError.getCause().getMessage()) + .isEqualTo(expectedError.getCause().getMessage()); + } + return; + } + assertThat(actual.value()).isEqualTo(expected.value()); + } + + private static AggregateInput aggregateInput() { + return new AggregateInput( + List.of(1L, 2L), + List.of(ULong.valueOf(1L), ULong.valueOf(-1L)), + Map.of("bits", List.of(3L, 4L)), + new LinkedHashSet<>(List.of("first", "second")), + Optional.of(List.of(5L, 6L))); + } + + @SuppressWarnings({"unused", "ClassCanBeRecord"}) + public static final class Person { + private final String email; + private final long priority; + + public Person(String email, long priority) { + this.email = email; + this.priority = priority; + } + + public String getEmail() { + return email; + } + + public long getPriority() { + return priority; + } + } + + @SuppressWarnings({"unused", "ClassCanBeRecord"}) + public static final class AggregateInput { + private final List numbers; + private final List unsigned; + private final Map> nested; + private final Set tags; + private final Optional> optionalNumbers; + private int numbersReadCount; + private int optionalNumbersReadCount; + + public AggregateInput( + List numbers, + List unsigned, + Map> nested, + Set tags, + Optional> optionalNumbers) { + this.numbers = numbers; + this.unsigned = unsigned; + this.nested = nested; + this.tags = tags; + this.optionalNumbers = optionalNumbers; + } + + public List getNumbers() { + numbersReadCount++; + return numbers; + } + + public List getUnsigned() { + return unsigned; + } + + public Map> getNested() { + return nested; + } + + public Set getTags() { + return tags; + } + + public Optional> getOptionalNumbers() { + optionalNumbersReadCount++; + return optionalNumbers; + } + + int numbersReadCount() { + return numbersReadCount; + } + + int optionalNumbersReadCount() { + return optionalNumbersReadCount; + } + } +} diff --git a/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2ScriptHostTest.java b/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2ScriptHostTest.java index 0820f827..7d9a8c35 100644 --- a/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2ScriptHostTest.java +++ b/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2ScriptHostTest.java @@ -47,8 +47,10 @@ void simple() throws Exception { MetaTest cmMatch = MetaTest.builder().author("foo@bar.baz").build(); MetaTest cmNoMatch = MetaTest.builder().author("foo@foo.foo").build(); - assertThat(script.execute(Boolean.class, singletonMap("param", cmMatch))).isTrue(); - assertThat(script.execute(Boolean.class, singletonMap("param", cmNoMatch))).isFalse(); + assertThat(script.executeWithActivation(Boolean.class, singletonMap("param", cmMatch))) + .isTrue(); + assertThat(script.executeWithActivation(Boolean.class, singletonMap("param", cmNoMatch))) + .isFalse(); script = scriptHost @@ -57,8 +59,9 @@ void simple() throws Exception { .withTypes(MetaTest.class) .build(); - assertThat(script.execute(Object.class, singletonMap("param", cmMatch))).isEqualTo(cmMatch); - assertThat(script.execute(ObjectT.class, singletonMap("param", cmMatch)).value()) + assertThat(script.executeWithActivation(Object.class, singletonMap("param", cmMatch))) + .isEqualTo(cmMatch); + assertThat(script.executeWithActivation(ObjectT.class, singletonMap("param", cmMatch)).value()) .isEqualTo(cmMatch); } @@ -84,7 +87,7 @@ void readmeExample() throws Exception { arguments.put("inp", pojo); arguments.put("checkName", checkName); - assertThat(script.execute(Boolean.class, arguments)).isTrue(); + assertThat(script.executeWithActivation(Boolean.class, arguments)).isTrue(); } @Test @@ -109,7 +112,7 @@ void complexInput() throws Exception { .build()) .build(); - assertThat(script.execute(Boolean.class, singletonMap("param", val))).isTrue(); + assertThat(script.executeWithActivation(Boolean.class, singletonMap("param", val))).isTrue(); // same as above, but use the 'container' @@ -122,7 +125,7 @@ void complexInput() throws Exception { .withTypes(ObjectListEnum.class) .build(); - assertThat(script.execute(Boolean.class, singletonMap("param", val))).isTrue(); + assertThat(script.executeWithActivation(Boolean.class, singletonMap("param", val))).isTrue(); // return the enum @@ -135,7 +138,7 @@ void complexInput() throws Exception { .withTypes(ObjectListEnum.class) .build(); - assertThat(script.execute(Integer.class, singletonMap("param", val))) + assertThat(script.executeWithActivation(Integer.class, singletonMap("param", val))) .isEqualTo(ClassEnum.VAL_2.ordinal()); } } diff --git a/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2TypeDescriptionTest.java b/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2TypeDescriptionTest.java index 3cf8a92c..a383ef53 100644 --- a/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2TypeDescriptionTest.java +++ b/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2TypeDescriptionTest.java @@ -26,6 +26,7 @@ import static org.projectnessie.cel.common.types.UintT.uintOf; import static org.projectnessie.cel.types.jackson.JacksonRegistry.newRegistry; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JavaType; import com.google.api.expr.v1alpha1.Type.ListType; import com.google.api.expr.v1alpha1.Type.MapType; @@ -51,6 +52,7 @@ import org.projectnessie.cel.common.types.ObjectT; import org.projectnessie.cel.common.types.TypeT; import org.projectnessie.cel.common.types.pb.Checked; +import org.projectnessie.cel.common.types.ref.FieldType; import org.projectnessie.cel.common.types.ref.Val; import org.projectnessie.cel.types.jackson.types.AnEnum; import org.projectnessie.cel.types.jackson.types.CollectionsObject; @@ -60,6 +62,27 @@ class Jackson2TypeDescriptionTest { + static final class AccessorObject { + public final String field; + + @JsonProperty private final String hidden; + + private final String original; + + AccessorObject(String field, String hidden, String original) { + this.field = field; + this.hidden = hidden; + this.original = original; + } + + @JsonProperty("renamed") + public String getOriginal() { + return original; + } + } + + record AccessorRecord(@JsonProperty("record_name") String name) {} + @Test void basics() { JacksonRegistry reg = (JacksonRegistry) newRegistry(); @@ -121,6 +144,32 @@ void basics() { .isInstanceOf(IllegalArgumentException.class); } + @Test + void cachedPropertyAccessorsPreserveDiscoveredMembers() { + JacksonRegistry reg = (JacksonRegistry) newRegistry(); + AccessorObject value = new AccessorObject("field-value", "hidden-value", "method-value"); + reg.register(AccessorObject.class); + + FieldType field = reg.findFieldType(AccessorObject.class.getName(), "field"); + FieldType hidden = reg.findFieldType(AccessorObject.class.getName(), "hidden"); + FieldType renamed = reg.findFieldType(AccessorObject.class.getName(), "renamed"); + + assertThat(field.getFrom.getFrom(value)).isEqualTo("field-value"); + assertThat(hidden.getFrom.getFrom(value)).isEqualTo("hidden-value"); + assertThat(renamed.getFrom.getFrom(value)).isEqualTo("method-value"); + assertThat(renamed.isSet.isSet(value)).isTrue(); + + ObjectT object = (ObjectT) reg.nativeToValue(value); + assertThat(object.get(stringOf("field"))).isEqualTo(stringOf("field-value")); + assertThat(object.get(stringOf("hidden"))).isEqualTo(stringOf("hidden-value")); + assertThat(object.get(stringOf("renamed"))).isEqualTo(stringOf("method-value")); + + AccessorRecord record = new AccessorRecord("record-value"); + reg.register(AccessorRecord.class); + FieldType recordName = reg.findFieldType(AccessorRecord.class.getName(), "record_name"); + assertThat(recordName.getFrom.getFrom(record)).isEqualTo("record-value"); + } + @Test void enumConstantSpecificClassBodyUsesDeclaringClassName() { JacksonRegistry reg = (JacksonRegistry) newRegistry(); diff --git a/jackson3/src/jmh/java/org/projectnessie/cel/Jackson3EvaluatorBaselineBench.java b/jackson3/src/jmh/java/org/projectnessie/cel/Jackson3EvaluatorBaselineBench.java new file mode 100644 index 00000000..b678e313 --- /dev/null +++ b/jackson3/src/jmh/java/org/projectnessie/cel/Jackson3EvaluatorBaselineBench.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel; + +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.customTypeProvider; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EnvOption.types; +import static org.projectnessie.cel.EvalOption.OptOptimize; +import static org.projectnessie.cel.ProgramOption.evalOptions; +import static org.projectnessie.cel.interpreter.Activation.newActivation; + +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.interpreter.Activation; +import org.projectnessie.cel.types.jackson3.Jackson3Registry; + +/** Measures checked Jackson 3 selection at each evaluator result boundary. */ +@Warmup(iterations = 3, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class Jackson3EvaluatorBaselineBench { + + @State(Scope.Benchmark) + public static class EvaluationState { + Program program; + Prog internalProgram; + Map variables; + Activation activation; + Policy policy; + + @Setup + public void init() { + Jackson3Registry registry = (Jackson3Registry) Jackson3Registry.newRegistry(); + Env env = + newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + types(Policy.class, Principal.class), + declarations( + Decls.newVar("policy", Decls.newObjectType(Policy.class.getName())), + Decls.newVar("expected", Decls.String))); + AstIssuesTuple ast = env.compile("policy.owner.email == expected"); + if (ast.hasIssues()) { + throw ast.getIssues().err(); + } + + policy = new Policy(new Principal("alice@example.com")); + variables = Map.of("policy", policy, "expected", "alice@example.com"); + program = env.program(ast.getAst(), evalOptions(OptOptimize)); + internalProgram = (Prog) program; + activation = newActivation(variables); + } + } + + @Benchmark + public Object rawEvaluator(EvaluationState state) { + return state.internalProgram.interpretable.eval(state.activation); + } + + @Benchmark + public Program.EvalResult programEval(EvaluationState state) { + return state.program.eval(state.variables); + } + + @Benchmark + public Object programEvalNative(EvaluationState state) { + return state.internalProgram.e.adapter.valueToNative( + state.program.eval(state.variables).getVal(), Boolean.class); + } + + @Benchmark + public boolean nativeJava(EvaluationState state) { + return state.policy.getOwner().getEmail().equals("alice@example.com"); + } + + public static final class Principal { + private final String email; + + public Principal(String email) { + this.email = email; + } + + public String getEmail() { + return email; + } + } + + public static final class Policy { + private final Principal owner; + + public Policy(Principal owner) { + this.owner = owner; + } + + public Principal getOwner() { + return owner; + } + } +} diff --git a/jackson3/src/jmh/java/org/projectnessie/cel/Jackson3ExactAggregateFieldBench.java b/jackson3/src/jmh/java/org/projectnessie/cel/Jackson3ExactAggregateFieldBench.java new file mode 100644 index 00000000..372389a1 --- /dev/null +++ b/jackson3/src/jmh/java/org/projectnessie/cel/Jackson3ExactAggregateFieldBench.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel; + +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.customTypeProvider; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EnvOption.types; +import static org.projectnessie.cel.EvalOption.OptDisableNativeEval; +import static org.projectnessie.cel.ProgramOption.evalOptions; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.ref.TypeRegistry; +import org.projectnessie.cel.types.jackson3.Jackson3Registry; + +/** End-to-end Jackson 3 exact aggregate-field consumer benchmarks. */ +@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 300, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class Jackson3ExactAggregateFieldBench { + @State(Scope.Benchmark) + public static class FieldState { + @Param({"size", "index", "exists", "setMembership", "mapLookup"}) + public String operation; + + @Param({"0", "1", "16", "1024"}) + public int size; + + Program exactNative; + Program exactDisabled; + Program defaultEstablished; + Map variables; + AggregateInput input; + + @Setup + public void setup() { + String expression = + switch (operation) { + case "size" -> "size(input.values)"; + case "index" -> "cardinality == 0 ? -1 : input.values[0]"; + case "exists" -> "input.values.exists(value, value == needle)"; + case "setMembership" -> "needle in input.members"; + case "mapLookup" -> "cardinality == 0 ? -1 : input.lookup['last']"; + default -> throw new IllegalArgumentException(operation); + }; + input = AggregateInput.create(size); + long needle = size == 0 ? -1L : size - 1L; + variables = Map.of("input", input, "cardinality", (long) size, "needle", needle); + + TypeRegistry exact = Jackson3Registry.newExactAggregateRegistry(); + Env exactEnv = environment(exact); + Ast exactAst = compile(exactEnv, expression); + exactNative = exactEnv.program(exactAst); + exactDisabled = exactEnv.program(exactAst, evalOptions(OptDisableNativeEval)); + + TypeRegistry general = Jackson3Registry.newRegistry(); + Env generalEnv = environment(general); + defaultEstablished = generalEnv.program(compile(generalEnv, expression)); + } + } + + @Benchmark + public Object exactNative(FieldState state) { + return state.exactNative.eval(state.variables); + } + + @Benchmark + public Object exactDisabled(FieldState state) { + return state.exactDisabled.eval(state.variables); + } + + @Benchmark + public Object defaultEstablished(FieldState state) { + return state.defaultEstablished.eval(state.variables); + } + + @Benchmark + public Object javaCeiling(FieldState state) { + return switch (state.operation) { + case "size" -> state.input.values.size(); + case "index" -> state.size == 0 ? -1L : state.input.values.get(0); + case "exists", "setMembership" -> state.input.members.contains((long) state.size - 1L); + case "mapLookup" -> state.size == 0 ? -1L : state.input.lookup.get("last"); + default -> throw new IllegalArgumentException(state.operation); + }; + } + + private static Env environment(TypeRegistry registry) { + return newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + types(AggregateInput.class), + declarations( + Decls.newVar("input", Decls.newObjectType(AggregateInput.class.getName())), + Decls.newVar("cardinality", Decls.Int), + Decls.newVar("needle", Decls.Int))); + } + + private static Ast compile(Env env, String expression) { + var result = env.compile(expression); + if (result.hasIssues()) { + throw new IllegalStateException(result.getIssues().toString()); + } + return result.getAst(); + } + + @SuppressWarnings("unused") + public static final class AggregateInput { + private final List values; + private final Set members; + private final Map lookup; + + private AggregateInput(List values, Set members, Map lookup) { + this.values = values; + this.members = members; + this.lookup = lookup; + } + + static AggregateInput create(int size) { + List values = new ArrayList<>(size); + Set members = new LinkedHashSet<>(); + Map lookup = new LinkedHashMap<>(); + for (long value = 0; value < size; value++) { + values.add(value); + members.add(value); + } + if (size > 0) { + lookup.put("last", (long) size - 1L); + } + return new AggregateInput(values, members, lookup); + } + + public List getValues() { + return values; + } + + public Set getMembers() { + return members; + } + + public Map getLookup() { + return lookup; + } + } +} diff --git a/jackson3/src/jmh/java/org/projectnessie/cel/Jackson3NativeSelectorBench.java b/jackson3/src/jmh/java/org/projectnessie/cel/Jackson3NativeSelectorBench.java new file mode 100644 index 00000000..d0839726 --- /dev/null +++ b/jackson3/src/jmh/java/org/projectnessie/cel/Jackson3NativeSelectorBench.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel; + +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.customTypeProvider; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EnvOption.types; +import static org.projectnessie.cel.EvalOption.OptDisableNativeEval; +import static org.projectnessie.cel.ProgramOption.evalOptions; +import static org.projectnessie.cel.interpreter.Activation.newActivation; + +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.Activation; +import org.projectnessie.cel.interpreter.Interpretable; +import org.projectnessie.cel.interpreter.NativeCapabilityBenchmark; +import org.projectnessie.cel.types.jackson3.Jackson3Registry; + +/** Measures native and current evaluation of a top-level Jackson 3 string selector. */ +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class Jackson3NativeSelectorBench { + @State(Scope.Benchmark) + public static class EvaluationState { + Prog nativeProgram; + NativeCapabilityBenchmark nativePlan; + Program currentProgram; + Interpretable currentPlan; + Activation activation; + Map variables; + Input input; + + @Setup + public void init() { + Jackson3Registry registry = (Jackson3Registry) Jackson3Registry.newRegistry(); + Env env = + newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + types(Input.class), + declarations( + Decls.newVar("input", Decls.newObjectType(Input.class.getName())), + Decls.newVar("expected", Decls.String))); + AstIssuesTuple result = env.compile("input.text == expected"); + if (result.hasIssues()) { + throw result.getIssues().err(); + } + Ast ast = result.getAst(); + + nativeProgram = (Prog) env.program(ast); + nativePlan = NativeCapabilityBenchmark.require(nativeProgram.interpretable); + currentProgram = env.program(ast, evalOptions(OptDisableNativeEval)); + currentPlan = ((Prog) currentProgram).interpretable; + + input = new Input("alice@example.com"); + variables = Map.of("input", input, "expected", "alice@example.com"); + activation = newActivation(variables); + assertEquivalent(nativePlan.eval(activation), currentPlan.eval(activation)); + } + } + + @Benchmark + public boolean nativePrimitive(EvaluationState state) { + return state.nativePlan.evalBoolean(state.activation); + } + + @Benchmark + public Val currentRaw(EvaluationState state) { + return state.currentPlan.eval(state.activation); + } + + @Benchmark + public Program.EvalResult nativeProgramEval(EvaluationState state) { + return state.nativeProgram.eval(state.variables); + } + + @Benchmark + public Program.EvalResult currentProgramEval(EvaluationState state) { + return state.currentProgram.eval(state.variables); + } + + @Benchmark + public boolean nativeJava(EvaluationState state) { + return state.input.getText().equals("alice@example.com"); + } + + private static void assertEquivalent(Val nativeValue, Val currentValue) { + if (!nativeValue.type().equals(currentValue.type()) + || !nativeValue.toString().equals(currentValue.toString())) { + throw new IllegalStateException( + String.format( + "native result %s differs from current result %s", nativeValue, currentValue)); + } + } + + public static final class Input { + private final String text; + + public Input(String text) { + this.text = text; + } + + public String getText() { + return text; + } + } +} diff --git a/jackson3/src/jmh/java/org/projectnessie/cel/interpreter/NativeCapabilityBenchmark.java b/jackson3/src/jmh/java/org/projectnessie/cel/interpreter/NativeCapabilityBenchmark.java new file mode 100644 index 00000000..09479896 --- /dev/null +++ b/jackson3/src/jmh/java/org/projectnessie/cel/interpreter/NativeCapabilityBenchmark.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import org.projectnessie.cel.common.types.ref.Val; + +/** Jackson 3 JMH-only typed access to an integrated native-capability root. */ +public final class NativeCapabilityBenchmark { + private final NativeIsland plan; + + private NativeCapabilityBenchmark(NativeIsland plan) { + this.plan = plan; + } + + public static NativeCapabilityBenchmark require(Interpretable root) { + if (!(root instanceof NativeIsland plan)) { + throw new IllegalStateException("expected integrated native-capability root but got " + root); + } + return new NativeCapabilityBenchmark(plan); + } + + public Val eval(Activation activation) { + return plan.eval(activation); + } + + public boolean evalBoolean(Activation activation) { + return ((NativeBooleanCapability) plan.root()).evalBoolean(activation); + } +} diff --git a/jackson3/src/jmh/java/org/projectnessie/cel/types/jackson3/Jackson3RegistryBench.java b/jackson3/src/jmh/java/org/projectnessie/cel/types/jackson3/Jackson3RegistryBench.java index 9e739a79..c15b3a4f 100644 --- a/jackson3/src/jmh/java/org/projectnessie/cel/types/jackson3/Jackson3RegistryBench.java +++ b/jackson3/src/jmh/java/org/projectnessie/cel/types/jackson3/Jackson3RegistryBench.java @@ -31,6 +31,7 @@ import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; +import org.projectnessie.cel.common.types.ref.FieldType; @Warmup(iterations = 1, time = 1500, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 5, time = 300, timeUnit = TimeUnit.MILLISECONDS) @@ -43,18 +44,18 @@ public class Jackson3RegistryBench { @State(Scope.Benchmark) public static class ReadState { Jackson3Registry registry; + Policy policy; JacksonObjectT value; + FieldType ownerField; @Setup public void init() { registry = (Jackson3Registry) Jackson3Registry.newRegistry(); registry.typeDescription(Policy.class); registry.enumDescription(Status.class); - value = - JacksonObjectT.newObject( - registry, - new Policy("policy-1", 7, new Principal("alice@example.com"), Status.ACTIVE), - registry.typeDescription(Policy.class)); + policy = new Policy("policy-1", 7, new Principal("alice@example.com"), Status.ACTIVE); + value = JacksonObjectT.newObject(registry, policy, registry.typeDescription(Policy.class)); + ownerField = registry.findFieldType(Policy.class.getName(), "owner"); } } @@ -91,6 +92,11 @@ public void propertyRead(ReadState state, Blackhole blackhole) { blackhole.consume(state.value.get(stringOf("owner"))); } + @Benchmark + public void preResolvedPropertyRead(ReadState state, Blackhole blackhole) { + blackhole.consume(state.ownerField.getFrom.getFrom(state.policy)); + } + @Benchmark public void enumConversion(ReadState state, Blackhole blackhole) { blackhole.consume(state.registry.nativeToValue(Status.ACTIVE)); diff --git a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/ExactJackson3Registry.java b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/ExactJackson3Registry.java new file mode 100644 index 00000000..ca5c2f0b --- /dev/null +++ b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/ExactJackson3Registry.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.types.jackson3; + +import com.google.api.expr.v1alpha1.Type; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.projectnessie.cel.common.types.ref.ExactAggregateFieldProvider; +import org.projectnessie.cel.common.types.ref.ExactAggregateTypeAdapter; +import org.projectnessie.cel.common.types.ref.FieldType; +import org.projectnessie.cel.common.types.ref.StandardScalarFieldProvider; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeRegistry; +import org.projectnessie.cel.common.types.ref.Val; + +/** Distinct runtime type for the opt-in exact aggregate contract. */ +final class ExactJackson3Registry + implements TypeRegistry, + StandardScalarTypeAdapter, + StandardScalarFieldProvider, + ExactAggregateTypeAdapter, + ExactAggregateFieldProvider { + private final Jackson3Registry delegate; + + ExactJackson3Registry(Jackson3Registry delegate) { + this.delegate = Objects.requireNonNull(delegate, "delegate"); + } + + @Override + public TypeRegistry copy() { + return new ExactJackson3Registry((Jackson3Registry) delegate.copy()); + } + + @Override + public void register(Object value) { + delegate.register(value); + } + + @Override + public void registerType(org.projectnessie.cel.common.types.ref.Type... types) { + delegate.registerType(types); + } + + @Override + public Val enumValue(String enumName) { + return delegate.enumValue(enumName); + } + + @Override + public Val findIdent(String identName) { + return delegate.findIdent(identName); + } + + @Override + public Type findType(String typeName) { + return delegate.findType(typeName); + } + + @Override + public FieldType findFieldType(String messageType, String fieldName) { + FieldType fieldType = delegate.findFieldType(messageType, fieldName); + if (fieldType == null + || (fieldType.type.getTypeKindCase() != Type.TypeKindCase.LIST_TYPE + && fieldType.type.getTypeKindCase() != Type.TypeKindCase.MAP_TYPE)) { + return fieldType; + } + return new FieldType( + fieldType.type, + fieldType.isSet, + target -> canonicalAggregate(fieldType.getFrom.getFrom(target))); + } + + private static Object canonicalAggregate(Object value) { + return value instanceof Optional optional ? optional.orElse(null) : value; + } + + @Override + public Val newValue(String typeName, Map fields) { + return delegate.newValue(typeName, fields); + } + + @Override + public Val nativeToValue(Object value) { + return delegate.nativeToValue(value); + } + + @Override + public Val nativeToValue(boolean value) { + return delegate.nativeToValue(value); + } + + @Override + public Val nativeToValue(byte value) { + return delegate.nativeToValue(value); + } + + @Override + public Val nativeToValue(short value) { + return delegate.nativeToValue(value); + } + + @Override + public Val nativeToValue(int value) { + return delegate.nativeToValue(value); + } + + @Override + public Val nativeToValue(long value) { + return delegate.nativeToValue(value); + } + + @Override + public Val nativeToValue(float value) { + return delegate.nativeToValue(value); + } + + @Override + public Val nativeToValue(double value) { + return delegate.nativeToValue(value); + } +} diff --git a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/Jackson3Registry.java b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/Jackson3Registry.java index 85c83e88..15be454d 100644 --- a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/Jackson3Registry.java +++ b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/Jackson3Registry.java @@ -20,6 +20,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.projectnessie.cel.common.types.ref.FieldType; +import org.projectnessie.cel.common.types.ref.StandardScalarFieldProvider; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; import org.projectnessie.cel.common.types.ref.Type; import org.projectnessie.cel.common.types.ref.TypeAdapterSupport; import org.projectnessie.cel.common.types.ref.TypeRegistry; @@ -40,7 +42,8 @@ * therefore returning Jackson objects from CEL expressions is not possible/implemented and results * in {@link UnsupportedOperationException}s. */ -public final class Jackson3Registry implements TypeRegistry { +public final class Jackson3Registry + implements TypeRegistry, StandardScalarTypeAdapter, StandardScalarFieldProvider { final ObjectMapper objectMapper; private final SerializationContextExt serializationContextExt; private final TypeFactory typeFactory; @@ -69,6 +72,25 @@ public static TypeRegistry newRegistry() { return new Jackson3Registry(); } + /** + * Creates an opt-in registry that certifies checked Java aggregate representations. + * + *

The returned registry accepts the canonical homogeneous representations defined by {@link + * org.projectnessie.cel.common.types.ref.ExactAggregateTypeAdapter}, recursively. Checked CEL + * types determine signed versus unsigned {@code long} representation. Aggregate-valued {@link + * java.util.Optional} fields must be present; an empty optional is a detected contract violation. + * Null list elements and map values follow their nested checked type; null map keys, + * CEL-equivalent duplicate keys, incompatible boxed values, and traversed cycles are contract + * violations. Sources and equality/hash-relevant keys must not be mutated during one evaluation. + * The default {@link #newRegistry()} deliberately does not acquire this stricter contract. {@link + * TypeRegistry#copy()} preserves exact mode and registered type state. + * + * @return a distinct registry instance implementing both exact aggregate contracts + */ + public static TypeRegistry newExactAggregateRegistry() { + return new ExactJackson3Registry(new Jackson3Registry()); + } + @Override public TypeRegistry copy() { Jackson3Registry copy = new Jackson3Registry(); @@ -160,6 +182,41 @@ public Val nativeToValue(Object value) { } } + @Override + public Val nativeToValue(boolean value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(byte value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(short value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(int value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(long value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(float value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(double value) { + return TypeAdapterSupport.nativeToValue(value); + } + synchronized JacksonEnumDescription enumDescription(Class clazz) { if (!Enum.class.isAssignableFrom(clazz)) { throw new IllegalArgumentException("only enum allowed here"); diff --git a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonObjectT.java b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonObjectT.java index 79c74cf3..cf7e098d 100644 --- a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonObjectT.java +++ b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonObjectT.java @@ -50,11 +50,12 @@ public Val isSet(Val field) { } String fieldName = (String) field.value(); - if (!typeDesc().hasProperty(fieldName)) { + JacksonFieldType fieldType = typeDesc().fieldType(fieldName); + if (fieldType == null) { return noSuchField(fieldName); } - Object value = typeDesc().fromObject(value(), fieldName); + Object value = fieldType.getFrom.getFrom(value()); return boolOf(value != null); } @@ -66,16 +67,18 @@ public Val get(Val index) { } String fieldName = (String) index.value(); - if (!typeDesc().hasProperty(fieldName)) { + JacksonFieldType fieldType = typeDesc().fieldType(fieldName); + if (fieldType == null) { return noSuchField(fieldName); } - Object v = typeDesc().fromObject(value(), fieldName); + Object v = fieldType.getFrom.getFrom(value()); return registry().nativeToValue(v); } @Override + @SuppressWarnings({"removal", "unchecked"}) public T convertToNative(Class typeDesc) { if (typeDesc.isAssignableFrom(value.getClass())) { return (T) value; diff --git a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonTypeDescription.java b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonTypeDescription.java index a1102676..64a869f7 100644 --- a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonTypeDescription.java +++ b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonTypeDescription.java @@ -29,7 +29,7 @@ import org.projectnessie.cel.common.ULong; import org.projectnessie.cel.common.types.TypeT; import org.projectnessie.cel.common.types.pb.Checked; -import org.projectnessie.cel.common.types.ref.FieldType; +import org.projectnessie.cel.common.types.ref.FieldGetter; import org.projectnessie.cel.common.types.ref.Type; import org.projectnessie.cel.common.types.ref.TypeDescription; import tools.jackson.databind.JavaType; @@ -58,12 +58,13 @@ final class JacksonTypeDescription implements TypeDescription { while (propIter.hasNext()) { PropertyWriter pw = propIter.next(); String n = pw.getName(); + FieldGetter getter = newFieldGetter(pw, n); JacksonFieldType ft = new JacksonFieldType( findTypeForJacksonType(pw.getType(), typeQuery), - target -> fromObject(target, n) != null, - target -> fromObject(target, n), + target -> getter.getFrom(target) != null, + getter, pw); fieldTypes.put(n, ft); } @@ -139,30 +140,22 @@ private JavaType elementType(JavaType type) { return elementType; } - boolean hasProperty(String property) { - return fieldTypes.containsKey(property); - } - - Object fromObject(Object value, String property) { - JacksonFieldType ft = fieldTypes.get(property); - if (ft == null) { - throw new IllegalArgumentException(String.format("No property named '%s'", property)); + private static FieldGetter newFieldGetter(PropertyWriter propertyWriter, String property) { + if (propertyWriter instanceof BeanPropertyWriter beanPropertyWriter) { + return target -> { + try { + return beanPropertyWriter.get(target); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; } - PropertyWriter pw = ft.propertyWriter(); - - if (pw instanceof BeanPropertyWriter) { - try { - return ((BeanPropertyWriter) pw).get(value); - } catch (Exception e) { - throw new RuntimeException(e); - } - } else if (pw == null) { - return null; - } else { + return target -> { throw new UnsupportedOperationException( String.format( - "Unknown property-writer '%s' for property '%s'", pw.getClass().getName(), property)); - } + "Unknown property-writer '%s' for property '%s'", + propertyWriter.getClass().getName(), property)); + }; } Type type() { @@ -173,7 +166,7 @@ com.google.api.expr.v1alpha1.Type pbType() { return pbType; } - FieldType fieldType(String fieldName) { + JacksonFieldType fieldType(String fieldName) { return fieldTypes.get(fieldName); } diff --git a/jackson3/src/test/java/org/projectnessie/cel/Jackson3NativePlanTest.java b/jackson3/src/test/java/org/projectnessie/cel/Jackson3NativePlanTest.java new file mode 100644 index 00000000..62401172 --- /dev/null +++ b/jackson3/src/test/java/org/projectnessie/cel/Jackson3NativePlanTest.java @@ -0,0 +1,404 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.customTypeProvider; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EnvOption.types; +import static org.projectnessie.cel.EvalOption.OptDisableNativeEval; +import static org.projectnessie.cel.ProgramOption.evalOptions; + +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.ref.ExactAggregateFieldProvider; +import org.projectnessie.cel.common.types.ref.ExactAggregateTypeAdapter; +import org.projectnessie.cel.common.types.ref.StandardScalarFieldProvider; +import org.projectnessie.cel.common.types.ref.StandardScalarTypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeRegistry; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.types.jackson3.Jackson3Registry; + +class Jackson3NativePlanTest { + @Test + void registryDeclaresStandardScalarSemantics() { + assertThat(Jackson3Registry.newRegistry()).isInstanceOf(StandardScalarTypeAdapter.class); + assertThat(Jackson3Registry.newRegistry()).isInstanceOf(StandardScalarFieldProvider.class); + assertThat(Jackson3Registry.newRegistry()).isNotInstanceOf(ExactAggregateTypeAdapter.class); + assertThat(Jackson3Registry.newRegistry()).isNotInstanceOf(ExactAggregateFieldProvider.class); + } + + @Test + void exactRegistryAndCopiesPreserveExplicitAggregateContract() { + TypeRegistry exact = Jackson3Registry.newExactAggregateRegistry(); + exact.register(AggregateInput.class); + TypeRegistry exactCopy = exact.copy(); + TypeRegistry defaultCopy = Jackson3Registry.newRegistry().copy(); + + assertThat(exact).isInstanceOf(ExactAggregateTypeAdapter.class); + assertThat(exact).isInstanceOf(ExactAggregateFieldProvider.class); + assertThat(exact).isInstanceOf(StandardScalarTypeAdapter.class); + assertThat(exact).isInstanceOf(StandardScalarFieldProvider.class); + assertThat(exactCopy).isInstanceOf(ExactAggregateTypeAdapter.class); + assertThat(exactCopy).isInstanceOf(ExactAggregateFieldProvider.class); + assertThat(exactCopy.findType(AggregateInput.class.getName())).isNotNull(); + assertThat(defaultCopy).isNotInstanceOf(ExactAggregateTypeAdapter.class); + assertThat(defaultCopy).isNotInstanceOf(ExactAggregateFieldProvider.class); + } + + @Test + void exactRegistryMaterializesCheckedSignedUnsignedAndNestedAggregates() { + TypeRegistry registry = Jackson3Registry.newExactAggregateRegistry(); + Env env = + newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + declarations( + Decls.newVar("signed", Decls.newListType(Decls.Int)), + Decls.newVar("unsigned", Decls.newListType(Decls.Uint)), + Decls.newVar( + "nested", Decls.newMapType(Decls.String, Decls.newListType(Decls.Uint))))); + Ast ast = + compile( + env, + "signed == [1, -1] && " + + "unsigned == [1u, 18446744073709551615u] && " + + "nested == {'bits': [18446744073709551615u]}"); + Program enabled = env.program(ast); + Prog disabled = (Prog) env.program(ast, evalOptions(OptDisableNativeEval)); + Map input = + Map.of( + "signed", new long[] {1L, -1L}, + "unsigned", List.of(1L, -1L), + "nested", Map.of("bits", new long[] {-1L})); + + assertThat(disabled.interpretable.getClass().getSimpleName()).isNotEqualTo("NativeIsland"); + assertEquivalent(enabled.eval(input).getVal(), disabled.eval(input).getVal()); + assertThat(enabled.eval(input).getVal().booleanValue()).isTrue(); + } + + @Test + void exactRegistryPreservesPresentNullAndReportsTypeMismatchInBothModes() { + TypeRegistry registry = Jackson3Registry.newExactAggregateRegistry(); + Env env = + newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + declarations( + Decls.newVar("nullable", Decls.newMapType(Decls.String, Decls.Null)), + Decls.newVar("numbers", Decls.newListType(Decls.Int)))); + Ast nullAst = compile(env, "nullable['present'] == null && !('absent' in nullable)"); + Ast mismatchAst = compile(env, "numbers[0] == 1"); + Map nullable = new HashMap<>(); + nullable.put("present", null); + + assertEnabledDisabledEquivalent( + env, nullAst, Map.of("nullable", nullable, "numbers", List.of(1L))); + Val mismatch = + assertEnabledDisabledEquivalent( + env, mismatchAst, Map.of("nullable", nullable, "numbers", List.of("wrong"))); + assertThat(mismatch).matches(Err::isError); + } + + @Test + void exactRegistryMaterializesCheckedAggregateFieldsInBothModes() { + TypeRegistry registry = Jackson3Registry.newExactAggregateRegistry(); + Env env = + newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + types(AggregateInput.class), + declarations( + Decls.newVar("input", Decls.newObjectType(AggregateInput.class.getName())))); + Ast ast = + compile( + env, + "input.numbers == [1, 2] && " + + "input.unsigned == [1u, 18446744073709551615u] && " + + "input.nested == {'bits': [3, 4]} && " + + "input.tags == ['first', 'second'] && " + + "input.optionalNumbers == [5, 6]"); + AggregateInput input = + new AggregateInput( + List.of(1L, 2L), + List.of(ULong.valueOf(1L), ULong.valueOf(-1L)), + Map.of("bits", List.of(3L, 4L)), + new LinkedHashSet<>(List.of("first", "second")), + Optional.of(List.of(5L, 6L))); + + Val result = assertEnabledDisabledEquivalent(env, ast, Map.of("input", input)); + assertThat(result.booleanValue()).isTrue(); + assertThat(input.numbersReadCount()).isEqualTo(2); + assertThat(input.optionalNumbersReadCount()).isEqualTo(2); + } + + @Test + void exactAggregateFieldsBecomeNativeSourcesWithoutGetterReplay() { + TypeRegistry registry = Jackson3Registry.newExactAggregateRegistry(); + Env env = + newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + types(AggregateInput.class), + declarations( + Decls.newVar("input", Decls.newObjectType(AggregateInput.class.getName())))); + + for (String expression : + List.of( + "size(input.numbers)", + "input.numbers[1]", + "input.numbers.exists(number, number == 2)")) { + Ast ast = compile(env, expression); + Prog enabled = (Prog) env.program(ast); + Prog disabled = (Prog) env.program(ast, evalOptions(OptDisableNativeEval)); + AggregateInput enabledInput = aggregateInput(); + AggregateInput disabledInput = aggregateInput(); + + assertThat(enabled.interpretable.getClass().getSimpleName()) + .as(expression) + .isEqualTo("NativeIsland"); + assertThat(disabled.interpretable.getClass().getSimpleName()) + .as(expression) + .isNotEqualTo("NativeIsland"); + assertEquivalent( + enabled.eval(Map.of("input", enabledInput)).getVal(), + disabled.eval(Map.of("input", disabledInput)).getVal()); + assertThat(enabledInput.numbersReadCount()).as(expression).isEqualTo(1); + assertThat(disabledInput.numbersReadCount()).as(expression).isEqualTo(1); + } + } + + @Test + void exactRegistryReportsEmptyOptionalAggregateWithoutReplayingGetter() { + TypeRegistry registry = Jackson3Registry.newExactAggregateRegistry(); + Env env = + newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + types(AggregateInput.class), + declarations( + Decls.newVar("input", Decls.newObjectType(AggregateInput.class.getName())))); + Ast ast = compile(env, "input.optionalNumbers == [5, 6]"); + AggregateInput input = + new AggregateInput( + List.of(1L, 2L), + List.of(ULong.valueOf(1L)), + Map.of("bits", List.of(3L, 4L)), + Set.of("first"), + Optional.empty()); + + Val result = assertEnabledDisabledEquivalent(env, ast, Map.of("input", input)); + + assertThat(result).matches(Err::isError); + assertThat(input.optionalNumbersReadCount()).isEqualTo(2); + } + + @Test + void evaluatesTopLevelStringSelectionLikeCurrentEvaluator() { + Jackson3Registry registry = (Jackson3Registry) Jackson3Registry.newRegistry(); + Env env = env(registry); + Ast ast = compile(env, "input.text == expected"); + + Prog nativeProgram = (Prog) env.program(ast); + Program currentProgram = env.program(ast, evalOptions(OptDisableNativeEval)); + assertThat(nativeProgram.interpretable.getClass().getSimpleName()).isEqualTo("NativeIsland"); + + assertEquivalent( + nativeProgram.eval(Map.of("input", new Input("cel", 42L), "expected", "cel")).getVal(), + currentProgram.eval(Map.of("input", new Input("cel", 42L), "expected", "cel")).getVal()); + + assertEquivalent( + nativeProgram.eval(Map.of("input", new Input(null, 42L), "expected", "cel")).getVal(), + currentProgram.eval(Map.of("input", new Input(null, 42L), "expected", "cel")).getVal()); + + assertEquivalent( + nativeProgram.eval(Map.of("input", "wrong object", "expected", "cel")).getVal(), + currentProgram.eval(Map.of("input", "wrong object", "expected", "cel")).getVal()); + + assertEquivalent( + nativeProgram.eval(Map.of("expected", "cel")).getVal(), + currentProgram.eval(Map.of("expected", "cel")).getVal()); + + Map nullInput = new HashMap<>(); + nullInput.put("input", null); + nullInput.put("expected", "cel"); + assertEquivalent( + nativeProgram.eval(nullInput).getVal(), currentProgram.eval(nullInput).getVal()); + } + + @Test + void copiedRegistryRemainsEligible() { + Jackson3Registry original = (Jackson3Registry) Jackson3Registry.newRegistry(); + original.register(Input.class); + TypeRegistry copied = original.copy(); + + assertThat(copied).isInstanceOf(StandardScalarTypeAdapter.class); + Env env = env((Jackson3Registry) copied); + Prog program = (Prog) env.program(compile(env, "input.text == 'cel'")); + + assertThat(program.interpretable.getClass().getSimpleName()).isEqualTo("NativeIsland"); + assertThat(program.eval(Map.of("input", new Input("cel", 42L))).getVal().booleanValue()) + .isTrue(); + } + + @Test + void numericSelectionStaysOnCurrentEvaluator() { + Jackson3Registry registry = (Jackson3Registry) Jackson3Registry.newRegistry(); + Env env = env(registry); + Prog program = (Prog) env.program(compile(env, "input.number == 42")); + + assertThat(program.interpretable).isNotNull(); + assertThat(program.interpretable.getClass().getSimpleName()).isNotEqualTo("NativeIsland"); + assertThat(program.eval(Map.of("input", new Input("cel", 42L))).getVal().booleanValue()) + .isTrue(); + } + + private static Env env(Jackson3Registry registry) { + return newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + types(Input.class), + declarations( + Decls.newVar("input", Decls.newObjectType(Input.class.getName())), + Decls.newVar("expected", Decls.String))); + } + + private static Ast compile(Env env, String expression) { + AstIssuesTuple result = env.compile(expression); + assertThat(result.hasIssues()).as(expression).isFalse(); + return result.getAst(); + } + + private static void assertEquivalent(Val actual, Val expected) { + assertThat(actual.getClass()).isEqualTo(expected.getClass()); + assertThat(actual.type()).isSameAs(expected.type()); + assertThat(actual.toString()).isEqualTo(expected.toString()); + if (actual instanceof Err actualError && expected instanceof Err expectedError) { + assertThat(actualError.hasCause()).isEqualTo(expectedError.hasCause()); + if (actualError.hasCause()) { + assertThat(actualError.getCause().getClass()) + .isEqualTo(expectedError.getCause().getClass()); + assertThat(actualError.getCause().getMessage()) + .isEqualTo(expectedError.getCause().getMessage()); + } + return; + } + assertThat(actual.value()).isEqualTo(expected.value()); + } + + private static Val assertEnabledDisabledEquivalent(Env env, Ast ast, Object input) { + Program enabled = env.program(ast); + Prog disabled = (Prog) env.program(ast, evalOptions(OptDisableNativeEval)); + + assertThat(disabled.interpretable.getClass().getSimpleName()).isNotEqualTo("NativeIsland"); + Val result = enabled.eval(input).getVal(); + assertEquivalent(result, disabled.eval(input).getVal()); + return result; + } + + private static AggregateInput aggregateInput() { + return new AggregateInput( + List.of(1L, 2L), + List.of(ULong.valueOf(1L), ULong.valueOf(-1L)), + Map.of("bits", List.of(3L, 4L)), + new LinkedHashSet<>(List.of("first", "second")), + Optional.of(List.of(5L, 6L))); + } + + @SuppressWarnings({"unused", "ClassCanBeRecord"}) + public static final class Input { + private final String text; + private final long number; + + public Input(String text, long number) { + this.text = text; + this.number = number; + } + + public String getText() { + return text; + } + + public long getNumber() { + return number; + } + } + + @SuppressWarnings({"unused", "ClassCanBeRecord"}) + public static final class AggregateInput { + private final List numbers; + private final List unsigned; + private final Map> nested; + private final Set tags; + private final Optional> optionalNumbers; + private int numbersReadCount; + private int optionalNumbersReadCount; + + public AggregateInput( + List numbers, + List unsigned, + Map> nested, + Set tags, + Optional> optionalNumbers) { + this.numbers = numbers; + this.unsigned = unsigned; + this.nested = nested; + this.tags = tags; + this.optionalNumbers = optionalNumbers; + } + + public List getNumbers() { + numbersReadCount++; + return numbers; + } + + public List getUnsigned() { + return unsigned; + } + + public Map> getNested() { + return nested; + } + + public Set getTags() { + return tags; + } + + public Optional> getOptionalNumbers() { + optionalNumbersReadCount++; + return optionalNumbers; + } + + int numbersReadCount() { + return numbersReadCount; + } + + int optionalNumbersReadCount() { + return optionalNumbersReadCount; + } + } +} diff --git a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3ScriptHostTest.java b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3ScriptHostTest.java index 977bd364..45743da6 100644 --- a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3ScriptHostTest.java +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3ScriptHostTest.java @@ -48,8 +48,10 @@ void simple() throws Exception { MetaTest cmMatch = MetaTest.builder().author("foo@bar.baz").build(); MetaTest cmNoMatch = MetaTest.builder().author("foo@foo.foo").build(); - assertThat(script.execute(Boolean.class, singletonMap("param", cmMatch))).isTrue(); - assertThat(script.execute(Boolean.class, singletonMap("param", cmNoMatch))).isFalse(); + assertThat(script.executeWithActivation(Boolean.class, singletonMap("param", cmMatch))) + .isTrue(); + assertThat(script.executeWithActivation(Boolean.class, singletonMap("param", cmNoMatch))) + .isFalse(); script = scriptHost @@ -58,8 +60,9 @@ void simple() throws Exception { .withTypes(MetaTest.class) .build(); - assertThat(script.execute(Object.class, singletonMap("param", cmMatch))).isEqualTo(cmMatch); - assertThat(script.execute(ObjectT.class, singletonMap("param", cmMatch)).value()) + assertThat(script.executeWithActivation(Object.class, singletonMap("param", cmMatch))) + .isEqualTo(cmMatch); + assertThat(script.executeWithActivation(ObjectT.class, singletonMap("param", cmMatch)).value()) .isEqualTo(cmMatch); } @@ -86,7 +89,7 @@ void readmeExample() throws Exception { arguments.put("inp", pojo); arguments.put("checkName", checkName); - assertThat(script.execute(Boolean.class, arguments)).isTrue(); + assertThat(script.executeWithActivation(Boolean.class, arguments)).isTrue(); } @Test @@ -112,7 +115,7 @@ void complexInput() throws Exception { .build()) .build(); - assertThat(script.execute(Boolean.class, singletonMap("param", val))).isTrue(); + assertThat(script.executeWithActivation(Boolean.class, singletonMap("param", val))).isTrue(); // same as above, but use the 'container' @@ -125,7 +128,7 @@ void complexInput() throws Exception { .withTypes(ObjectListEnum.class) .build(); - assertThat(script.execute(Boolean.class, singletonMap("param", val))).isTrue(); + assertThat(script.executeWithActivation(Boolean.class, singletonMap("param", val))).isTrue(); // return the enum @@ -138,7 +141,7 @@ void complexInput() throws Exception { .withTypes(ObjectListEnum.class) .build(); - assertThat(script.execute(Integer.class, singletonMap("param", val))) + assertThat(script.executeWithActivation(Integer.class, singletonMap("param", val))) .isEqualTo(ClassEnum.VAL_2.ordinal()); } } diff --git a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3TypeDescriptionTest.java b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3TypeDescriptionTest.java index 23057396..53aa9649 100644 --- a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3TypeDescriptionTest.java +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3TypeDescriptionTest.java @@ -26,6 +26,7 @@ import static org.projectnessie.cel.common.types.UintT.uintOf; import static org.projectnessie.cel.types.jackson3.Jackson3Registry.newRegistry; +import com.fasterxml.jackson.annotation.JsonProperty; import com.google.api.expr.v1alpha1.Type.ListType; import com.google.api.expr.v1alpha1.Type.MapType; import com.google.api.expr.v1alpha1.Type.TypeKindCase; @@ -50,6 +51,7 @@ import org.projectnessie.cel.common.types.ObjectT; import org.projectnessie.cel.common.types.TypeT; import org.projectnessie.cel.common.types.pb.Checked; +import org.projectnessie.cel.common.types.ref.FieldType; import org.projectnessie.cel.common.types.ref.Val; import org.projectnessie.cel.types.jackson3.types.AnEnum; import org.projectnessie.cel.types.jackson3.types.CollectionsObject; @@ -59,6 +61,27 @@ class Jackson3TypeDescriptionTest { + static final class AccessorObject { + public final String field; + + @JsonProperty private final String hidden; + + private final String original; + + AccessorObject(String field, String hidden, String original) { + this.field = field; + this.hidden = hidden; + this.original = original; + } + + @JsonProperty("renamed") + public String getOriginal() { + return original; + } + } + + record AccessorRecord(@JsonProperty("record_name") String name) {} + @Test void basics() { Jackson3Registry reg = (Jackson3Registry) newRegistry(); @@ -120,6 +143,32 @@ void basics() { .isInstanceOf(IllegalArgumentException.class); } + @Test + void cachedPropertyAccessorsPreserveDiscoveredMembers() { + Jackson3Registry reg = (Jackson3Registry) newRegistry(); + AccessorObject value = new AccessorObject("field-value", "hidden-value", "method-value"); + reg.register(AccessorObject.class); + + FieldType field = reg.findFieldType(AccessorObject.class.getName(), "field"); + FieldType hidden = reg.findFieldType(AccessorObject.class.getName(), "hidden"); + FieldType renamed = reg.findFieldType(AccessorObject.class.getName(), "renamed"); + + assertThat(field.getFrom.getFrom(value)).isEqualTo("field-value"); + assertThat(hidden.getFrom.getFrom(value)).isEqualTo("hidden-value"); + assertThat(renamed.getFrom.getFrom(value)).isEqualTo("method-value"); + assertThat(renamed.isSet.isSet(value)).isTrue(); + + ObjectT object = (ObjectT) reg.nativeToValue(value); + assertThat(object.get(stringOf("field"))).isEqualTo(stringOf("field-value")); + assertThat(object.get(stringOf("hidden"))).isEqualTo(stringOf("hidden-value")); + assertThat(object.get(stringOf("renamed"))).isEqualTo(stringOf("method-value")); + + AccessorRecord record = new AccessorRecord("record-value"); + reg.register(AccessorRecord.class); + FieldType recordName = reg.findFieldType(AccessorRecord.class.getName(), "record_name"); + assertThat(recordName.getFrom.getFrom(record)).isEqualTo("record-value"); + } + @Test void enumConstantSpecificClassBodyUsesDeclaringClassName() { Jackson3Registry reg = (Jackson3Registry) newRegistry(); diff --git a/quarkus-smoke-core-pb3-jackson3/src/main/java/org/projectnessie/cel/quarkus/smoke/corepb3jackson3/SmokeResource.java b/quarkus-smoke-core-pb3-jackson3/src/main/java/org/projectnessie/cel/quarkus/smoke/corepb3jackson3/SmokeResource.java index 21e06b8a..1f37c041 100644 --- a/quarkus-smoke-core-pb3-jackson3/src/main/java/org/projectnessie/cel/quarkus/smoke/corepb3jackson3/SmokeResource.java +++ b/quarkus-smoke-core-pb3-jackson3/src/main/java/org/projectnessie/cel/quarkus/smoke/corepb3jackson3/SmokeResource.java @@ -22,8 +22,9 @@ import static org.projectnessie.cel.EnvOption.types; import static org.projectnessie.cel.Library.StdLib; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes; import com.google.protobuf.Int32Value; +import dev.cel.expr.conformance.proto3.NestedTestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes; import io.quarkus.runtime.annotations.RegisterForReflection; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @@ -123,7 +124,7 @@ private static boolean booleanResult(EvalResult result) { public record SmokeResponse(String engine, boolean jackson, boolean protobuf) {} - @RegisterForReflection(targets = TestAllTypes.class) + @RegisterForReflection(targets = {TestAllTypes.class, NestedTestAllTypes.class}) static final class ProtobufReflection {} @RegisterForReflection diff --git a/settings.gradle.kts b/settings.gradle.kts index 75e3c1c4..2b80cc22 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -139,8 +139,6 @@ fun celProject(name: String) { project(":cel-$name").projectDir = file(name) } -celProject("generated-antlr") - celProject("generated-pb") celProject("generated-pb3") diff --git a/standalone/build.gradle.kts b/standalone/build.gradle.kts index bb4a05d9..5683db47 100644 --- a/standalone/build.gradle.kts +++ b/standalone/build.gradle.kts @@ -37,7 +37,6 @@ dependencies { api(project(":cel-tools")) api(project(":cel-jackson")) api(project(":cel-jackson3")) - api(project(":cel-generated-antlr")) compileOnly(project(":cel-generated-pb")) compileOnly(libs.protobuf.java) @@ -47,7 +46,6 @@ dependencies { standaloneShadow(project(":cel-tools")) standaloneShadow(project(":cel-jackson")) standaloneShadow(project(":cel-jackson3")) - standaloneShadow(project(":cel-generated-antlr")) standaloneShadow(project(":cel-generated-pb")) standaloneShadow(libs.protobuf.java) standaloneShadow(libs.agrona) diff --git a/submodules/cel-spec b/submodules/cel-spec index ae15d293..cb51b417 160000 --- a/submodules/cel-spec +++ b/submodules/cel-spec @@ -1 +1 @@ -Subproject commit ae15d293dc49482180e967942612fb85e33bcde9 +Subproject commit cb51b4176013ad19bd00df94be273c322916a620 diff --git a/submodules/googleapis b/submodules/googleapis index bc059246..3e4ab771 160000 --- a/submodules/googleapis +++ b/submodules/googleapis @@ -1 +1 @@ -Subproject commit bc05924644a4bb93c0ac5973a07b83387a93b71f +Subproject commit 3e4ab771a856b7fd2a837321f5ba484487e831f9 diff --git a/tools/src/main/java/org/projectnessie/cel/tools/Script.java b/tools/src/main/java/org/projectnessie/cel/tools/Script.java index 2ee6d939..fb5e658c 100644 --- a/tools/src/main/java/org/projectnessie/cel/tools/Script.java +++ b/tools/src/main/java/org/projectnessie/cel/tools/Script.java @@ -26,6 +26,7 @@ import org.projectnessie.cel.Program.EvalResult; import org.projectnessie.cel.common.types.Err; import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.ActivationFunction; public final class Script { private final Env env; @@ -36,12 +37,24 @@ public final class Script { this.prg = prg; } + /** Migrate to {@link #executeWithActivation(Class, ActivationFunction)}. */ + @Deprecated(forRemoval = true) public T execute(Class resultType, Function arguments) throws ScriptException { return evaluate(resultType, arguments); } public T execute(Class resultType, Map arguments) throws ScriptException { + return executeWithActivation(resultType, arguments); + } + + public T executeWithActivation(Class resultType, ActivationFunction arguments) + throws ScriptException { + return evaluate(resultType, arguments); + } + + public T executeWithActivation(Class resultType, Map arguments) + throws ScriptException { return evaluate(resultType, arguments); } @@ -71,6 +84,6 @@ private T evaluate(Class resultType, Object arguments) throws ScriptExecu return (T) result; } - return result.convertToNative(resultType); + return env.getTypeAdapter().valueToNative(result, resultType); } } diff --git a/tools/src/test/java/org/projectnessie/cel/tools/ScriptHostTest.java b/tools/src/test/java/org/projectnessie/cel/tools/ScriptHostTest.java index d2d9c042..6affface 100644 --- a/tools/src/test/java/org/projectnessie/cel/tools/ScriptHostTest.java +++ b/tools/src/test/java/org/projectnessie/cel/tools/ScriptHostTest.java @@ -37,6 +37,7 @@ import org.projectnessie.cel.common.types.MapT; import org.projectnessie.cel.common.types.StringT; import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.ActivationFunction; import org.projectnessie.cel.interpreter.functions.Overload; import org.projectnessie.cel.toolstests.Dummy; @@ -63,7 +64,7 @@ void basic() throws Exception { arguments.put("x", "hello"); arguments.put("y", "world"); - String result = script.execute(String.class, arguments); + String result = script.executeWithActivation(String.class, arguments); assertThat(result).isEqualTo("hello world"); } @@ -81,7 +82,7 @@ void function() throws Exception { .build(); String result = - script.execute( + script.executeWithActivation( String.class, arg -> { if ("x".equals(arg)) { @@ -89,7 +90,7 @@ void function() throws Exception { } else if ("y".equals(arg)) { return "world"; } else { - return null; + return ActivationFunction.ABSENT; } }); @@ -101,12 +102,12 @@ void executeValReturnsCelNativeResult() throws Exception { ScriptHost scriptHost = ScriptHost.newBuilder().build(); Script listScript = scriptHost.buildScript("[1, 2, 3]").build(); - Val list = listScript.execute(Val.class, Collections.emptyMap()); + Val list = listScript.executeWithActivation(Val.class, Collections.emptyMap()); assertThat(list).isInstanceOf(ListT.class); assertThat((Object[]) list.value()).containsExactly(1L, 2L, 3L); Script mapScript = scriptHost.buildScript("{\"a\": 1, \"b\": 2}").build(); - Val map = mapScript.execute(Val.class, Collections.emptyMap()); + Val map = mapScript.executeWithActivation(Val.class, Collections.emptyMap()); assertThat(map).isInstanceOf(MapT.class); Map expectedMap = new HashMap<>(); expectedMap.put("a", 1L); @@ -118,7 +119,7 @@ void executeValReturnsCelNativeResult() throws Exception { void executeObjectStillConvertsToNativeResult() throws Exception { Script script = ScriptHost.newBuilder().build().buildScript("[1, 2, 3]").build(); - Object result = script.execute(Object.class, Collections.emptyMap()); + Object result = script.executeWithActivation(Object.class, Collections.emptyMap()); assertThat(result).isEqualTo(Arrays.asList(1L, 2L, 3L)); } @@ -130,7 +131,8 @@ void execFail() throws Exception { // create the script, will be parsed and checked Script script = scriptHost.buildScript("1/0 != 0").build(); - assertThatThrownBy(() -> script.execute(String.class, singletonMap("x", "hello world"))) + assertThatThrownBy( + () -> script.executeWithActivation(String.class, singletonMap("x", "hello world"))) .isInstanceOf(ScriptExecutionException.class) .hasMessage("divide by zero"); } @@ -142,7 +144,7 @@ void badSyntax() { assertThatThrownBy(() -> scriptHost.buildScript("-.,").build()) .isInstanceOf(ScriptCreateException.class) .hasMessageStartingWith( - "parse failed: ERROR: :1:3: Syntax error: mismatched input ',' expecting IDENTIFIER"); + "parse failed: ERROR: :1:3: Syntax error: Encountered an error"); } @Test @@ -177,7 +179,7 @@ public List getProgramOptions() { Script script = scriptHost.buildScript("foo()").withLibraries(new MyLib()).build(); - assertThat(script.execute(Integer.class, Collections.emptyMap())).isEqualTo(42); + assertThat(script.executeWithActivation(Integer.class, Collections.emptyMap())).isEqualTo(42); } @Test @@ -202,7 +204,7 @@ void readmeExample() throws Exception { arguments.put("inp", pojo); arguments.put("checkName", checkName); - assertThat(script.execute(Boolean.class, arguments)).isTrue(); + assertThat(script.executeWithActivation(Boolean.class, arguments)).isTrue(); } @Test @@ -277,7 +279,7 @@ void authorizationExpressionRuntimeErrorIsNonGranting() throws Exception { Map arguments = authorizationArguments("storage.googleapis.com"); arguments.remove("resource.name"); - assertThatThrownBy(() -> script.execute(Boolean.class, arguments)) + assertThatThrownBy(() -> script.executeWithActivation(Boolean.class, arguments)) .isInstanceOf(ScriptExecutionException.class); assertThat(grants(script, arguments)).isFalse(); } @@ -325,7 +327,7 @@ void customGlobalFunctionErrorIsNonGranting() throws Exception { .withLibraries(new AttributeLibrary("prod")) .build(); - assertThatThrownBy(() -> script.execute(Boolean.class, Collections.emptyMap())) + assertThatThrownBy(() -> script.executeWithActivation(Boolean.class, Collections.emptyMap())) .isInstanceOf(ScriptExecutionException.class) .hasMessageContaining("forced getAttribute error"); assertThat(grants(script, Collections.emptyMap())).isFalse(); @@ -375,7 +377,7 @@ void customReceiverFunctionErrorIsNonGranting() throws Exception { .isFalse(); assertThatThrownBy( () -> - script.execute( + script.executeWithActivation( Boolean.class, Collections.singletonMap( "resource.name", "projects/_/buckets/example/objects/reports/q1.csv"))) @@ -415,7 +417,7 @@ private static Map authorizationArguments( private static boolean grants(Script script, Map arguments) { try { - return Boolean.TRUE.equals(script.execute(Boolean.class, arguments)); + return Boolean.TRUE.equals(script.executeWithActivation(Boolean.class, arguments)); } catch (ScriptException | RuntimeException e) { return false; } @@ -450,7 +452,7 @@ public List getProgramOptions() { if (!(keyVal instanceof StringT) || !(defaultVal instanceof StringT)) { return newErr("invalid arguments to getAttribute"); } - String key = keyVal.convertToNative(String.class); + String key = keyVal.value().toString(); if ("error".equals(key)) { return newErr("forced getAttribute error"); } @@ -485,8 +487,8 @@ public List getProgramOptions() { if (!(valueVal instanceof StringT) || !(prefixVal instanceof StringT)) { return newErr("invalid arguments to extractAfter"); } - String value = valueVal.convertToNative(String.class); - String prefix = prefixVal.convertToNative(String.class); + String value = valueVal.value().toString(); + String prefix = prefixVal.value().toString(); if ("error".equals(prefix)) { return newErr("forced extractAfter error"); }