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
2 changes: 2 additions & 0 deletions runtime/src/main/java/dev/cel/runtime/CelAttribute.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ public static Qualifier fromGeneric(Object value) {
return ofUint((UnsignedLong) value);
} else if (value instanceof Long) {
return ofInt((Long) value);
} else if (value instanceof Integer) {
return ofInt(((Integer) value).longValue());
} else if (value instanceof Boolean) {
return ofBool((boolean) value);
} else if (value instanceof String) {
Expand Down
36 changes: 35 additions & 1 deletion runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ java_library(
":eval_exhaustive_conditional",
":eval_exhaustive_or",
":eval_fold",
":eval_index",
":eval_late_bound_call",
":eval_optional_or",
":eval_optional_or_value",
Expand Down Expand Up @@ -230,6 +231,22 @@ java_library(
],
)

java_library(
name = "eval_index",
srcs = ["EvalIndex.java"],
deps = [
":eval_helpers",
":planned_interpretable",
"//common/ast",
"//common/values",
"//runtime:accumulated_unknowns",
"//runtime:evaluation_exception",
"//runtime:interpretable",
"//runtime:resolved_overload",
"@maven//:com_google_errorprone_error_prone_annotations",
],
)

java_library(
name = "eval_block",
srcs = ["EvalBlock.java"],
Expand Down Expand Up @@ -544,6 +561,7 @@ cel_android_library(
":eval_exhaustive_conditional_android",
":eval_exhaustive_or_android",
":eval_fold_android",
":eval_index_android",
":eval_late_bound_call_android",
":eval_optional_or_android",
":eval_optional_or_value_android",
Expand Down Expand Up @@ -735,10 +753,26 @@ cel_android_library(
":planned_interpretable_android",
"//common/ast:ast_android",
"//common/values:values_android",
"//runtime:accumulated_unknowns_android",
"//runtime:evaluation_exception",
"//runtime:interpretable_android",
"//runtime:resolved_overload_android",
],
)

cel_android_library(
name = "eval_index_android",
srcs = ["EvalIndex.java"],
deps = [
":eval_helpers_android",
":planned_interpretable_android",
"//common/ast:ast_android",
"//common/values:values_android",
"//runtime:accumulated_unknowns_android",
"//runtime:evaluation_exception",
"//runtime:interpretable_android",
"//runtime:resolved_overload_android",
"//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android",
"@maven//:com_google_errorprone_error_prone_annotations",
],
)

Expand Down
81 changes: 81 additions & 0 deletions runtime/src/main/java/dev/cel/runtime/planner/EvalIndex.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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 static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly;
import static dev.cel.runtime.planner.EvalHelpers.evalStrictly;

import com.google.errorprone.annotations.Immutable;
import dev.cel.common.ast.CelExpr;
import dev.cel.common.values.CelValueConverter;
import dev.cel.runtime.AccumulatedUnknowns;
import dev.cel.runtime.CelEvaluationException;
import dev.cel.runtime.CelResolvedOverload;
import dev.cel.runtime.GlobalResolver;

@Immutable
final class EvalIndex extends PlannedInterpretable {

private final String functionName;
private final CelResolvedOverload resolvedOverload;
private final PlannedInterpretable target;
private final PlannedInterpretable index;
private final CelValueConverter celValueConverter;

@Override
Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException {
boolean isStrict = resolvedOverload.isStrict();
Object targetVal =
isStrict ? evalStrictly(target, resolver, frame) : evalNonstrictly(target, resolver, frame);
Object indexVal =
isStrict ? evalStrictly(index, resolver, frame) : evalNonstrictly(index, resolver, frame);

if (isStrict) {
AccumulatedUnknowns unknowns = AccumulatedUnknowns.maybeMerge(null, targetVal);
unknowns = AccumulatedUnknowns.maybeMerge(unknowns, indexVal);
if (unknowns != null) {
return unknowns;
}
}

return EvalHelpers.dispatch(
functionName, resolvedOverload, celValueConverter, targetVal, indexVal);
}

static EvalIndex create(
CelExpr expr,
String functionName,
CelResolvedOverload resolvedOverload,
PlannedInterpretable target,
PlannedInterpretable index,
CelValueConverter celValueConverter) {
return new EvalIndex(expr, functionName, resolvedOverload, target, index, celValueConverter);
}

private EvalIndex(
CelExpr expr,
String functionName,
CelResolvedOverload resolvedOverload,
PlannedInterpretable target,
PlannedInterpretable index,
CelValueConverter celValueConverter) {
super(expr);
this.functionName = functionName;
this.resolvedOverload = resolvedOverload;
this.target = target;
this.index = index;
this.celValueConverter = celValueConverter;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,15 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) {
return EvalUnary.create(
expr, functionName, resolvedOverload, evaluatedArgs[0], celValueConverter);
case 2:
if (functionName.equals(Operator.INDEX.getFunction())) {
return EvalIndex.create(
expr,
functionName,
resolvedOverload,
evaluatedArgs[0],
evaluatedArgs[1],
celValueConverter);
}
return EvalBinary.create(
expr,
functionName,
Expand Down Expand Up @@ -385,7 +394,7 @@ private Optional<PlannedInterpretable> maybeInterceptOptionalCalls(
break;
}

if (Operator.OPTIONAL_SELECT.getFunction().equals(functionName)) {
if (functionName.equals(Operator.OPTIONAL_SELECT.getFunction())) {
String field = expr.call().args().get(1).constant().stringValue();
InterpretableAttribute attribute;
if (evaluatedArgs[0] instanceof EvalAttribute) {
Expand Down
17 changes: 17 additions & 0 deletions runtime/src/test/java/dev/cel/runtime/CelAttributeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,16 +143,33 @@ public void fromQualifiedIdentifier_parseIdents() {

@Test
public void fromGeneric_supportedTypes() {
assertThat(Qualifier.fromGeneric(1)).isEqualTo(Qualifier.ofInt(1));
assertThat(Qualifier.fromGeneric(Long.valueOf(1))).isEqualTo(Qualifier.ofInt(1));
assertThat(Qualifier.fromGeneric(UnsignedLong.valueOf(1))).isEqualTo(Qualifier.ofUint(1));
assertThat(Qualifier.fromGeneric("abcd")).isEqualTo(Qualifier.ofString("abcd"));
assertThat(Qualifier.fromGeneric(Boolean.valueOf(false))).isEqualTo(Qualifier.ofBool(false));
}

@Test
public void fromGeneric_integerBoundaryValues() {
assertThat(Qualifier.fromGeneric(Integer.MAX_VALUE))
.isEqualTo(Qualifier.ofInt(Integer.MAX_VALUE));
assertThat(Qualifier.fromGeneric(Integer.MIN_VALUE))
.isEqualTo(Qualifier.ofInt(Integer.MIN_VALUE));
assertThat(Qualifier.fromGeneric(0)).isEqualTo(Qualifier.ofInt(0));
}

@Test
public void fromGeneric_nullThrows() {
assertThrows(IllegalArgumentException.class, () -> Qualifier.fromGeneric(null));
}

@Test
public void fromGeneric_unsupportedTypeThrows() {
assertThrows(
IllegalArgumentException.class, () -> Qualifier.fromGeneric(new ArrayList<String>()));
assertThrows(IllegalArgumentException.class, () -> Qualifier.fromGeneric(1.0));
assertThrows(IllegalArgumentException.class, () -> Qualifier.fromGeneric(new byte[] {1, 2}));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,99 @@ public void plan_call_mapIndex() throws Exception {
assertThat(result).isEqualTo(2L);
}

@Test
public void plan_call_listIndex() throws Exception {
CelAbstractSyntaxTree ast = compile("[10, 20, 30][1]");
Program program = PLANNER.plan(ast);

Long result = (Long) program.eval();

assertThat(result).isEqualTo(20L);
}

@Test
public void plan_call_listIndex_outOfBounds_throws() throws Exception {
CelAbstractSyntaxTree ast = compile("[10, 20, 30][5]");
Program program = PLANNER.plan(ast);

assertThrows(CelEvaluationException.class, program::eval);
}

@Test
public void plan_call_listIndex_negative_throws() throws Exception {
CelAbstractSyntaxTree ast = compile("[10, 20, 30][-1]");
Program program = PLANNER.plan(ast);

assertThrows(CelEvaluationException.class, program::eval);
}

@Test
public void plan_call_mapIndex_missingKey_throws() throws Exception {
CelAbstractSyntaxTree ast = compile("map_var['missing']");
Program program = PLANNER.plan(ast);

assertThrows(
CelEvaluationException.class,
() -> program.eval(ImmutableMap.of("map_var", ImmutableMap.of("key", 1L))));
}

@Test
public void plan_call_index_withUnknownTarget() throws Exception {
CelCompiler compiler =
CelCompilerFactory.standardCelCompilerBuilder()
.addVar("unk_list", ListType.create(SimpleType.INT))
.build();
CelAbstractSyntaxTree ast = compile(compiler, "unk_list[0]");
Program program = PLANNER.plan(ast);

CelUnknownSet result =
(CelUnknownSet) program.eval(PartialVars.of(CelAttributePattern.create("unk_list")));

assertThat(result)
.isEqualTo(
CelUnknownSet.create(
ImmutableSet.of(CelAttribute.create("unk_list")), ImmutableSet.of(1L)));
}

@Test
public void plan_call_index_withUnknownIndex() throws Exception {
CelCompiler compiler =
CelCompilerFactory.standardCelCompilerBuilder().addVar("unk_index", SimpleType.INT).build();
CelAbstractSyntaxTree ast = compile(compiler, "[10, 20, 30][unk_index]");
Program program = PLANNER.plan(ast);

CelUnknownSet result =
(CelUnknownSet) program.eval(PartialVars.of(CelAttributePattern.create("unk_index")));

assertThat(result)
.isEqualTo(
CelUnknownSet.create(
ImmutableSet.of(CelAttribute.create("unk_index")), ImmutableSet.of(6L)));
}

@Test
public void plan_call_index_withMultipleUnknowns_mergesUnknowns() throws Exception {
CelCompiler compiler =
CelCompilerFactory.standardCelCompilerBuilder()
.addVar("unk_map", MapType.create(SimpleType.STRING, SimpleType.INT))
.addVar("unk_key", SimpleType.STRING)
.build();
CelAbstractSyntaxTree ast = compile(compiler, "unk_map[unk_key]");
Program program = PLANNER.plan(ast);

CelUnknownSet result =
(CelUnknownSet)
program.eval(
PartialVars.of(
CelAttributePattern.create("unk_map"), CelAttributePattern.create("unk_key")));

assertThat(result)
.isEqualTo(
CelUnknownSet.create(
ImmutableSet.of(CelAttribute.create("unk_map"), CelAttribute.create("unk_key")),
ImmutableSet.of(1L, 3L)));
}

@Test
public void plan_call_noMatchingOverload_throws() throws Exception {
CelAbstractSyntaxTree ast = compile("concat(b'abc', dyn_var)");
Expand Down
Loading