Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
321 changes: 244 additions & 77 deletions bundle/src/test/java/dev/cel/bundle/CelImplTest.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage;
import dev.cel.parser.CelMacro;
import dev.cel.parser.CelStandardMacro;
import dev.cel.runtime.CelAttribute.Qualifier;
import dev.cel.runtime.CelAttributePattern;
import dev.cel.runtime.CelEvaluationException;
import dev.cel.runtime.CelFunctionBinding;
Expand Down Expand Up @@ -985,6 +986,132 @@ public void optionalIndex_onList_returnsOptionalValue() throws Exception {
assertThat(result).isEqualTo(Optional.of("hello"));
}

@Test
public void optionalIndex_partialUnknownOnList_returnsUnknown() throws Exception {
if (testMode.equals(TestMode.LEGACY_CHECKED)) {
// Legacy runtime executes optional indexing through standard function bindings without
// attribute
// trail tracking, so it cannot intercept partial sub-attribute unknowns on known containers.
return;
}

Cel cel =
newCelBuilder()
.addVar("l", ListType.create(SimpleType.STRING))
.setResultType(OptionalType.create(SimpleType.STRING))
.build();
CelAbstractSyntaxTree ast = compile(cel, "l[?1]");
PartialVars partialVars =
PartialVars.of(
ImmutableMap.of("l", ImmutableList.of("hello", "world")),
CelAttributePattern.fromQualifiedIdentifier("l").qualify(Qualifier.ofInt(1)));

Object result = cel.createProgram(ast).eval(partialVars);

assertThat(result).isInstanceOf(CelUnknownSet.class);
}

@Test
public void optionalIndex_partialUnknownOnList_unrelatedIndexEvaluatesNormally()
throws Exception {
if (testMode.equals(TestMode.LEGACY_CHECKED)) {
// Legacy runtime executes optional indexing through standard function bindings without
// attribute
// trail tracking, so it cannot intercept partial sub-attribute unknowns on known containers.
return;
}

Cel cel =
newCelBuilder()
.addVar("l", ListType.create(SimpleType.STRING))
.setResultType(OptionalType.create(SimpleType.STRING))
.build();
CelAbstractSyntaxTree ast = compile(cel, "l[?0]");
PartialVars partialVars =
PartialVars.of(
ImmutableMap.of("l", ImmutableList.of("hello", "world")),
CelAttributePattern.fromQualifiedIdentifier("l").qualify(Qualifier.ofInt(1)));

Object result = cel.createProgram(ast).eval(partialVars);

assertThat((Optional<?>) result).hasValue("hello");
}

@Test
public void optionalIndex_partialUnknownOnMap_returnsUnknown() throws Exception {
if (testMode.equals(TestMode.LEGACY_CHECKED)) {
// Legacy runtime executes optional indexing through standard function bindings without
// attribute
// trail tracking, so it cannot intercept partial sub-attribute unknowns on known containers.
return;
}

Cel cel =
newCelBuilder()
.addVar("m", MapType.create(SimpleType.STRING, SimpleType.INT))
.setResultType(OptionalType.create(SimpleType.INT))
.build();
CelAbstractSyntaxTree ast = compile(cel, "m[?'b']");
PartialVars partialVars =
PartialVars.of(
ImmutableMap.of("m", ImmutableMap.of("a", 1, "b", 2)),
CelAttributePattern.fromQualifiedIdentifier("m").qualify(Qualifier.ofString("b")));

Object result = cel.createProgram(ast).eval(partialVars);

assertThat(result).isInstanceOf(CelUnknownSet.class);
}

@Test
public void optionalIndex_partialUnknownOnMap_unrelatedKeyEvaluatesNormally() throws Exception {
if (testMode.equals(TestMode.LEGACY_CHECKED)) {
// Legacy runtime executes optional indexing through standard function bindings without
// attribute
// trail tracking, so it cannot intercept partial sub-attribute unknowns on known containers.
return;
}

Cel cel =
newCelBuilder()
.addVar("m", MapType.create(SimpleType.STRING, SimpleType.INT))
.setResultType(OptionalType.create(SimpleType.INT))
.build();
CelAbstractSyntaxTree ast = compile(cel, "m[?'a']");
PartialVars partialVars =
PartialVars.of(
ImmutableMap.of("m", ImmutableMap.of("a", 1, "b", 2)),
CelAttributePattern.fromQualifiedIdentifier("m").qualify(Qualifier.ofString("b")));

Object result = cel.createProgram(ast).eval(partialVars);

assertThat((Optional<?>) result).hasValue(1);
}

