From f2a69c454804ba22853f9c41919c33842709b4a7 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 16 Dec 2025 22:15:54 -0800 Subject: [PATCH] Add iteration limit control to planned comprehensions PiperOrigin-RevId: 845589739 --- common/exceptions/BUILD.bazel | 6 + .../dev/cel/common/exceptions/BUILD.bazel | 13 + .../CelIterationLimitExceededException.java | 31 ++ .../cel/common/values/CelValueConverter.java | 2 +- runtime/BUILD.bazel | 8 + .../dev/cel/runtime/AccumulatedUnknowns.java | 2 +- .../src/main/java/dev/cel/runtime/BUILD.bazel | 4 +- .../CelEvaluationExceptionBuilder.java | 12 +- .../dev/cel/runtime/ConcatenatedListView.java | 11 +- .../dev/cel/runtime/planner/Attribute.java | 57 +--- .../cel/runtime/planner/AttributeFactory.java | 33 +- .../java/dev/cel/runtime/planner/BUILD.bazel | 139 ++++++-- .../java/dev/cel/runtime/planner/EvalAnd.java | 27 +- .../cel/runtime/planner/EvalAttribute.java | 31 +- .../cel/runtime/planner/EvalConditional.java | 44 +-- .../dev/cel/runtime/planner/EvalConstant.java | 22 +- .../cel/runtime/planner/EvalCreateList.java | 36 +- .../cel/runtime/planner/EvalCreateMap.java | 37 +-- .../cel/runtime/planner/EvalCreateStruct.java | 34 +- .../dev/cel/runtime/planner/EvalFold.java | 186 +++++++++++ .../dev/cel/runtime/planner/EvalHelpers.java | 10 +- .../java/dev/cel/runtime/planner/EvalOr.java | 27 +- .../dev/cel/runtime/planner/EvalTestOnly.java | 45 +++ .../dev/cel/runtime/planner/EvalUnary.java | 29 +- .../cel/runtime/planner/EvalVarArgsCall.java | 29 +- .../cel/runtime/planner/EvalZeroArity.java | 25 +- .../cel/runtime/planner/ExecutionFrame.java | 54 +++ .../planner/InterpretableAttribute.java | 27 ++ .../cel/runtime/planner/MaybeAttribute.java | 81 +++++ .../cel/runtime/planner/MissingAttribute.java | 47 +++ .../runtime/planner/NamespacedAttribute.java | 126 +++++++ .../runtime/planner/PlannedInterpretable.java | 10 +- .../cel/runtime/planner/PlannedProgram.java | 11 +- .../planner/PresenceTestQualifier.java | 53 +++ .../cel/runtime/planner/ProgramPlanner.java | 73 +++- .../dev/cel/runtime/planner/Qualifier.java | 28 ++ .../runtime/planner/RelativeAttribute.java | 70 ++++ .../cel/runtime/planner/StringQualifier.java | 61 ++++ .../java/dev/cel/runtime/CelRuntimeTest.java | 2 +- .../cel/runtime/DefaultDispatcherTest.java | 2 +- .../java/dev/cel/runtime/planner/BUILD.bazel | 3 + .../runtime/planner/ProgramPlannerTest.java | 312 +++++++++++++++++- 42 files changed, 1436 insertions(+), 424 deletions(-) create mode 100644 common/src/main/java/dev/cel/common/exceptions/CelIterationLimitExceededException.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/EvalTestOnly.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/InterpretableAttribute.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/PresenceTestQualifier.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/Qualifier.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java diff --git a/common/exceptions/BUILD.bazel b/common/exceptions/BUILD.bazel index 6ffb2dcb9..96e07ac65 100644 --- a/common/exceptions/BUILD.bazel +++ b/common/exceptions/BUILD.bazel @@ -40,3 +40,9 @@ java_library( # used_by_android exports = ["//common/src/main/java/dev/cel/common/exceptions:invalid_argument"], ) + +java_library( + name = "iteration_budget_exceeded", + # used_by_android + exports = ["//common/src/main/java/dev/cel/common/exceptions:iteration_budget_exceeded"], +) diff --git a/common/src/main/java/dev/cel/common/exceptions/BUILD.bazel b/common/src/main/java/dev/cel/common/exceptions/BUILD.bazel index 6bd1ad9ca..203866928 100644 --- a/common/src/main/java/dev/cel/common/exceptions/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/exceptions/BUILD.bazel @@ -85,3 +85,16 @@ java_library( "//common/annotations", ], ) + +java_library( + name = "iteration_budget_exceeded", + srcs = ["CelIterationLimitExceededException.java"], + # used_by_android + tags = [ + ], + deps = [ + "//common:error_codes", + "//common:runtime_exception", + "//common/annotations", + ], +) diff --git a/common/src/main/java/dev/cel/common/exceptions/CelIterationLimitExceededException.java b/common/src/main/java/dev/cel/common/exceptions/CelIterationLimitExceededException.java new file mode 100644 index 000000000..ef0f1d8e3 --- /dev/null +++ b/common/src/main/java/dev/cel/common/exceptions/CelIterationLimitExceededException.java @@ -0,0 +1,31 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.exceptions; + +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelRuntimeException; +import dev.cel.common.annotations.Internal; +import java.util.Locale; + +/** Indicates that the iteration budget for a comprehension has been exceeded. */ +@Internal +public final class CelIterationLimitExceededException extends CelRuntimeException { + + public CelIterationLimitExceededException(int budget) { + super( + String.format(Locale.US, "Iteration budget exceeded: %d", budget), + CelErrorCode.ITERATION_BUDGET_EXCEEDED); + } +} diff --git a/common/src/main/java/dev/cel/common/values/CelValueConverter.java b/common/src/main/java/dev/cel/common/values/CelValueConverter.java index b58790eaf..c3f3727a1 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 unwrap(CelValue celValue) { diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index 7760d96b8..07bfdebbc 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -255,3 +255,11 @@ java_library( visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime:metadata"], ) + +java_library( + name = "concatenated_list_view", + visibility = ["//:internal"], + exports = [ + "//runtime/src/main/java/dev/cel/runtime:concatenated_list_view", + ], +) diff --git a/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java b/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java index 77435a042..d27de2da2 100644 --- a/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java +++ b/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java @@ -2,7 +2,7 @@ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. -// You may obtain a copy of the License aj +// You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 847092cc3..b55aec00f 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -1142,7 +1142,9 @@ java_library( name = "concatenated_list_view", srcs = ["ConcatenatedListView.java"], # used_by_android - visibility = ["//visibility:private"], + tags = [ + ], + deps = ["//common/annotations"], ) java_library( diff --git a/runtime/src/main/java/dev/cel/runtime/CelEvaluationExceptionBuilder.java b/runtime/src/main/java/dev/cel/runtime/CelEvaluationExceptionBuilder.java index 6aaed4da7..6adbcb672 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelEvaluationExceptionBuilder.java +++ b/runtime/src/main/java/dev/cel/runtime/CelEvaluationExceptionBuilder.java @@ -83,10 +83,16 @@ public static CelEvaluationExceptionBuilder newBuilder(String message, Object... */ @Internal public static CelEvaluationExceptionBuilder newBuilder(CelRuntimeException celRuntimeException) { + // TODO: Temporary until migration is complete. Throwable cause = celRuntimeException.getCause(); - return new CelEvaluationExceptionBuilder(cause.getMessage()) - .setCause(cause) - .setErrorCode(celRuntimeException.getErrorCode()); + String message = + cause == null + ? celRuntimeException.getMessage() + : celRuntimeException.getCause().getMessage(); + + return new CelEvaluationExceptionBuilder(message) + .setErrorCode(celRuntimeException.getErrorCode()) + .setCause(cause); } private CelEvaluationExceptionBuilder(String message) { diff --git a/runtime/src/main/java/dev/cel/runtime/ConcatenatedListView.java b/runtime/src/main/java/dev/cel/runtime/ConcatenatedListView.java index ac7696751..c15e76f77 100644 --- a/runtime/src/main/java/dev/cel/runtime/ConcatenatedListView.java +++ b/runtime/src/main/java/dev/cel/runtime/ConcatenatedListView.java @@ -2,7 +2,7 @@ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. -// You may obtain a copy of the License aj +// You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // @@ -14,6 +14,7 @@ package dev.cel.runtime; +import dev.cel.common.annotations.Internal; import java.util.AbstractList; import java.util.ArrayList; import java.util.Collection; @@ -27,8 +28,12 @@ * comprehensions that dispatch `add_list` to concat N lists together). * *

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

CEL Library Internals. Do Not Use. */ -final class ConcatenatedListView extends AbstractList { +@Internal +public final class ConcatenatedListView extends AbstractList { private final List> sourceLists; private int totalSize = 0; @@ -36,7 +41,7 @@ final class ConcatenatedListView extends AbstractList { this.sourceLists = new ArrayList<>(); } - ConcatenatedListView(Collection collection) { + public ConcatenatedListView(Collection collection) { this(); addAll(collection); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java b/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java index 0ce487ceb..cc011ed34 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java @@ -14,64 +14,13 @@ 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; +/** Represents a resolvable symbol or path (such as a variable or a field selection). */ @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"); - } + Object resolve(GlobalResolver ctx, ExecutionFrame frame); - NamespacedAttribute(CelTypeProvider typeProvider, ImmutableList namespacedNames) { - this.typeProvider = typeProvider; - this.namespacedNames = namespacedNames; - } - } + Attribute addQualifier(Qualifier qualifier); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/AttributeFactory.java b/runtime/src/main/java/dev/cel/runtime/planner/AttributeFactory.java index bda7c46a6..632c6cd91 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/AttributeFactory.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/AttributeFactory.java @@ -15,35 +15,46 @@ package dev.cel.runtime.planner; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.Immutable; import dev.cel.common.CelContainer; import dev.cel.common.types.CelTypeProvider; -import dev.cel.runtime.planner.Attribute.MaybeAttribute; -import dev.cel.runtime.planner.Attribute.NamespacedAttribute; +import dev.cel.common.values.CelValueConverter; @Immutable final class AttributeFactory { - private final CelContainer unusedContainer; + private final CelContainer container; private final CelTypeProvider typeProvider; + private final CelValueConverter celValueConverter; NamespacedAttribute newAbsoluteAttribute(String... names) { - return new NamespacedAttribute(typeProvider, ImmutableList.copyOf(names)); + return new NamespacedAttribute(typeProvider, celValueConverter, ImmutableSet.copyOf(names)); } - MaybeAttribute newMaybeAttribute(String... names) { - // TODO: Resolve container names + RelativeAttribute newRelativeAttribute(PlannedInterpretable operand) { + return new RelativeAttribute(operand, celValueConverter); + } + + MaybeAttribute newMaybeAttribute(String name) { return new MaybeAttribute( - ImmutableList.of(new NamespacedAttribute(typeProvider, ImmutableList.copyOf(names)))); + this, + ImmutableList.of( + new NamespacedAttribute( + typeProvider, celValueConverter, container.resolveCandidateNames(name)))); } static AttributeFactory newAttributeFactory( - CelContainer celContainer, CelTypeProvider typeProvider) { - return new AttributeFactory(celContainer, typeProvider); + CelContainer celContainer, + CelTypeProvider typeProvider, + CelValueConverter celValueConverter) { + return new AttributeFactory(celContainer, typeProvider, celValueConverter); } - private AttributeFactory(CelContainer container, CelTypeProvider typeProvider) { - this.unusedContainer = container; + private AttributeFactory( + CelContainer container, CelTypeProvider typeProvider, CelValueConverter celValueConverter) { + this.container = container; this.typeProvider = typeProvider; + this.celValueConverter = celValueConverter; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index e7d7b8bdd..b99b41fd5 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -22,25 +22,31 @@ java_library( ":eval_create_list", ":eval_create_map", ":eval_create_struct", + ":eval_fold", ":eval_or", + ":eval_test_only", ":eval_unary", ":eval_var_args_call", ":eval_zero_arity", + ":interpretable_attribute", ":planned_interpretable", ":planned_program", + ":qualifier", + ":string_qualifier", "//:auto_value", "//common:cel_ast", "//common:container", "//common:operator", + "//common:options", "//common/annotations", "//common/ast", "//common/types", "//common/types:type_providers", + "//common/values", "//common/values:cel_value_provider", "//runtime:dispatcher", "//runtime:evaluation_exception", "//runtime:evaluation_exception_builder", - "//runtime:interpretable", "//runtime:program", "//runtime:resolved_overload", "@maven//:com_google_code_findbugs_annotations", @@ -54,11 +60,14 @@ java_library( srcs = ["PlannedProgram.java"], deps = [ ":error_metadata", + ":execution_frame", ":planned_interpretable", ":strict_error_exception", "//:auto_value", + "//common:options", "//common:runtime_exception", "//common/values", + "//runtime", "//runtime:activation", "//runtime:evaluation_exception", "//runtime:evaluation_exception_builder", @@ -73,55 +82,116 @@ java_library( name = "eval_const", srcs = ["EvalConstant.java"], deps = [ + ":execution_frame", ":planned_interpretable", "//common/values", "//common/values:cel_byte_string", - "//runtime:evaluation_listener", - "//runtime:function_resolver", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], ) +java_library( + name = "interpretable_attribute", + srcs = ["InterpretableAttribute.java"], + deps = [ + ":planned_interpretable", + ":qualifier", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + java_library( name = "attribute", srcs = [ "Attribute.java", "AttributeFactory.java", + "MaybeAttribute.java", + "MissingAttribute.java", + "NamespacedAttribute.java", + "RelativeAttribute.java", ], deps = [ + ":eval_helpers", + ":execution_frame", + ":planned_interpretable", + ":qualifier", "//common:container", + "//common/exceptions:attribute_not_found", "//common/types", "//common/types:type_providers", + "//common/values", + "//common/values:cel_value", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], ) +java_library( + name = "qualifier", + srcs = ["Qualifier.java"], + deps = [ + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "presence_test_qualifier", + srcs = ["PresenceTestQualifier.java"], + deps = [ + ":attribute", + ":qualifier", + "//common/values", + ], +) + +java_library( + name = "string_qualifier", + srcs = ["StringQualifier.java"], + deps = [ + ":qualifier", + "//common/exceptions:attribute_not_found", + "//common/values", + ], +) + java_library( name = "eval_attribute", srcs = ["EvalAttribute.java"], deps = [ ":attribute", - ":planned_interpretable", - "//runtime:evaluation_listener", - "//runtime:function_resolver", + ":execution_frame", + ":interpretable_attribute", + ":qualifier", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], ) +java_library( + name = "eval_test_only", + srcs = ["EvalTestOnly.java"], + deps = [ + ":execution_frame", + ":interpretable_attribute", + ":presence_test_qualifier", + ":qualifier", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + java_library( name = "eval_zero_arity", srcs = ["EvalZeroArity.java"], deps = [ + ":execution_frame", ":planned_interpretable", "//runtime:evaluation_exception", - "//runtime:evaluation_listener", - "//runtime:function_resolver", "//runtime:interpretable", "//runtime:resolved_overload", ], @@ -132,10 +202,9 @@ java_library( srcs = ["EvalUnary.java"], deps = [ ":eval_helpers", + ":execution_frame", ":planned_interpretable", "//runtime:evaluation_exception", - "//runtime:evaluation_listener", - "//runtime:function_resolver", "//runtime:interpretable", "//runtime:resolved_overload", ], @@ -146,10 +215,9 @@ java_library( srcs = ["EvalVarArgsCall.java"], deps = [ ":eval_helpers", + ":execution_frame", ":planned_interpretable", "//runtime:evaluation_exception", - "//runtime:evaluation_listener", - "//runtime:function_resolver", "//runtime:interpretable", "//runtime:resolved_overload", ], @@ -160,10 +228,9 @@ java_library( srcs = ["EvalOr.java"], deps = [ ":eval_helpers", + ":execution_frame", ":planned_interpretable", "//common/values", - "//runtime:evaluation_listener", - "//runtime:function_resolver", "//runtime:interpretable", "@maven//:com_google_guava_guava", ], @@ -174,10 +241,9 @@ java_library( srcs = ["EvalAnd.java"], deps = [ ":eval_helpers", + ":execution_frame", ":planned_interpretable", "//common/values", - "//runtime:evaluation_listener", - "//runtime:function_resolver", "//runtime:interpretable", "@maven//:com_google_guava_guava", ], @@ -187,10 +253,9 @@ java_library( name = "eval_conditional", srcs = ["EvalConditional.java"], deps = [ + ":execution_frame", ":planned_interpretable", "//runtime:evaluation_exception", - "//runtime:evaluation_listener", - "//runtime:function_resolver", "//runtime:interpretable", "@maven//:com_google_guava_guava", ], @@ -200,13 +265,12 @@ java_library( name = "eval_create_struct", srcs = ["EvalCreateStruct.java"], deps = [ + ":execution_frame", ":planned_interpretable", "//common/types", "//common/values", "//common/values:cel_value_provider", "//runtime:evaluation_exception", - "//runtime:evaluation_listener", - "//runtime:function_resolver", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -217,10 +281,9 @@ java_library( name = "eval_create_list", srcs = ["EvalCreateList.java"], deps = [ + ":execution_frame", ":planned_interpretable", "//runtime:evaluation_exception", - "//runtime:evaluation_listener", - "//runtime:function_resolver", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -231,20 +294,46 @@ java_library( name = "eval_create_map", srcs = ["EvalCreateMap.java"], deps = [ + ":execution_frame", ":planned_interpretable", "//runtime:evaluation_exception", - "//runtime:evaluation_listener", - "//runtime:function_resolver", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], ) +java_library( + name = "eval_fold", + srcs = ["EvalFold.java"], + deps = [ + ":execution_frame", + ":planned_interpretable", + "//runtime:concatenated_list_view", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + +java_library( + name = "execution_frame", + srcs = ["ExecutionFrame.java"], + deps = [ + "//common:options", + "//common/exceptions:iteration_budget_exceeded", + "//runtime:interpretable", + "@maven//:org_jspecify_jspecify", + ], +) + java_library( name = "eval_helpers", srcs = ["EvalHelpers.java"], deps = [ + ":execution_frame", ":planned_interpretable", ":strict_error_exception", "//common:error_codes", @@ -277,6 +366,8 @@ java_library( name = "planned_interpretable", srcs = ["PlannedInterpretable.java"], deps = [ + ":execution_frame", + "//runtime:evaluation_exception", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", ], diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java index a3a39ce8a..b09191e9f 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java @@ -18,8 +18,6 @@ import com.google.common.base.Preconditions; import dev.cel.common.values.ErrorValue; -import dev.cel.runtime.CelEvaluationListener; -import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.GlobalResolver; final class EvalAnd extends PlannedInterpretable { @@ -28,10 +26,10 @@ final class EvalAnd extends PlannedInterpretable { private final PlannedInterpretable[] args; @Override - public Object eval(GlobalResolver resolver) { + public Object eval(GlobalResolver resolver, ExecutionFrame frame) { ErrorValue errorValue = null; for (PlannedInterpretable arg : args) { - Object argVal = evalNonstrictly(arg, resolver); + Object argVal = evalNonstrictly(arg, resolver, frame); if (argVal instanceof Boolean) { // Short-circuit on false if (!((boolean) argVal)) { @@ -53,27 +51,6 @@ public Object eval(GlobalResolver resolver) { return true; } - @Override - public Object eval(GlobalResolver resolver, CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval( - GlobalResolver resolver, - CelFunctionResolver lateBoundFunctionResolver, - CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - static EvalAnd create(long exprId, PlannedInterpretable[] args) { return new EvalAnd(exprId, 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 index 6e20d4f59..fdd7ad2a3 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java @@ -15,38 +15,27 @@ package dev.cel.runtime.planner; import com.google.errorprone.annotations.Immutable; -import dev.cel.runtime.CelEvaluationListener; -import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.GlobalResolver; @Immutable -final class EvalAttribute extends PlannedInterpretable { +final class EvalAttribute extends InterpretableAttribute { private final Attribute attr; @Override - public Object eval(GlobalResolver resolver) { - return attr.resolve(resolver); - } + public Object eval(GlobalResolver resolver, ExecutionFrame frame) { + Object resolved = attr.resolve(resolver, frame); + if (resolved instanceof MissingAttribute) { + ((MissingAttribute) resolved).resolve(resolver, frame); + } - @Override - public Object eval(GlobalResolver resolver, CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); + return resolved; } @Override - public Object eval( - GlobalResolver resolver, - CelFunctionResolver lateBoundFunctionResolver, - CelEvaluationListener listener) { - throw new UnsupportedOperationException("Not yet supported"); + public EvalAttribute addQualifier(long exprId, Qualifier qualifier) { + Attribute newAttribute = attr.addQualifier(qualifier); + return create(exprId, newAttribute); } static EvalAttribute create(long exprId, Attribute attr) { diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java index 4445d3e71..74482d629 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java @@ -16,23 +16,20 @@ import com.google.common.base.Preconditions; import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelEvaluationListener; -import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.GlobalResolver; -import dev.cel.runtime.Interpretable; final class EvalConditional extends PlannedInterpretable { @SuppressWarnings("Immutable") - private final Interpretable[] args; + private final PlannedInterpretable[] args; @Override - public Object eval(GlobalResolver resolver) throws CelEvaluationException { - Interpretable condition = args[0]; - Interpretable truthy = args[1]; - Interpretable falsy = args[2]; + public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + PlannedInterpretable condition = args[0]; + PlannedInterpretable truthy = args[1]; + PlannedInterpretable falsy = args[2]; // TODO: Handle unknowns - Object condResult = condition.eval(resolver); + Object condResult = condition.eval(resolver, frame); if (!(condResult instanceof Boolean)) { throw new IllegalArgumentException( String.format("Expected boolean value, found :%s", condResult)); @@ -40,38 +37,17 @@ public Object eval(GlobalResolver resolver) throws CelEvaluationException { // TODO: Handle exhaustive eval if ((boolean) condResult) { - return truthy.eval(resolver); + return truthy.eval(resolver, frame); } - return falsy.eval(resolver); + return falsy.eval(resolver, frame); } - @Override - public Object eval(GlobalResolver resolver, CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval( - GlobalResolver resolver, - CelFunctionResolver lateBoundFunctionResolver, - CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - static EvalConditional create(long exprId, Interpretable[] args) { + static EvalConditional create(long exprId, PlannedInterpretable[] args) { return new EvalConditional(exprId, args); } - private EvalConditional(long exprId, Interpretable[] args) { + private EvalConditional(long exprId, PlannedInterpretable[] args) { super(exprId); Preconditions.checkArgument(args.length == 3); this.args = args; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java index 408d04046..74d2811ea 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java @@ -18,8 +18,6 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.common.values.CelByteString; import dev.cel.common.values.NullValue; -import dev.cel.runtime.CelEvaluationListener; -import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.GlobalResolver; @Immutable @@ -40,25 +38,7 @@ final class EvalConstant extends PlannedInterpretable { private final Object constant; @Override - public Object eval(GlobalResolver resolver) { - return constant; - } - - @Override - public Object eval(GlobalResolver resolver, CelEvaluationListener listener) { - return constant; - } - - @Override - public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { - return constant; - } - - @Override - public Object eval( - GlobalResolver resolver, - CelFunctionResolver lateBoundFunctionResolver, - CelEvaluationListener listener) { + public Object eval(GlobalResolver resolver, ExecutionFrame frame) { return 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 index 4ec275eef..e519b968c 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java @@ -17,53 +17,29 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelEvaluationListener; -import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.GlobalResolver; -import dev.cel.runtime.Interpretable; @Immutable final class EvalCreateList extends PlannedInterpretable { // Array contents are not mutated @SuppressWarnings("Immutable") - private final Interpretable[] values; + private final PlannedInterpretable[] values; @Override - public Object eval(GlobalResolver resolver) throws CelEvaluationException { + public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(values.length); - for (Interpretable value : values) { - builder.add(value.eval(resolver)); + for (PlannedInterpretable value : values) { + builder.add(value.eval(resolver, frame)); } return builder.build(); } - @Override - public Object eval(GlobalResolver resolver, CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval( - GlobalResolver resolver, - CelFunctionResolver lateBoundFunctionResolver, - CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - static EvalCreateList create(long exprId, Interpretable[] values) { + static EvalCreateList create(long exprId, PlannedInterpretable[] values) { return new EvalCreateList(exprId, values); } - private EvalCreateList(long exprId, Interpretable[] values) { + private EvalCreateList(long exprId, PlannedInterpretable[] values) { super(exprId); 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 index 38d690303..abdba90db 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java @@ -18,59 +18,34 @@ import com.google.common.collect.ImmutableMap; import com.google.errorprone.annotations.Immutable; import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelEvaluationListener; -import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.GlobalResolver; -import dev.cel.runtime.Interpretable; @Immutable final class EvalCreateMap extends PlannedInterpretable { // Array contents are not mutated @SuppressWarnings("Immutable") - private final Interpretable[] keys; + private final PlannedInterpretable[] keys; // Array contents are not mutated @SuppressWarnings("Immutable") - private final Interpretable[] values; + private final PlannedInterpretable[] values; @Override - public Object eval(GlobalResolver resolver) throws CelEvaluationException { + public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { 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)); + builder.put(keys[i].eval(resolver, frame), values[i].eval(resolver, frame)); } return builder.buildOrThrow(); } - - @Override - public Object eval(GlobalResolver resolver, CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval( - GlobalResolver resolver, - CelFunctionResolver lateBoundFunctionResolver, - CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - static EvalCreateMap create(long exprId, Interpretable[] keys, Interpretable[] values) { + static EvalCreateMap create(long exprId, PlannedInterpretable[] keys, PlannedInterpretable[] values) { return new EvalCreateMap(exprId, keys, values); } - private EvalCreateMap(long exprId, Interpretable[] keys, Interpretable[] values) { + private EvalCreateMap(long exprId, PlannedInterpretable[] keys, PlannedInterpretable[] values) { super(exprId); Preconditions.checkArgument(keys.length == values.length); this.keys = keys; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java index 7553add80..f1d6f75e5 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java @@ -19,10 +19,7 @@ import dev.cel.common.values.CelValueProvider; import dev.cel.common.values.StructValue; import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelEvaluationListener; -import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.GlobalResolver; -import dev.cel.runtime.Interpretable; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -39,13 +36,13 @@ final class EvalCreateStruct extends PlannedInterpretable { // Array contents are not mutated @SuppressWarnings("Immutable") - private final Interpretable[] values; + private final PlannedInterpretable[] values; @Override - public Object eval(GlobalResolver resolver) throws CelEvaluationException { + public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { Map fieldValues = new HashMap<>(); for (int i = 0; i < keys.length; i++) { - Object value = values[i].eval(resolver); + Object value = values[i].eval(resolver, frame); fieldValues.put(keys[i], value); } @@ -62,33 +59,12 @@ public Object eval(GlobalResolver resolver) throws CelEvaluationException { return value; } - @Override - public Object eval(GlobalResolver resolver, CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval( - GlobalResolver resolver, - CelFunctionResolver lateBoundFunctionResolver, - CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - static EvalCreateStruct create( long exprId, CelValueProvider valueProvider, StructType structType, String[] keys, - Interpretable[] values) { + PlannedInterpretable[] values) { return new EvalCreateStruct(exprId, valueProvider, structType, keys, values); } @@ -97,7 +73,7 @@ private EvalCreateStruct( CelValueProvider valueProvider, StructType structType, String[] keys, - Interpretable[] values) { + PlannedInterpretable[] values) { super(exprId); this.valueProvider = valueProvider; this.structType = structType; 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..49047f3a4 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java @@ -0,0 +1,186 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.Immutable; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.ConcatenatedListView; +import dev.cel.runtime.GlobalResolver; +import java.util.Collection; +import java.util.Map; +import org.jspecify.annotations.Nullable; + +@Immutable +final class EvalFold extends PlannedInterpretable { + + private final String accuVar; + private final PlannedInterpretable accuInit; + private final String iterVar; + private final String iterVar2; + private final PlannedInterpretable iterRange; + private final PlannedInterpretable condition; + private final PlannedInterpretable loopStep; + private final PlannedInterpretable result; + + static EvalFold create( + long exprId, + String accuVar, + PlannedInterpretable accuInit, + String iterVar, + String iterVar2, + PlannedInterpretable iterRange, + PlannedInterpretable loopCondition, + PlannedInterpretable loopStep, + PlannedInterpretable result) { + return new EvalFold( + exprId, accuVar, accuInit, iterVar, iterVar2, iterRange, loopCondition, loopStep, result); + } + + private EvalFold( + long exprId, + String accuVar, + PlannedInterpretable accuInit, + String iterVar, + String iterVar2, + PlannedInterpretable iterRange, + PlannedInterpretable condition, + PlannedInterpretable loopStep, + PlannedInterpretable result) { + super(exprId); + this.accuVar = accuVar; + this.accuInit = accuInit; + this.iterVar = iterVar; + this.iterVar2 = iterVar2; + this.iterRange = iterRange; + this.condition = condition; + this.loopStep = loopStep; + this.result = result; + } + + @Override + public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object iterRangeRaw = iterRange.eval(resolver, frame); + Folder folder = new Folder(resolver, accuVar, iterVar, iterVar2); + folder.accuVal = maybeWrapAccumulator(accuInit.eval(folder, frame)); + + Object result; + if (iterRangeRaw instanceof Map) { + result = evalMap((Map) iterRangeRaw, folder, frame); + } else if (iterRangeRaw instanceof Collection) { + result = evalList((Collection) iterRangeRaw, folder, frame); + } else { + throw new IllegalArgumentException("Unexpected iter_range type: " + iterRangeRaw.getClass()); + } + + return maybeUnwrapAccumulator(result); + } + + private Object evalMap(Map iterRange, Folder folder, ExecutionFrame frame) + throws CelEvaluationException { + for (Map.Entry entry : iterRange.entrySet()) { + frame.incrementIterations(); + + folder.iterVarVal = entry.getKey(); + if (!iterVar2.isEmpty()) { + folder.iterVar2Val = entry.getValue(); + } + + boolean cond = (boolean) condition.eval(folder, frame); + if (!cond) { + return result.eval(folder, frame); + } + + folder.accuVal = loopStep.eval(folder, frame); + } + return result.eval(folder, frame); + } + + private Object evalList(Collection iterRange, Folder folder, ExecutionFrame frame) + throws CelEvaluationException { + int index = 0; + for (Object item : iterRange) { + frame.incrementIterations(); + + if (iterVar2.isEmpty()) { + folder.iterVarVal = item; + } else { + folder.iterVarVal = (long) index; + folder.iterVar2Val = item; + } + + boolean cond = (boolean) condition.eval(folder, frame); + if (!cond) { + return result.eval(folder, frame); + } + + folder.accuVal = loopStep.eval(folder, frame); + index++; + } + return result.eval(folder, frame); + } + + private static Object maybeWrapAccumulator(Object val) { + if (val instanceof Collection) { + return new ConcatenatedListView<>((Collection) val); + } + // TODO: Introduce mutable map support (for comp v2) + return val; + } + + private static Object maybeUnwrapAccumulator(Object val) { + if (val instanceof ConcatenatedListView) { + return ImmutableList.copyOf((ConcatenatedListView) val); + } + + // TODO: Introduce mutable map support (for comp v2) + return val; + } + + private static class Folder implements GlobalResolver { + private final GlobalResolver resolver; + private final String accuVar; + private final String iterVar; + private final String iterVar2; + + private Object iterVarVal; + private Object iterVar2Val; + private Object 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; + } + + if (name.equals(iterVar)) { + return this.iterVarVal; + } + + if (name.equals(iterVar2)) { + return this.iterVar2Val; + } + + return resolver.resolve(name); + } + } +} 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 3b5bda1bc..8d2805469 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java @@ -21,9 +21,10 @@ final class EvalHelpers { - static Object evalNonstrictly(PlannedInterpretable interpretable, GlobalResolver resolver) { + static Object evalNonstrictly( + PlannedInterpretable interpretable, GlobalResolver resolver, ExecutionFrame frame) { try { - return interpretable.eval(resolver); + return interpretable.eval(resolver, frame); } catch (StrictErrorException e) { // Intercept the strict exception to get a more localized expr ID for error reporting purposes // Example: foo [1] && strict_err [2] -> ID 2 is propagated. @@ -33,9 +34,10 @@ static Object evalNonstrictly(PlannedInterpretable interpretable, GlobalResolver } } - static Object evalStrictly(PlannedInterpretable interpretable, GlobalResolver resolver) { + static Object evalStrictly( + PlannedInterpretable interpretable, GlobalResolver resolver, ExecutionFrame frame) { try { - return interpretable.eval(resolver); + return interpretable.eval(resolver, frame); } catch (CelRuntimeException e) { throw new StrictErrorException(e, interpretable.exprId()); } catch (Exception e) { diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java index f287bdd59..8c8f5954d 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java @@ -18,8 +18,6 @@ import com.google.common.base.Preconditions; import dev.cel.common.values.ErrorValue; -import dev.cel.runtime.CelEvaluationListener; -import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.GlobalResolver; final class EvalOr extends PlannedInterpretable { @@ -28,10 +26,10 @@ final class EvalOr extends PlannedInterpretable { private final PlannedInterpretable[] args; @Override - public Object eval(GlobalResolver resolver) { + public Object eval(GlobalResolver resolver, ExecutionFrame frame) { ErrorValue errorValue = null; for (PlannedInterpretable arg : args) { - Object argVal = evalNonstrictly(arg, resolver); + Object argVal = evalNonstrictly(arg, resolver, frame); if (argVal instanceof Boolean) { // Short-circuit on true if (((boolean) argVal)) { @@ -53,27 +51,6 @@ public Object eval(GlobalResolver resolver) { return false; } - @Override - public Object eval(GlobalResolver resolver, CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval( - GlobalResolver resolver, - CelFunctionResolver lateBoundFunctionResolver, - CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - static EvalOr create(long exprId, PlannedInterpretable[] args) { return new EvalOr(exprId, args); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalTestOnly.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalTestOnly.java new file mode 100644 index 000000000..30ecdbd83 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalTestOnly.java @@ -0,0 +1,45 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.GlobalResolver; + +@Immutable +final class EvalTestOnly extends InterpretableAttribute { + + private final InterpretableAttribute attr; + + @Override + public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + return attr.eval(resolver, frame); + } + + @Override + public EvalTestOnly addQualifier(long exprId, Qualifier qualifier) { + PresenceTestQualifier presenceTestQualifier = PresenceTestQualifier.create(qualifier.value()); + return new EvalTestOnly(exprId(), attr.addQualifier(exprId, presenceTestQualifier)); + } + + static EvalTestOnly create(long exprId, InterpretableAttribute attr) { + return new EvalTestOnly(exprId, attr); + } + + private EvalTestOnly(long exprId, InterpretableAttribute attr) { + super(exprId); + this.attr = attr; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java index 13b59d11e..d1a33017b 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java @@ -18,8 +18,6 @@ import static dev.cel.runtime.planner.EvalHelpers.evalStrictly; import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelEvaluationListener; -import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.GlobalResolver; @@ -29,35 +27,16 @@ final class EvalUnary extends PlannedInterpretable { private final PlannedInterpretable arg; @Override - public Object eval(GlobalResolver resolver) throws CelEvaluationException { + public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { Object argVal = - resolvedOverload.isStrict() ? evalStrictly(arg, resolver) : evalNonstrictly(arg, resolver); + resolvedOverload.isStrict() + ? evalStrictly(arg, resolver, frame) + : evalNonstrictly(arg, resolver, frame); Object[] arguments = new Object[] {argVal}; return resolvedOverload.getDefinition().apply(arguments); } - @Override - public Object eval(GlobalResolver resolver, CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval( - GlobalResolver resolver, - CelFunctionResolver lateBoundFunctionResolver, - CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - static EvalUnary create( long exprId, CelResolvedOverload resolvedOverload, PlannedInterpretable arg) { return new EvalUnary(exprId, resolvedOverload, 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 index a2a4c0acc..da2979ad1 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java @@ -18,8 +18,6 @@ import static dev.cel.runtime.planner.EvalHelpers.evalStrictly; import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelEvaluationListener; -import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.GlobalResolver; @@ -30,40 +28,19 @@ final class EvalVarArgsCall extends PlannedInterpretable { private final PlannedInterpretable[] args; @Override - public Object eval(GlobalResolver resolver) throws CelEvaluationException { + public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { Object[] argVals = new Object[args.length]; for (int i = 0; i < args.length; i++) { PlannedInterpretable arg = args[i]; argVals[i] = resolvedOverload.isStrict() - ? evalStrictly(arg, resolver) - : evalNonstrictly(arg, resolver); + ? evalStrictly(arg, resolver, frame) + : evalNonstrictly(arg, resolver, frame); } return resolvedOverload.getDefinition().apply(argVals); } - @Override - public Object eval(GlobalResolver resolver, CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval( - GlobalResolver resolver, - CelFunctionResolver lateBoundFunctionResolver, - CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - static EvalVarArgsCall create( long exprId, CelResolvedOverload resolvedOverload, PlannedInterpretable[] args) { return new EvalVarArgsCall(exprId, resolvedOverload, 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 index 628e4a70f..6bda7619d 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalZeroArity.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalZeroArity.java @@ -15,8 +15,6 @@ package dev.cel.runtime.planner; import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelEvaluationListener; -import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.GlobalResolver; @@ -26,31 +24,10 @@ final class EvalZeroArity extends PlannedInterpretable { private final CelResolvedOverload resolvedOverload; @Override - public Object eval(GlobalResolver resolver) throws CelEvaluationException { + public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { return resolvedOverload.getDefinition().apply(EMPTY_ARRAY); } - @Override - public Object eval(GlobalResolver resolver, CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - - @Override - public Object eval( - GlobalResolver resolver, - CelFunctionResolver lateBoundFunctionResolver, - CelEvaluationListener listener) { - // TODO: Implement support - throw new UnsupportedOperationException("Not yet supported"); - } - static EvalZeroArity create(long exprId, CelResolvedOverload resolvedOverload) { return new EvalZeroArity(exprId, resolvedOverload); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java b/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java new file mode 100644 index 000000000..55be92471 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.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 dev.cel.common.CelOptions; +import dev.cel.common.exceptions.CelIterationLimitExceededException; +import dev.cel.runtime.GlobalResolver; +import java.util.concurrent.atomic.AtomicInteger; +import org.jspecify.annotations.Nullable; + +/** Tracks execution context within a planned program. */ +final class ExecutionFrame implements GlobalResolver { + + private final GlobalResolver delegate; + private final int comprehensionIterationLimit; + private final AtomicInteger iterationCount; + + @Override + public @Nullable Object resolve(String name) { + return delegate.resolve(name); + } + + void incrementIterations() { + if (comprehensionIterationLimit < 0) { + return; + } + int iterations = iterationCount.incrementAndGet(); + if (iterations > comprehensionIterationLimit) { + throw new CelIterationLimitExceededException(comprehensionIterationLimit); + } + } + + static ExecutionFrame create(GlobalResolver delegate, CelOptions celOptions) { + return new ExecutionFrame(delegate, celOptions.comprehensionMaxIterations()); + } + + private ExecutionFrame(GlobalResolver delegate, int limit) { + this.delegate = delegate; + this.comprehensionIterationLimit = limit; + this.iterationCount = new AtomicInteger(0); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/InterpretableAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/InterpretableAttribute.java new file mode 100644 index 000000000..547380c11 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/InterpretableAttribute.java @@ -0,0 +1,27 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.errorprone.annotations.Immutable; + +@Immutable +abstract class InterpretableAttribute extends PlannedInterpretable { + + abstract InterpretableAttribute addQualifier(long exprId, Qualifier qualifier); + + InterpretableAttribute(long exprId) { + super(exprId); + } +} \ No newline at end of file diff --git a/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java new file mode 100644 index 000000000..40a9f6203 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java @@ -0,0 +1,81 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.Immutable; +import dev.cel.runtime.GlobalResolver; + +/** + * An attribute that attempts to resolve a variable against a list of potential namespaced + * attributes. This is used during parsed-only evaluation. + */ +@Immutable +final class MaybeAttribute implements Attribute { + private final AttributeFactory attrFactory; + private final ImmutableList attributes; + + @Override + public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { + MissingAttribute maybeError = null; + for (NamespacedAttribute attr : attributes) { + Object value = attr.resolve(ctx, frame); + if (value == null) { + continue; + } + + if (value instanceof MissingAttribute) { + maybeError = (MissingAttribute) value; + // When the variable is missing in a maybe attribute, defer erroring. + // The variable may exist in other namespaced attributes. + continue; + } + + return value; + } + + return maybeError; + } + + @Override + public Attribute addQualifier(Qualifier qualifier) { + Object strQualifier = qualifier.value(); + ImmutableList.Builder augmentedNamesBuilder = ImmutableList.builder(); + ImmutableList.Builder attributesBuilder = ImmutableList.builder(); + for (NamespacedAttribute attr : attributes) { + if (strQualifier instanceof String && attr.qualifiers().isEmpty()) { + for (String varName : attr.candidateVariableNames()) { + augmentedNamesBuilder.add(varName + "." + strQualifier); + } + } + + attributesBuilder.add(attr.addQualifier(qualifier)); + } + ImmutableList augmentedNames = augmentedNamesBuilder.build(); + ImmutableList.Builder namespacedAttributeBuilder = ImmutableList.builder(); + if (!augmentedNames.isEmpty()) { + namespacedAttributeBuilder.add( + attrFactory.newAbsoluteAttribute(augmentedNames.toArray(new String[0]))); + } + + namespacedAttributeBuilder.addAll(attributesBuilder.build()); + return new MaybeAttribute(attrFactory, namespacedAttributeBuilder.build()); + } + + MaybeAttribute(AttributeFactory attrFactory, ImmutableList attributes) { + this.attrFactory = attrFactory; + this.attributes = attributes; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java new file mode 100644 index 000000000..596d1bae4 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.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 com.google.common.collect.ImmutableSet; +import dev.cel.common.exceptions.CelAttributeNotFoundException; +import dev.cel.runtime.GlobalResolver; + +/** Represents a missing attribute that is surfaced while resolving a struct field or a map key. */ +final class MissingAttribute implements Attribute { + + private final ImmutableSet missingAttributes; + + @Override + public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { + throw CelAttributeNotFoundException.forFieldResolution(missingAttributes); + } + + @Override + public Attribute addQualifier(Qualifier qualifier) { + throw new UnsupportedOperationException("Unsupported operation"); + } + + static MissingAttribute newMissingAttribute(String... attributeNames) { + return newMissingAttribute(ImmutableSet.copyOf(attributeNames)); + } + + static MissingAttribute newMissingAttribute(ImmutableSet attributeNames) { + return new MissingAttribute(attributeNames); + } + + private MissingAttribute(ImmutableSet missingAttributes) { + this.missingAttributes = missingAttributes; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java new file mode 100644 index 000000000..d513bc7ba --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java @@ -0,0 +1,126 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.EnumType; +import dev.cel.common.types.TypeType; +import dev.cel.common.values.CelValue; +import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.GlobalResolver; +import java.util.NoSuchElementException; + +@Immutable +final class NamespacedAttribute implements Attribute { + private final ImmutableSet namespacedNames; + private final ImmutableList qualifiers; + private final CelValueConverter celValueConverter; + private final CelTypeProvider typeProvider; + + @Override + public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { + for (String name : namespacedNames) { + Object value = ctx.resolve(name); + if (value != null) { + if (!qualifiers.isEmpty()) { + return applyQualifiers(value, celValueConverter, qualifiers); + } else { + return value; + } + } + + CelType type = typeProvider.findType(name).orElse(null); + if (type != null) { + if (qualifiers.isEmpty()) { + // Resolution of a fully qualified type name: foo.bar.baz + return TypeType.create(type); + } else { + // This is potentially a fully qualified reference to an enum value + if (type instanceof EnumType && qualifiers.size() == 1) { + EnumType enumType = (EnumType) type; + String strQualifier = (String) qualifiers.get(0).value(); + return enumType + .findNumberByName(strQualifier) + .orElseThrow( + () -> + new NoSuchElementException( + String.format( + "Field %s was not found on enum %s", + enumType.name(), strQualifier))); + } + } + + throw new IllegalStateException( + "Unexpected type resolution when there were remaining qualifiers: " + type.name()); + } + } + + return MissingAttribute.newMissingAttribute(namespacedNames); + } + + ImmutableList qualifiers() { + return qualifiers; + } + + ImmutableSet candidateVariableNames() { + return namespacedNames; + } + + @Override + public NamespacedAttribute addQualifier(Qualifier qualifier) { + return new NamespacedAttribute( + typeProvider, + celValueConverter, + namespacedNames, + ImmutableList.builder().addAll(qualifiers).add(qualifier).build()); + } + + private static Object applyQualifiers( + Object value, CelValueConverter celValueConverter, ImmutableList qualifiers) { + Object obj = celValueConverter.toRuntimeValue(value); + + for (Qualifier qualifier : qualifiers) { + obj = qualifier.qualify(obj); + } + + if (obj instanceof CelValue) { + obj = celValueConverter.unwrap((CelValue) obj); + } + + return obj; + } + + NamespacedAttribute( + CelTypeProvider typeProvider, + CelValueConverter celValueConverter, + ImmutableSet namespacedNames) { + this(typeProvider, celValueConverter, namespacedNames, ImmutableList.of()); + } + + private NamespacedAttribute( + CelTypeProvider typeProvider, + CelValueConverter celValueConverter, + ImmutableSet namespacedNames, + ImmutableList qualifiers) { + this.typeProvider = typeProvider; + this.celValueConverter = celValueConverter; + this.namespacedNames = namespacedNames; + this.qualifiers = qualifiers; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java b/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java index 87a1a7dc4..5ce3208f8 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java @@ -15,12 +15,18 @@ package dev.cel.runtime.planner; import com.google.errorprone.annotations.Immutable; -import dev.cel.runtime.Interpretable; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.GlobalResolver; @Immutable -abstract class PlannedInterpretable implements Interpretable { +abstract class PlannedInterpretable { private final long exprId; + /** Runs interpretation with the given activation which supplies name/value bindings. */ + abstract Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException; + + // TODO: Implement support for late-bound functions and evaluation listener + long exprId() { return exprId; } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java index d1214fab0..646ad6c85 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java @@ -16,6 +16,7 @@ import com.google.auto.value.AutoValue; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.CelOptions; import dev.cel.common.CelRuntimeException; import dev.cel.common.values.ErrorValue; import dev.cel.runtime.Activation; @@ -33,6 +34,8 @@ abstract class PlannedProgram implements Program { abstract ErrorMetadata metadata(); + abstract CelOptions options(); + @Override public Object eval() throws CelEvaluationException { return evalOrThrow(interpretable(), GlobalResolver.EMPTY); @@ -52,7 +55,8 @@ public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctio private Object evalOrThrow(PlannedInterpretable interpretable, GlobalResolver resolver) throws CelEvaluationException { try { - Object evalResult = interpretable.eval(resolver); + ExecutionFrame frame = ExecutionFrame.create(resolver, options()); + Object evalResult = interpretable.eval(resolver, frame); if (evalResult instanceof ErrorValue) { ErrorValue errorValue = (ErrorValue) evalResult; throw newCelEvaluationException(errorValue.exprId(), errorValue.value()); @@ -78,7 +82,8 @@ private CelEvaluationException newCelEvaluationException(long exprId, Exception return builder.setMetadata(metadata(), exprId).build(); } - static Program create(PlannedInterpretable interpretable, ErrorMetadata metadata) { - return new AutoValue_PlannedProgram(interpretable, metadata); + static Program create( + PlannedInterpretable interpretable, ErrorMetadata metadata, CelOptions options) { + return new AutoValue_PlannedProgram(interpretable, metadata, options); } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PresenceTestQualifier.java b/runtime/src/main/java/dev/cel/runtime/planner/PresenceTestQualifier.java new file mode 100644 index 000000000..973182b9b --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/PresenceTestQualifier.java @@ -0,0 +1,53 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static dev.cel.runtime.planner.MissingAttribute.newMissingAttribute; + +import dev.cel.common.values.SelectableValue; +import java.util.Map; + +/** A qualifier for presence testing a field or a map key. */ +final class PresenceTestQualifier implements Qualifier { + + @SuppressWarnings("Immutable") + private final Object value; + + @Override + public Object value() { + return value; + } + + @Override + @SuppressWarnings("unchecked") // SelectableValue cast is safe + public Object qualify(Object obj) { + if (obj instanceof SelectableValue) { + return ((SelectableValue) obj).find(value).isPresent(); + } else if (obj instanceof Map) { + Map map = (Map) obj; + return map.containsKey(value); + } + + return newMissingAttribute(value.toString()); + } + + static PresenceTestQualifier create(Object value) { + return new PresenceTestQualifier(value); + } + + private PresenceTestQualifier(Object value) { + this.value = value; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index 252695ed7..1559b8482 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -21,11 +21,13 @@ import javax.annotation.concurrent.ThreadSafe; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; +import dev.cel.common.CelOptions; import dev.cel.common.Operator; import dev.cel.common.annotations.Internal; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; import dev.cel.common.ast.CelExpr.CelCall; +import dev.cel.common.ast.CelExpr.CelComprehension; import dev.cel.common.ast.CelExpr.CelList; import dev.cel.common.ast.CelExpr.CelMap; import dev.cel.common.ast.CelExpr.CelSelect; @@ -37,12 +39,12 @@ import dev.cel.common.types.CelTypeProvider; import dev.cel.common.types.StructType; import dev.cel.common.types.TypeType; +import dev.cel.common.values.CelValueConverter; import dev.cel.common.values.CelValueProvider; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelEvaluationExceptionBuilder; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.DefaultDispatcher; -import dev.cel.runtime.Interpretable; import dev.cel.runtime.Program; import java.util.NoSuchElementException; import java.util.Optional; @@ -60,6 +62,8 @@ public final class ProgramPlanner { private final DefaultDispatcher dispatcher; private final AttributeFactory attributeFactory; private final CelContainer container; + private final CelOptions options; + /** * Plans a {@link Program} from the provided parsed-only or type-checked {@link @@ -75,7 +79,7 @@ public Program plan(CelAbstractSyntaxTree ast) throws CelEvaluationException { ErrorMetadata errorMetadata = ErrorMetadata.create(ast.getSource().getPositionsMap(), ast.getSource().getDescription()); - return PlannedProgram.create(plannedInterpretable, errorMetadata); + return PlannedProgram.create(plannedInterpretable, errorMetadata, options); } private PlannedInterpretable plan(CelExpr celExpr, PlannerContext ctx) { @@ -84,6 +88,8 @@ private PlannedInterpretable plan(CelExpr celExpr, PlannerContext ctx) { return planConstant(celExpr.constant()); case IDENT: return planIdent(celExpr, ctx); + case SELECT: + return planSelect(celExpr, ctx); case CALL: return planCall(celExpr, ctx); case LIST: @@ -92,13 +98,36 @@ private PlannedInterpretable plan(CelExpr celExpr, PlannerContext ctx) { return planCreateStruct(celExpr, ctx); case MAP: return planCreateMap(celExpr, ctx); + case COMPREHENSION: + return planComprehension(celExpr, ctx); case NOT_SET: throw new UnsupportedOperationException("Unsupported kind: " + celExpr.getKind()); default: - throw new IllegalArgumentException("Not yet implemented kind: " + celExpr.getKind()); + throw new UnsupportedOperationException("Unexpected kind: " + celExpr.getKind()); } } + private PlannedInterpretable planSelect(CelExpr celExpr, PlannerContext ctx) { + CelSelect select = celExpr.select(); + PlannedInterpretable operand = plan(select.operand(), ctx); + + InterpretableAttribute attribute; + if (operand instanceof EvalAttribute) { + attribute = (EvalAttribute) operand; + } else { + attribute = + EvalAttribute.create(celExpr.id(), attributeFactory.newRelativeAttribute(operand)); + } + + if (select.testOnly()) { + attribute = EvalTestOnly.create(celExpr.id(), attribute); + } + + Qualifier qualifier = StringQualifier.create(select.field()); + + return attribute.addQualifier(celExpr.id(), qualifier); + } + private PlannedInterpretable planConstant(CelConstant celConstant) { switch (celConstant.getKind()) { case NULL_VALUE: @@ -217,7 +246,7 @@ private PlannedInterpretable planCreateStruct(CelExpr celExpr, PlannerContext ct ImmutableList entries = struct.entries(); String[] keys = new String[entries.size()]; - Interpretable[] values = new Interpretable[entries.size()]; + PlannedInterpretable[] values = new PlannedInterpretable[entries.size()]; for (int i = 0; i < entries.size(); i++) { Entry entry = entries.get(i); @@ -257,6 +286,27 @@ private PlannedInterpretable planCreateMap(CelExpr celExpr, PlannerContext ctx) return EvalCreateMap.create(celExpr.id(), keys, values); } + private PlannedInterpretable planComprehension(CelExpr expr, PlannerContext ctx) { + CelComprehension comprehension = expr.comprehension(); + + PlannedInterpretable accuInit = plan(comprehension.accuInit(), ctx); + PlannedInterpretable iterRange = plan(comprehension.iterRange(), ctx); + PlannedInterpretable loopCondition = plan(comprehension.loopCondition(), ctx); + PlannedInterpretable loopStep = plan(comprehension.loopStep(), ctx); + PlannedInterpretable result = plan(comprehension.result(), ctx); + + return EvalFold.create( + expr.id(), + 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. @@ -403,19 +453,26 @@ public static ProgramPlanner newPlanner( CelTypeProvider typeProvider, CelValueProvider valueProvider, DefaultDispatcher dispatcher, - CelContainer container) { - return new ProgramPlanner(typeProvider, valueProvider, dispatcher, container); + CelValueConverter celValueConverter, + CelContainer container, + CelOptions options) { + return new ProgramPlanner( + typeProvider, valueProvider, dispatcher, celValueConverter, container, options); } private ProgramPlanner( CelTypeProvider typeProvider, CelValueProvider valueProvider, DefaultDispatcher dispatcher, - CelContainer container) { + CelValueConverter celValueConverter, + CelContainer container, + CelOptions options) { this.typeProvider = typeProvider; this.valueProvider = valueProvider; this.dispatcher = dispatcher; this.container = container; - this.attributeFactory = AttributeFactory.newAttributeFactory(container, typeProvider); + this.options = options; + this.attributeFactory = + AttributeFactory.newAttributeFactory(container, typeProvider, celValueConverter); } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/Qualifier.java b/runtime/src/main/java/dev/cel/runtime/planner/Qualifier.java new file mode 100644 index 000000000..82e48e95a --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/Qualifier.java @@ -0,0 +1,28 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.errorprone.annotations.Immutable; + +/** + * Represents a qualification step (such as a field selection or map key lookup) applied to an + * intermediate value during attribute resolution. + */ +@Immutable +interface Qualifier { + Object value(); + + Object qualify(Object value); +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java new file mode 100644 index 000000000..a913849f6 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java @@ -0,0 +1,70 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.GlobalResolver; + +/** + * An attribute resolved relative to a base expression (operand) by applying a sequence of + * qualifiers. + */ +@Immutable +final class RelativeAttribute implements Attribute { + + private final PlannedInterpretable operand; + private final CelValueConverter celValueConverter; + private final ImmutableList qualifiers; + + @Override + public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { + Object obj = EvalHelpers.evalStrictly(operand, ctx, frame); + obj = celValueConverter.toRuntimeValue(obj); + + for (Qualifier qualifier : qualifiers) { + obj = qualifier.qualify(obj); + } + + // TODO: Handle unknowns + + return obj; + } + + @Override + public Attribute addQualifier(Qualifier qualifier) { + return new RelativeAttribute( + this.operand, + celValueConverter, + ImmutableList.builderWithExpectedSize(qualifiers.size() + 1) + .addAll(this.qualifiers) + .add(qualifier) + .build()); + } + + RelativeAttribute(PlannedInterpretable operand, CelValueConverter celValueConverter) { + this(operand, celValueConverter, ImmutableList.of()); + } + + private RelativeAttribute( + PlannedInterpretable operand, + CelValueConverter celValueConverter, + ImmutableList qualifiers) { + this.operand = operand; + this.celValueConverter = celValueConverter; + this.qualifiers = qualifiers; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java b/runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java new file mode 100644 index 000000000..4ceaa0e51 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java @@ -0,0 +1,61 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import dev.cel.common.exceptions.CelAttributeNotFoundException; +import dev.cel.common.values.SelectableValue; +import java.util.Map; + +/** A qualifier that accesses fields or map keys using a string identifier. */ +final class StringQualifier implements Qualifier { + + private final String value; + + @Override + public String value() { + return value; + } + + @Override + @SuppressWarnings("unchecked") // Qualifications on maps/structs must be a string + public Object qualify(Object obj) { + if (obj instanceof SelectableValue) { + return ((SelectableValue) obj).select(value); + } else if (obj instanceof Map) { + Map map = (Map) obj; + if (!map.containsKey(value)) { + throw CelAttributeNotFoundException.forMissingMapKey(value); + } + + Object mapVal = map.get(value); + + if (mapVal == null) { + throw CelAttributeNotFoundException.of( + String.format("Map value cannot be null for key: %s", value)); + } + return map.get(value); + } + + throw CelAttributeNotFoundException.forFieldResolution(value); + } + + static StringQualifier create(String value) { + return new StringQualifier(value); + } + + private StringQualifier(String value) { + this.value = value; + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java b/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java index 39723083e..7d7243384 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java @@ -2,7 +2,7 @@ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. -// You may obtain a copy of the License aj +// You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // diff --git a/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java b/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java index fbcfbd813..255360ee1 100644 --- a/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java +++ b/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java @@ -2,7 +2,7 @@ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. -// You may obtain a copy of the License aj +// You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // diff --git a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel index 9e3a79ed9..01df7c9ee 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel @@ -33,10 +33,12 @@ java_library( "//common/values", "//common/values:cel_byte_string", "//common/values:cel_value_provider", + "//common/values:proto_message_value", "//common/values:proto_message_value_provider", "//compiler", "//compiler:compiler_builder", "//extensions", + "//parser:macro", "//runtime", "//runtime:dispatcher", "//runtime:function_binding", @@ -47,6 +49,7 @@ java_library( "//runtime/planner:program_planner", "//runtime/standard:add", "//runtime/standard:divide", + "//runtime/standard:dyn", "//runtime/standard:equals", "//runtime/standard:greater", "//runtime/standard:greater_equals", 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 bb580cbb3..968fdcc94 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -51,16 +51,21 @@ import dev.cel.common.types.OptionalType; import dev.cel.common.types.ProtoMessageTypeProvider; import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructTypeReference; import dev.cel.common.types.TypeType; import dev.cel.common.values.CelByteString; +import dev.cel.common.values.CelValueConverter; import dev.cel.common.values.CelValueProvider; import dev.cel.common.values.NullValue; +import dev.cel.common.values.ProtoCelValueConverter; import dev.cel.common.values.ProtoMessageValueProvider; import dev.cel.compiler.CelCompiler; import dev.cel.compiler.CelCompilerFactory; import dev.cel.expr.conformance.proto3.GlobalEnum; import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import dev.cel.extensions.CelExtensions; +import dev.cel.parser.CelStandardMacro; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelFunctionOverload; @@ -72,6 +77,7 @@ import dev.cel.runtime.standard.AddOperator; import dev.cel.runtime.standard.CelStandardFunction; import dev.cel.runtime.standard.DivideOperator; +import dev.cel.runtime.standard.DynFunction; import dev.cel.runtime.standard.EqualsOperator; import dev.cel.runtime.standard.GreaterEqualsOperator; import dev.cel.runtime.standard.GreaterOperator; @@ -99,17 +105,32 @@ public final class ProgramPlannerTest { private static final DynamicProto DYNAMIC_PROTO = DynamicProto.create(DefaultMessageFactory.create(DESCRIPTOR_POOL)); private static final CelValueProvider VALUE_PROVIDER = - ProtoMessageValueProvider.newInstance(CelOptions.DEFAULT, DYNAMIC_PROTO); + ProtoMessageValueProvider.newInstance(CEL_OPTIONS, DYNAMIC_PROTO); + private static final CelValueConverter CEL_VALUE_CONVERTER = + ProtoCelValueConverter.newInstance(DESCRIPTOR_POOL, DYNAMIC_PROTO); private static final CelContainer CEL_CONTAINER = - CelContainer.newBuilder().setName("cel.expr.conformance.proto3").build(); + CelContainer.newBuilder() + .setName("cel.expr.conformance.proto3") + .addAbbreviations("really.long.abbr") + .build(); private static final ProgramPlanner PLANNER = - ProgramPlanner.newPlanner(TYPE_PROVIDER, VALUE_PROVIDER, newDispatcher(), CEL_CONTAINER); + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + VALUE_PROVIDER, + newDispatcher(), + CEL_VALUE_CONVERTER, + CEL_CONTAINER, + CEL_OPTIONS); + private static final CelCompiler CEL_COMPILER = CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) .addVar("map_var", MapType.create(SimpleType.STRING, SimpleType.DYN)) .addVar("int_var", SimpleType.INT) .addVar("dyn_var", SimpleType.DYN) + .addVar("really.long.abbr.ident", SimpleType.DYN) .addFunctionDeclarations( newFunctionDeclaration("zero", newGlobalOverload("zero_overload", SimpleType.INT)), newFunctionDeclaration("error", newGlobalOverload("error_overload", SimpleType.INT)), @@ -128,7 +149,7 @@ public final class ProgramPlannerTest { newMemberOverload( "bytes_concat_bytes", SimpleType.BYTES, SimpleType.BYTES, SimpleType.BYTES))) .addMessageTypes(TestAllTypes.getDescriptor()) - .addLibraries(CelExtensions.optional()) + .addLibraries(CelExtensions.optional(), CelExtensions.comprehensions()) .setContainer(CEL_CONTAINER) .build(); @@ -162,6 +183,7 @@ private static DefaultDispatcher newDispatcher() { builder, Operator.NOT_STRICTLY_FALSE.getFunction(), fromStandardFunction(NotStrictlyFalseFunction.create())); + addBindings(builder, "dyn", fromStandardFunction(DynFunction.create())); // Custom functions addBindings( @@ -296,10 +318,6 @@ public void plan_constant(@TestParameter ConstantTestCase testCase) throws Excep @Test public void plan_ident_enum() throws Exception { - if (isParseOnly) { - // TODO Skip for now, requires attribute qualification - return; - } CelAbstractSyntaxTree ast = compile(GlobalEnum.getDescriptor().getFullName() + "." + GlobalEnum.GAR); Program program = PLANNER.plan(ast); @@ -321,14 +339,6 @@ public void plan_ident_variable() throws Exception { @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); @@ -337,6 +347,16 @@ public void planIdent_typeLiteral(@TestParameter TypeLiteralTestCase testCase) t assertThat(result).isEqualTo(testCase.type); } + @Test + public void plan_ident_withContainer() throws Exception { + CelAbstractSyntaxTree ast = compile("abbr.ident"); + Program program = PLANNER.plan(ast); + + Object result = program.eval(ImmutableMap.of("really.long.abbr.ident", 1L)); + + assertThat(result).isEqualTo(1); + } + @Test @SuppressWarnings("unchecked") // test only public void plan_createList() throws Exception { @@ -597,6 +617,239 @@ public void plan_call_withContainer(String expression) throws Exception { assertThat(result).isEqualTo(8); } + @Test + public void plan_select_protoMessageField() throws Exception { + CelAbstractSyntaxTree ast = compile("msg.single_string"); + Program program = PLANNER.plan(ast); + + String result = + (String) + program.eval( + ImmutableMap.of("msg", TestAllTypes.newBuilder().setSingleString("foo").build())); + + assertThat(result).isEqualTo("foo"); + } + + @Test + public void plan_select_nestedProtoMessage() throws Exception { + CelAbstractSyntaxTree ast = compile("msg.single_nested_message"); + NestedMessage nestedMessage = NestedMessage.newBuilder().setBb(42).build(); + Program program = PLANNER.plan(ast); + + Object result = + program.eval( + ImmutableMap.of( + "msg", TestAllTypes.newBuilder().setSingleNestedMessage(nestedMessage).build())); + + assertThat(result).isEqualTo(nestedMessage); + } + + @Test + public void plan_select_nestedProtoMessageField() throws Exception { + CelAbstractSyntaxTree ast = compile("msg.single_nested_message.bb"); + Program program = PLANNER.plan(ast); + + Object result = + program.eval( + ImmutableMap.of( + "msg", + TestAllTypes.newBuilder() + .setSingleNestedMessage(NestedMessage.newBuilder().setBb(42)) + .build())); + + assertThat(result).isEqualTo(42); + } + + @Test + public void plan_select_safeTraversal() throws Exception { + CelAbstractSyntaxTree ast = compile("msg.single_nested_message.bb"); + Program program = PLANNER.plan(ast); + + Object result = program.eval(ImmutableMap.of("msg", TestAllTypes.getDefaultInstance())); + + assertThat(result).isEqualTo(0L); + } + + @Test + public void plan_select_onCreateStruct() throws Exception { + CelAbstractSyntaxTree ast = + compile("cel.expr.conformance.proto3.TestAllTypes{ single_string: 'foo'}.single_string"); + Program program = PLANNER.plan(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo("foo"); + } + + @Test + public void plan_select_onCreateMap() throws Exception { + CelAbstractSyntaxTree ast = compile("{'foo':'bar'}.foo"); + Program program = PLANNER.plan(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo("bar"); + } + + @Test + public void plan_select_onMapVariable() throws Exception { + CelAbstractSyntaxTree ast = compile("map_var.foo"); + Program program = PLANNER.plan(ast); + + Object result = program.eval(ImmutableMap.of("map_var", ImmutableMap.of("foo", 42L))); + + assertThat(result).isEqualTo(42L); + } + + @Test + public void plan_select_mapVarInputMissing_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("map_var.foo"); + Program program = PLANNER.plan(ast); + String errorMessage = "evaluation error at :7: Error resolving "; + if (isParseOnly) { + errorMessage += + "fields 'cel.expr.conformance.proto3.map_var, cel.expr.conformance.map_var," + + " cel.expr.map_var, cel.map_var, map_var'"; + } else { + errorMessage += "field 'map_var'"; + } + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> program.eval(ImmutableMap.of())); + + assertThat(e).hasMessageThat().contains(errorMessage); + } + + @Test + public void plan_select_mapVarKeyMissing_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("map_var.foo"); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, + () -> program.eval(ImmutableMap.of("map_var", ImmutableMap.of()))); + assertThat(e) + .hasMessageThat() + .contains("evaluation error at :7: key 'foo' is not present in map"); + } + + @Test + public void plan_select_stringQualificationFail_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("map_var.foo"); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, + () -> program.eval(ImmutableMap.of("map_var", "bogus string"))); + + assertThat(e) + .hasMessageThat() + .isEqualTo( + "evaluation error at :7: Error resolving field 'foo'. Field selections must be" + + " performed on messages or maps."); + } + + @Test + public void plan_select_presenceTest(@TestParameter PresenceTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = compile(testCase.expression); + Program program = PLANNER.plan(ast); + + boolean result = + (boolean) + program.eval( + ImmutableMap.of("msg", testCase.inputParam, "map_var", testCase.inputParam)); + + assertThat(result).isEqualTo(testCase.expected); + } + + @Test + public void plan_select_badPresenceTest_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("has(dyn([]).invalid)"); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + assertThat(e) + .hasMessageThat() + .contains( + "Error resolving field 'invalid'. Field selections must be performed on messages or" + + " maps."); + } + + @Test + @TestParameters("{expression: '[1,2,3].exists(x, x > 0) == true'}") + @TestParameters("{expression: '[1,2,3].exists(x, x < 0) == false'}") + @TestParameters("{expression: '[1,2,3].exists(i, v, i >= 0 && v > 0) == true'}") + @TestParameters("{expression: '[1,2,3].exists(i, v, i < 0 || v < 0) == false'}") + @TestParameters("{expression: '[1,2,3].map(x, x + 1) == [2,3,4]'}") + public void plan_comprehension_lists(String expression) throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).isTrue(); + } + + @Test + @TestParameters("{expression: '{\"a\": 1, \"b\": 2}.exists(k, k == \"a\")'}") + @TestParameters("{expression: '{\"a\": 1, \"b\": 2}.exists(k, k == \"c\") == false'}") + @TestParameters("{expression: '{\"a\": \"b\", \"c\": \"c\"}.exists(k, v, k == v)'}") + @TestParameters("{expression: '{\"a\": 1, \"b\": 2}.exists(k, v, v == 3) == false'}") + public void plan_comprehension_maps(String expression) throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).isTrue(); + } + + @Test + @TestParameters("{expression: '[1, 2, 3, 4, 5, 6].map(x, x)'}") + @TestParameters("{expression: '[1, 2, 3].map(x, [1, 2].map(y, x + y))'}") + public void plan_comprehension_iterationLimit_throws(String expression) throws Exception { + CelOptions options = CelOptions.current().comprehensionMaxIterations(5).build(); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + ProtoMessageValueProvider.newInstance(options, DYNAMIC_PROTO), + newDispatcher(), + CEL_VALUE_CONVERTER, + CEL_CONTAINER, + options); + CelAbstractSyntaxTree ast = compile(expression); + + Program program = planner.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + assertThat(e).hasMessageThat().contains("Iteration budget exceeded: 5"); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.ITERATION_BUDGET_EXCEEDED); + } + + @Test + public void plan_comprehension_iterationLimit_success() throws Exception { + CelOptions options = CelOptions.current().comprehensionMaxIterations(10).build(); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + ProtoMessageValueProvider.newInstance(options, DYNAMIC_PROTO), + newDispatcher(), + CEL_VALUE_CONVERTER, + CEL_CONTAINER, + options); + CelAbstractSyntaxTree ast = compile("[1, 2, 3].map(x, [1, 2].map(y, x + y))"); + + Program program = planner.plan(ast); + + Object result = program.eval(); + assertThat(result) + .isEqualTo( + ImmutableList.of( + ImmutableList.of(2L, 3L), ImmutableList.of(3L, 4L), ImmutableList.of(4L, 5L))); + } + private CelAbstractSyntaxTree compile(String expression) throws Exception { CelAbstractSyntaxTree ast = CEL_COMPILER.parse(expression).getAst(); if (isParseOnly) { @@ -669,4 +922,31 @@ private enum TypeLiteralTestCase { this.type = TypeType.create(type); } } + + @SuppressWarnings("Immutable") // Test only + private enum PresenceTestCase { + PROTO_FIELD_PRESENT( + "has(msg.single_string)", TestAllTypes.newBuilder().setSingleString("foo").build(), true), + PROTO_FIELD_ABSENT("has(msg.single_string)", TestAllTypes.getDefaultInstance(), false), + PROTO_NESTED_FIELD_PRESENT( + "has(msg.single_nested_message.bb)", + TestAllTypes.newBuilder() + .setSingleNestedMessage(NestedMessage.newBuilder().setBb(42).build()) + .build(), + true), + PROTO_NESTED_FIELD_ABSENT( + "has(msg.single_nested_message.bb)", TestAllTypes.getDefaultInstance(), false), + PROTO_MAP_KEY_PRESENT("has(map_var.foo)", ImmutableMap.of("foo", "1"), true), + PROTO_MAP_KEY_ABSENT("has(map_var.bar)", ImmutableMap.of(), false); + + private final String expression; + private final Object inputParam; + private final Object expected; + + PresenceTestCase(String expression, Object inputParam, Object expected) { + this.expression = expression; + this.inputParam = inputParam; + this.expected = expected; + } + } }