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/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/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 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 45936203a..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,7 +22,9 @@ 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", @@ -35,6 +37,7 @@ java_library( "//common:cel_ast", "//common:container", "//common:operator", + "//common:options", "//common/annotations", "//common/ast", "//common/types", @@ -57,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", @@ -76,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", @@ -109,6 +114,7 @@ java_library( ], deps = [ ":eval_helpers", + ":execution_frame", ":planned_interpretable", ":qualifier", "//common:container", @@ -131,6 +137,16 @@ java_library( ], ) +java_library( + name = "presence_test_qualifier", + srcs = ["PresenceTestQualifier.java"], + deps = [ + ":attribute", + ":qualifier", + "//common/values", + ], +) + java_library( name = "string_qualifier", srcs = ["StringQualifier.java"], @@ -146,24 +162,36 @@ 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", ], ) +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", ], @@ -174,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", ], @@ -188,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", ], @@ -202,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", ], @@ -216,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", ], @@ -229,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", ], @@ -242,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", @@ -259,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", @@ -273,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", @@ -319,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 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..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/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 be197649f..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; @@ -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) { @@ -94,10 +98,12 @@ 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()); } } @@ -114,7 +120,7 @@ private PlannedInterpretable planSelect(CelExpr celExpr, PlannerContext ctx) { } if (select.testOnly()) { - throw new UnsupportedOperationException("Presence tests not supported yet"); + attribute = EvalTestOnly.create(celExpr.id(), attribute); } Qualifier qualifier = StringQualifier.create(select.field()); @@ -280,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. @@ -427,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( @@ -437,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/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel index 9e5855f54..01df7c9ee 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel @@ -38,6 +38,7 @@ java_library( "//compiler", "//compiler:compiler_builder", "//extensions", + "//parser:macro", "//runtime", "//runtime:dispatcher", "//runtime:function_binding", @@ -48,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 60b629f7b..968fdcc94 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -65,6 +65,7 @@ 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; @@ -76,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; @@ -114,10 +116,16 @@ 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() + .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) @@ -141,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(); @@ -175,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( @@ -656,7 +665,7 @@ 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.newBuilder().build())); + Object result = program.eval(ImmutableMap.of("msg", TestAllTypes.getDefaultInstance())); assertThat(result).isEqualTo(0L); } @@ -742,6 +751,105 @@ public void plan_select_stringQualificationFail_throws() throws Exception { + " 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) { @@ -814,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; + } + } }