From ed99a457494ea6ed7abb5229ff56e605c937ba39 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 5 Dec 2025 18:20:52 -0800 Subject: [PATCH] Support attribute qualification, plan select, allow parsed-only ident resolution on enums PiperOrigin-RevId: 840949005 --- .../cel/common/values/CelValueConverter.java | 2 +- .../CelEvaluationExceptionBuilder.java | 12 +- .../dev/cel/runtime/planner/Attribute.java | 55 +----- .../cel/runtime/planner/AttributeFactory.java | 33 ++-- .../java/dev/cel/runtime/planner/BUILD.bazel | 46 ++++- .../cel/runtime/planner/EvalAttribute.java | 16 +- .../planner/InterpretableAttribute.java | 27 +++ .../cel/runtime/planner/MaybeAttribute.java | 81 ++++++++ .../cel/runtime/planner/MissingAttribute.java | 47 +++++ .../runtime/planner/NamespacedAttribute.java | 126 +++++++++++++ .../cel/runtime/planner/ProgramPlanner.java | 35 +++- .../dev/cel/runtime/planner/Qualifier.java | 28 +++ .../runtime/planner/RelativeAttribute.java | 70 +++++++ .../cel/runtime/planner/StringQualifier.java | 61 ++++++ .../java/dev/cel/runtime/planner/BUILD.bazel | 1 + .../runtime/planner/ProgramPlannerTest.java | 175 ++++++++++++++++-- 16 files changed, 724 insertions(+), 91 deletions(-) create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/InterpretableAttribute.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/Qualifier.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java diff --git a/common/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/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/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..45936203a 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -26,8 +26,11 @@ java_library( ":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 +39,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 +87,67 @@ 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 = "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", 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/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/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index 252695ed7..be197649f 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -37,12 +37,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; @@ -84,6 +84,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: @@ -99,6 +101,27 @@ private PlannedInterpretable plan(CelExpr celExpr, PlannerContext ctx) { } } + 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()) { + throw new UnsupportedOperationException("Presence tests not supported yet"); + } + + 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 +240,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); @@ -403,19 +426,23 @@ public static ProgramPlanner newPlanner( CelTypeProvider typeProvider, CelValueProvider valueProvider, DefaultDispatcher dispatcher, + CelValueConverter celValueConverter, CelContainer container) { - return new ProgramPlanner(typeProvider, valueProvider, dispatcher, container); + return new ProgramPlanner( + typeProvider, valueProvider, dispatcher, celValueConverter, container); } private ProgramPlanner( CelTypeProvider typeProvider, CelValueProvider valueProvider, DefaultDispatcher dispatcher, + CelValueConverter celValueConverter, CelContainer container) { this.typeProvider = typeProvider; this.valueProvider = valueProvider; this.dispatcher = dispatcher; this.container = container; - this.attributeFactory = AttributeFactory.newAttributeFactory(container, typeProvider); + 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..7357d8147 --- /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) { + Object obj = EvalHelpers.evalStrictly(operand, ctx); + 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/planner/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel index 9e3a79ed9..9e5855f54 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel @@ -33,6 +33,7 @@ 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", 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..60b629f7b 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -51,15 +51,19 @@ 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.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; @@ -99,17 +103,26 @@ 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); + private static final CelCompiler CEL_COMPILER = CelCompilerFactory.standardCelCompilerBuilder() + .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)), @@ -296,10 +309,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 +330,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 +338,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 +608,140 @@ 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.newBuilder().build())); + + 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."); + } + private CelAbstractSyntaxTree compile(String expression) throws Exception { CelAbstractSyntaxTree ast = CEL_COMPILER.parse(expression).getAst(); if (isParseOnly) {