From 5d7a04d6cf9c6dcc51c126ae91ca5df98a69d3b3 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 16 Jan 2026 21:01:38 -0800 Subject: [PATCH] Introduce top-level runtime APIs based on Program Planner PiperOrigin-RevId: 857407924 --- .../java/dev/cel/common/values/BUILD.bazel | 6 +- .../cel/common/values/CelValueConverter.java | 8 +- .../cel/common/values/CelValueProvider.java | 4 + .../common/values/ProtoCelValueConverter.java | 8 +- .../values/ProtoMessageValueProvider.java | 4 +- .../cel/common/values/StructValueTest.java | 35 +- runtime/BUILD.bazel | 9 + .../src/main/java/dev/cel/runtime/BUILD.bazel | 44 ++ .../dev/cel/runtime/CelRuntimeBuilder.java | 28 +- .../java/dev/cel/runtime/CelRuntimeImpl.java | 467 ++++++++++++++++++ .../dev/cel/runtime/CelRuntimeLegacyImpl.java | 32 +- .../runtime/planner/ActivationWrapper.java | 22 + .../cel/runtime/planner/AttributeFactory.java | 13 +- .../java/dev/cel/runtime/planner/BUILD.bazel | 11 + .../cel/runtime/planner/EvalCreateList.java | 2 +- .../dev/cel/runtime/planner/EvalFold.java | 9 +- .../dev/cel/runtime/planner/EvalHelpers.java | 3 + .../runtime/planner/NamespacedAttribute.java | 51 +- .../cel/runtime/planner/ProgramPlanner.java | 76 ++- .../runtime/planner/RelativeAttribute.java | 5 +- .../src/test/java/dev/cel/runtime/BUILD.bazel | 13 +- .../cel/runtime/CelValueInterpreterTest.java | 30 -- .../cel/runtime/PlannerInterpreterTest.java | 60 +++ .../runtime/planner/ProgramPlannerTest.java | 107 +++- 24 files changed, 943 insertions(+), 104 deletions(-) create mode 100644 runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/ActivationWrapper.java delete mode 100644 runtime/src/test/java/dev/cel/runtime/CelValueInterpreterTest.java create mode 100644 runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java diff --git a/common/src/main/java/dev/cel/common/values/BUILD.bazel b/common/src/main/java/dev/cel/common/values/BUILD.bazel index e9b4be4f1..3a78091bf 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -57,6 +57,7 @@ java_library( tags = [ ], deps = [ + "//common/values", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], @@ -68,6 +69,7 @@ cel_android_library( tags = [ ], deps = [ + "//common/values:values_android", "@maven//:com_google_errorprone_error_prone_annotations", "@maven_android//:com_google_guava_guava", ], @@ -207,13 +209,13 @@ java_library( tags = [ ], deps = [ - ":base_proto_message_value_provider", ":proto_message_value", "//common:options", "//common/annotations", "//common/internal:dynamic_proto", "//common/internal:proto_message_factory", - "//common/values:base_proto_cel_value_converter", + "//common/values", + "//common/values:cel_value_provider", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_protobuf_protobuf_java", ], 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 c3f3727a1..ae0b40ef7 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,13 @@ @SuppressWarnings("unchecked") // Unchecked cast of generics due to type-erasure (ex: MapValue). @Internal @Immutable -public abstract class CelValueConverter { +public class CelValueConverter { + + private static final CelValueConverter DEFAULT_INSTANCE = new CelValueConverter(); + + public static CelValueConverter getDefaultInstance() { + return DEFAULT_INSTANCE; + } /** Adapts a {@link CelValue} to a plain old Java Object. */ public Object unwrap(CelValue celValue) { diff --git a/common/src/main/java/dev/cel/common/values/CelValueProvider.java b/common/src/main/java/dev/cel/common/values/CelValueProvider.java index 717834660..20ae865e7 100644 --- a/common/src/main/java/dev/cel/common/values/CelValueProvider.java +++ b/common/src/main/java/dev/cel/common/values/CelValueProvider.java @@ -27,4 +27,8 @@ public interface CelValueProvider { * a wrapper. */ Optional newValue(String structType, Map fields); + + default CelValueConverter celValueConverter() { + return CelValueConverter.getDefaultInstance(); + } } diff --git a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java index 08f30b9d1..9400ae961 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java @@ -82,7 +82,13 @@ public Object toRuntimeValue(Object value) { } if (value instanceof MessageOrBuilder) { - MessageOrBuilder message = (MessageOrBuilder) value; + Message message; + if (value instanceof Message.Builder) { + message = ((Message.Builder) value).build(); + } else { + message = (Message) value; + } + // Attempt to convert the proto from a dynamic message into a concrete message if possible. if (message instanceof DynamicMessage) { message = dynamicProto.maybeAdaptDynamicMessage((DynamicMessage) message); diff --git a/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java b/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java index 5bf2927ab..a05658c8f 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java +++ b/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java @@ -34,13 +34,13 @@ */ @Immutable @Internal -public class ProtoMessageValueProvider extends BaseProtoMessageValueProvider { +public class ProtoMessageValueProvider implements CelValueProvider { private final ProtoAdapter protoAdapter; private final ProtoMessageFactory protoMessageFactory; private final ProtoCelValueConverter protoCelValueConverter; @Override - public BaseProtoCelValueConverter protoCelValueConverter() { + public CelValueConverter celValueConverter() { return protoCelValueConverter; } diff --git a/common/src/test/java/dev/cel/common/values/StructValueTest.java b/common/src/test/java/dev/cel/common/values/StructValueTest.java index 1db147882..b8d6371a8 100644 --- a/common/src/test/java/dev/cel/common/values/StructValueTest.java +++ b/common/src/test/java/dev/cel/common/values/StructValueTest.java @@ -31,7 +31,6 @@ import dev.cel.common.types.CelTypeProvider; import dev.cel.common.types.SimpleType; import dev.cel.common.types.StructType; -import dev.cel.expr.conformance.proto3.TestAllTypes; import java.util.Map; import java.util.Optional; import org.junit.Test; @@ -185,7 +184,7 @@ public void evaluate_usingMultipleProviders_selectFieldFromCustomClass() throws .setValueProvider( CombinedCelValueProvider.combine( ProtoMessageValueProvider.newInstance( - CelOptions.DEFAULT, DynamicProto.create(typeName -> Optional.empty())), + CelOptions.DEFAULT, DynamicProto.create(unused -> Optional.empty())), CUSTOM_STRUCT_VALUE_PROVIDER)) .build(); CelAbstractSyntaxTree ast = cel.compile("custom_struct{data: 5}.data").getAst(); @@ -195,36 +194,8 @@ public void evaluate_usingMultipleProviders_selectFieldFromCustomClass() throws assertThat(result).isEqualTo(5L); } - @Test - public void evaluate_usingMultipleProviders_selectFieldFromProtobufMessage() throws Exception { - Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableCelValue(true).build()) - .addMessageTypes(TestAllTypes.getDescriptor()) - .setTypeProvider(CUSTOM_STRUCT_TYPE_PROVIDER) - .setValueProvider( - CombinedCelValueProvider.combine( - ProtoMessageValueProvider.newInstance( - CelOptions.DEFAULT, - // Note: this is unideal. Future iterations should make DynamicProto - // completely an internal concern, and not expose it at all. - DynamicProto.create( - typeName -> { - if (typeName.equals(TestAllTypes.getDescriptor().getFullName())) { - return Optional.of(TestAllTypes.newBuilder()); - } - return Optional.empty(); - })), - CUSTOM_STRUCT_VALUE_PROVIDER)) - .build(); - CelAbstractSyntaxTree ast = - cel.compile("cel.expr.conformance.proto3.TestAllTypes{single_string: 'foo'}.single_string") - .getAst(); - - String result = (String) cel.createProgram(ast).eval(); - - assertThat(result).isEqualTo("foo"); - } + // TODO: Bring back evaluate_usingMultipleProviders_selectFieldFromProtobufMessage + // once planner is exposed from factory @SuppressWarnings("Immutable") // Test only private static class CelCustomStructValue extends StructValue { diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index 244145960..a42ee7fb4 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -271,3 +271,12 @@ java_library( "//runtime/src/main/java/dev/cel/runtime:variable_resolver", ], ) + +java_library( + name = "runtime_planner_impl", + testonly = 1, # TODO: Move to factory when ready for exposure + visibility = ["//:internal"], + exports = [ + "//runtime/src/main/java/dev/cel/runtime:runtime_planner_impl", + ], +) diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index d253edba3..f77c48d96 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -801,6 +801,48 @@ cel_android_library( ], ) +java_library( + name = "runtime_planner_impl", + testonly = 1, + srcs = ["CelRuntimeImpl.java"], + tags = [ + ], + deps = [ + "//:auto_value", + "//common:cel_ast", + "//common:cel_descriptor_util", + "//common:cel_descriptors", + "//common:container", + "//common:options", + "//common/annotations", + "//common/internal:cel_descriptor_pools", + "//common/internal:default_message_factory", + "//common/internal:dynamic_proto", + "//common/types:default_type_provider", + "//common/types:message_type_provider", + "//common/types:type_providers", + "//common/values", + "//common/values:cel_value_provider", + "//common/values:combined_cel_value_provider", + "//common/values:proto_message_value_provider", + "//runtime", + "//runtime:dispatcher", + "//runtime:evaluation_listener", + "//runtime:function_binding", + "//runtime:function_resolver", + "//runtime:program", + "//runtime:proto_message_runtime_helpers", + "//runtime:runtime_equality", + "//runtime:standard_functions", + "//runtime:variable_resolver", + "//runtime/planner:program_planner", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:org_jspecify_jspecify", + ], +) + java_library( name = "runtime", srcs = RUNTIME_SOURCES, @@ -829,6 +871,7 @@ java_library( "//common:cel_ast", "//common:cel_descriptor_util", "//common:cel_descriptors", + "//common:container", "//common:options", "//common/annotations", "//common/internal:cel_descriptor_pools", @@ -836,6 +879,7 @@ java_library( "//common/internal:dynamic_proto", "//common/internal:proto_message_factory", "//common/types:cel_types", + "//common/types:type_providers", "//common/values:cel_value_provider", "//common/values:proto_message_value_provider", "//runtime:variable_resolver", diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java index e1e3c1b51..87f11fde2 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java @@ -21,7 +21,9 @@ import com.google.protobuf.Descriptors.FileDescriptor; import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.Message; +import dev.cel.common.CelContainer; import dev.cel.common.CelOptions; +import dev.cel.common.types.CelTypeProvider; import dev.cel.common.values.CelValueProvider; import java.util.function.Function; @@ -48,6 +50,14 @@ public interface CelRuntimeBuilder { @CanIgnoreReturnValue CelRuntimeBuilder addFunctionBindings(Iterable bindings); + /** Adds bindings for functions that are allowed to be late-bound (resolved at execution time). */ + @CanIgnoreReturnValue + CelRuntimeBuilder addLateBoundFunctions(String... lateBoundFunctionNames); + + /** Adds bindings for functions that are allowed to be late-bound (resolved at execution time). */ + @CanIgnoreReturnValue + CelRuntimeBuilder addLateBoundFunctions(Iterable lateBoundFunctionNames); + /** * Add message {@link Descriptor}s to the builder for type-checking and object creation at * interpretation time. @@ -123,6 +133,13 @@ public interface CelRuntimeBuilder { @CanIgnoreReturnValue CelRuntimeBuilder addFileTypes(FileDescriptorSet fileDescriptorSet); + /** + * Sets the {@link CelTypeProvider} for resolving CEL types during evaluation, such as a fully + * qualified type name to a struct or an enum value. + */ + @CanIgnoreReturnValue + CelRuntimeBuilder setTypeProvider(CelTypeProvider celTypeProvider); + /** * Set a custom type factory for the runtime. * @@ -145,7 +162,7 @@ public interface CelRuntimeBuilder { * support proto messages in addition to custom struct values, protobuf value provider must be * configured first before the custom value provider. * - *

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

Note that this option is only supported for planner-based runtime. */ @CanIgnoreReturnValue CelRuntimeBuilder setValueProvider(CelValueProvider celValueProvider); @@ -179,6 +196,15 @@ public interface CelRuntimeBuilder { @CanIgnoreReturnValue CelRuntimeBuilder setExtensionRegistry(ExtensionRegistry extensionRegistry); + + /** + * Set the {@link CelContainer} to use as the namespace for resolving CEL expression variables and + * functions. + */ + @CanIgnoreReturnValue + CelRuntimeBuilder setContainer(CelContainer container); + + /** Build a new instance of the {@code CelRuntime}. */ @CheckReturnValue CelRuntime build(); diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java new file mode 100644 index 000000000..1417007e0 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -0,0 +1,467 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.DescriptorProtos; +import com.google.protobuf.Descriptors; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.Message; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; +import dev.cel.common.CelDescriptorUtil; +import dev.cel.common.CelDescriptors; +import dev.cel.common.CelOptions; +import dev.cel.common.annotations.Internal; +import dev.cel.common.internal.CelDescriptorPool; +import dev.cel.common.internal.CombinedDescriptorPool; +import dev.cel.common.internal.DefaultDescriptorPool; +import dev.cel.common.internal.DefaultMessageFactory; +import dev.cel.common.internal.DynamicProto; +// CEL-Internal-1 +import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.DefaultTypeProvider; +import dev.cel.common.types.ProtoMessageTypeProvider; +import dev.cel.common.values.CelValueConverter; +import dev.cel.common.values.CelValueProvider; +import dev.cel.common.values.CombinedCelValueProvider; +import dev.cel.common.values.ProtoMessageValueProvider; +import dev.cel.runtime.planner.ProgramPlanner; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import org.jspecify.annotations.Nullable; + +@AutoValue +@Internal +@Immutable +abstract class CelRuntimeImpl implements CelRuntime { + + abstract ProgramPlanner planner(); + + abstract CelOptions options(); + + abstract CelContainer container(); + + abstract ImmutableMap functionBindings(); + + abstract ImmutableSet fileDescriptors(); + + // Callers must guarantee that a custom runtime library is immutable. CEL provided ones are + // immutable by default. + @SuppressWarnings("Immutable") + @AutoValue.CopyAnnotations + abstract ImmutableSet runtimeLibraries(); + + abstract ImmutableSet lateBoundFunctionNames(); + + abstract CelStandardFunctions standardFunctions(); + + abstract @Nullable CelTypeProvider typeProvider(); + + abstract @Nullable CelValueProvider valueProvider(); + + // Extension registry is unmodifiable. Just not marked as such from Protobuf's implementation. + @SuppressWarnings("Immutable") + @AutoValue.CopyAnnotations + abstract @Nullable ExtensionRegistry extensionRegistry(); + + @Override + public Program createProgram(CelAbstractSyntaxTree ast) throws CelEvaluationException { + return toRuntimeProgram(planner().plan(ast)); + } + + public Program toRuntimeProgram(dev.cel.runtime.Program program) { + return new Program() { + + @Override + public Object eval() throws CelEvaluationException { + return program.eval(); + } + + @Override + public Object eval(Map mapValue) throws CelEvaluationException { + return program.eval(mapValue); + } + + @Override + public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException { + return program.eval(mapValue, lateBoundFunctionResolver); + } + + @Override + public Object eval(Message message) throws CelEvaluationException { + throw new UnsupportedOperationException("Not yet supported."); + } + + @Override + public Object eval(CelVariableResolver resolver) throws CelEvaluationException { + return program.eval(resolver); + } + + @Override + public Object eval( + CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException { + return program.eval(resolver, lateBoundFunctionResolver); + } + + @Override + public Object trace(CelEvaluationListener listener) throws CelEvaluationException { + throw new UnsupportedOperationException("Trace is not yet supported."); + } + + @Override + public Object trace(Map mapValue, CelEvaluationListener listener) + throws CelEvaluationException { + throw new UnsupportedOperationException("Trace is not yet supported."); + } + + @Override + public Object trace(Message message, CelEvaluationListener listener) + throws CelEvaluationException { + throw new UnsupportedOperationException("Trace is not yet supported."); + } + + @Override + public Object trace(CelVariableResolver resolver, CelEvaluationListener listener) + throws CelEvaluationException { + throw new UnsupportedOperationException("Trace is not yet supported."); + } + + @Override + public Object trace( + CelVariableResolver resolver, + CelFunctionResolver lateBoundFunctionResolver, + CelEvaluationListener listener) + throws CelEvaluationException { + throw new UnsupportedOperationException("Trace is not yet supported."); + } + + @Override + public Object trace( + Map mapValue, + CelFunctionResolver lateBoundFunctionResolver, + CelEvaluationListener listener) + throws CelEvaluationException { + throw new UnsupportedOperationException("Trace is not yet supported."); + } + + @Override + public Object advanceEvaluation(UnknownContext context) throws CelEvaluationException { + throw new UnsupportedOperationException("Unsupported operation."); + } + }; + } + + @Override + public abstract Builder toRuntimeBuilder(); + + static Builder newBuilder() { + return new AutoValue_CelRuntimeImpl.Builder() + .setStandardFunctions(CelStandardFunctions.newBuilder().build()) + .setContainer(CelContainer.newBuilder().build()) + .setExtensionRegistry(ExtensionRegistry.getEmptyRegistry()); + } + + @AutoValue.Builder + abstract static class Builder implements CelRuntimeBuilder { + + public abstract Builder setPlanner(ProgramPlanner planner); + + @Override + public abstract Builder setOptions(CelOptions options); + + @Override + public abstract Builder setStandardFunctions(CelStandardFunctions standardFunctions); + + @Override + public abstract Builder setExtensionRegistry(ExtensionRegistry extensionRegistry); + + @Override + public abstract Builder setTypeProvider(CelTypeProvider celTypeProvider); + + @Override + public abstract Builder setValueProvider(CelValueProvider celValueProvider); + + @Override + public abstract Builder setContainer(CelContainer container); + + abstract CelOptions options(); + + abstract CelContainer container(); + + abstract CelTypeProvider typeProvider(); + + abstract CelValueProvider valueProvider(); + + abstract CelStandardFunctions standardFunctions(); + + abstract ExtensionRegistry extensionRegistry(); + + abstract ImmutableSet.Builder fileDescriptorsBuilder(); + + abstract ImmutableSet.Builder runtimeLibrariesBuilder(); + + abstract ImmutableSet.Builder lateBoundFunctionNamesBuilder(); + + private final Map mutableFunctionBindings = new HashMap<>(); + + @Override + @CanIgnoreReturnValue + public Builder addFunctionBindings(CelFunctionBinding... bindings) { + checkNotNull(bindings); + return addFunctionBindings(Arrays.asList(bindings)); + } + + @Override + @CanIgnoreReturnValue + public Builder addFunctionBindings(Iterable bindings) { + checkNotNull(bindings); + bindings.forEach(o -> mutableFunctionBindings.putIfAbsent(o.getOverloadId(), o)); + return this; + } + + @Override + @CanIgnoreReturnValue + public Builder addLateBoundFunctions(String... lateBoundFunctionNames) { + checkNotNull(lateBoundFunctionNames); + return addLateBoundFunctions(Arrays.asList(lateBoundFunctionNames)); + } + + @Override + @CanIgnoreReturnValue + public Builder addLateBoundFunctions(Iterable lateBoundFunctionNames) { + checkNotNull(lateBoundFunctionNames); + this.lateBoundFunctionNamesBuilder().addAll(lateBoundFunctionNames); + return this; + } + + @Override + @CanIgnoreReturnValue + public Builder addMessageTypes(Descriptors.Descriptor... descriptors) { + checkNotNull(descriptors); + return addMessageTypes(Arrays.asList(descriptors)); + } + + @Override + @CanIgnoreReturnValue + public Builder addMessageTypes(Iterable descriptors) { + checkNotNull(descriptors); + return addFileTypes(CelDescriptorUtil.getFileDescriptorsForDescriptors(descriptors)); + } + + @Override + @CanIgnoreReturnValue + public Builder addFileTypes(DescriptorProtos.FileDescriptorSet fileDescriptorSet) { + checkNotNull(fileDescriptorSet); + return addFileTypes( + CelDescriptorUtil.getFileDescriptorsFromFileDescriptorSet(fileDescriptorSet)); + } + + @Override + @CanIgnoreReturnValue + public Builder addFileTypes(Descriptors.FileDescriptor... fileDescriptors) { + checkNotNull(fileDescriptors); + return addFileTypes(Arrays.asList(fileDescriptors)); + } + + @Override + @CanIgnoreReturnValue + public Builder addFileTypes(Iterable fileDescriptors) { + checkNotNull(fileDescriptors); + this.fileDescriptorsBuilder().addAll(fileDescriptors); + return this; + } + + @Override + @CanIgnoreReturnValue + public Builder addLibraries(CelRuntimeLibrary... libraries) { + checkNotNull(libraries); + return this.addLibraries(Arrays.asList(libraries)); + } + + @Override + @CanIgnoreReturnValue + public Builder addLibraries(Iterable libraries) { + checkNotNull(libraries); + this.runtimeLibrariesBuilder().addAll(libraries); + return this; + } + + abstract Builder setFunctionBindings(ImmutableMap value); + + @Override + public Builder setTypeFactory(Function typeFactory) { + throw new UnsupportedOperationException("Unsupported. Use a custom value provider instead."); + } + + @Override + public Builder setStandardEnvironmentEnabled(boolean value) { + throw new UnsupportedOperationException( + "Unsupported. Subset the environment using setStandardFunctions instead."); + } + + /** Throws if an unsupported flag in CelOptions is toggled. */ + private static void assertAllowedCelOptions(CelOptions celOptions) { + String prefix = "Misconfigured CelOptions: "; + if (!celOptions.enableUnsignedLongs()) { + throw new IllegalArgumentException(prefix + "enableUnsignedLongs cannot be disabled."); + } + if (!celOptions.unwrapWellKnownTypesOnFunctionDispatch()) { + throw new IllegalArgumentException( + prefix + "unwrapWellKnownTypesOnFunctionDispatch cannot be disabled."); + } + + // Disallowed options in favor of subsetting + String subsettingError = "Subset the environment instead using setStandardFunctions method."; + if (!celOptions.enableStringConcatenation()) { + throw new IllegalArgumentException( + prefix + "enableStringConcatenation cannot be disabled. " + subsettingError); + } + + if (!celOptions.enableStringConversion()) { + throw new IllegalArgumentException( + prefix + "enableStringConversion cannot be disabled. " + subsettingError); + } + + if (!celOptions.enableListConcatenation()) { + throw new IllegalArgumentException( + prefix + "enableListConcatenation cannot be disabled. " + subsettingError); + } + + if (!celOptions.enableTimestampEpoch()) { + throw new IllegalArgumentException( + prefix + "enableTimestampEpoch cannot be disabled. " + subsettingError); + } + + if (!celOptions.enableHeterogeneousNumericComparisons()) { + throw new IllegalArgumentException( + prefix + + "enableHeterogeneousNumericComparisons cannot be disabled. " + + subsettingError); + } + } + + abstract CelRuntimeImpl autoBuild(); + + private static DefaultDispatcher newDispatcher( + CelStandardFunctions standardFunctions, + Collection customFunctionBindings, + RuntimeEquality runtimeEquality, + CelOptions options) { + DefaultDispatcher.Builder builder = DefaultDispatcher.newBuilder(); + for (CelFunctionBinding binding : + standardFunctions.newFunctionBindings(runtimeEquality, options)) { + builder.addOverload( + binding.getOverloadId(), + binding.getArgTypes(), + binding.isStrict(), + binding.getDefinition()); + } + + for (CelFunctionBinding binding : customFunctionBindings) { + builder.addOverload( + binding.getOverloadId(), + binding.getArgTypes(), + binding.isStrict(), + binding.getDefinition()); + } + + return builder.build(); + } + + private static CelDescriptorPool newDescriptorPool( + CelDescriptors celDescriptors, + ExtensionRegistry extensionRegistry) { + ImmutableList.Builder descriptorPools = new ImmutableList.Builder<>(); + + descriptorPools.add(DefaultDescriptorPool.create(celDescriptors, extensionRegistry)); + + return CombinedDescriptorPool.create(descriptorPools.build()); + } + + @Override + public CelRuntime build() { + assertAllowedCelOptions(options()); + CelDescriptors celDescriptors = + CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(fileDescriptorsBuilder().build()); + + CelDescriptorPool descriptorPool = + newDescriptorPool( + celDescriptors, + extensionRegistry()); + DefaultMessageFactory defaultMessageFactory = DefaultMessageFactory.create(descriptorPool); + DynamicProto dynamicProto = DynamicProto.create(defaultMessageFactory); + CelValueProvider protoMessageValueProvider = + ProtoMessageValueProvider.newInstance(options(), dynamicProto); + CelValueConverter celValueConverter = protoMessageValueProvider.celValueConverter(); + if (valueProvider() != null) { + protoMessageValueProvider = + CombinedCelValueProvider.combine(protoMessageValueProvider, valueProvider()); + } + + RuntimeEquality runtimeEquality = + RuntimeEquality.create( + ProtoMessageRuntimeHelpers.create(dynamicProto, options()), options()); + ImmutableSet runtimeLibraries = runtimeLibrariesBuilder().build(); + // Add libraries, such as extensions + for (CelRuntimeLibrary celLibrary : runtimeLibraries) { + if (celLibrary instanceof CelInternalRuntimeLibrary) { + ((CelInternalRuntimeLibrary) celLibrary) + .setRuntimeOptions(this, runtimeEquality, options()); + } else { + celLibrary.setRuntimeOptions(this); + } + } + + CelTypeProvider combinedTypeProvider = + new CelTypeProvider.CombinedCelTypeProvider( + new ProtoMessageTypeProvider(celDescriptors), DefaultTypeProvider.getInstance()); + if (typeProvider() != null) { + combinedTypeProvider = + new CelTypeProvider.CombinedCelTypeProvider(combinedTypeProvider, typeProvider()); + } + + DefaultDispatcher dispatcher = + newDispatcher( + standardFunctions(), mutableFunctionBindings.values(), runtimeEquality, options()); + + ProgramPlanner planner = + ProgramPlanner.newPlanner( + combinedTypeProvider, + protoMessageValueProvider, + dispatcher, + celValueConverter, + container(), + options(), + lateBoundFunctionNamesBuilder().build()); + setPlanner(planner); + + setFunctionBindings(ImmutableMap.copyOf(mutableFunctionBindings)); + return autoBuild(); + } + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java index 983f68055..60c2642d7 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java @@ -29,6 +29,7 @@ import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.Message; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; import dev.cel.common.CelDescriptorUtil; import dev.cel.common.CelDescriptors; import dev.cel.common.CelOptions; @@ -40,6 +41,7 @@ import dev.cel.common.internal.DynamicProto; // CEL-Internal-1 import dev.cel.common.internal.ProtoMessageFactory; +import dev.cel.common.types.CelTypeProvider; import dev.cel.common.types.CelTypes; import dev.cel.common.values.CelValueProvider; import dev.cel.common.values.ProtoMessageValueProvider; @@ -161,6 +163,18 @@ public CelRuntimeBuilder addFunctionBindings(Iterable bindin return this; } + @Override + public CelRuntimeBuilder addLateBoundFunctions(String... lateBoundFunctionNames) { + throw new UnsupportedOperationException( + "This method is not supported for the legacy runtime"); + } + + @Override + public CelRuntimeBuilder addLateBoundFunctions(Iterable lateBoundFunctionNames) { + throw new UnsupportedOperationException( + "This method is not supported for the legacy runtime"); + } + @Override public CelRuntimeBuilder addMessageTypes(Descriptor... descriptors) { return addMessageTypes(Arrays.asList(descriptors)); @@ -189,9 +203,9 @@ public CelRuntimeBuilder addFileTypes(FileDescriptorSet fileDescriptorSet) { } @Override - public CelRuntimeBuilder setTypeFactory(Function typeFactory) { - this.customTypeFactory = typeFactory; - return this; + public CelRuntimeBuilder setTypeProvider(CelTypeProvider celTypeProvider) { + throw new UnsupportedOperationException( + "setTypeProvider is not supported for legacy runtime"); } @Override @@ -200,6 +214,12 @@ public CelRuntimeBuilder setValueProvider(CelValueProvider celValueProvider) { return this; } + @Override + public CelRuntimeBuilder setTypeFactory(Function typeFactory) { + this.customTypeFactory = typeFactory; + return this; + } + @Override public CelRuntimeBuilder setStandardEnvironmentEnabled(boolean value) { standardEnvironmentEnabled = value; @@ -232,6 +252,12 @@ public CelRuntimeBuilder setExtensionRegistry(ExtensionRegistry extensionRegistr return this; } + @Override + public CelRuntimeBuilder setContainer(CelContainer container) { + throw new UnsupportedOperationException( + "This method is not supported for the legacy runtime"); + } + /** Build a new {@code CelRuntimeLegacyImpl} instance from the builder config. */ @Override public CelRuntimeLegacyImpl build() { diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ActivationWrapper.java b/runtime/src/main/java/dev/cel/runtime/planner/ActivationWrapper.java new file mode 100644 index 000000000..f844ab232 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/ActivationWrapper.java @@ -0,0 +1,22 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import dev.cel.runtime.GlobalResolver; + +/** Identifies a resolver that can be unwrapped to bypass local variable state. */ +public interface ActivationWrapper extends GlobalResolver { + GlobalResolver unwrap(); +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/AttributeFactory.java b/runtime/src/main/java/dev/cel/runtime/planner/AttributeFactory.java index 632c6cd91..fabf7ade5 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/AttributeFactory.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/AttributeFactory.java @@ -29,7 +29,7 @@ final class AttributeFactory { private final CelValueConverter celValueConverter; NamespacedAttribute newAbsoluteAttribute(String... names) { - return new NamespacedAttribute(typeProvider, celValueConverter, ImmutableSet.copyOf(names)); + return NamespacedAttribute.create(typeProvider, celValueConverter, ImmutableSet.copyOf(names)); } RelativeAttribute newRelativeAttribute(PlannedInterpretable operand) { @@ -37,11 +37,14 @@ RelativeAttribute newRelativeAttribute(PlannedInterpretable operand) { } MaybeAttribute newMaybeAttribute(String name) { + // When there's a single name with a dot prefix, it indicates that the 'maybe' attribute is a + // globally namespaced identifier. + // Otherwise, the candidate names resolved from the container should be inferred. + ImmutableSet names = + name.startsWith(".") ? ImmutableSet.of(name) : container.resolveCandidateNames(name); + return new MaybeAttribute( - this, - ImmutableList.of( - new NamespacedAttribute( - typeProvider, celValueConverter, container.resolveCandidateNames(name)))); + this, ImmutableList.of(NamespacedAttribute.create(typeProvider, celValueConverter, names))); } static AttributeFactory newAttributeFactory( diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index d15d9af92..59f39fc91 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -114,6 +114,7 @@ java_library( "RelativeAttribute.java", ], deps = [ + ":activation_wrapper", ":eval_helpers", ":execution_frame", ":planned_interpretable", @@ -130,6 +131,14 @@ java_library( ], ) +java_library( + name = "activation_wrapper", + srcs = ["ActivationWrapper.java"], + deps = [ + "//runtime:interpretable", + ], +) + java_library( name = "qualifier", srcs = ["Qualifier.java"], @@ -305,6 +314,7 @@ java_library( name = "eval_create_list", srcs = ["EvalCreateList.java"], deps = [ + ":eval_helpers", ":execution_frame", ":planned_interpretable", "//runtime:evaluation_exception", @@ -333,6 +343,7 @@ java_library( name = "eval_fold", srcs = ["EvalFold.java"], deps = [ + ":activation_wrapper", ":execution_frame", ":planned_interpretable", "//runtime:concatenated_list_view", diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java index e519b968c..389a21a82 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java @@ -30,7 +30,7 @@ final class EvalCreateList extends PlannedInterpretable { public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(values.length); for (PlannedInterpretable value : values) { - builder.add(value.eval(resolver, frame)); + builder.add(EvalHelpers.evalStrictly(value, resolver, frame)); } return builder.build(); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java index 49047f3a4..3545ee4f7 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java @@ -97,7 +97,7 @@ private Object evalMap(Map iterRange, Folder folder, ExecutionFrame frame) if (!iterVar2.isEmpty()) { folder.iterVar2Val = entry.getValue(); } - + boolean cond = (boolean) condition.eval(folder, frame); if (!cond) { return result.eval(folder, frame); @@ -149,7 +149,7 @@ private static Object maybeUnwrapAccumulator(Object val) { return val; } - private static class Folder implements GlobalResolver { + private static class Folder implements ActivationWrapper { private final GlobalResolver resolver; private final String accuVar; private final String iterVar; @@ -166,6 +166,11 @@ private Folder(GlobalResolver resolver, String accuVar, String iterVar, String i this.iterVar2 = iterVar2; } + @Override + public GlobalResolver unwrap() { + return resolver; + } + @Override public @Nullable Object resolve(String name) { if (name.equals(accuVar)) { diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java index 6e7fd7e68..ee86c9dd6 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java @@ -38,6 +38,9 @@ static Object evalStrictly( PlannedInterpretable interpretable, GlobalResolver resolver, ExecutionFrame frame) { try { return interpretable.eval(resolver, frame); + } catch (LocalizedEvaluationException e) { + // Already localized - propagate as-is to preserve inner expression ID + throw e; } catch (CelRuntimeException e) { // Wrap with current interpretable's location throw new LocalizedEvaluationException(e, interpretable.exprId()); diff --git a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java index d513bc7ba..37bb04cf6 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java @@ -28,6 +28,7 @@ @Immutable final class NamespacedAttribute implements Attribute { + private final ImmutableSet disambiguateNames; private final ImmutableSet namespacedNames; private final ImmutableList qualifiers; private final CelValueConverter celValueConverter; @@ -35,8 +36,22 @@ final class NamespacedAttribute implements Attribute { @Override public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { + GlobalResolver inputVars = ctx; + // Unwrap any local activations to ensure that we reach the variables provided as input + // to the expression in the event that we need to disambiguate between global and local + // variables. + if (!disambiguateNames.isEmpty()) { + inputVars = unwrapToRoot(ctx); + } + + int i = 0; for (String name : namespacedNames) { - Object value = ctx.resolve(name); + GlobalResolver resolver = ctx; + if (disambiguateNames.contains(i)) { + resolver = inputVars; + } + + Object value = resolver.resolve(name); if (value != null) { if (!qualifiers.isEmpty()) { return applyQualifiers(value, celValueConverter, qualifiers); @@ -69,6 +84,7 @@ public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { throw new IllegalStateException( "Unexpected type resolution when there were remaining qualifiers: " + type.name()); } + i++; } return MissingAttribute.newMissingAttribute(namespacedNames); @@ -82,12 +98,20 @@ ImmutableSet candidateVariableNames() { return namespacedNames; } + private GlobalResolver unwrapToRoot(GlobalResolver resolver) { + while (resolver instanceof ActivationWrapper) { + resolver = ((ActivationWrapper) resolver).unwrap(); + } + return resolver; + } + @Override public NamespacedAttribute addQualifier(Qualifier qualifier) { return new NamespacedAttribute( typeProvider, celValueConverter, namespacedNames, + disambiguateNames, ImmutableList.builder().addAll(qualifiers).add(qualifier).build()); } @@ -106,21 +130,40 @@ private static Object applyQualifiers( return obj; } - NamespacedAttribute( + static NamespacedAttribute create( CelTypeProvider typeProvider, CelValueConverter celValueConverter, ImmutableSet namespacedNames) { - this(typeProvider, celValueConverter, namespacedNames, ImmutableList.of()); + ImmutableSet.Builder namesBuilder = ImmutableSet.builder(); + ImmutableSet.Builder indicesBuilder = ImmutableSet.builder(); + int i = 0; + for (String name : namespacedNames) { + if (name.startsWith(".")) { + indicesBuilder.add(i); + namesBuilder.add(name.substring(1)); + } else { + namesBuilder.add(name); + } + i++; + } + return new NamespacedAttribute( + typeProvider, + celValueConverter, + namesBuilder.build(), + indicesBuilder.build(), + ImmutableList.of()); } - private NamespacedAttribute( + NamespacedAttribute( CelTypeProvider typeProvider, CelValueConverter celValueConverter, ImmutableSet namespacedNames, + ImmutableSet disambiguateNames, ImmutableList qualifiers) { this.typeProvider = typeProvider; this.celValueConverter = celValueConverter; this.namespacedNames = namespacedNames; + this.disambiguateNames = disambiguateNames; this.qualifiers = qualifiers; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index 1c2bae9c2..b5d43728b 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -15,11 +15,12 @@ package dev.cel.runtime.planner; import com.google.auto.value.AutoValue; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CheckReturnValue; -import javax.annotation.concurrent.ThreadSafe; +import com.google.errorprone.annotations.Immutable; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelOptions; @@ -48,6 +49,7 @@ import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.DefaultDispatcher; import dev.cel.runtime.Program; +import java.util.HashMap; import java.util.NoSuchElementException; import java.util.Optional; @@ -55,7 +57,7 @@ * {@code ProgramPlanner} resolves functions, types, and identifiers at plan time given a * parsed-only or a type-checked expression. */ -@ThreadSafe +@Immutable @Internal public final class ProgramPlanner { @@ -161,8 +163,12 @@ private PlannedInterpretable planIdent(CelExpr celExpr, PlannerContext ctx) { return planCheckedIdent(celExpr.id(), ref, ctx.typeMap()); } - return EvalAttribute.create( - celExpr.id(), attributeFactory.newMaybeAttribute(celExpr.ident().name())); + String identName = celExpr.ident().name(); + if (ctx.isLocalVar(identName)) { + return EvalAttribute.create(celExpr.id(), attributeFactory.newAbsoluteAttribute(identName)); + } + + return EvalAttribute.create(celExpr.id(), attributeFactory.newMaybeAttribute(identName)); } private PlannedInterpretable planCheckedIdent( @@ -314,10 +320,18 @@ private PlannedInterpretable planComprehension(CelExpr expr, PlannerContext ctx) PlannedInterpretable accuInit = plan(comprehension.accuInit(), ctx); PlannedInterpretable iterRange = plan(comprehension.iterRange(), ctx); + + ctx.pushLocalVars(comprehension.accuVar(), comprehension.iterVar(), comprehension.iterVar2()); + PlannedInterpretable loopCondition = plan(comprehension.loopCondition(), ctx); PlannedInterpretable loopStep = plan(comprehension.loopStep(), ctx); + + ctx.popLocalVars(comprehension.iterVar(), comprehension.iterVar2()); + PlannedInterpretable result = plan(comprehension.result(), ctx); + ctx.popLocalVars(comprehension.accuVar()); + return EvalFold.create( expr.id(), comprehension.accuVar(), @@ -460,15 +474,57 @@ private static Builder newBuilder() { } } - @AutoValue - abstract static class PlannerContext { + static final class PlannerContext { + private final ImmutableMap referenceMap; + private final ImmutableMap typeMap; + private final HashMap localVars = new HashMap<>(); + + ImmutableMap referenceMap() { + return referenceMap; + } - abstract ImmutableMap referenceMap(); + ImmutableMap typeMap() { + return typeMap; + } - abstract ImmutableMap typeMap(); + private void pushLocalVars(String... names) { + for (String name : names) { + if (Strings.isNullOrEmpty(name)) { + continue; + } + localVars.merge(name, 1, Integer::sum); + } + } + + private void popLocalVars(String... names) { + for (String name : names) { + if (Strings.isNullOrEmpty(name)) { + continue; + } + Integer count = localVars.get(name); + if (count != null) { + if (count == 1) { + localVars.remove(name); + } else { + localVars.put(name, count - 1); + } + } + } + } + + /** Checks if the given name is a local variable in the current scope. */ + private boolean isLocalVar(String name) { + return localVars.containsKey(name); + } + + private PlannerContext( + ImmutableMap referenceMap, ImmutableMap typeMap) { + this.referenceMap = referenceMap; + this.typeMap = typeMap; + } - private static PlannerContext create(CelAbstractSyntaxTree ast) { - return new AutoValue_ProgramPlanner_PlannerContext(ast.getReferenceMap(), ast.getTypeMap()); + static PlannerContext create(CelAbstractSyntaxTree ast) { + return new PlannerContext(ast.getReferenceMap(), ast.getTypeMap()); } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java index a913849f6..54eb26f21 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java @@ -16,6 +16,7 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.values.CelValue; import dev.cel.common.values.CelValueConverter; import dev.cel.runtime.GlobalResolver; @@ -40,7 +41,9 @@ public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { } // TODO: Handle unknowns - + if (obj instanceof CelValue) { + obj = celValueConverter.unwrap((CelValue) obj); + } return obj; } diff --git a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel index ed76fdbe7..30691fa4a 100644 --- a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel @@ -19,8 +19,8 @@ java_library( # keep sorted exclude = [ "CelLiteInterpreterTest.java", - "CelValueInterpreterTest.java", "InterpreterTest.java", + "PlannerInterpreterTest.java", ] + ANDROID_TESTS, ), deps = [ @@ -125,16 +125,17 @@ java_library( ) java_library( - name = "cel_value_interpreter_test", + name = "planner_interpreter_test", testonly = 1, srcs = [ - "CelValueInterpreterTest.java", + "PlannerInterpreterTest.java", ], deps = [ - # "//java/com/google/testing/testsize:annotations", + "//extensions", + "//runtime:runtime_planner_impl", "//testing:base_interpreter_test", - "@maven//:junit_junit", "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", ], ) @@ -203,8 +204,8 @@ junit4_test_suites( src_dir = "src/test/java", deps = [ ":cel_lite_interpreter_test", - ":cel_value_interpreter_test", ":interpreter_test", + ":planner_interpreter_test", ":tests", ], ) diff --git a/runtime/src/test/java/dev/cel/runtime/CelValueInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/CelValueInterpreterTest.java deleted file mode 100644 index f56bb3012..000000000 --- a/runtime/src/test/java/dev/cel/runtime/CelValueInterpreterTest.java +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2023 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dev.cel.runtime; - -import com.google.testing.junit.testparameterinjector.TestParameterInjector; -// import com.google.testing.testsize.MediumTest; -import dev.cel.testing.BaseInterpreterTest; -import org.junit.runner.RunWith; - -/** Tests for {@link Interpreter} and related functionality using {@code CelValue}. */ -// @MediumTest -@RunWith(TestParameterInjector.class) -public class CelValueInterpreterTest extends BaseInterpreterTest { - - public CelValueInterpreterTest() { - super(newBaseCelOptions().toBuilder().enableCelValue(true).build()); - } -} diff --git a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java new file mode 100644 index 000000000..4e3a00a34 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java @@ -0,0 +1,60 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.extensions.CelExtensions; +import dev.cel.testing.BaseInterpreterTest; +import org.junit.runner.RunWith; + +/** Interpreter tests using ProgramPlanner */ +@RunWith(TestParameterInjector.class) +public class PlannerInterpreterTest extends BaseInterpreterTest { + + public PlannerInterpreterTest() { + super( + CelRuntimeImpl.newBuilder() + .addLateBoundFunctions("record") + // CEL-Internal-2 + .setOptions(newBaseCelOptions()) + .addLibraries(CelExtensions.optional()) + .addFileTypes(TEST_FILE_DESCRIPTORS) + .build()); + } + + @Override + public void unknownField() { + // TODO: Unknown support not implemented yet + skipBaselineVerification(); + } + + @Override + public void unknownResultSet() { + // TODO: Unknown support not implemented yet + skipBaselineVerification(); + } + + @Override + public void typeComparisons() { + // TODO: type() standard function needs to be implemented first. + skipBaselineVerification(); + } + + @Override + public void optional_errors() { + // TODO: Fix error message for function dispatch failures + skipBaselineVerification(); + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index 7b9e1920b..ca862ed27 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -824,7 +824,7 @@ public void plan_comprehension_iterationLimit_success() throws Exception { CEL_VALUE_CONVERTER, CEL_CONTAINER, options, - ImmutableSet.of()); + /* lateBoundFunctionNames= */ ImmutableSet.of()); CelAbstractSyntaxTree ast = compile("[1, 2, 3].map(x, [1, 2].map(y, x + y))"); Program program = planner.plan(ast); @@ -836,13 +836,114 @@ public void plan_comprehension_iterationLimit_success() throws Exception { ImmutableList.of(2L, 3L), ImmutableList.of(3L, 4L), ImmutableList.of(4L, 5L))); } + @Test + public void localShadowIdentifier_inSelect() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addVar("cel.example.y", SimpleType.INT) + .build(); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + ProtoMessageValueProvider.newInstance(CEL_OPTIONS, DYNAMIC_PROTO), + newDispatcher(), + CEL_VALUE_CONVERTER, + CelContainer.ofName("cel.example"), + CEL_OPTIONS, + /* lateBoundFunctionNames= */ ImmutableSet.of()); + CelAbstractSyntaxTree ast = compile(celCompiler, "[{'z': 0}].exists(y, y.z == 0)"); + + Program program = planner.plan(ast); + + boolean result = + (boolean) program.eval(ImmutableMap.of("cel.example.y", ImmutableMap.of("z", 1))); + assertThat(result).isTrue(); + } + + @Test + public void localShadowIdentifier_inSelect_globalDisambiguation() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addVar("y.z", SimpleType.INT) + .build(); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + ProtoMessageValueProvider.newInstance(CEL_OPTIONS, DYNAMIC_PROTO), + newDispatcher(), + CEL_VALUE_CONVERTER, + CelContainer.ofName("y"), + CEL_OPTIONS, + /* lateBoundFunctionNames= */ ImmutableSet.of()); + CelAbstractSyntaxTree ast = compile(celCompiler, "[{'z': 0}].exists(y, y.z == 0 && .y.z == 1)"); + + Program program = planner.plan(ast); + + boolean result = (boolean) program.eval(ImmutableMap.of("y.z", 1)); + assertThat(result).isTrue(); + } + + @Test + public void localShadowIdentifier_withGlobalDisambiguation() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addVar("x", SimpleType.INT) + .build(); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + ProtoMessageValueProvider.newInstance(CEL_OPTIONS, DYNAMIC_PROTO), + newDispatcher(), + CEL_VALUE_CONVERTER, + CelContainer.newBuilder().build(), + CEL_OPTIONS, + /* lateBoundFunctionNames= */ ImmutableSet.of()); + CelAbstractSyntaxTree ast = compile(celCompiler, "[0].exists(x, x == 0 && .x == 1)"); + + Program program = planner.plan(ast); + + boolean result = (boolean) program.eval(ImmutableMap.of("x", 1)); + assertThat(result).isTrue(); + } + + @Test + public void localDoubleShadowIdentifier_withGlobalDisambiguation() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addVar("x", SimpleType.INT) + .build(); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + ProtoMessageValueProvider.newInstance(CEL_OPTIONS, DYNAMIC_PROTO), + newDispatcher(), + CEL_VALUE_CONVERTER, + CelContainer.newBuilder().build(), + CEL_OPTIONS, + /* lateBoundFunctionNames= */ ImmutableSet.of()); + CelAbstractSyntaxTree ast = compile(celCompiler, "[0].exists(x, [x+1].exists(x, x == .x))"); + + Program program = planner.plan(ast); + + boolean result = (boolean) program.eval(ImmutableMap.of("x", 1)); + assertThat(result).isTrue(); + } + private CelAbstractSyntaxTree compile(String expression) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.parse(expression).getAst(); + return compile(CEL_COMPILER, expression); + } + + private CelAbstractSyntaxTree compile(CelCompiler compiler, String expression) throws Exception { + CelAbstractSyntaxTree ast = compiler.parse(expression).getAst(); if (isParseOnly) { return ast; } - return CEL_COMPILER.check(ast).getAst(); + return compiler.check(ast).getAst(); } private static CelByteString concatenateByteArrays(CelByteString bytes1, CelByteString bytes2) {