diff --git a/common/src/main/java/dev/cel/common/ast/CelConstant.java b/common/src/main/java/dev/cel/common/ast/CelConstant.java index c27f4ff8f..7b8490183 100644 --- a/common/src/main/java/dev/cel/common/ast/CelConstant.java +++ b/common/src/main/java/dev/cel/common/ast/CelConstant.java @@ -207,4 +207,26 @@ public static CelConstant ofObjectValue(Object value) { throw new IllegalArgumentException("Value is not a CelConstant: " + value); } + + /** Gets the underlying value held by this constant. */ + public Object objectValue() { + switch (getKind()) { + case NULL_VALUE: + return nullValue(); + case BOOLEAN_VALUE: + return booleanValue(); + case INT64_VALUE: + return int64Value(); + case UINT64_VALUE: + return uint64Value(); + case DOUBLE_VALUE: + return doubleValue(); + case STRING_VALUE: + return stringValue(); + case BYTES_VALUE: + return bytesValue(); + default: + throw new IllegalStateException("Unsupported kind: " + getKind()); + } + } } diff --git a/common/src/main/java/dev/cel/common/types/BUILD.bazel b/common/src/main/java/dev/cel/common/types/BUILD.bazel index 579fd31c5..a35a897b8 100644 --- a/common/src/main/java/dev/cel/common/types/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/types/BUILD.bazel @@ -183,6 +183,20 @@ java_library( ], ) +java_library( + name = "default_type_provider", + srcs = [ + "DefaultTypeProvider.java", + ], + tags = [ + ], + deps = [ + ":type_providers", + ":types", + "@maven//:com_google_guava_guava", + ], +) + cel_android_library( name = "cel_types_android", srcs = ["CelTypes.java"], diff --git a/common/src/main/java/dev/cel/common/types/DefaultTypeProvider.java b/common/src/main/java/dev/cel/common/types/DefaultTypeProvider.java new file mode 100644 index 000000000..84e6c9ede --- /dev/null +++ b/common/src/main/java/dev/cel/common/types/DefaultTypeProvider.java @@ -0,0 +1,52 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.types; + +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableMap; +import java.util.Optional; + +/** {@code DefaultTypeProvider} is a registry of common CEL types. */ +public class DefaultTypeProvider implements CelTypeProvider { + + private static final DefaultTypeProvider INSTANCE = new DefaultTypeProvider(); + private final ImmutableMap commonTypes; + + @Override + public ImmutableCollection types() { + return commonTypes.values(); + } + + @Override + public Optional findType(String typeName) { + return Optional.ofNullable(commonTypes.get(typeName)); + } + + public static DefaultTypeProvider getInstance() { + return INSTANCE; + } + + private DefaultTypeProvider() { + ImmutableMap.Builder typeMapBuilder = ImmutableMap.builder(); + typeMapBuilder.putAll(SimpleType.TYPE_MAP); + typeMapBuilder.put("list", ListType.create(SimpleType.DYN)); + typeMapBuilder.put("map", MapType.create(SimpleType.DYN, SimpleType.DYN)); + typeMapBuilder.put( + "optional_type", + // TODO: Move to CelOptionalLibrary and register it on demand + OptionalType.create(SimpleType.DYN)); + this.commonTypes = typeMapBuilder.buildOrThrow(); + } +} diff --git a/common/src/main/java/dev/cel/common/types/SimpleType.java b/common/src/main/java/dev/cel/common/types/SimpleType.java index cbf02e9c6..6c43ab53f 100644 --- a/common/src/main/java/dev/cel/common/types/SimpleType.java +++ b/common/src/main/java/dev/cel/common/types/SimpleType.java @@ -44,7 +44,7 @@ public abstract class SimpleType extends CelType { public static final CelType TIMESTAMP = create(CelKind.TIMESTAMP, "google.protobuf.Timestamp"); public static final CelType UINT = create(CelKind.UINT, "uint"); - private static final ImmutableMap TYPE_MAP = + public static final ImmutableMap TYPE_MAP = ImmutableMap.of( DYN.name(), DYN, BOOL.name(), BOOL, diff --git a/common/src/main/java/dev/cel/common/values/CelValueConverter.java b/common/src/main/java/dev/cel/common/values/CelValueConverter.java index a281318f8..05fbf0e38 100644 --- a/common/src/main/java/dev/cel/common/values/CelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/CelValueConverter.java @@ -33,7 +33,7 @@ @SuppressWarnings("unchecked") // Unchecked cast of generics due to type-erasure (ex: MapValue). @Internal @Immutable -abstract class CelValueConverter { +public abstract class CelValueConverter { /** Adapts a {@link CelValue} to a plain old Java Object. */ public Object fromCelValueToJavaObject(CelValue celValue) { @@ -41,7 +41,8 @@ public Object fromCelValueToJavaObject(CelValue celValue) { if (celValue instanceof MapValue) { MapValue mapValue = (MapValue) celValue; - ImmutableMap.Builder mapBuilder = ImmutableMap.builder(); + ImmutableMap.Builder mapBuilder = + ImmutableMap.builderWithExpectedSize(mapValue.size()); for (Entry entry : mapValue.value().entrySet()) { Object key = fromCelValueToJavaObject(entry.getKey()); Object value = fromCelValueToJavaObject(entry.getValue()); @@ -51,7 +52,8 @@ public Object fromCelValueToJavaObject(CelValue celValue) { return mapBuilder.buildOrThrow(); } else if (celValue instanceof ListValue) { ListValue listValue = (ListValue) celValue; - ImmutableList.Builder listBuilder = ImmutableList.builder(); + ImmutableList.Builder listBuilder = + ImmutableList.builderWithExpectedSize(listValue.size()); for (CelValue element : listValue.value()) { listBuilder.add(fromCelValueToJavaObject(element)); } diff --git a/common/types/BUILD.bazel b/common/types/BUILD.bazel index 6c2b1a269..41d3d59b2 100644 --- a/common/types/BUILD.bazel +++ b/common/types/BUILD.bazel @@ -50,6 +50,12 @@ java_library( exports = ["//common/src/main/java/dev/cel/common/types:cel_proto_message_types"], ) +java_library( + name = "default_type_provider", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/types:default_type_provider"], +) + java_library( name = "cel_v1alpha1_types", visibility = ["//:internal"], diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index 251d45650..4596e41e9 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -19,6 +19,14 @@ java_library( ], ) +java_library( + name = "cel_value_dispatcher", + visibility = ["//:internal"], + exports = [ + "//runtime/src/main/java/dev/cel/runtime:cel_value_dispatcher", + ], +) + java_library( name = "dispatcher", visibility = ["//:internal"], @@ -233,6 +241,16 @@ java_library( exports = ["//runtime/src/main/java/dev/cel/runtime:resolved_overload_internal"], ) +java_library( + name = "cel_value_function_binding", + exports = ["//runtime/src/main/java/dev/cel/runtime:cel_value_function_binding"], +) + +java_library( + name = "cel_value_function_overload", + exports = ["//runtime/src/main/java/dev/cel/runtime:cel_value_function_overload"], +) + java_library( name = "internal_function_binder", visibility = ["//:internal"], diff --git a/runtime/planner/BUILD.bazel b/runtime/planner/BUILD.bazel new file mode 100644 index 000000000..8da29f270 --- /dev/null +++ b/runtime/planner/BUILD.bazel @@ -0,0 +1,11 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//:internal"], +) + +java_library( + name = "program_planner", + exports = ["//runtime/src/main/java/dev/cel/runtime/planner:program_planner"], +) diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 56f9a7bd2..1cda2ef3d 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -115,6 +115,21 @@ java_library( ], ) +java_library( + name = "cel_value_dispatcher", + srcs = ["CelValueDispatcher.java"], + tags = [ + ], + deps = [ + ":cel_value_function_binding", + ":cel_value_function_overload", + "//:auto_value", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + java_library( name = "dispatcher", srcs = DISPATCHER_SOURCES, @@ -583,8 +598,8 @@ java_library( srcs = INTERPRABLE_SOURCES, deps = [ ":evaluation_exception", - ":evaluation_listener", "//common/annotations", + "//runtime:evaluation_listener", "//runtime:function_resolver", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:org_jspecify_jspecify", @@ -804,6 +819,7 @@ java_library( "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_protobuf_protobuf_java", + "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) @@ -837,6 +853,7 @@ java_library( ":function_resolver", ":interpretable", ":interpreter", + ":program", ":proto_message_activation_factory", ":proto_message_runtime_equality", ":runtime_equality", @@ -873,6 +890,7 @@ java_library( deps = [ ":evaluation_exception", ":function_binding", + ":program", "//:auto_value", "//common:cel_ast", "//common:options", @@ -908,6 +926,7 @@ java_library( "//runtime:program", "//runtime/standard:standard_function", "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], ) @@ -1194,6 +1213,34 @@ cel_android_library( ], ) +java_library( + name = "cel_value_function_binding", + srcs = ["CelValueFunctionBinding.java"], + tags = [ + ], + deps = [ + ":cel_value_function_overload", + "//common/values:cel_value", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "cel_value_function_overload", + srcs = [ + "CelValueFunctionOverload.java", + ], + tags = [ + ], + deps = [ + ":evaluation_exception", + "//common/values:cel_value", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + java_library( name = "resolved_overload", srcs = ["CelResolvedOverload.java"], diff --git a/runtime/src/main/java/dev/cel/runtime/CelValueDispatcher.java b/runtime/src/main/java/dev/cel/runtime/CelValueDispatcher.java new file mode 100644 index 000000000..ebef3c5c0 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelValueDispatcher.java @@ -0,0 +1,67 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.CheckReturnValue; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.annotations.Internal; +import java.util.Optional; + +/** TODO */ +@Internal +@AutoValue +@Immutable +public abstract class CelValueDispatcher { + + abstract ImmutableMap overloads(); + + public Optional findOverload(String overloadId) { + return Optional.ofNullable(overloads().get(overloadId)); + } + + public static Builder newBuilder() { + return new AutoValue_CelValueDispatcher.Builder(); + } + + /** TODO */ + @AutoValue.Builder + public abstract static class Builder { + public abstract ImmutableMap.Builder overloadsBuilder(); + + @CanIgnoreReturnValue + public Builder addOverload(CelValueFunctionBinding functionBinding) { + overloadsBuilder().put(functionBinding.overloadId(), functionBinding); + return this; + } + + @CanIgnoreReturnValue + public Builder addDynamicDispatchOverload( + String functionName, CelValueFunctionOverload definition) { + overloadsBuilder() + .put( + functionName, + CelValueFunctionBinding.from(functionName, ImmutableList.of(), definition)); + + return this; + } + + @CheckReturnValue + public abstract CelValueDispatcher build(); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelValueFunctionBinding.java b/runtime/src/main/java/dev/cel/runtime/CelValueFunctionBinding.java new file mode 100644 index 000000000..952967832 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelValueFunctionBinding.java @@ -0,0 +1,107 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.values.CelValue; + +/** TODO */ +@Immutable +public final class CelValueFunctionBinding { + + private final String overloadId; + private final ImmutableList> argTypes; + private final CelValueFunctionOverload definition; + + public String overloadId() { + return overloadId; + } + + public ImmutableList> argTypes() { + return argTypes; + } + + public CelValueFunctionOverload definition() { + return definition; + } + + public static CelValueFunctionBinding from( + String overloadId, CelValueFunctionOverload.Nullary impl) { + return from(overloadId, ImmutableList.of(), (args) -> impl.apply()); + } + + @SuppressWarnings("unchecked") + public static CelValueFunctionBinding from( + String overloadId, Class argType, CelValueFunctionOverload.Unary impl) { + return from(overloadId, ImmutableList.of(argType), (args) -> impl.apply((T) args[0])); + } + + @SuppressWarnings("unchecked") + public static CelValueFunctionBinding from( + String overloadId, + Class argType1, + Class argType2, + CelValueFunctionOverload.Binary impl) { + return from( + overloadId, + ImmutableList.of(argType1, argType2), + (args) -> impl.apply((T1) args[0], (T2) args[1])); + } + + public static + CelValueFunctionBinding from( + String overloadId, + Class argType1, + Class argType2, + Class argType3, + CelValueFunctionOverload.Ternary impl) { + return from( + overloadId, + ImmutableList.of(argType1, argType2, argType3), + (args) -> impl.apply((T1) args[0], (T2) args[1], (T3) args[2])); + } + + public static CelValueFunctionBinding from( + String overloadId, + ImmutableList> argTypes, + CelValueFunctionOverload impl) { + return new CelValueFunctionBinding(overloadId, argTypes, impl); + } + + public boolean canHandle(CelValue[] arguments) { + if (argTypes().size() != arguments.length) { + return false; + } + + for (int i = 0; i < argTypes().size(); i++) { + Class paramType = argTypes().get(i); + CelValue arg = arguments[i]; + if (!paramType.isInstance(arg)) { + return false; + } + } + return true; + } + + private CelValueFunctionBinding( + String overloadId, + ImmutableList> argTypes, + CelValueFunctionOverload definition) { + this.overloadId = overloadId; + this.argTypes = argTypes; + this.definition = definition; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelValueFunctionOverload.java b/runtime/src/main/java/dev/cel/runtime/CelValueFunctionOverload.java new file mode 100644 index 000000000..36305a41f --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelValueFunctionOverload.java @@ -0,0 +1,51 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.values.CelValue; + +/** TODO */ +@Immutable +@FunctionalInterface +public interface CelValueFunctionOverload { + + /** TODO */ + CelValue apply(CelValue... args); + + /** TODO */ + @Immutable + interface Nullary { + CelValue apply(); + } + + /** TODO */ + @Immutable + interface Unary { + CelValue apply(T arg); + } + + /** TODO */ + @Immutable + interface Binary { + CelValue apply(T1 arg1, T2 arg2); + } + + /** TODO */ + @Immutable + interface Ternary { + CelValue apply(T1 arg1, T2 arg2, T3 arg3); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java b/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java new file mode 100644 index 000000000..0ce487ceb --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java @@ -0,0 +1,77 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.TypeType; +import dev.cel.runtime.GlobalResolver; + +@Immutable +interface Attribute { + Object resolve(GlobalResolver ctx); + + final class MaybeAttribute implements Attribute { + private final ImmutableList attributes; + + @Override + public Object resolve(GlobalResolver ctx) { + for (Attribute attr : attributes) { + Object value = attr.resolve(ctx); + if (value != null) { + return value; + } + } + + // TODO: Handle unknowns + throw new UnsupportedOperationException("Unknown attributes is not supported yet"); + } + + MaybeAttribute(ImmutableList attributes) { + this.attributes = attributes; + } + } + + final class NamespacedAttribute implements Attribute { + private final ImmutableList namespacedNames; + private final CelTypeProvider typeProvider; + + @Override + public Object resolve(GlobalResolver ctx) { + for (String name : namespacedNames) { + Object value = ctx.resolve(name); + if (value != null) { + // TODO: apply qualifiers + return value; + } + + TypeType type = typeProvider.findType(name).map(TypeType::create).orElse(null); + if (type != null) { + return type; + } + } + + // TODO: Handle unknowns + throw new UnsupportedOperationException("Unknown attributes is not supported yet"); + } + + NamespacedAttribute(CelTypeProvider typeProvider, ImmutableList namespacedNames) { + this.typeProvider = typeProvider; + this.namespacedNames = namespacedNames; + } + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/AttributeFactory.java b/runtime/src/main/java/dev/cel/runtime/planner/AttributeFactory.java new file mode 100644 index 000000000..bda7c46a6 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/AttributeFactory.java @@ -0,0 +1,49 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.CelContainer; +import dev.cel.common.types.CelTypeProvider; +import dev.cel.runtime.planner.Attribute.MaybeAttribute; +import dev.cel.runtime.planner.Attribute.NamespacedAttribute; + +@Immutable +final class AttributeFactory { + + private final CelContainer unusedContainer; + private final CelTypeProvider typeProvider; + + NamespacedAttribute newAbsoluteAttribute(String... names) { + return new NamespacedAttribute(typeProvider, ImmutableList.copyOf(names)); + } + + MaybeAttribute newMaybeAttribute(String... names) { + // TODO: Resolve container names + return new MaybeAttribute( + ImmutableList.of(new NamespacedAttribute(typeProvider, ImmutableList.copyOf(names)))); + } + + static AttributeFactory newAttributeFactory( + CelContainer celContainer, CelTypeProvider typeProvider) { + return new AttributeFactory(celContainer, typeProvider); + } + + private AttributeFactory(CelContainer container, CelTypeProvider typeProvider) { + this.unusedContainer = container; + this.typeProvider = typeProvider; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel new file mode 100644 index 000000000..a5017b331 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -0,0 +1,260 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = [ + "//runtime/planner:__pkg__", + ], +) + +java_library( + name = "program_planner", + srcs = ["ProgramPlanner.java"], + tags = [ + ], + deps = [ + ":attribute_factory", + ":cel_value_interpretable", + ":cel_value_program", + ":eval_and", + ":eval_attribute", + ":eval_const", + ":eval_create_list", + ":eval_create_map", + ":eval_create_struct", + ":eval_fold", + ":eval_or", + ":eval_unary", + ":eval_var_args_call", + ":eval_zero_arity", + "//:auto_value", + "//common:cel_ast", + "//common:container", + "//common/annotations", + "//common/ast", + "//common/types", + "//common/types:type_providers", + "//common/values", + "//common/values:cel_value", + "//common/values:cel_value_provider", + "//runtime:cel_value_dispatcher", + "//runtime:cel_value_function_binding", + "//runtime:evaluation_exception", + "//runtime:evaluation_exception_builder", + "//runtime:program", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "cel_value_interpretable", + srcs = ["CelValueInterpretable.java"], + deps = [ + "//common/annotations", + "//common/values:cel_value", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "cel_value_program", + srcs = ["CelValueProgram.java"], + tags = [ + ], + deps = [ + ":cel_value_interpretable", + "//:auto_value", + "//common:runtime_exception", + "//common/values", + "//common/values:cel_value", + "//runtime:activation", + "//runtime:evaluation_exception", + "//runtime:evaluation_exception_builder", + "//runtime:function_resolver", + "//runtime:interpretable", + "//runtime:program", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "eval_const", + srcs = ["EvalConstant.java"], + deps = [ + ":cel_value_interpretable", + "//common/values:cel_value", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "attribute", + srcs = ["Attribute.java"], + deps = [ + "//common/types", + "//common/types:type_providers", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "attribute_factory", + srcs = ["AttributeFactory.java"], + deps = [ + ":attribute", + "//common:container", + "//common/types:type_providers", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_attribute", + srcs = ["EvalAttribute.java"], + deps = [ + ":attribute", + ":cel_value_interpretable", + "//common/values", + "//common/values:cel_value", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_zero_arity", + srcs = ["EvalZeroArity.java"], + deps = [ + ":cel_value_interpretable", + "//common/values:cel_value", + "//runtime:cel_value_function_binding", + "//runtime:evaluation_exception", + "//runtime:interpretable", + ], +) + +java_library( + name = "eval_unary", + srcs = ["EvalUnary.java"], + deps = [ + ":cel_value_interpretable", + "//common/values:cel_value", + "//runtime:cel_value_function_binding", + "//runtime:evaluation_exception", + "//runtime:interpretable", + ], +) + +java_library( + name = "eval_var_args_call", + srcs = ["EvalVarArgsCall.java"], + deps = [ + ":cel_value_interpretable", + "//common/values:cel_value", + "//runtime:cel_value_function_binding", + "//runtime:evaluation_exception", + "//runtime:interpretable", + ], +) + +java_library( + name = "eval_or", + srcs = ["EvalOr.java"], + deps = [ + ":cel_value_interpretable", + ":eval_helpers", + "//common/values", + "//common/values:cel_value", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_and", + srcs = ["EvalAnd.java"], + deps = [ + ":cel_value_interpretable", + ":eval_helpers", + "//common/values", + "//common/values:cel_value", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_create_struct", + srcs = ["EvalCreateStruct.java"], + deps = [ + ":cel_value_interpretable", + "//common/values:cel_value", + "//common/values:cel_value_provider", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_create_list", + srcs = ["EvalCreateList.java"], + deps = [ + ":cel_value_interpretable", + "//common/values", + "//common/values:cel_value", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_create_map", + srcs = ["EvalCreateMap.java"], + deps = [ + ":cel_value_interpretable", + "//common/values", + "//common/values:cel_value", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_fold", + srcs = ["EvalFold.java"], + deps = [ + ":cel_value_interpretable", + "//common/values", + "//common/values:cel_value", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + +java_library( + name = "eval_helpers", + srcs = ["EvalHelpers.java"], + deps = [ + ":cel_value_interpretable", + "//common/values", + "//common/values:cel_value", + "//runtime:interpretable", + ], +) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/CelValueInterpretable.java b/runtime/src/main/java/dev/cel/runtime/planner/CelValueInterpretable.java new file mode 100644 index 000000000..1a1f3a47d --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/CelValueInterpretable.java @@ -0,0 +1,32 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.annotations.Internal; +import dev.cel.common.values.CelValue; +import dev.cel.runtime.GlobalResolver; + +/** + * Represent an expression which can be interpreted repeatedly using a given activation. + * + *

CEL Library Internals. Do Not Use. + */ +@Immutable +@Internal +interface CelValueInterpretable { + + CelValue eval(GlobalResolver resolver); +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/CelValueProgram.java b/runtime/src/main/java/dev/cel/runtime/planner/CelValueProgram.java new file mode 100644 index 000000000..9890cca45 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/CelValueProgram.java @@ -0,0 +1,84 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.CelRuntimeException; +import dev.cel.common.values.CelValue; +import dev.cel.common.values.CelValueConverter; +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.Activation; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelEvaluationExceptionBuilder; +import dev.cel.runtime.CelFunctionResolver; +import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.Program; +import java.util.Map; + +@Immutable +@AutoValue +abstract class CelValueProgram implements Program { + abstract CelValueInterpretable interpretable(); + + abstract CelValueConverter celValueConverter(); + + @Override + public Object eval() throws CelEvaluationException { + return evalThenUnwrap(interpretable(), GlobalResolver.EMPTY); + } + + @Override + public Object eval(Map mapValue) throws CelEvaluationException { + return evalThenUnwrap(interpretable(), Activation.copyOf(mapValue)); + } + + @Override + public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException { + throw new UnsupportedOperationException("Late bound functions not supported yet"); + } + + private Object evalThenUnwrap(CelValueInterpretable interpretable, GlobalResolver resolver) + throws CelEvaluationException { + try { + CelValue evalResult = interpretable.eval(resolver); + if (evalResult instanceof ErrorValue) { + ErrorValue errorValue = (ErrorValue) evalResult; + throw newCelEvaluationException(errorValue.value()); + } + return celValueConverter().fromCelValueToJavaObject(evalResult); + } catch (CelRuntimeException e) { + throw CelEvaluationExceptionBuilder.newBuilder(e).build(); + } catch (RuntimeException e) { + throw newCelEvaluationException(e); + } + } + + private static CelEvaluationException newCelEvaluationException(Exception e) { + CelEvaluationExceptionBuilder builder; + if (e instanceof CelRuntimeException) { + builder = CelEvaluationExceptionBuilder.newBuilder((CelRuntimeException) e); + } else { + builder = CelEvaluationExceptionBuilder.newBuilder(e.getMessage()).setCause(e); + } + + return builder.build(); + } + + static Program create(CelValueInterpretable interpretable, CelValueConverter celValueConverter) { + return new AutoValue_CelValueProgram(interpretable, celValueConverter); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java new file mode 100644 index 000000000..0988d3242 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java @@ -0,0 +1,60 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; + +import com.google.common.base.Preconditions; +import dev.cel.common.values.BoolValue; +import dev.cel.common.values.CelValue; +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.GlobalResolver; + +final class EvalAnd implements CelValueInterpretable { + + @SuppressWarnings("Immutable") + private final CelValueInterpretable[] args; + + @Override + public CelValue eval(GlobalResolver resolver) { + ErrorValue errorValue = null; + for (CelValueInterpretable arg : args) { + CelValue argVal = evalNonstrictly(arg, resolver); + if (argVal instanceof BoolValue) { + // Short-circuit on false + if (!((boolean) argVal.value())) { + return argVal; + } + } else if (argVal instanceof ErrorValue) { + errorValue = (ErrorValue) argVal; + } + } + + if (errorValue != null) { + return errorValue; + } + + return BoolValue.create(true); + } + + static EvalAnd create(CelValueInterpretable[] args) { + return new EvalAnd(args); + } + + private EvalAnd(CelValueInterpretable[] args) { + Preconditions.checkArgument(args.length == 2); + this.args = args; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java new file mode 100644 index 000000000..5734c4d9a --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java @@ -0,0 +1,42 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.values.CelValue; +import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.GlobalResolver; + +@Immutable +final class EvalAttribute implements CelValueInterpretable { + + private final CelValueConverter celValueConverter; + private final Attribute attr; + + @Override + public CelValue eval(GlobalResolver resolver) { + Object obj = attr.resolve(resolver); + return celValueConverter.fromJavaObjectToCelValue(obj); + } + + static EvalAttribute create(CelValueConverter celValueConverter, Attribute attr) { + return new EvalAttribute(celValueConverter, attr); + } + + private EvalAttribute(CelValueConverter celValueConverter, Attribute attr) { + this.celValueConverter = celValueConverter; + this.attr = attr; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java new file mode 100644 index 000000000..d902aa1c9 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java @@ -0,0 +1,38 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.values.CelValue; +import dev.cel.runtime.GlobalResolver; + +@Immutable +final class EvalConstant implements CelValueInterpretable { + + private final CelValue constant; + + @Override + public CelValue eval(GlobalResolver resolver) { + return constant; + } + + static EvalConstant create(CelValue constant) { + return new EvalConstant(constant); + } + + private EvalConstant(CelValue constant) { + this.constant = constant; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java new file mode 100644 index 000000000..61317fc0e --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java @@ -0,0 +1,46 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.values.CelValue; +import dev.cel.common.values.ImmutableListValue; +import dev.cel.runtime.GlobalResolver; + +@Immutable +final class EvalCreateList implements CelValueInterpretable { + + // Array contents are not mutated + @SuppressWarnings("Immutable") + private final CelValueInterpretable[] values; + + @Override + public CelValue eval(GlobalResolver resolver) { + ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(values.length); + for (CelValueInterpretable value : values) { + builder.add(value.eval(resolver)); + } + return ImmutableListValue.create(builder.build()); + } + + static EvalCreateList create(CelValueInterpretable[] values) { + return new EvalCreateList(values); + } + + private EvalCreateList(CelValueInterpretable[] values) { + this.values = values; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java new file mode 100644 index 000000000..9b991de0e --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java @@ -0,0 +1,54 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.values.CelValue; +import dev.cel.common.values.ImmutableMapValue; +import dev.cel.runtime.GlobalResolver; + +@Immutable +final class EvalCreateMap implements CelValueInterpretable { + + // Array contents are not mutated + @SuppressWarnings("Immutable") + private final CelValueInterpretable[] keys; + + // Array contents are not mutated + @SuppressWarnings("Immutable") + private final CelValueInterpretable[] values; + + @Override + public CelValue eval(GlobalResolver resolver) { + ImmutableMap.Builder builder = + ImmutableMap.builderWithExpectedSize(keys.length); + for (int i = 0; i < keys.length; i++) { + builder.put(keys[i].eval(resolver), values[i].eval(resolver)); + } + return ImmutableMapValue.create(builder.buildOrThrow()); + } + + static EvalCreateMap create(CelValueInterpretable[] keys, CelValueInterpretable[] values) { + return new EvalCreateMap(keys, values); + } + + private EvalCreateMap(CelValueInterpretable[] keys, CelValueInterpretable[] values) { + Preconditions.checkArgument(keys.length == values.length); + this.keys = keys; + this.values = values; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java new file mode 100644 index 000000000..0806b1169 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java @@ -0,0 +1,68 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.values.CelValue; +import dev.cel.common.values.CelValueProvider; +import dev.cel.runtime.GlobalResolver; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Immutable +final class EvalCreateStruct implements CelValueInterpretable { + + private final CelValueProvider valueProvider; + private final String typeName; + + @SuppressWarnings("Immutable") + private final String[] keys; + + @SuppressWarnings("Immutable") + private final CelValueInterpretable[] values; + + @Override + public CelValue eval(GlobalResolver resolver) { + Map fieldValues = new HashMap<>(); + for (int i = 0; i < keys.length; i++) { + Object value = values[i].eval(resolver).value(); + fieldValues.put(keys[i], value); + } + + return valueProvider + .newValue(typeName, Collections.unmodifiableMap(fieldValues)) + .orElseThrow(() -> new IllegalArgumentException("Type name not found: " + typeName)); + } + + static EvalCreateStruct create( + CelValueProvider valueProvider, + String typeName, + String[] keys, + CelValueInterpretable[] values) { + return new EvalCreateStruct(valueProvider, typeName, keys, values); + } + + private EvalCreateStruct( + CelValueProvider valueProvider, + String typeName, + String[] keys, + CelValueInterpretable[] values) { + this.valueProvider = valueProvider; + this.typeName = typeName; + this.keys = keys; + this.values = values; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java new file mode 100644 index 000000000..bb5ac50da --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java @@ -0,0 +1,130 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.values.CelValue; +import dev.cel.common.values.IntValue; +import dev.cel.runtime.GlobalResolver; +import java.util.Collection; +import java.util.Iterator; +import org.jspecify.annotations.Nullable; + +@Immutable +final class EvalFold implements CelValueInterpretable { + + private final String accuVar; + private final CelValueInterpretable accuInit; + private final String iterVar; + private final String iterVar2; + private final CelValueInterpretable iterRange; + private final CelValueInterpretable condition; + private final CelValueInterpretable loopStep; + private final CelValueInterpretable result; + + @Override + public CelValue eval(GlobalResolver resolver) { + // TODO: Consider creating a folder abstraction like in cel-go. This requires some + // legwork in attribute qualification. + Collection foldRange = (Collection) iterRange.eval(resolver); + + Folder folder = new Folder(resolver, accuVar, iterVar, iterVar2); + + folder.accuVal = accuInit.eval(folder); + + long index = 0; + for (Iterator iterator = foldRange.iterator(); iterator.hasNext(); ) { + // TODO: Implement condition + if (iterVar2.isEmpty()) { + folder.iterVarVal = iterator.next(); + } else { + folder.iterVarVal = IntValue.create(index); + folder.iterVar2Val = iterator.next(); + } + folder.accuVal = loopStep.eval(folder); + index++; + } + + return result.eval(folder); + } + + private static class Folder implements GlobalResolver { + private final GlobalResolver resolver; + private final String accuVar; + private final String iterVar; + private final String iterVar2; + + private CelValue iterVarVal; + private CelValue iterVar2Val; + private CelValue accuVal; + + private Folder(GlobalResolver resolver, String accuVar, String iterVar, String iterVar2) { + this.resolver = resolver; + this.accuVar = accuVar; + this.iterVar = iterVar; + this.iterVar2 = iterVar2; + } + + @Override + public @Nullable Object resolve(String name) { + if (name.equals(accuVar)) { + return accuVal; + } + + // Todo: !f.computeResult check + if (name.equals(iterVar)) { + return this.iterVarVal; + } + + if (name.equals(iterVar2)) { + return this.iterVar2Val; + } + + return resolver.resolve(name); + } + } + + static EvalFold create( + String accuVar, + CelValueInterpretable accuInit, + String iterVar, + String iterVar2, + CelValueInterpretable iterRange, + CelValueInterpretable condition, + CelValueInterpretable loopStep, + CelValueInterpretable result) { + return new EvalFold( + accuVar, accuInit, iterVar, iterVar2, iterRange, condition, loopStep, result); + } + + private EvalFold( + String accuVar, + CelValueInterpretable accuInit, + String iterVar, + String iterVar2, + CelValueInterpretable iterRange, + CelValueInterpretable condition, + CelValueInterpretable loopStep, + CelValueInterpretable result) { + this.accuVar = accuVar; + this.accuInit = accuInit; + this.iterVar = iterVar; + this.iterVar2 = iterVar2; + this.iterRange = iterRange; + this.condition = condition; + this.loopStep = loopStep; + this.result = result; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java new file mode 100644 index 000000000..76e153678 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java @@ -0,0 +1,32 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import dev.cel.common.values.CelValue; +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.GlobalResolver; + +final class EvalHelpers { + + static CelValue evalNonstrictly(CelValueInterpretable interpretable, GlobalResolver resolver) { + try { + return interpretable.eval(resolver); + } catch (Exception e) { + return ErrorValue.create(e); + } + } + + private EvalHelpers() {} +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java new file mode 100644 index 000000000..d3d1094ef --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java @@ -0,0 +1,60 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; + +import com.google.common.base.Preconditions; +import dev.cel.common.values.BoolValue; +import dev.cel.common.values.CelValue; +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.GlobalResolver; + +final class EvalOr implements CelValueInterpretable { + + @SuppressWarnings("Immutable") + private final CelValueInterpretable[] args; + + @Override + public CelValue eval(GlobalResolver resolver) { + ErrorValue errorValue = null; + for (CelValueInterpretable arg : args) { + CelValue argVal = evalNonstrictly(arg, resolver); + if (argVal instanceof BoolValue) { + // Short-circuit on true + if (((boolean) argVal.value())) { + return argVal; + } + } else if (argVal instanceof ErrorValue) { + errorValue = (ErrorValue) argVal; + } + } + + if (errorValue != null) { + return errorValue; + } + + return BoolValue.create(false); + } + + static EvalOr create(CelValueInterpretable[] args) { + return new EvalOr(args); + } + + private EvalOr(CelValueInterpretable[] args) { + Preconditions.checkArgument(args.length == 2); + this.args = args; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java new file mode 100644 index 000000000..35e08962f --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java @@ -0,0 +1,40 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import dev.cel.common.values.CelValue; +import dev.cel.runtime.CelValueFunctionBinding; +import dev.cel.runtime.GlobalResolver; + +final class EvalUnary implements CelValueInterpretable { + + private final CelValueFunctionBinding resolvedOverload; + private final CelValueInterpretable arg; + + @Override + public CelValue eval(GlobalResolver resolver) { + CelValue argVal = arg.eval(resolver); + return resolvedOverload.definition().apply(argVal); + } + + static EvalUnary create(CelValueFunctionBinding resolvedOverload, CelValueInterpretable arg) { + return new EvalUnary(resolvedOverload, arg); + } + + private EvalUnary(CelValueFunctionBinding resolvedOverload, CelValueInterpretable arg) { + this.resolvedOverload = resolvedOverload; + this.arg = arg; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java new file mode 100644 index 000000000..f634aaa37 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java @@ -0,0 +1,47 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import dev.cel.common.values.CelValue; +import dev.cel.runtime.CelValueFunctionBinding; +import dev.cel.runtime.GlobalResolver; + +@SuppressWarnings("Immutable") +final class EvalVarArgsCall implements CelValueInterpretable { + + private final CelValueFunctionBinding resolvedOverload; + private final CelValueInterpretable[] args; + + @Override + public CelValue eval(GlobalResolver resolver) { + CelValue[] argVals = new CelValue[args.length]; + for (int i = 0; i < args.length; i++) { + CelValueInterpretable arg = args[i]; + argVals[i] = arg.eval(resolver); + } + + return resolvedOverload.definition().apply(argVals); + } + + static EvalVarArgsCall create( + CelValueFunctionBinding resolvedOverload, CelValueInterpretable[] args) { + return new EvalVarArgsCall(resolvedOverload, args); + } + + private EvalVarArgsCall(CelValueFunctionBinding resolvedOverload, CelValueInterpretable[] args) { + this.resolvedOverload = resolvedOverload; + this.args = args; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalZeroArity.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalZeroArity.java new file mode 100644 index 000000000..399387e2e --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalZeroArity.java @@ -0,0 +1,37 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import dev.cel.common.values.CelValue; +import dev.cel.runtime.CelValueFunctionBinding; +import dev.cel.runtime.GlobalResolver; + +final class EvalZeroArity implements CelValueInterpretable { + + private final CelValueFunctionBinding resolvedOverload; + + @Override + public CelValue eval(GlobalResolver resolver) { + return resolvedOverload.definition().apply(); + } + + static EvalZeroArity create(CelValueFunctionBinding resolvedOverload) { + return new EvalZeroArity(resolvedOverload); + } + + private EvalZeroArity(CelValueFunctionBinding resolvedOverload) { + this.resolvedOverload = resolvedOverload; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java new file mode 100644 index 000000000..60dc4f957 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -0,0 +1,373 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.CheckReturnValue; +import javax.annotation.concurrent.ThreadSafe; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; +import dev.cel.common.annotations.Internal; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.ast.CelExpr.CelCall; +import dev.cel.common.ast.CelExpr.CelComprehension; +import dev.cel.common.ast.CelExpr.CelList; +import dev.cel.common.ast.CelExpr.CelMap; +import dev.cel.common.ast.CelExpr.CelStruct; +import dev.cel.common.ast.CelExpr.CelStruct.Entry; +import dev.cel.common.ast.CelReference; +import dev.cel.common.types.CelKind; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.TypeType; +import dev.cel.common.values.CelValue; +import dev.cel.common.values.CelValueConverter; +import dev.cel.common.values.CelValueProvider; +import dev.cel.common.values.TypeValue; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelEvaluationExceptionBuilder; +import dev.cel.runtime.CelValueDispatcher; +import dev.cel.runtime.CelValueFunctionBinding; +import dev.cel.runtime.Program; +import java.util.HashMap; +import java.util.NoSuchElementException; +import java.util.Optional; + +/** + * {@code ProgramPlanner} resolves functions, types, and identifiers at plan time given a + * parsed-only or a type-checked expression. + */ +@ThreadSafe +@Internal +public final class ProgramPlanner { + private final CelTypeProvider typeProvider; + private final CelValueProvider valueProvider; + private final CelValueConverter celValueConverter; + private final CelValueDispatcher dispatcher; + private final AttributeFactory attributeFactory; + + /** + * Plans a {@link Program} from the provided parsed-only or type-checked {@link + * CelAbstractSyntaxTree}. + */ + public Program plan(CelAbstractSyntaxTree ast) throws CelEvaluationException { + CelValueInterpretable plannedInterpretable; + try { + plannedInterpretable = plan(ast.getExpr(), PlannerContext.create(ast)); + } catch (RuntimeException e) { + throw CelEvaluationExceptionBuilder.newBuilder(e.getMessage()).setCause(e).build(); + } + + return CelValueProgram.create(plannedInterpretable, celValueConverter); + } + + private CelValueInterpretable plan(CelExpr celExpr, PlannerContext ctx) { + switch (celExpr.getKind()) { + case CONSTANT: + return fromCelConstant(celExpr.constant()); + case IDENT: + return planIdent(celExpr, ctx); + case SELECT: + break; + case CALL: + return planCall(celExpr, ctx); + case LIST: + return planCreateList(celExpr, ctx); + case STRUCT: + return planCreateStruct(celExpr, ctx); + case MAP: + return planCreateMap(celExpr, ctx); + case COMPREHENSION: + return planComprehension(celExpr, ctx); + case NOT_SET: + throw new UnsupportedOperationException("Unsupported kind: " + celExpr.getKind()); + } + + throw new IllegalArgumentException("Not yet implemented kind: " + celExpr.getKind()); + } + + + private CelValueInterpretable planIdent(CelExpr celExpr, PlannerContext ctx) { + CelReference ref = ctx.referenceMap().get(celExpr.id()); + if (ref != null) { + return planCheckedIdent(celExpr.id(), ref, ctx.typeMap()); + } + + return EvalAttribute.create( + celValueConverter, attributeFactory.newMaybeAttribute(celExpr.ident().name())); + } + + private CelValueInterpretable planCheckedIdent( + long id, CelReference identRef, ImmutableMap typeMap) { + if (identRef.value().isPresent()) { + return fromCelConstant(identRef.value().get()); + } + + CelType type = typeMap.get(id); + if (type.kind().equals(CelKind.TYPE)) { + TypeType identType = + typeProvider + .findType(identRef.name()) + .map(TypeType::create) + .orElseThrow( + () -> + new NoSuchElementException( + "Reference to an undefined type: " + identRef.name())); + return EvalConstant.create(TypeValue.create(identType)); + } + + return EvalAttribute.create( + celValueConverter, attributeFactory.newAbsoluteAttribute(identRef.name())); + } + + private EvalConstant fromCelConstant(CelConstant celConstant) { + CelValue celValue = celValueConverter.fromJavaObjectToCelValue(celConstant.objectValue()); + return EvalConstant.create(celValue); + } + + private CelValueInterpretable planCall(CelExpr expr, PlannerContext ctx) { + ResolvedFunction resolvedFunction = resolveFunction(expr, ctx.referenceMap()); + CelExpr target = resolvedFunction.target().orElse(null); + int argCount = expr.call().args().size(); + if (target != null) { + argCount++; + } + + CelValueInterpretable[] evaluatedArgs = new CelValueInterpretable[argCount]; + + int offset = 0; + if (target != null) { + evaluatedArgs[0] = plan(target, ctx); + offset++; + } + + ImmutableList args = expr.call().args(); + for (int argIndex = 0; argIndex < args.size(); argIndex++) { + evaluatedArgs[argIndex + offset] = plan(args.get(argIndex), ctx); + } + + // TODO: Handle all specialized calls (logical operators, conditionals, equals etc) + String functionName = resolvedFunction.functionName(); + switch (functionName) { + // TODO: Move Operator.java out to common package and use that instead. + case "_||_": + return EvalOr.create(evaluatedArgs); + case "_&&_": + return EvalAnd.create(evaluatedArgs); + default: + // fall-through + } + + CelValueFunctionBinding resolvedOverload = null; + + if (resolvedFunction.overloadId().isPresent()) { + resolvedOverload = dispatcher.findOverload(resolvedFunction.overloadId().get()).orElse(null); + } + + if (resolvedOverload == null) { + resolvedOverload = + dispatcher + .findOverload(functionName) + .orElseThrow(() -> new NoSuchElementException("Overload not found: " + functionName)); + } + + switch (argCount) { + case 0: + return EvalZeroArity.create(resolvedOverload); + case 1: + return EvalUnary.create(resolvedOverload, evaluatedArgs[0]); + // TODO: Handle binary + default: + return EvalVarArgsCall.create(resolvedOverload, evaluatedArgs); + } + } + + private CelValueInterpretable planCreateStruct(CelExpr celExpr, PlannerContext ctx) { + CelStruct struct = celExpr.struct(); + // TODO: maybe perform the check via type provider? + CelValue unused = + valueProvider + .newValue(struct.messageName(), new HashMap<>()) + .orElseThrow( + () -> new IllegalArgumentException("Undefined type name: " + struct.messageName())); + + ImmutableList entries = struct.entries(); + String[] keys = new String[entries.size()]; + CelValueInterpretable[] values = new CelValueInterpretable[entries.size()]; + + for (int i = 0; i < entries.size(); i++) { + Entry entry = entries.get(i); + keys[i] = entry.fieldKey(); + values[i] = plan(entry.value(), ctx); + } + + return EvalCreateStruct.create(valueProvider, struct.messageName(), keys, values); + } + + private CelValueInterpretable planCreateList(CelExpr celExpr, PlannerContext ctx) { + CelList list = celExpr.list(); + + ImmutableList elements = list.elements(); + CelValueInterpretable[] values = new CelValueInterpretable[elements.size()]; + + for (int i = 0; i < elements.size(); i++) { + values[i] = plan(elements.get(i), ctx); + } + + return EvalCreateList.create(values); + } + + private CelValueInterpretable planCreateMap(CelExpr celExpr, PlannerContext ctx) { + CelMap map = celExpr.map(); + + ImmutableList entries = map.entries(); + CelValueInterpretable[] keys = new CelValueInterpretable[entries.size()]; + CelValueInterpretable[] values = new CelValueInterpretable[entries.size()]; + + for (int i = 0; i < entries.size(); i++) { + CelMap.Entry entry = entries.get(i); + keys[i] = plan(entry.key(), ctx); + values[i] = plan(entry.value(), ctx); + } + + return EvalCreateMap.create(keys, values); + } + + private CelValueInterpretable planComprehension(CelExpr expr, PlannerContext ctx) { + CelComprehension comprehension = expr.comprehension(); + + CelValueInterpretable accuInit = plan(comprehension.accuInit(), ctx); + CelValueInterpretable iterRange = plan(comprehension.iterRange(), ctx); + CelValueInterpretable loopCondition = plan(comprehension.loopCondition(), ctx); + CelValueInterpretable loopStep = plan(comprehension.loopStep(), ctx); + CelValueInterpretable result = plan(comprehension.result(), ctx); + + return EvalFold.create( + comprehension.accuVar(), + accuInit, + comprehension.iterVar(), + comprehension.iterVar2(), + iterRange, + loopCondition, + loopStep, + result); + } + + /** + * resolveFunction determines the call target, function name, and overload name (when unambiguous) + * from the given call expr. + */ + private ResolvedFunction resolveFunction( + CelExpr expr, ImmutableMap referenceMap) { + CelCall call = expr.call(); + Optional target = call.target(); + String functionName = call.function(); + + CelReference reference = referenceMap.get(expr.id()); + if (reference != null) { + // Checked expression + if (reference.overloadIds().size() == 1) { + ResolvedFunction.Builder builder = + ResolvedFunction.newBuilder() + .setFunctionName(functionName) + .setOverloadId(reference.overloadIds().get(0)); + + target.ifPresent(builder::setTarget); + + return builder.build(); + } + } + + // Parse-only from this point on + // dispatcher.findOverload(functionName) + // .orElseThrow(() -> new NoSuchElementException(String.format("Function %s not found", + // call.function()))); + + if (!target.isPresent()) { + // TODO: Handle containers. + + return ResolvedFunction.newBuilder().setFunctionName(functionName).build(); + } else { + // TODO: Handle qualifications + return ResolvedFunction.newBuilder() + .setFunctionName(functionName) + .setTarget(target.get()) + .build(); + } + } + + @AutoValue + abstract static class ResolvedFunction { + + abstract String functionName(); + + abstract Optional target(); + + abstract Optional overloadId(); + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setFunctionName(String functionName); + + abstract Builder setTarget(CelExpr target); + + abstract Builder setOverloadId(String overloadId); + + @CheckReturnValue + abstract ResolvedFunction build(); + } + + private static Builder newBuilder() { + return new AutoValue_ProgramPlanner_ResolvedFunction.Builder(); + } + } + + @AutoValue + abstract static class PlannerContext { + + abstract ImmutableMap referenceMap(); + + abstract ImmutableMap typeMap(); + + private static PlannerContext create(CelAbstractSyntaxTree ast) { + return new AutoValue_ProgramPlanner_PlannerContext(ast.getReferenceMap(), ast.getTypeMap()); + } + } + + public static ProgramPlanner newPlanner( + CelTypeProvider typeProvider, + CelValueProvider valueProvider, + CelValueConverter celValueConverter, + CelValueDispatcher dispatcher) { + return new ProgramPlanner(typeProvider, valueProvider, celValueConverter, dispatcher); + } + + private ProgramPlanner( + CelTypeProvider typeProvider, + CelValueProvider valueProvider, + CelValueConverter celValueConverter, + CelValueDispatcher dispatcher) { + this.typeProvider = typeProvider; + this.valueProvider = valueProvider; + this.celValueConverter = celValueConverter; + this.dispatcher = dispatcher; + // TODO: Container support + this.attributeFactory = + AttributeFactory.newAttributeFactory(CelContainer.newBuilder().build(), typeProvider); + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel index 092f61f04..4eb81dee1 100644 --- a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel @@ -154,6 +154,7 @@ cel_android_local_test( "//runtime:lite_runtime_android", "//runtime:lite_runtime_factory_android", "//runtime:lite_runtime_impl_android", + "//runtime:program_android", "//runtime:standard_functions_android", "//runtime:unknown_attributes_android", "//runtime/src/main/java/dev/cel/runtime:program_android", diff --git a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel new file mode 100644 index 000000000..4e373f9ac --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel @@ -0,0 +1,78 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:testing.bzl", "junit4_test_suites") + +package( + default_applicable_licenses = ["//:license"], + default_testonly = True, +) + +java_library( + name = "tests", + testonly = 1, + srcs = glob( + ["*.java"], + ), + deps = [ + "//:java_truth", + "//common:cel_ast", + "//common:cel_descriptor_util", + "//common:cel_source", + "//common:compiler_common", + "//common:error_codes", + "//common:operator", + "//common:options", + "//common:runtime_exception", + "//common/ast", + "//common/internal:cel_descriptor_pools", + "//common/internal:default_message_factory", + "//common/internal:dynamic_proto", + "//common/types", + "//common/types:default_type_provider", + "//common/types:message_type_provider", + "//common/types:type_providers", + "//common/values", + "//common/values:cel_byte_string", + "//common/values:cel_value", + "//common/values:proto_message_value", + "//common/values:proto_message_value_provider", + "//compiler", + "//compiler:compiler_builder", + "//extensions", + "//extensions:optional_library", + "//parser:macro", + "//runtime", + "//runtime:cel_value_dispatcher", + "//runtime:cel_value_function_binding", + "//runtime:cel_value_function_overload", + "//runtime:function_binding", + "//runtime:program", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "//runtime/planner:program_planner", + "//runtime/standard:add", + "//runtime/standard:divide", + "//runtime/standard:equals", + "//runtime/standard:greater", + "//runtime/standard:greater_equals", + "//runtime/standard:index", + "//runtime/standard:less", + "//runtime/standard:logical_not", + "//runtime/standard:not_strictly_false", + "//runtime/standard:standard_function", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + +junit4_test_suites( + name = "test_suites", + sizes = [ + "small", + ], + src_dir = "src/test/java", + deps = [ + ":tests", + ], +) diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java new file mode 100644 index 000000000..f5b9a2e02 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -0,0 +1,694 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static com.google.common.truth.Truth.assertThat; +import static dev.cel.common.CelFunctionDecl.newFunctionDeclaration; +import static dev.cel.common.CelOverloadDecl.newGlobalOverload; +import static dev.cel.common.CelOverloadDecl.newMemberOverload; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.primitives.UnsignedLong; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.junit.testparameterinjector.TestParameters; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelDescriptorUtil; +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelOptions; +import dev.cel.common.CelRuntimeException; +import dev.cel.common.CelSource; +import dev.cel.common.Operator; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.internal.CelDescriptorPool; +import dev.cel.common.internal.DefaultDescriptorPool; +import dev.cel.common.internal.DefaultMessageFactory; +import dev.cel.common.internal.DynamicProto; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.CelTypeProvider.CombinedCelTypeProvider; +import dev.cel.common.types.DefaultTypeProvider; +import dev.cel.common.types.ListType; +import dev.cel.common.types.MapType; +import dev.cel.common.types.OptionalType; +import dev.cel.common.types.ProtoMessageTypeProvider; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.common.types.TypeType; +import dev.cel.common.values.BoolValue; +import dev.cel.common.values.BytesValue; +import dev.cel.common.values.CelByteString; +import dev.cel.common.values.CelValue; +import dev.cel.common.values.CelValueConverter; +import dev.cel.common.values.DoubleValue; +import dev.cel.common.values.DurationValue; +import dev.cel.common.values.IntValue; +import dev.cel.common.values.ListValue; +import dev.cel.common.values.MapValue; +import dev.cel.common.values.NullValue; +import dev.cel.common.values.ProtoCelValueConverter; +import dev.cel.common.values.ProtoMessageValueProvider; +import dev.cel.common.values.StringValue; +import dev.cel.common.values.TimestampValue; +import dev.cel.common.values.TypeValue; +import dev.cel.common.values.UintValue; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.expr.conformance.proto3.GlobalEnum; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.extensions.CelExtensions; +import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.CelValueDispatcher; +import dev.cel.runtime.CelValueFunctionBinding; +import dev.cel.runtime.CelValueFunctionOverload; +import dev.cel.runtime.Program; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +import dev.cel.runtime.standard.AddOperator; +import dev.cel.runtime.standard.CelStandardFunction; +import dev.cel.runtime.standard.DivideOperator; +import dev.cel.runtime.standard.EqualsOperator; +import dev.cel.runtime.standard.GreaterEqualsOperator; +import dev.cel.runtime.standard.GreaterOperator; +import dev.cel.runtime.standard.IndexOperator; +import dev.cel.runtime.standard.LessOperator; +import dev.cel.runtime.standard.LogicalNotOperator; +import dev.cel.runtime.standard.NotStrictlyFalseFunction; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class ProgramPlannerTest { + // Note that the following deps are ordinarily built from top-level builder APIs + private static final CelOptions CEL_OPTIONS = CelOptions.current().build(); + private static final CelDescriptorPool DESCRIPTOR_POOL = + DefaultDescriptorPool.create( + CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( + TestAllTypes.getDescriptor().getFile())); + private static final DynamicProto DYNAMIC_PROTO = + DynamicProto.create(DefaultMessageFactory.create(DESCRIPTOR_POOL)); + private static final CelValueConverter CEL_VALUE_CONVERTER = + ProtoCelValueConverter.newInstance(DESCRIPTOR_POOL, DYNAMIC_PROTO); + private static final CelTypeProvider TYPE_PROVIDER = + new CombinedCelTypeProvider( + DefaultTypeProvider.getInstance(), + new ProtoMessageTypeProvider(ImmutableSet.of(TestAllTypes.getDescriptor()))); + private static final RuntimeEquality RUNTIME_EQUALITY = + RuntimeEquality.create(RuntimeHelpers.create(), CEL_OPTIONS); + + private static final ProgramPlanner PLANNER = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + ProtoMessageValueProvider.newInstance(CEL_OPTIONS, DYNAMIC_PROTO), + CEL_VALUE_CONVERTER, + newDispatcher()); + + private static final CelCompiler CEL_COMPILER = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .addVar("int_var", SimpleType.INT) + .addVar("map_var", MapType.create(SimpleType.STRING, SimpleType.DYN)) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addFunctionDeclarations( + newFunctionDeclaration("zero", newGlobalOverload("zero_overload", SimpleType.INT)), + newFunctionDeclaration("error", newGlobalOverload("error_overload", SimpleType.INT)), + newFunctionDeclaration( + "neg", + newGlobalOverload("neg_int", SimpleType.INT, SimpleType.INT), + newGlobalOverload("neg_double", SimpleType.DOUBLE, SimpleType.DOUBLE)), + newFunctionDeclaration( + "concat", + newGlobalOverload( + "concat_bytes_bytes", SimpleType.BYTES, SimpleType.BYTES, SimpleType.BYTES), + newMemberOverload( + "bytes_concat_bytes", SimpleType.BYTES, SimpleType.BYTES, SimpleType.BYTES))) + .addLibraries(CelOptionalLibrary.INSTANCE, CelExtensions.comprehensions()) + .addMessageTypes(TestAllTypes.getDescriptor()) + .build(); + + /** + * Configure dispatcher for testing purposes. This is done manually here, but this should be + * driven by the top-level runtime APIs in the future + */ + private static CelValueDispatcher newDispatcher() { + CelValueDispatcher.Builder builder = CelValueDispatcher.newBuilder(); + + // Subsetted StdLib + addBindings( + builder, Operator.INDEX.getFunction(), fromStandardFunction(IndexOperator.create())); + addBindings( + builder, + Operator.LOGICAL_NOT.getFunction(), + fromStandardFunction(LogicalNotOperator.create())); + addBindings(builder, Operator.ADD.getFunction(), fromStandardFunction(AddOperator.create())); + addBindings( + builder, Operator.GREATER.getFunction(), fromStandardFunction(GreaterOperator.create())); + addBindings( + builder, + Operator.GREATER_EQUALS.getFunction(), + fromStandardFunction(GreaterEqualsOperator.create())); + addBindings(builder, Operator.LESS.getFunction(), fromStandardFunction(LessOperator.create())); + addBindings( + builder, Operator.DIVIDE.getFunction(), fromStandardFunction(DivideOperator.create())); + addBindings( + builder, Operator.EQUALS.getFunction(), fromStandardFunction(EqualsOperator.create())); + addBindings( + builder, + Operator.NOT_STRICTLY_FALSE.getFunction(), + fromStandardFunction(NotStrictlyFalseFunction.create())); + + // Custom functions + addBindings( + builder, "zero", CelValueFunctionBinding.from("zero_overload", () -> IntValue.create(0L))); + addBindings( + builder, + "error", + CelValueFunctionBinding.from( + "error_overload", + () -> { + throw new IllegalArgumentException("Intentional error"); + })); + addBindings( + builder, + "neg", + CelValueFunctionBinding.from( + "neg_int", IntValue.class, arg -> IntValue.create(-arg.longValue())), + CelValueFunctionBinding.from( + "neg_double", DoubleValue.class, arg -> DoubleValue.create(-arg.doubleValue()))); + addBindings( + builder, + "concat", + CelValueFunctionBinding.from( + "concat_bytes_bytes", + BytesValue.class, + BytesValue.class, + ProgramPlannerTest::concatenateByteArrays), + CelValueFunctionBinding.from( + "bytes_concat_bytes", + BytesValue.class, + BytesValue.class, + ProgramPlannerTest::concatenateByteArrays)); + + return builder.build(); + } + + private static void addBindings( + CelValueDispatcher.Builder builder, + String functionName, + CelValueFunctionBinding... functionBindings) { + addBindings(builder, functionName, ImmutableSet.copyOf(functionBindings)); + } + + private static void addBindings( + CelValueDispatcher.Builder builder, + String functionName, + ImmutableCollection overloadBindings) { + if (overloadBindings.isEmpty()) { + throw new IllegalArgumentException("Invalid bindings"); + } + // TODO: Runtime top-level APIs currently does not allow grouping overloads with + // the function name. This capability will have to be added. + if (overloadBindings.size() == 1) { + CelValueFunctionBinding singleBinding = Iterables.getOnlyElement(overloadBindings); + builder.addOverload( + CelValueFunctionBinding.from( + functionName, singleBinding.argTypes(), singleBinding.definition())); + } else { + overloadBindings.forEach(builder::addOverload); + + // Setup dynamic dispatch + CelValueFunctionOverload dynamicDispatchDef = + args -> { + for (CelValueFunctionBinding overload : overloadBindings) { + if (overload.canHandle(args)) { + return overload.definition().apply(args); + } + } + + throw new IllegalArgumentException("Overload not found: " + functionName); + }; + + builder.addDynamicDispatchOverload(functionName, dynamicDispatchDef); + } + } + + @TestParameter boolean isParseOnly; + + @Test + public void plan_notSet_throws() { + CelAbstractSyntaxTree invalidAst = + CelAbstractSyntaxTree.newParsedAst(CelExpr.ofNotSet(0L), CelSource.newBuilder().build()); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> PLANNER.plan(invalidAst)); + + assertThat(e).hasMessageThat().contains("evaluation error: Unsupported kind: NOT_SET"); + } + + @Test + public void plan_constant(@TestParameter ConstantTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = compile(testCase.expression); + Program program = PLANNER.plan(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo(testCase.expected); + } + + @Test + public void plan_ident_enum() throws Exception { + CelAbstractSyntaxTree ast = + compile(GlobalEnum.getDescriptor().getFullName() + "." + GlobalEnum.GAR); + Program program = PLANNER.plan(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo(1); + } + + @Test + public void plan_ident_variable() throws Exception { + CelAbstractSyntaxTree ast = compile("int_var"); + Program program = PLANNER.plan(ast); + + Object result = program.eval(ImmutableMap.of("int_var", 1L)); + + assertThat(result).isEqualTo(1); + } + + @Test + @SuppressWarnings("unchecked") // test only + public void plan_createStruct() throws Exception { + CelAbstractSyntaxTree ast = compile("cel.expr.conformance.proto3.TestAllTypes{}"); + Program program = PLANNER.plan(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo(TestAllTypes.getDefaultInstance()); + } + + @Test + public void planCreateStruct_withFields() throws Exception { + CelAbstractSyntaxTree ast = + compile( + "cel.expr.conformance.proto3.TestAllTypes{" + + "single_string: 'foo'," + + "single_bool: true" + + "}"); + Program program = PLANNER.plan(ast); + + Object result = program.eval(); + + assertThat(result) + .isEqualTo(TestAllTypes.newBuilder().setSingleString("foo").setSingleBool(true).build()); + } + + @Test + @SuppressWarnings("unchecked") // test only + public void plan_createList() throws Exception { + CelAbstractSyntaxTree ast = compile("[1, 'foo', true, [2, false]]"); + Program program = PLANNER.plan(ast); + + ImmutableList result = (ImmutableList) program.eval(); + + assertThat(result).containsExactly(1L, "foo", true, ImmutableList.of(2L, false)).inOrder(); + } + + @Test + @SuppressWarnings("unchecked") // test only + public void plan_createMap() throws Exception { + CelAbstractSyntaxTree ast = compile("{'foo': 1, true: 'bar'}"); + Program program = PLANNER.plan(ast); + + ImmutableMap result = (ImmutableMap) program.eval(); + + assertThat(result).containsExactly("foo", 1L, true, "bar").inOrder(); + } + + @Test + public void planIdent_typeLiteral(@TestParameter TypeLiteralTestCase testCase) throws Exception { + if (isParseOnly) { + if (testCase.equals(TypeLiteralTestCase.DURATION) + || testCase.equals(TypeLiteralTestCase.TIMESTAMP) + || testCase.equals(TypeLiteralTestCase.PROTO_MESSAGE_TYPE)) { + // TODO Skip for now, requires attribute qualification + return; + } + } + CelAbstractSyntaxTree ast = compile(testCase.expression); + Program program = PLANNER.plan(ast); + + TypeType result = (TypeType) program.eval(); + + assertThat(result).isEqualTo(testCase.type); + } + + @Test + public void planCall_zeroArgs() throws Exception { + CelAbstractSyntaxTree ast = compile("zero()"); + Program program = PLANNER.plan(ast); + + Long result = (Long) program.eval(); + + assertThat(result).isEqualTo(0L); + } + + @Test + public void planCall_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("error()"); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + assertThat(e).hasMessageThat().contains("evaluation error: Intentional error"); + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void planCall_oneArg_int() throws Exception { + CelAbstractSyntaxTree ast = compile("neg(1)"); + Program program = PLANNER.plan(ast); + + Long result = (Long) program.eval(); + + assertThat(result).isEqualTo(-1L); + } + + @Test + public void planCall_oneArg_double() throws Exception { + CelAbstractSyntaxTree ast = compile("neg(2.5)"); + Program program = PLANNER.plan(ast); + + Double result = (Double) program.eval(); + + assertThat(result).isEqualTo(-2.5d); + } + + @Test + public void planCall_twoArgs_global() throws Exception { + CelAbstractSyntaxTree ast = compile("concat(b'abc', b'def')"); + Program program = PLANNER.plan(ast); + + CelByteString result = (CelByteString) program.eval(); + + assertThat(result).isEqualTo(CelByteString.of("abcdef".getBytes(UTF_8))); + } + + @Test + public void planCall_twoArgs_receiver() throws Exception { + CelAbstractSyntaxTree ast = compile("b'abc'.concat(b'def')"); + Program program = PLANNER.plan(ast); + + CelByteString result = (CelByteString) program.eval(); + + assertThat(result).isEqualTo(CelByteString.of("abcdef".getBytes(UTF_8))); + } + + @Test + public void planCall_mapIndex() throws Exception { + CelAbstractSyntaxTree ast = compile("map_var['key'][1]"); + Program program = PLANNER.plan(ast); + ImmutableMap mapVarPayload = ImmutableMap.of("key", ImmutableList.of(1L, 2L)); + + Long result = (Long) program.eval(ImmutableMap.of("map_var", mapVarPayload)); + + assertThat(result).isEqualTo(2L); + } + + @Test + @TestParameters("{expression: 'true || true', expectedResult: true}") + @TestParameters("{expression: 'true || false', expectedResult: true}") + @TestParameters("{expression: 'false || true', expectedResult: true}") + @TestParameters("{expression: 'false || false', expectedResult: false}") + @TestParameters("{expression: 'true || (1 / 0 > 2)', expectedResult: true}") + @TestParameters("{expression: '(1 / 0 > 2) || true', expectedResult: true}") + public void planCall_logicalOr_shortCircuit(String expression, boolean expectedResult) + throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expression: '(1 / 0 > 2) || (1 / 0 > 2)'}") + @TestParameters("{expression: 'false || (1 / 0 > 2)'}") + @TestParameters("{expression: '(1 / 0 > 2) || false'}") + public void planCall_logicalOr_throws(String expression) throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + // TODO: Tag metadata (source loc) + assertThat(e).hasMessageThat().isEqualTo("evaluation error: / by zero"); + assertThat(e).hasCauseThat().isInstanceOf(ArithmeticException.class); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.DIVIDE_BY_ZERO); + } + + @Test + @TestParameters("{expression: 'true && true', expectedResult: true}") + @TestParameters("{expression: 'true && false', expectedResult: false}") + @TestParameters("{expression: 'false && true', expectedResult: false}") + @TestParameters("{expression: 'false && false', expectedResult: false}") + @TestParameters("{expression: 'false && (1 / 0 > 2)', expectedResult: false}") + @TestParameters("{expression: '(1 / 0 > 2) && false', expectedResult: false}") + public void planCall_logicalAnd_shortCircuit(String expression, boolean expectedResult) + throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expression: '(1 / 0 > 2) && (1 / 0 > 2)'}") + @TestParameters("{expression: 'true && (1 / 0 > 2)'}") + @TestParameters("{expression: '(1 / 0 > 2) && true'}") + public void planCall_logicalAnd_throws(String expression) throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + // TODO: Tag metadata (source loc) + assertThat(e).hasMessageThat().isEqualTo("evaluation error: / by zero"); + assertThat(e).hasCauseThat().isInstanceOf(ArithmeticException.class); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.DIVIDE_BY_ZERO); + } + + @Test + public void planSelect() throws Exception { + CelAbstractSyntaxTree ast = compile("msg.single_string"); + Program program = PLANNER.plan(ast); + + String result = + (String) + program.eval( + ImmutableMap.of("msg", TestAllTypes.newBuilder().setSingleString("foo").build())); + + assertThat(result).isEqualTo("foo"); + } + + @Test + @TestParameters("{expression: '[1,2,3].exists(x, x > 0) == true'}") + @TestParameters("{expression: '[1,2,3].exists(x, x < 0) == false'}") + @TestParameters("{expression: '[1,2,3].exists(i, v, i >= 0 && v > 0) == true'}") + @TestParameters("{expression: '[1,2,3].exists(i, v, i < 0 || v < 0) == false'}") + @TestParameters("{expression: '[1,2,3].map(x, x + 1) == [2,3,4]'}") + public void planComprehension_lists(String expression) throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).isTrue(); + } + + @Test + @TestParameters("{expression: '[1,2,3].exists(x, x > 0) == true'}") + @TestParameters("{expression: '[1,2,3].exists(x, x < 0) == false'}") + @TestParameters("{expression: '[1,2,3].exists(i, v, i >= 0 && v > 0) == true'}") + @TestParameters("{expression: '[1,2,3].exists(i, v, i < 0 || v < 0) == false'}") + public void planComprehension_maps(String expression) throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).isTrue(); + } + + private CelAbstractSyntaxTree compile(String expression) throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.parse(expression).getAst(); + if (isParseOnly) { + return ast; + } + + return CEL_COMPILER.check(ast).getAst(); + } + + private static BytesValue concatenateByteArrays(BytesValue bytes1, BytesValue bytes2) { + if (bytes1.isZeroValue()) { + return bytes2; + } + + if (bytes2.isZeroValue()) { + return bytes1; + } + + CelByteString combined = bytes1.value().concat(bytes2.value()); + return BytesValue.create(combined); + } + + // TODO: The following native -> CelValue function binding will need to be an + // adapter. + private static ImmutableSet fromStandardFunction( + CelStandardFunction standardFunction) { + ImmutableSet functionBindings = + standardFunction.newFunctionBindings(CEL_OPTIONS, RUNTIME_EQUALITY); + ImmutableSet.Builder builder = ImmutableSet.builder(); + + for (CelFunctionBinding functionBinding : functionBindings) { + CelValueFunctionBinding adaptedBinding = + CelValueFunctionBinding.from( + functionBinding.getOverloadId(), + adaptArgumentTypes(functionBinding.getArgTypes()), + celValueArgs -> { + Object[] nativeArgs = new Object[celValueArgs.length]; + for (int i = 0; i < celValueArgs.length; i++) { + nativeArgs[i] = CEL_VALUE_CONVERTER.fromCelValueToJavaObject(celValueArgs[i]); + } + + Object nativeResult; + try { + nativeResult = functionBinding.getDefinition().apply(nativeArgs); + } catch (CelRuntimeException e) { + throw e; + } catch (CelEvaluationException e) { + throw new CelRuntimeException(e.getCause(), e.getErrorCode()); + } + return CEL_VALUE_CONVERTER.fromJavaObjectToCelValue(nativeResult); + }); + builder.add(adaptedBinding); + } + + return builder.build(); + } + + private static ImmutableList> adaptArgumentTypes( + ImmutableList> argTypes) { + ImmutableList.Builder> builder = ImmutableList.builder(); + + for (Class argType : argTypes) { + if (argType.equals(String.class)) { + builder.add(StringValue.class); + } else if (argType.equals(Long.class)) { + builder.add(IntValue.class); + } else if (argType.equals(Double.class)) { + builder.add(DoubleValue.class); + } else if (argType.equals(Boolean.class)) { + builder.add(BoolValue.class); + } else if (argType.equals(UnsignedLong.class)) { + builder.add(UintValue.class); + } else if (argType.equals(CelByteString.class)) { + builder.add(BytesValue.class); + } else if (argType.equals(Instant.class)) { + builder.add(TimestampValue.class); + } else if (argType.equals(Duration.class)) { + builder.add(DurationValue.class); + } else if (List.class.isAssignableFrom(argType)) { + builder.add(ListValue.class); + } else if (Map.class.isAssignableFrom(argType)) { + builder.add(MapValue.class); + } else if (CelType.class.isAssignableFrom(argType)) { + builder.add(TypeValue.class); + } else if (argType.equals(NullValue.class)) { + builder.add(NullValue.class); + } else if (argType.equals(Object.class) + || + // Using Number.class was probably a mistake (see index_list). This particular overload + // will benefit from a concrete definition. + argType.equals(Number.class)) { + builder.add(CelValue.class); + } else { + // In all likelihood -- we should probably do an OpaqueValue here + throw new IllegalArgumentException("Unknown argument type: " + argType); + } + } + return builder.build(); + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum ConstantTestCase { + NULL("null", NullValue.NULL_VALUE), + BOOLEAN("true", true), + INT64("42", 42L), + UINT64("42u", UnsignedLong.valueOf(42)), + DOUBLE("1.5", 1.5d), + STRING("'hello world'", "hello world"), + BYTES("b'abc'", CelByteString.of("abc".getBytes(UTF_8))); + + private final String expression; + private final Object expected; + + ConstantTestCase(String expression, Object expected) { + this.expression = expression; + this.expected = expected; + } + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum TypeLiteralTestCase { + BOOL("bool", SimpleType.BOOL), + BYTES("bytes", SimpleType.BYTES), + DOUBLE("double", SimpleType.DOUBLE), + INT("int", SimpleType.INT), + UINT("uint", SimpleType.UINT), + STRING("string", SimpleType.STRING), + DYN("dyn", SimpleType.DYN), + LIST("list", ListType.create(SimpleType.DYN)), + MAP("map", MapType.create(SimpleType.DYN, SimpleType.DYN)), + NULL("null_type", SimpleType.NULL_TYPE), + DURATION("google.protobuf.Duration", SimpleType.DURATION), + TIMESTAMP("google.protobuf.Timestamp", SimpleType.TIMESTAMP), + OPTIONAL("optional_type", OptionalType.create(SimpleType.DYN)), + PROTO_MESSAGE_TYPE( + "cel.expr.conformance.proto3.TestAllTypes", + TYPE_PROVIDER.findType(TestAllTypes.getDescriptor().getFullName()).get()); + + private final String expression; + private final TypeType type; + + TypeLiteralTestCase(String expression, CelType type) { + this.expression = expression; + this.type = TypeType.create(type); + } + } +}