Skip to content

Commit a0de42c

Browse files
l46kokcopybara-github
authored andcommitted
Plan PresenceTests
PiperOrigin-RevId: 848018549
1 parent 40d81de commit a0de42c

6 files changed

Lines changed: 211 additions & 1 deletion

File tree

runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ java_library(
2323
":eval_create_map",
2424
":eval_create_struct",
2525
":eval_or",
26+
":eval_test_only",
2627
":eval_unary",
2728
":eval_var_args_call",
2829
":eval_zero_arity",
@@ -131,6 +132,16 @@ java_library(
131132
],
132133
)
133134

135+
java_library(
136+
name = "presence_test_qualifier",
137+
srcs = ["PresenceTestQualifier.java"],
138+
deps = [
139+
":attribute",
140+
":qualifier",
141+
"//common/values",
142+
],
143+
)
144+
134145
java_library(
135146
name = "string_qualifier",
136147
srcs = ["StringQualifier.java"],
@@ -156,6 +167,21 @@ java_library(
156167
],
157168
)
158169

170+
java_library(
171+
name = "eval_test_only",
172+
srcs = ["EvalTestOnly.java"],
173+
deps = [
174+
":interpretable_attribute",
175+
":presence_test_qualifier",
176+
":qualifier",
177+
"//runtime:evaluation_exception",
178+
"//runtime:evaluation_listener",
179+
"//runtime:function_resolver",
180+
"//runtime:interpretable",
181+
"@maven//:com_google_errorprone_error_prone_annotations",
182+
],
183+
)
184+
159185
java_library(
160186
name = "eval_zero_arity",
161187
srcs = ["EvalZeroArity.java"],
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.runtime.planner;
16+
17+
import com.google.errorprone.annotations.Immutable;
18+
import dev.cel.runtime.CelEvaluationException;
19+
import dev.cel.runtime.CelEvaluationListener;
20+
import dev.cel.runtime.CelFunctionResolver;
21+
import dev.cel.runtime.GlobalResolver;
22+
23+
@Immutable
24+
final class EvalTestOnly extends InterpretableAttribute {
25+
26+
private final InterpretableAttribute attr;
27+
28+
@Override
29+
public Object eval(GlobalResolver resolver) throws CelEvaluationException {
30+
return attr.eval(resolver);
31+
}
32+
33+
@Override
34+
public Object eval(GlobalResolver resolver, CelEvaluationListener listener) {
35+
// TODO: Implement support
36+
throw new UnsupportedOperationException("Not yet supported");
37+
}
38+
39+
@Override
40+
public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) {
41+
// TODO: Implement support
42+
throw new UnsupportedOperationException("Not yet supported");
43+
}
44+
45+
@Override
46+
public Object eval(
47+
GlobalResolver resolver,
48+
CelFunctionResolver lateBoundFunctionResolver,
49+
CelEvaluationListener listener) {
50+
// TODO: Implement support
51+
throw new UnsupportedOperationException("Not yet supported");
52+
}
53+
54+
@Override
55+
public EvalTestOnly addQualifier(long exprId, Qualifier qualifier) {
56+
PresenceTestQualifier presenceTestQualifier = PresenceTestQualifier.create(qualifier.value());
57+
return new EvalTestOnly(exprId(), attr.addQualifier(exprId, presenceTestQualifier));
58+
}
59+
60+
static EvalTestOnly create(long exprId, InterpretableAttribute attr) {
61+
return new EvalTestOnly(exprId, attr);
62+
}
63+
64+
private EvalTestOnly(long exprId, InterpretableAttribute attr) {
65+
super(exprId);
66+
this.attr = attr;
67+
}
68+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.runtime.planner;
16+
17+
import static dev.cel.runtime.planner.MissingAttribute.newMissingAttribute;
18+
19+
import dev.cel.common.values.SelectableValue;
20+
import java.util.Map;
21+
22+
/** A qualifier for presence testing a field or a map key. */
23+
final class PresenceTestQualifier implements Qualifier {
24+
25+
@SuppressWarnings("Immutable")
26+
private final Object value;
27+
28+
@Override
29+
public Object value() {
30+
return value;
31+
}
32+
33+
@Override
34+
@SuppressWarnings("unchecked") // SelectableValue cast is safe
35+
public Object qualify(Object obj) {
36+
if (obj instanceof SelectableValue) {
37+
return ((SelectableValue<Object>) obj).find(value).isPresent();
38+
} else if (obj instanceof Map) {
39+
Map<?, ?> map = (Map<?, ?>) obj;
40+
return map.containsKey(value);
41+
}
42+
43+
return newMissingAttribute(value.toString());
44+
}
45+
46+
static PresenceTestQualifier create(Object value) {
47+
return new PresenceTestQualifier(value);
48+
}
49+
50+
private PresenceTestQualifier(Object value) {
51+
this.value = value;
52+
}
53+
}

runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ private PlannedInterpretable planSelect(CelExpr celExpr, PlannerContext ctx) {
114114
}
115115

116116
if (select.testOnly()) {
117-
throw new UnsupportedOperationException("Presence tests not supported yet");
117+
attribute = EvalTestOnly.create(celExpr.id(), attribute);
118118
}
119119

120120
Qualifier qualifier = StringQualifier.create(select.field());

runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ java_library(
3838
"//compiler",
3939
"//compiler:compiler_builder",
4040
"//extensions",
41+
"//parser:macro",
4142
"//runtime",
4243
"//runtime:dispatcher",
4344
"//runtime:function_binding",
@@ -48,6 +49,7 @@ java_library(
4849
"//runtime/planner:program_planner",
4950
"//runtime/standard:add",
5051
"//runtime/standard:divide",
52+
"//runtime/standard:dyn",
5153
"//runtime/standard:equals",
5254
"//runtime/standard:greater",
5355
"//runtime/standard:greater_equals",

runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import dev.cel.expr.conformance.proto3.TestAllTypes;
6666
import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage;
6767
import dev.cel.extensions.CelExtensions;
68+
import dev.cel.parser.CelStandardMacro;
6869
import dev.cel.runtime.CelEvaluationException;
6970
import dev.cel.runtime.CelFunctionBinding;
7071
import dev.cel.runtime.CelFunctionOverload;
@@ -76,6 +77,7 @@
7677
import dev.cel.runtime.standard.AddOperator;
7778
import dev.cel.runtime.standard.CelStandardFunction;
7879
import dev.cel.runtime.standard.DivideOperator;
80+
import dev.cel.runtime.standard.DynFunction;
7981
import dev.cel.runtime.standard.EqualsOperator;
8082
import dev.cel.runtime.standard.GreaterEqualsOperator;
8183
import dev.cel.runtime.standard.GreaterOperator;
@@ -118,6 +120,7 @@ public final class ProgramPlannerTest {
118120

119121
private static final CelCompiler CEL_COMPILER =
120122
CelCompilerFactory.standardCelCompilerBuilder()
123+
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
121124
.addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName()))
122125
.addVar("map_var", MapType.create(SimpleType.STRING, SimpleType.DYN))
123126
.addVar("int_var", SimpleType.INT)
@@ -175,6 +178,10 @@ private static DefaultDispatcher newDispatcher() {
175178
builder,
176179
Operator.NOT_STRICTLY_FALSE.getFunction(),
177180
fromStandardFunction(NotStrictlyFalseFunction.create()));
181+
addBindings(
182+
builder,
183+
"dyn",
184+
fromStandardFunction(DynFunction.create()));
178185

179186
// Custom functions
180187
addBindings(
@@ -742,6 +749,32 @@ public void plan_select_stringQualificationFail_throws() throws Exception {
742749
+ " performed on messages or maps.");
743750
}
744751

752+
@Test
753+
public void plan_select_presenceTest(@TestParameter PresenceTestCase testCase) throws Exception {
754+
CelAbstractSyntaxTree ast = compile(testCase.expression);
755+
Program program = PLANNER.plan(ast);
756+
757+
boolean result =
758+
(boolean)
759+
program.eval(
760+
ImmutableMap.of("msg", testCase.inputParam, "map_var", testCase.inputParam));
761+
762+
assertThat(result).isEqualTo(testCase.expected);
763+
}
764+
765+
@Test
766+
public void plan_select_badPresenceTest_throws() throws Exception {
767+
CelAbstractSyntaxTree ast = compile("has(dyn([]).invalid)");
768+
Program program = PLANNER.plan(ast);
769+
770+
CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval);
771+
assertThat(e)
772+
.hasMessageThat()
773+
.contains(
774+
"Error resolving field 'invalid'. Field selections must be performed on messages or"
775+
+ " maps.");
776+
}
777+
745778
private CelAbstractSyntaxTree compile(String expression) throws Exception {
746779
CelAbstractSyntaxTree ast = CEL_COMPILER.parse(expression).getAst();
747780
if (isParseOnly) {
@@ -814,4 +847,32 @@ private enum TypeLiteralTestCase {
814847
this.type = TypeType.create(type);
815848
}
816849
}
850+
851+
852+
@SuppressWarnings("Immutable") // Test only
853+
private enum PresenceTestCase {
854+
PROTO_FIELD_PRESENT(
855+
"has(msg.single_string)", TestAllTypes.newBuilder().setSingleString("foo").build(), true),
856+
PROTO_FIELD_ABSENT("has(msg.single_string)", TestAllTypes.newBuilder().build(), false),
857+
PROTO_NESTED_FIELD_PRESENT(
858+
"has(msg.single_nested_message.bb)",
859+
TestAllTypes.newBuilder()
860+
.setSingleNestedMessage(NestedMessage.newBuilder().setBb(42).build())
861+
.build(),
862+
true),
863+
PROTO_NESTED_FIELD_ABSENT(
864+
"has(msg.single_nested_message.bb)", TestAllTypes.newBuilder().build(), false),
865+
PROTO_MAP_KEY_PRESENT("has(map_var.foo)", ImmutableMap.of("foo", "1"), true),
866+
PROTO_MAP_KEY_ABSENT("has(map_var.bar)", ImmutableMap.of(), false);
867+
868+
private final String expression;
869+
private final Object inputParam;
870+
private final Object expected;
871+
872+
PresenceTestCase(String expression, Object inputParam, Object expected) {
873+
this.expression = expression;
874+
this.inputParam = inputParam;
875+
this.expected = expected;
876+
}
877+
}
817878
}

0 commit comments

Comments
 (0)