Skip to content

Commit dce7379

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

3 files changed

Lines changed: 87 additions & 1 deletion

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: 67 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,64 @@ 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+
private BoolExpr createJsonWktTypeConstraint(String messageName, Expr<?> val) {
464+
if (messageName.equals(CelTypes.VALUE_MESSAGE)) {
465+
return ctx.mkTrue();
466+
}
467+
if (messageName.equals(CelTypes.LIST_VALUE_MESSAGE)) {
468+
return typeSystem.isList(val);
469+
}
470+
return typeSystem.isMap(val);
471+
}
472+
473+
// Concretize JSON WKT unwrapping directly into native Z3 primitives to avoid
474+
// sort incompatibilities (Message == String) and solver performance penalties (quantifiers).
475+
private TranslatedValue translateJsonWktStruct(
476+
CelExpr celExpr, CelExpr.CelStruct createStruct, CelAbstractSyntaxTree ast) {
477+
Expr<?> fallback;
478+
if (createStruct.messageName().equals(CelTypes.VALUE_MESSAGE)) {
479+
fallback = typeSystem.mkNull();
480+
} else if (createStruct.messageName().equals(CelTypes.LIST_VALUE_MESSAGE)) {
481+
fallback = getDefaultValueForType(ListType.create(SimpleType.DYN));
482+
} else {
483+
fallback = getDefaultValueForType(MapType.create(SimpleType.STRING, SimpleType.DYN));
484+
}
485+
486+
if (createStruct.entries().isEmpty()) {
487+
return TranslatedValue.create(fallback, celExpr, typeSystem, ctx.mkFalse());
488+
}
489+
490+
CelExpr.CelStruct.Entry entry = createStruct.entries().get(0);
491+
TranslatedValue entryTv;
492+
493+
// null_value is a proto enum represented as integer 0 in the AST.
494+
// We must explicitly force it to CelNull to match runtime semantics.
495+
if (createStruct.messageName().equals(CelTypes.VALUE_MESSAGE)
496+
&& entry.fieldKey().equals(NULL_VALUE_FIELD)) {
497+
entryTv =
498+
TranslatedValue.create(typeSystem.mkNull(), entry.value(), typeSystem, ctx.mkFalse());
499+
} else {
500+
entryTv = translateExpr(entry.value(), ast);
501+
}
502+
503+
Expr<?> finalVal = entryTv.z3Expr();
504+
505+
if (entry.optionalEntry()) {
506+
Expr<?> optRef = typeSystem.getOptionalRef(finalVal);
507+
BoolExpr hasValue = typeSystem.optHasValue(optRef);
508+
finalVal = ctx.mkITE(hasValue, typeSystem.getOptionalValue(optRef), fallback);
509+
}
510+
511+
return TranslatedValue.propagateStrict(
512+
ctx, typeSystem, finalVal, celExpr, ImmutableList.of(entryTv));
513+
}
514+
451515
private Expr<?> getDefaultValueForType(CelType type) {
452516
if (type instanceof NullableType) {
453517
return typeSystem.mkNull();
@@ -1244,6 +1308,9 @@ private BoolExpr createTypeConstraintForType(Expr<?> val, CelType type) {
12441308
return typeSystem.isMap(val);
12451309
}
12461310
if (type.kind() == CelKind.STRUCT) {
1311+
if (isJsonWkt(type.name())) {
1312+
return createJsonWktTypeConstraint(type.name(), val);
1313+
}
12471314
return ctx.mkAnd(
12481315
typeSystem.isMessage(val),
12491316
ctx.mkEq(

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1590,7 +1590,25 @@ private enum EquivalenceTestCase {
15901590
"true"),
15911591
OPTIONAL_FIELD_SELECTION_MAP_COMPREHENSION(
15921592
"{'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)");
1593+
OPTIONAL_FIELD_SELECTION_BINDER("cel.bind(m, {'a': 1}, m.?a)", "optional.of(1)"),
1594+
JSON_VALUE_BOOL("google.protobuf.Value{bool_value: true}", "true"),
1595+
JSON_VALUE_NUMBER("google.protobuf.Value{number_value: 1.0}", "1.0"),
1596+
JSON_VALUE_NULL("google.protobuf.Value{null_value: 0}", "null"),
1597+
JSON_VALUE_EMPTY("google.protobuf.Value{}", "null"),
1598+
JSON_LIST_VALUE_EMPTY("google.protobuf.ListValue{}", "[]"),
1599+
JSON_STRUCT_EMPTY("google.protobuf.Struct{}", "{}"),
1600+
JSON_LIST_VALUE("google.protobuf.ListValue{values: [1, 2]}", "[1, 2]"),
1601+
JSON_STRUCT("google.protobuf.Struct{fields: {'a': 1}}", "{'a': 1}"),
1602+
JSON_DEEP_NESTING(
1603+
"google.protobuf.ListValue{values: [google.protobuf.Struct{fields: {'a':"
1604+
+ " google.protobuf.Value{number_value: 1.0}}}]}",
1605+
"[{'a': 1.0}]"),
1606+
JSON_NUMBER_HETEROGENEOUS_EQUALITY("google.protobuf.Value{number_value: 1.0} == 1", "true"),
1607+
JSON_VALUE_OPTIONAL_NONE("google.protobuf.Value{?string_value: optional.none()}", "null"),
1608+
JSON_LIST_VALUE_OPTIONAL_NONE("google.protobuf.ListValue{?values: optional.none()}", "[]"),
1609+
JSON_STRUCT_OPTIONAL_NONE("google.protobuf.Struct{?fields: optional.none()}", "{}"),
1610+
JSON_VALUE_TYPE_REFLECTION("type(google.protobuf.Value{string_value: 'hi'}) == string", "true"),
1611+
JSON_STRUCT_TYPE_REFLECTION("type(google.protobuf.Struct{fields: {'a': 1}}) == map", "true");
15941612

15951613
private final String exprA;
15961614
private final String exprB;

0 commit comments

Comments
 (0)