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