Skip to content

Commit 5c20601

Browse files
l46kokcopybara-github
authored andcommitted
Implement JSON value unwrapping capability in verifier
PiperOrigin-RevId: 954872975
1 parent f502672 commit 5c20601

4 files changed

Lines changed: 178 additions & 26 deletions

File tree

verifier/src/main/java/dev/cel/verifier/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ java_library(
129129
"//common/ast",
130130
"//common/ast:cel_block",
131131
"//common/types",
132+
"//common/types:cel_types",
132133
"//common/types:type_providers",
133134
"//verifier/axioms",
134135
"@maven//:com_google_errorprone_error_prone_annotations",

verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import dev.cel.common.types.CelKind;
3838
import dev.cel.common.types.CelType;
3939
import dev.cel.common.types.CelTypeProvider;
40+
import dev.cel.common.types.CelTypes;
4041
import dev.cel.common.types.ListType;
4142
import dev.cel.common.types.MapType;
4243
import dev.cel.common.types.NullableType;
@@ -79,6 +80,7 @@ final class CelAstToZ3Translator {
7980
private static final String EMPTY_MSG_REF_PREFIX = "!empty_msg_ref_";
8081
private static final String EMPTY_LIST_PREFIX = "!empty_list";
8182
private static final String EMPTY_MAP_PREFIX = "!empty_map";
83+
private static final String NULL_VALUE_FIELD = "null_value";
8284
private final Context ctx;
8385
private final CelZ3TypeSystem typeSystem;
8486
private final CelZ3OperatorTranslator operatorTranslator;
@@ -370,6 +372,10 @@ private TranslatedValue translateMap(CelExpr celExpr, CelAbstractSyntaxTree ast)
370372

371373
private TranslatedValue translateStruct(CelExpr celExpr, CelAbstractSyntaxTree ast) {
372374
CelExpr.CelStruct createStruct = celExpr.struct();
375+
if (isJsonWkt(createStruct.messageName())) {
376+
return translateJsonWktStruct(celExpr, createStruct, ast);
377+
}
378+
373379
// Bypass SMT when the struct is empty (return the cached SMT default pointer)
374380
if (createStruct.entries().isEmpty()) {
375381
return TranslatedValue.create(
@@ -448,6 +454,56 @@ private TranslatedValue translateStruct(CelExpr celExpr, CelAbstractSyntaxTree a
448454
return TranslatedValue.propagateStrict(ctx, typeSystem, result, celExpr, elementsTv);
449455
}
450456

457+
private static boolean isJsonWkt(String messageName) {
458+
return messageName.equals(CelTypes.VALUE_MESSAGE)
459+
|| messageName.equals(CelTypes.LIST_VALUE_MESSAGE)
460+
|| messageName.equals(CelTypes.STRUCT_MESSAGE);
461+
}
462+
463+
// Concretize JSON WKT unwrapping directly into native Z3 primitives to avoid
464+
// sort incompatibilities (Message == String) and solver performance penalties (quantifiers).
465+
private TranslatedValue translateJsonWktStruct(
466+
CelExpr celExpr, CelExpr.CelStruct createStruct, CelAbstractSyntaxTree ast) {
467+
Expr<?> fallback;
468+
if (createStruct.messageName().equals(CelTypes.VALUE_MESSAGE)) {
469+
fallback = typeSystem.mkNull();
470+
} else if (createStruct.messageName().equals(CelTypes.LIST_VALUE_MESSAGE)) {
471+
fallback = getDefaultValueForType(ListType.create(SimpleType.DYN));
472+
} else {
473+
fallback = getDefaultValueForType(MapType.create(SimpleType.STRING, SimpleType.DYN));
474+
}
475+
476+
if (createStruct.entries().isEmpty()) {
477+
return TranslatedValue.create(fallback, celExpr, typeSystem, ctx.mkFalse());
478+
}
479+
480+
CelExpr.CelStruct.Entry entry = createStruct.entries().get(0);
481+
482+
// Translate the value to properly capture approximations and Optionals
483+
TranslatedValue entryTv = translateExpr(entry.value(), ast);
484+
Expr<?> finalVal = entryTv.z3Expr();
485+
486+
boolean isNullValueField =
487+
createStruct.messageName().equals(CelTypes.VALUE_MESSAGE)
488+
&& entry.fieldKey().equals(NULL_VALUE_FIELD);
489+
490+
if (entry.optionalEntry()) {
491+
Expr<?> optRef = typeSystem.getOptionalRef(finalVal);
492+
BoolExpr hasValue = typeSystem.optHasValue(optRef);
493+
494+
Expr<?> unpackedVal = typeSystem.getOptionalValue(optRef);
495+
if (isNullValueField) {
496+
unpackedVal = typeSystem.mkNull();
497+
}
498+
finalVal = ctx.mkITE(hasValue, unpackedVal, fallback);
499+
} else if (isNullValueField) {
500+
finalVal = typeSystem.mkNull();
501+
}
502+
503+
return TranslatedValue.propagateStrict(
504+
ctx, typeSystem, finalVal, celExpr, ImmutableList.of(entryTv));
505+
}
506+
451507
private Expr<?> getDefaultValueForType(CelType type) {
452508
if (type instanceof NullableType) {
453509
return typeSystem.mkNull();

verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -805,38 +805,83 @@ private Expr<?> buildMapIndex(
805805

806806
private TranslatedValue translateIndex(
807807
List<TranslatedValue> args, CelAbstractSyntaxTree ast, boolean isOptional) {
808-
Expr<?> lhsTrans = args.get(0).z3Expr();
809-
Expr<?> rhsTrans = args.get(1).z3Expr();
810-
811808
TranslatedValue lhs = args.get(0);
812809
TranslatedValue rhs = args.get(1);
813810
CelType lhsType = extractAstTypeOrDefault(lhs, ast);
814811
CelType rhsType = extractAstTypeOrDefault(rhs, ast);
815812

816-
Expr<?> actualValue;
813+
Expr<?> lhsTrans = lhs.z3Expr();
814+
Expr<?> rhsTrans = rhs.z3Expr();
815+
816+
BoolExpr isLhsOpt = ctx.mkFalse();
817+
BoolExpr lhsHasValue = ctx.mkFalse();
818+
BoolExpr shouldEvaluate = ctx.mkTrue();
819+
820+
if (isOptional) {
821+
isLhsOpt = typeSystem.isOptional(lhsTrans);
822+
Expr<?> optRef = typeSystem.getOptionalRef(lhsTrans);
823+
lhsHasValue = typeSystem.optHasValue(optRef);
824+
825+
lhsTrans = ctx.mkITE(isLhsOpt, typeSystem.getOptionalValue(optRef), lhsTrans);
826+
shouldEvaluate = (BoolExpr) ctx.mkITE(isLhsOpt, lhsHasValue, ctx.mkTrue());
827+
828+
if (lhsType instanceof OptionalType) {
829+
lhsType = ((OptionalType) lhsType).parameters().get(0);
830+
}
831+
}
832+
833+
Expr<?> actualValue =
834+
buildAndConstrainIndex(lhsTrans, rhsTrans, lhsType, rhsType, shouldEvaluate, isOptional);
835+
836+
if (isOptional) {
837+
actualValue =
838+
ctx.mkITE(
839+
ctx.mkAnd(isLhsOpt, ctx.mkNot(lhsHasValue)),
840+
typeSystem.mkOptionalNone(),
841+
actualValue);
842+
}
843+
844+
return TranslatedValue.propagateStrict(ctx, typeSystem, actualValue, args);
845+
}
846+
847+
private Expr<?> buildAndConstrainIndex(
848+
Expr<?> lhsTrans,
849+
Expr<?> rhsTrans,
850+
CelType lhsType,
851+
CelType rhsType,
852+
BoolExpr shouldEvaluate,
853+
boolean isOptional) {
854+
CelType expectedElemType = null;
817855
if (lhsType.kind() == CelKind.LIST && rhsType.kind() == CelKind.INT) {
818-
actualValue = buildListIndex(lhsTrans, rhsTrans, ctx.mkTrue(), isOptional);
819-
constraintSink.accept(
820-
ctx.mkImplies(
821-
ctx.mkNot(typeSystem.isError(actualValue)),
822-
typeConstraintGenerator.apply(actualValue, ((ListType) lhsType).elemType())));
856+
expectedElemType = ((ListType) lhsType).elemType();
823857
} else if (lhsType.kind() == CelKind.MAP) {
824-
actualValue = buildMapIndex(lhsTrans, rhsTrans, ctx.mkTrue(), isOptional);
858+
expectedElemType = ((MapType) lhsType).valueType();
859+
}
860+
861+
if (expectedElemType != null) {
862+
Expr<?> actualValue =
863+
lhsType.kind() == CelKind.LIST
864+
? buildListIndex(lhsTrans, rhsTrans, shouldEvaluate, isOptional)
865+
: buildMapIndex(lhsTrans, rhsTrans, shouldEvaluate, isOptional);
866+
867+
CelType finalType = isOptional ? OptionalType.create(expectedElemType) : expectedElemType;
868+
825869
constraintSink.accept(
826870
ctx.mkImplies(
827-
ctx.mkNot(typeSystem.isError(actualValue)),
828-
typeConstraintGenerator.apply(actualValue, ((MapType) lhsType).valueType())));
829-
} else {
830-
BoolExpr isListGuard = ctx.mkAnd(typeSystem.isList(lhsTrans), typeSystem.isInt(rhsTrans));
831-
BoolExpr isMapGuard = typeSystem.isMap(lhsTrans);
832-
actualValue =
833-
CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx)
834-
.addCase(isListGuard, buildListIndex(lhsTrans, rhsTrans, isListGuard, isOptional))
835-
.addCase(isMapGuard, buildMapIndex(lhsTrans, rhsTrans, isMapGuard, isOptional))
836-
.build(typeSystem.mkError());
871+
ctx.mkAnd(shouldEvaluate, ctx.mkNot(typeSystem.isError(actualValue))),
872+
typeConstraintGenerator.apply(actualValue, finalType)));
873+
874+
return actualValue;
837875
}
838876

839-
return TranslatedValue.propagateStrict(ctx, typeSystem, actualValue, args);
877+
BoolExpr isListGuard =
878+
ctx.mkAnd(shouldEvaluate, typeSystem.isList(lhsTrans), typeSystem.isInt(rhsTrans));
879+
BoolExpr isMapGuard = ctx.mkAnd(shouldEvaluate, typeSystem.isMap(lhsTrans));
880+
881+
return CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx)
882+
.addCase(isListGuard, buildListIndex(lhsTrans, rhsTrans, isListGuard, isOptional))
883+
.addCase(isMapGuard, buildMapIndex(lhsTrans, rhsTrans, isMapGuard, isOptional))
884+
.build(typeSystem.mkError());
840885
}
841886

842887
private TranslatedValue translateConditional(

verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ public final class CelVerifierZ3ImplTest {
115115
.addVar(
116116
"test_all_types",
117117
StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"))
118+
.addVar("json_val", StructTypeReference.create("google.protobuf.Value"))
119+
.addVar("json_list", StructTypeReference.create("google.protobuf.ListValue"))
120+
.addVar("json_struct", StructTypeReference.create("google.protobuf.Struct"))
118121
.build();
119122

120123
private static final CelVerifier VERIFIER =
@@ -226,9 +229,6 @@ private enum IsSatisfiableInconclusiveTestCase {
226229
MASKED_BY_BMC_MAP(
227230
"string_int_map == {'a':1, 'b':2, 'c':3, 'd':4, 'e':5, 'f':6} ? string_int_map.exists(k,"
228231
+ " k == 'g') : false"),
229-
MASKED_BY_BMC_NESTED(
230-
"nested_list == [[1, 2, 3, 4, 5, 6]] ? nested_list.exists(row, row.exists(x, x == 42)) :"
231-
+ " false"),
232232
APPROXIMATED_STRING_TO_INT("int('123') == 123"),
233233
APPROXIMATED_DOUBLE_TO_INT("int(1.5) == 1"),
234234
APPROXIMATED_INT_TO_STRING("string(123) == '123'"),
@@ -252,6 +252,26 @@ public void isSatisfiable_inconclusive(@TestParameter IsSatisfiableInconclusiveT
252252
assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE);
253253
}
254254

255+
@Test
256+
public void isSatisfiable_maskedByBmcNested_inconclusive() throws Exception {
257+
String expr =
258+
"nested_list == [[1, 2, 3, 4, 5, 6]] ? nested_list.exists(row, row.exists(x, x == 42)) :"
259+
+ " false";
260+
CelAbstractSyntaxTree ast = CEL.compile(expr).getAst();
261+
262+
// Reduce the comprehension unroll limit specifically for this heavy nested query
263+
// to prevent Z3 from timing out during the macro expansion.
264+
CelVerifier customVerifier =
265+
CelVerifierFactory.newVerifier()
266+
.setComprehensionUnrollLimit(3)
267+
.setTypeProvider(VERIFIER.getTypeProvider())
268+
.build();
269+
270+
CelVerificationResult result = customVerifier.isSatisfiable(ast);
271+
272+
assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE);
273+
}
274+
255275
@Test
256276
public void isSatisfiable_comprehensionZeroUnrollLimit_inconclusive() throws Exception {
257277
String expr = "int_list == [1] ? int_list.exists(x, x == 1) : false";
@@ -1590,7 +1610,34 @@ private enum EquivalenceTestCase {
15901610
"true"),
15911611
OPTIONAL_FIELD_SELECTION_MAP_COMPREHENSION(
15921612
"{'a': 1, 'b': 2}.transformMap(k, v, v > 1, v).?b", "optional.of(2)"),
1593-
OPTIONAL_FIELD_SELECTION_BINDER("cel.bind(m, {'a': 1}, m.?a)", "optional.of(1)");
1613+
OPTIONAL_FIELD_SELECTION_BINDER("cel.bind(m, {'a': 1}, m.?a)", "optional.of(1)"),
1614+
JSON_VALUE_BOOL("google.protobuf.Value{bool_value: true}", "true"),
1615+
JSON_VALUE_NUMBER("google.protobuf.Value{number_value: 1.0}", "1.0"),
1616+
JSON_VALUE_NULL("google.protobuf.Value{null_value: 0}", "null"),
1617+
JSON_VALUE_EMPTY("google.protobuf.Value{}", "null"),
1618+
JSON_LIST_VALUE_EMPTY("google.protobuf.ListValue{}", "[]"),
1619+
JSON_STRUCT_EMPTY("google.protobuf.Struct{}", "{}"),
1620+
JSON_LIST_VALUE("google.protobuf.ListValue{values: [1, 2]}", "[1, 2]"),
1621+
JSON_STRUCT("google.protobuf.Struct{fields: {'a': 1}}", "{'a': 1}"),
1622+
JSON_DEEP_NESTING(
1623+
"google.protobuf.ListValue{values: [google.protobuf.Struct{fields: {'a':"
1624+
+ " google.protobuf.Value{number_value: 1.0}}}]}",
1625+
"[{'a': 1.0}]"),
1626+
JSON_NUMBER_HETEROGENEOUS_EQUALITY("google.protobuf.Value{number_value: 1.0} == 1", "true"),
1627+
JSON_VALUE_OPTIONAL_NONE("google.protobuf.Value{?string_value: optional.none()}", "null"),
1628+
JSON_LIST_VALUE_OPTIONAL_NONE("google.protobuf.ListValue{?values: optional.none()}", "[]"),
1629+
JSON_STRUCT_OPTIONAL_NONE("google.protobuf.Struct{?fields: optional.none()}", "{}"),
1630+
JSON_VALUE_TYPE_REFLECTION("type(google.protobuf.Value{string_value: 'hi'}) == string", "true"),
1631+
JSON_STRUCT_TYPE_REFLECTION("type(google.protobuf.Struct{fields: {'a': 1}}) == map", "true"),
1632+
JSON_VAR_VALUE_EQUALITY("json_val == 'hi' || json_val != 'hi'", "true"),
1633+
JSON_VAR_LIST_EQUALITY("json_list == [1, 2] || json_list != [1, 2]", "true"),
1634+
JSON_VAR_MAP_EQUALITY("json_struct == {'a': 1} || json_struct != {'a': 1}", "true"),
1635+
JSON_VALUE_OPTIONAL_NULL_VALUE_NONE(
1636+
"google.protobuf.Value{?null_value: optional.none()}", "null"),
1637+
JSON_VALUE_OPTIONAL_NULL_VALUE_OF("google.protobuf.Value{?null_value: optional.of(0)}", "null"),
1638+
OPTIONAL_INDEX_LIST_UNWRAPPING("optional.of([1, 2, 3])[?0]", "optional.of(1)"),
1639+
OPTIONAL_INDEX_MAP_UNWRAPPING("optional.of({'a': 1})[?'a']", "optional.of(1)"),
1640+
OPTIONAL_INDEX_UNWRAPPING_NONE("optional.none()[?0]", "optional.none()");
15941641

15951642
private final String exprA;
15961643
private final String exprB;
@@ -1644,7 +1691,10 @@ private enum EquivalenceViolationTestCase {
16441691
"TestAllTypes{single_int32: 0}.?single_int32", "optional.of(0)"),
16451692
OPTIONAL_PROTO3_WRAPPER_ZERO_VS_UNSET(
16461693
"TestAllTypes{single_int64_wrapper: 0}.?single_int64_wrapper",
1647-
"TestAllTypes{}.?single_int64_wrapper");
1694+
"TestAllTypes{}.?single_int64_wrapper"),
1695+
OPTIONAL_DYNAMIC_TARGET_TYPE_MISMATCH("dyn_var.?a == optional.none()", "false"),
1696+
OPTIONAL_NESTED_NONE_VS_MISSING(
1697+
"{'a': optional.none()}.?a.orValue(optional.of(1))", "optional.of(1)");
16481698

16491699
final String exprA;
16501700
final String exprB;

0 commit comments

Comments
 (0)