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/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..f20c9aadd 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");
- }
-
- 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..09a082e87 100644
--- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel
+++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel
@@ -22,12 +22,17 @@ 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",
@@ -36,11 +41,11 @@ java_library(
"//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",
@@ -84,28 +89,77 @@ java_library(
],
)
+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",
+ ":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",
+ ":interpretable_attribute",
+ ":qualifier",
"//runtime:evaluation_listener",
"//runtime:function_resolver",
"//runtime:interpretable",
@@ -114,6 +168,21 @@ java_library(
],
)
+java_library(
+ name = "eval_test_only",
+ srcs = ["EvalTestOnly.java"],
+ deps = [
+ ":interpretable_attribute",
+ ":presence_test_qualifier",
+ ":qualifier",
+ "//runtime:evaluation_exception",
+ "//runtime:evaluation_listener",
+ "//runtime:function_resolver",
+ "//runtime:interpretable",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ ],
+)
+
java_library(
name = "eval_zero_arity",
srcs = ["EvalZeroArity.java"],
@@ -241,6 +310,22 @@ java_library(
],
)
+java_library(
+ name = "eval_fold",
+ srcs = ["EvalFold.java"],
+ deps = [
+ ":planned_interpretable",
+ "//runtime:concatenated_list_view",
+ "//runtime:evaluation_exception",
+ "//runtime:evaluation_listener",
+ "//runtime:function_resolver",
+ "//runtime:interpretable",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
+ "@maven//:org_jspecify_jspecify",
+ ],
+)
+
java_library(
name = "eval_helpers",
srcs = ["EvalHelpers.java"],
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..826f7e1fa 100644
--- a/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java
+++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java
@@ -20,13 +20,18 @@
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);
+ Object resolved = attr.resolve(resolver);
+ if (resolved instanceof MissingAttribute) {
+ ((MissingAttribute) resolved).resolve(resolver);
+ }
+
+ return resolved;
}
@Override
@@ -46,9 +51,16 @@ public Object eval(
GlobalResolver resolver,
CelFunctionResolver lateBoundFunctionResolver,
CelEvaluationListener listener) {
+ // TODO: Implement support
throw new UnsupportedOperationException("Not yet supported");
}
+ @Override
+ public EvalAttribute addQualifier(long exprId, Qualifier qualifier) {
+ Attribute newAttribute = attr.addQualifier(qualifier);
+ return create(exprId, newAttribute);
+ }
+
static EvalAttribute create(long exprId, Attribute attr) {
return new EvalAttribute(exprId, attr);
}
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..2a8ba1603
--- /dev/null
+++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java
@@ -0,0 +1,204 @@
+// 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.CelEvaluationListener;
+import dev.cel.runtime.CelFunctionResolver;
+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) throws CelEvaluationException {
+ Object iterRangeRaw = iterRange.eval(resolver);
+ Folder folder = new Folder(resolver, accuVar, iterVar, iterVar2);
+ folder.accuVal = maybeWrapAccumulator(accuInit.eval(folder));
+
+ Object result;
+ if (iterRangeRaw instanceof Map) {
+ result = evalMap((Map, ?>) iterRangeRaw, folder);
+ } else if (iterRangeRaw instanceof Collection) {
+ result = evalList((Collection>) iterRangeRaw, folder);
+ } else {
+ throw new IllegalArgumentException("Unexpected iter_range type: " + iterRangeRaw.getClass());
+ }
+
+ return maybeUnwrapAccumulator(result);
+ }
+
+ @Override
+ public Object eval(GlobalResolver resolver, CelEvaluationListener listener) {
+ // TODO: Implement support
+ throw new UnsupportedOperationException("Not yet supported");
+ }
+
+ @Override
+ public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) {
+ // TODO: Implement support
+ throw new UnsupportedOperationException("Not yet supported");
+ }
+
+ @Override
+ public Object eval(
+ GlobalResolver resolver,
+ CelFunctionResolver lateBoundFunctionResolver,
+ CelEvaluationListener listener) {
+ // TODO: Implement support
+ throw new UnsupportedOperationException("Not yet supported");
+ }
+
+ private Object evalMap(Map, ?> iterRange, Folder folder) throws CelEvaluationException {
+ for (Map.Entry, ?> entry : iterRange.entrySet()) {
+ folder.iterVarVal = entry.getKey();
+ if (!iterVar2.isEmpty()) {
+ folder.iterVar2Val = entry.getValue();
+ }
+
+ boolean cond = (boolean) condition.eval(folder);
+ if (!cond) {
+ return result.eval(folder);
+ }
+
+ // TODO: Introduce comprehension safety controls, such as iteration limit.
+ folder.accuVal = loopStep.eval(folder);
+ }
+ return result.eval(folder);
+ }
+
+ private Object evalList(Collection> iterRange, Folder folder) throws CelEvaluationException {
+ int index = 0;
+ for (Object item : iterRange) {
+ if (iterVar2.isEmpty()) {
+ folder.iterVarVal = item;
+ } else {
+ folder.iterVarVal = (long) index;
+ folder.iterVar2Val = item;
+ }
+
+ boolean cond = (boolean) condition.eval(folder);
+ if (!cond) {
+ return result.eval(folder);
+ }
+
+ folder.accuVal = loopStep.eval(folder);
+ index++;
+ }
+ return result.eval(folder);
+ }
+
+ 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/EvalTestOnly.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalTestOnly.java
new file mode 100644
index 000000000..a48016537
--- /dev/null
+++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalTestOnly.java
@@ -0,0 +1,68 @@
+// 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.CelEvaluationListener;
+import dev.cel.runtime.CelFunctionResolver;
+import dev.cel.runtime.GlobalResolver;
+
+@Immutable
+final class EvalTestOnly extends InterpretableAttribute {
+
+ private final InterpretableAttribute attr;
+
+ @Override
+ public Object eval(GlobalResolver resolver) throws CelEvaluationException {
+ return attr.eval(resolver);
+ }
+
+ @Override
+ public Object eval(GlobalResolver resolver, CelEvaluationListener listener) {
+ // TODO: Implement support
+ throw new UnsupportedOperationException("Not yet supported");
+ }
+
+ @Override
+ public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) {
+ // TODO: Implement support
+ throw new UnsupportedOperationException("Not yet supported");
+ }
+
+ @Override
+ public Object eval(
+ GlobalResolver resolver,
+ CelFunctionResolver lateBoundFunctionResolver,
+ CelEvaluationListener listener) {
+ // TODO: Implement support
+ throw new UnsupportedOperationException("Not yet supported");
+ }
+
+ @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/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..542067349
--- /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) {
+ MissingAttribute maybeError = null;
+ for (NamespacedAttribute attr : attributes) {
+ Object value = attr.resolve(ctx);
+ 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..bbb4e0422
--- /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) {
+ 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..b90ac0824
--- /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) {
+ 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/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