@Test
public void optionalIndex_partialUnknownOnMap_missingKeyEvaluatesToEmpty() throws Exception {
if (testMode.equals(TestMode.LEGACY_CHECKED)) {
// Legacy runtime executes optional indexing through standard function bindings without
// attribute
// trail tracking, so it cannot intercept partial sub-attribute unknowns on known containers.
return;
}

Cel cel =
newCelBuilder()
.addVar("m", MapType.create(SimpleType.STRING, SimpleType.INT))
.setResultType(OptionalType.create(SimpleType.INT))
.build();
CelAbstractSyntaxTree ast = compile(cel, "m[?'c']");
PartialVars partialVars =
PartialVars.of(
ImmutableMap.of("m", ImmutableMap.of("a", 1, "b", 2)),
CelAttributePattern.fromQualifiedIdentifier("m").qualify(Qualifier.ofString("b")));

Object result = cel.createProgram(ast).eval(partialVars);

assertThat((Optional<?>) result).isEmpty();
}

@Test
public void optionalIndex_onOptionalList_returnsOptionalEmpty() throws Exception {
Cel cel =
Expand Down Expand Up @@ -1049,7 +1176,8 @@ public void traditionalIndex_onOptionalList_returnsOptionalEmpty() throws Except
@Test
public void optionalFieldSelect_fieldMarkedUnknown_returnsUnknownSet() throws Exception {
if (testMode.equals(TestMode.LEGACY_CHECKED)) {
// This case is not possible to setup for legacy runtime
// Legacy runtime does not support attribute trail tracking for optional field selection
// (.?field).
return;
}

Expand Down
6 changes: 6 additions & 0 deletions runtime/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -379,3 +379,9 @@ cel_android_library(
name = "partial_vars_android",
exports = ["//runtime/src/main/java/dev/cel/runtime:partial_vars_android"],
)

cel_android_library(
name = "function_resolver_android",
visibility = ["//:internal"],
exports = ["//runtime/src/main/java/dev/cel/runtime:function_resolver_android"],
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.jspecify.annotations.Nullable;
Expand All @@ -36,12 +37,12 @@ public final class AccumulatedUnknowns {
private final Set<Long> exprIds;
private final Set<CelAttribute> attributes;

Set<Long> exprIds() {
return exprIds;
public Set<Long> exprIds() {
return Collections.unmodifiableSet(exprIds);
}

Set<CelAttribute> attributes() {
return attributes;
public Set<CelAttribute> attributes() {
return Collections.unmodifiableSet(attributes);
}

/**
Expand Down
2 changes: 2 additions & 0 deletions runtime/src/main/java/dev/cel/runtime/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,8 @@ java_library(
cel_android_library(
name = "function_resolver_android",
srcs = ["CelFunctionResolver.java"],
tags = [
],
deps = [
":evaluation_exception",
":resolved_overload_android",
Expand Down
16 changes: 15 additions & 1 deletion runtime/src/main/java/dev/cel/runtime/CelAttribute.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.google.common.primitives.UnsignedLong;
import com.google.errorprone.annotations.Immutable;
import com.google.re2j.Pattern;
import org.jspecify.annotations.Nullable;

/**
* CelAttribute represents the select path from the root (.) to a single leaf value that may be
Expand Down Expand Up @@ -100,6 +101,19 @@ public static Qualifier ofWildCard() {
* index.
*/
public static Qualifier fromGeneric(Object value) {
Qualifier qualifier = fromGenericOrNull(value);
if (qualifier != null) {
return qualifier;
}
throw new IllegalArgumentException("Unsupported attribute qualifier kind");
}

/**
* Creates a Qualifier from a generic object, or null if the value cannot be interpreted as an
* attribute qualifier.
*/
@SuppressWarnings("IfChainToSwitch")
public static @Nullable Qualifier fromGenericOrNull(Object value) {
if (value instanceof UnsignedLong) {
return ofUint((UnsignedLong) value);
} else if (value instanceof Long) {
Expand All @@ -111,7 +125,7 @@ public static Qualifier fromGeneric(Object value) {
} else if (value instanceof String) {
return ofString((String) value);
}
throw new IllegalArgumentException("Unsupported attribute qualifier kind");
return null;
}

public String toIndexFormat() {
Expand Down
20 changes: 19 additions & 1 deletion runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,25 @@ public Object trace(PartialVars partialVars, CelEvaluationListener listener)

@Override
public Object advanceEvaluation(UnknownContext context) throws CelEvaluationException {
throw new UnsupportedOperationException("Unsupported operation.");
PlannedProgram plannedProgram = (PlannedProgram) program;
if (!plannedProgram.options().enableUnknownTracking()) {
return plannedProgram.evalOrThrow(
plannedProgram.interpretable(),
context.variableResolver(),
EMPTY_FUNCTION_RESOLVER,
/* partialVars= */ null,
/* attributeResolver= */ null,
/* listener= */ null);
}
return plannedProgram.evalOrThrow(
plannedProgram.interpretable(),
context.variableResolver(),
EMPTY_FUNCTION_RESOLVER,
PartialVars.of(
(name) -> Optional.ofNullable(context.variableResolver().resolve(name)),
context.unresolvedAttributes()),
context.createAttributeResolver(),
/* listener= */ null);
}
};
}
Expand Down
57 changes: 50 additions & 7 deletions runtime/src/main/java/dev/cel/runtime/UnknownContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ public GlobalResolver variableResolver() {
return variableResolver;
}

/** Accessor for unresolved attribute patterns. */
ImmutableList<CelAttributePattern> unresolvedAttributes() {
return unresolvedAttributes;
}

/** Accessor for resolved attribute values. */
ImmutableMap<CelAttribute, Object> resolvedAttributes() {
return resolvedAttributes;
}

/**
* Creates a new unknown context that is a copy of the current context with the provided
* additional attribute values.
Expand All @@ -123,12 +133,28 @@ public UnknownContext withResolvedAttributes(Map<CelAttribute, Object> resolvedA
ImmutableMap.<CelAttribute, Object>builder()
.putAll(this.resolvedAttributes)
.putAll(resolvedAttributes)
.buildOrThrow());
.buildKeepingLast());
}

private boolean patternMaskedByResolvedAttribute(
private static boolean patternMaskedByResolvedAttribute(
Map<CelAttribute, Object> resolved, CelAttributePattern pattern) {
return resolved.keySet().stream().anyMatch(pattern::isPartialMatch);
return resolved.keySet().stream().anyMatch(attr -> isPatternMaskedByAttribute(pattern, attr));
}

private static boolean isPatternMaskedByAttribute(
CelAttributePattern pattern, CelAttribute attribute) {
if (attribute.qualifiers().size() > pattern.qualifiers().size()) {
return false;
}
for (int i = 0; i < attribute.qualifiers().size(); i++) {
CelAttribute.Qualifier patternQualifier = pattern.qualifiers().get(i);
CelAttribute.Qualifier attrQualifier = attribute.qualifiers().get(i);
if (patternQualifier.kind() == CelAttribute.Qualifier.Kind.WILD_CARD
|| !patternQualifier.equals(attrQualifier)) {
return false;
}
}
return true;
}

/**
Expand Down Expand Up @@ -168,10 +194,27 @@ public Optional<Object> resolve(CelAttribute attribute) {

@Override
public Optional<CelUnknownSet> maybePartialUnknown(CelAttribute attribute) {
return unresolvedAttributes.stream()
.filter(p -> p.isPartialMatch(attribute))
.findFirst()
.map(p -> CelUnknownSet.create(p.simplify(attribute)));
if (attribute.equals(CelAttribute.EMPTY)) {
return Optional.empty();
}
Optional<CelUnknownSet> fromUnresolved =
unresolvedAttributes.stream()
.filter(p -> p.isPartialMatch(attribute))
.findFirst()
.map(p -> CelUnknownSet.create(p.simplify(attribute)));
if (fromUnresolved.isPresent()) {
return fromUnresolved;
}
for (CelAttribute resolved : resolvedAttributes.keySet()) {
if (resolved.qualifiers().size() > attribute.qualifiers().size()
&& resolved
.qualifiers()
.subList(0, attribute.qualifiers().size())
.equals(attribute.qualifiers())) {
return Optional.of(CelUnknownSet.create(attribute));
}
}
return Optional.empty();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
/** Represents a resolvable symbol or path (such as a variable or a field selection). */
@Immutable
interface Attribute {
Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame);
AttributeResolution resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame);

Attribute addQualifier(Qualifier qualifier);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2026 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.CelAttribute;
import org.jspecify.annotations.Nullable;

/** Bundles a resolved value and its corresponding {@link CelAttribute} trail. */
@Immutable
final class AttributeResolution {

@SuppressWarnings("Immutable")
private final @Nullable Object value;

private final @Nullable CelAttribute attribute;

static AttributeResolution of(@Nullable Object value, @Nullable CelAttribute attribute) {
return new AttributeResolution(value, attribute);
}

static AttributeResolution ofValue(@Nullable Object value) {
return new AttributeResolution(value, null);
}

@Nullable Object value() {
return value;
}

@Nullable CelAttribute attribute() {
return attribute;
}

private AttributeResolution(@Nullable Object value, @Nullable CelAttribute attribute) {
this.value = value;
this.attribute = attribute;
}
}
Loading
Loading