From 541e22f1baacdc367537c08112097f9614e3bded 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 ++++++++++ .../dev/cel/runtime/AccumulatedUnknowns.java | 2 +- .../dev/cel/runtime/planner/Attribute.java | 2 +- .../java/dev/cel/runtime/planner/BUILD.bazel | 58 +++++++++-------- .../java/dev/cel/runtime/planner/EvalAnd.java | 27 +------- .../cel/runtime/planner/EvalAttribute.java | 29 +-------- .../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 | 62 +++++++------------ .../dev/cel/runtime/planner/EvalHelpers.java | 10 +-- .../java/dev/cel/runtime/planner/EvalOr.java | 27 +------- .../dev/cel/runtime/planner/EvalTestOnly.java | 27 +------- .../dev/cel/runtime/planner/EvalUnary.java | 29 ++------- .../cel/runtime/planner/EvalVarArgsCall.java | 29 +-------- .../cel/runtime/planner/EvalZeroArity.java | 25 +------- .../cel/runtime/planner/ExecutionFrame.java | 51 +++++++++++++++ .../cel/runtime/planner/MaybeAttribute.java | 4 +- .../cel/runtime/planner/MissingAttribute.java | 2 +- .../runtime/planner/NamespacedAttribute.java | 2 +- .../runtime/planner/PlannedInterpretable.java | 10 ++- .../cel/runtime/planner/PlannedProgram.java | 11 +++- .../cel/runtime/planner/ProgramPlanner.java | 14 +++-- .../runtime/planner/RelativeAttribute.java | 4 +- .../java/dev/cel/runtime/CelRuntimeTest.java | 2 +- .../cel/runtime/DefaultDispatcherTest.java | 2 +- .../runtime/planner/ProgramPlannerTest.java | 51 ++++++++++++++- 31 files changed, 292 insertions(+), 411 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/ExecutionFrame.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/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/planner/Attribute.java b/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java index f20c9aadd..cc011ed34 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java @@ -20,7 +20,7 @@ /** Represents a resolvable symbol or path (such as a variable or a field selection). */ @Immutable interface Attribute { - Object resolve(GlobalResolver ctx); + Object resolve(GlobalResolver ctx, ExecutionFrame frame); Attribute addQualifier(Qualifier qualifier); } 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 09a082e87..b99b41fd5 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -37,6 +37,7 @@ java_library( "//common:cel_ast", "//common:container", "//common:operator", + "//common:options", "//common/annotations", "//common/ast", "//common/types", @@ -59,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", @@ -78,11 +82,10 @@ 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", @@ -111,6 +114,7 @@ java_library( ], deps = [ ":eval_helpers", + ":execution_frame", ":planned_interpretable", ":qualifier", "//common:container", @@ -158,10 +162,9 @@ java_library( srcs = ["EvalAttribute.java"], deps = [ ":attribute", + ":execution_frame", ":interpretable_attribute", ":qualifier", - "//runtime:evaluation_listener", - "//runtime:function_resolver", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -172,12 +175,11 @@ java_library( name = "eval_test_only", srcs = ["EvalTestOnly.java"], deps = [ + ":execution_frame", ":interpretable_attribute", ":presence_test_qualifier", ":qualifier", "//runtime:evaluation_exception", - "//runtime:evaluation_listener", - "//runtime:function_resolver", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", ], @@ -187,10 +189,9 @@ 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", ], @@ -201,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", ], @@ -215,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", ], @@ -229,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", ], @@ -243,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", ], @@ -256,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", ], @@ -269,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", @@ -286,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", @@ -300,10 +294,9 @@ 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", @@ -314,11 +307,10 @@ java_library( name = "eval_fold", srcs = ["EvalFold.java"], deps = [ + ":execution_frame", ":planned_interpretable", "//runtime:concatenated_list_view", "//runtime:evaluation_exception", - "//runtime:evaluation_listener", - "//runtime:function_resolver", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -326,10 +318,22 @@ java_library( ], ) +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", @@ -362,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 826f7e1fa..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,8 +15,6 @@ 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 @@ -25,36 +23,15 @@ final class EvalAttribute extends InterpretableAttribute { private final Attribute attr; @Override - public Object eval(GlobalResolver resolver) { - Object resolved = attr.resolve(resolver); + public Object eval(GlobalResolver resolver, ExecutionFrame frame) { + Object resolved = attr.resolve(resolver, frame); if (resolved instanceof MissingAttribute) { - ((MissingAttribute) resolved).resolve(resolver); + ((MissingAttribute) resolved).resolve(resolver, frame); } return resolved; } - @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"); - } - @Override public EvalAttribute addQualifier(long exprId, Qualifier qualifier) { Attribute newAttribute = attr.addQualifier(qualifier); 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 index 2a8ba1603..49047f3a4 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java @@ -17,8 +17,6 @@ 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.ConcatenatedListView; import dev.cel.runtime.GlobalResolver; import java.util.Collection; @@ -73,16 +71,16 @@ private EvalFold( } @Override - public Object eval(GlobalResolver resolver) throws CelEvaluationException { - Object iterRangeRaw = iterRange.eval(resolver); + 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)); + folder.accuVal = maybeWrapAccumulator(accuInit.eval(folder, frame)); Object result; if (iterRangeRaw instanceof Map) { - result = evalMap((Map) iterRangeRaw, folder); + result = evalMap((Map) iterRangeRaw, folder, frame); } else if (iterRangeRaw instanceof Collection) { - result = evalList((Collection) iterRangeRaw, folder); + result = evalList((Collection) iterRangeRaw, folder, frame); } else { throw new IllegalArgumentException("Unexpected iter_range type: " + iterRangeRaw.getClass()); } @@ -90,48 +88,32 @@ public Object eval(GlobalResolver resolver) throws CelEvaluationException { return maybeUnwrapAccumulator(result); } - @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"); - } - - private Object evalMap(Map iterRange, Folder folder) throws CelEvaluationException { + 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); + + boolean cond = (boolean) condition.eval(folder, frame); if (!cond) { - return result.eval(folder); + return result.eval(folder, frame); } - // TODO: Introduce comprehension safety controls, such as iteration limit. - folder.accuVal = loopStep.eval(folder); + folder.accuVal = loopStep.eval(folder, frame); } - return result.eval(folder); + return result.eval(folder, frame); } - private Object evalList(Collection iterRange, Folder folder) throws CelEvaluationException { + 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 { @@ -139,15 +121,15 @@ private Object evalList(Collection iterRange, Folder folder) throws CelEvalua folder.iterVar2Val = item; } - boolean cond = (boolean) condition.eval(folder); + boolean cond = (boolean) condition.eval(folder, frame); if (!cond) { - return result.eval(folder); + return result.eval(folder, frame); } - folder.accuVal = loopStep.eval(folder); + folder.accuVal = loopStep.eval(folder, frame); index++; } - return result.eval(folder); + return result.eval(folder, frame); } private static Object maybeWrapAccumulator(Object val) { 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 index a48016537..30ecdbd83 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalTestOnly.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalTestOnly.java @@ -16,8 +16,6 @@ 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; @Immutable @@ -26,29 +24,8 @@ final class EvalTestOnly extends InterpretableAttribute { private final InterpretableAttribute attr; @Override - public Object eval(GlobalResolver resolver) throws CelEvaluationException { - return attr.eval(resolver); - } - - @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"); + public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + return attr.eval(resolver, frame); } @Override 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..a436d397a --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java @@ -0,0 +1,51 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import dev.cel.common.CelOptions; +import dev.cel.common.exceptions.CelIterationLimitExceededException; +import dev.cel.runtime.GlobalResolver; +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 int iterationCount; + + @Override + public @Nullable Object resolve(String name) { + return delegate.resolve(name); + } + + void incrementIterations() { + if (comprehensionIterationLimit < 0) { + return; + } + if (++iterationCount > 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; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java index 542067349..40a9f6203 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java @@ -28,10 +28,10 @@ final class MaybeAttribute implements Attribute { private final ImmutableList attributes; @Override - public Object resolve(GlobalResolver ctx) { + public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { MissingAttribute maybeError = null; for (NamespacedAttribute attr : attributes) { - Object value = attr.resolve(ctx); + Object value = attr.resolve(ctx, frame); if (value == null) { continue; } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java index bbb4e0422..596d1bae4 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java @@ -24,7 +24,7 @@ final class MissingAttribute implements Attribute { private final ImmutableSet missingAttributes; @Override - public Object resolve(GlobalResolver ctx) { + public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { throw CelAttributeNotFoundException.forFieldResolution(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 index b90ac0824..d513bc7ba 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java @@ -34,7 +34,7 @@ final class NamespacedAttribute implements Attribute { private final CelTypeProvider typeProvider; @Override - public Object resolve(GlobalResolver ctx) { + public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { for (String name : namespacedNames) { Object value = ctx.resolve(name); if (value != null) { 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/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index c751ba88c..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,6 +21,7 @@ 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; @@ -61,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 @@ -76,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) { @@ -451,9 +454,10 @@ public static ProgramPlanner newPlanner( CelValueProvider valueProvider, DefaultDispatcher dispatcher, CelValueConverter celValueConverter, - CelContainer container) { + CelContainer container, + CelOptions options) { return new ProgramPlanner( - typeProvider, valueProvider, dispatcher, celValueConverter, container); + typeProvider, valueProvider, dispatcher, celValueConverter, container, options); } private ProgramPlanner( @@ -461,11 +465,13 @@ private ProgramPlanner( CelValueProvider valueProvider, DefaultDispatcher dispatcher, CelValueConverter celValueConverter, - CelContainer container) { + CelContainer container, + CelOptions options) { this.typeProvider = typeProvider; this.valueProvider = valueProvider; this.dispatcher = dispatcher; this.container = container; + this.options = options; this.attributeFactory = AttributeFactory.newAttributeFactory(container, typeProvider, celValueConverter); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java index 7357d8147..a913849f6 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java @@ -31,8 +31,8 @@ final class RelativeAttribute implements Attribute { private final ImmutableList qualifiers; @Override - public Object resolve(GlobalResolver ctx) { - Object obj = EvalHelpers.evalStrictly(operand, ctx); + public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { + Object obj = EvalHelpers.evalStrictly(operand, ctx, frame); obj = celValueConverter.toRuntimeValue(obj); for (Qualifier qualifier : qualifiers) { 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/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index 642f3e5ca..968fdcc94 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -116,7 +116,12 @@ public final class ProgramPlannerTest { private static final ProgramPlanner PLANNER = ProgramPlanner.newPlanner( - TYPE_PROVIDER, VALUE_PROVIDER, newDispatcher(), CEL_VALUE_CONVERTER, CEL_CONTAINER); + TYPE_PROVIDER, + VALUE_PROVIDER, + newDispatcher(), + CEL_VALUE_CONVERTER, + CEL_CONTAINER, + CEL_OPTIONS); private static final CelCompiler CEL_COMPILER = CelCompilerFactory.standardCelCompilerBuilder() @@ -801,6 +806,50 @@ public void plan_comprehension_maps(String expression) throws Exception { 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) {