Skip to content

Commit d9ca059

Browse files
l46kokcopybara-github
authored andcommitted
Add variable bounds for optionals
PiperOrigin-RevId: 952363986
1 parent f4ee049 commit d9ca059

3 files changed

Lines changed: 88 additions & 39 deletions

File tree

verifier/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,17 @@ properties about your expressions.
4747
counterexample showing the inputs that caused the violation. When checking
4848
satisfiability (`isSatisfiable`), it generates concrete variable assignments
4949
(satisfying model / witness) showing the inputs that satisfy the condition.
50-
* **Partial Evaluation (Unknowns) Support:** Define variables that are
51-
permitted to evaluate to `Unknown` during verification, mirroring CEL's
52-
runtime partial evaluation.
5350
* **Custom Invariants Verification:** Allows policy authors to define safety
5451
invariants (e.g., "port must always be secure if external access is
5552
allowed") and mathematically prove that the policy never violates them
5653
across all possible input states.
54+
* **Partial Evaluation (Unknowns) Support:** Define variables that are
55+
permitted to evaluate to `Unknown` during verification, mirroring CEL's
56+
runtime partial evaluation.
5757

5858
```java
5959
CelVerifier verifier = CelVerifierFactory.newVerifier()
60-
.addUnknownIdentifier("request.headers") // Exclude dynamic fields from failure paths
60+
.addUnknownIdentifier("request.headers") // Exclude unknown fields from failure paths
6161
.build();
6262
```
6363

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

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import com.microsoft.z3.Expr;
2424
import com.microsoft.z3.FuncDecl;
2525
import com.microsoft.z3.IntExpr;
26-
import com.microsoft.z3.Pattern;
2726
import com.microsoft.z3.Quantifier;
2827
import com.microsoft.z3.SeqExpr;
2928
import com.microsoft.z3.Sort;
@@ -41,6 +40,7 @@
4140
import dev.cel.common.types.ListType;
4241
import dev.cel.common.types.MapType;
4342
import dev.cel.common.types.NullableType;
43+
import dev.cel.common.types.OptionalType;
4444
import dev.cel.common.types.SimpleType;
4545
import dev.cel.common.types.StructType;
4646
import dev.cel.common.types.StructTypeReference;
@@ -79,7 +79,6 @@ final class CelAstToZ3Translator {
7979
private static final String EMPTY_MSG_REF_PREFIX = "!empty_msg_ref_";
8080
private static final String EMPTY_LIST_PREFIX = "!empty_list";
8181
private static final String EMPTY_MAP_PREFIX = "!empty_map";
82-
private static final String MAP_BIJECTION_PREFIX = "k_map_bijection";
8382
private final Context ctx;
8483
private final CelZ3TypeSystem typeSystem;
8584
private final CelZ3OperatorTranslator operatorTranslator;
@@ -289,9 +288,13 @@ private TranslatedValue translateList(CelExpr celExpr, CelAbstractSyntaxTree ast
289288
for (int i = 0; i < elements.size(); i++) {
290289
CelExpr element = elements.get(i);
291290
TranslatedValue elem = translateExpr(element, ast);
292-
elementsTv.add(elem);
293291

294292
if (optionalIndices.contains(i)) {
293+
Expr<?> checkedValue =
294+
typeSystem.withRuntimeError(
295+
elem.z3Expr(), ctx.mkNot(typeSystem.isOptional(elem.z3Expr())));
296+
elem = TranslatedValue.create(checkedValue, element, typeSystem, elem.isApproximate());
297+
295298
Expr<?> optRef = typeSystem.getOptionalRef(elem.z3Expr());
296299
seq =
297300
(SeqExpr)
@@ -302,6 +305,7 @@ private TranslatedValue translateList(CelExpr celExpr, CelAbstractSyntaxTree ast
302305
} else {
303306
seq = typeSystem.mkConcatSafe(seq, ctx.mkUnit(elem.z3Expr()));
304307
}
308+
elementsTv.add(elem);
305309
}
306310
listRef = typeSystem.mkListRefConst(LIST_REF_PREFIX);
307311
typeConstraints.add(ctx.mkEq(typeSystem.getSeq(listRef), seq));
@@ -329,15 +333,22 @@ private TranslatedValue translateMap(CelExpr celExpr, CelAbstractSyntaxTree ast)
329333
elementsTv.add(keyTv);
330334
TranslatedValue valueTv = translateExpr(entryAst.value(), ast);
331335
Expr<?> value = valueTv.z3Expr();
332-
elementsTv.add(valueTv);
333336

334337
Expr<?> finalValue = value;
335338
BoolExpr finalPresence = ctx.mkTrue();
336339
if (entryAst.optionalEntry()) {
340+
Expr<?> checkedValue =
341+
typeSystem.withRuntimeError(value, ctx.mkNot(typeSystem.isOptional(value)));
342+
valueTv =
343+
TranslatedValue.create(
344+
checkedValue, entryAst.value(), typeSystem, valueTv.isApproximate());
345+
value = checkedValue;
346+
337347
Expr<?> optRef = typeSystem.getOptionalRef(value);
338348
finalPresence = typeSystem.optHasValue(optRef);
339349
finalValue = typeSystem.getOptionalValue(optRef);
340350
}
351+
elementsTv.add(valueTv);
341352

342353
BoolExpr keyAlreadyPresent = (BoolExpr) ctx.mkSelect(mapPresence, key);
343354
BoolExpr shouldInsertKey = ctx.mkAnd(ctx.mkNot(keyAlreadyPresent), finalPresence);
@@ -384,7 +395,6 @@ private TranslatedValue translateStruct(CelExpr celExpr, CelAbstractSyntaxTree a
384395
Expr<?> key = ctx.mkString(entryAst.fieldKey());
385396
TranslatedValue valueTv = translateExpr(entryAst.value(), ast);
386397
Expr<?> value = valueTv.z3Expr();
387-
elementsTv.add(valueTv);
388398

389399
CelType fieldType =
390400
typeProvider
@@ -399,10 +409,18 @@ private TranslatedValue translateStruct(CelExpr celExpr, CelAbstractSyntaxTree a
399409
Expr<?> finalValue = value;
400410
BoolExpr optionalHasValue = ctx.mkTrue();
401411
if (entryAst.optionalEntry()) {
412+
Expr<?> checkedValue =
413+
typeSystem.withRuntimeError(value, ctx.mkNot(typeSystem.isOptional(value)));
414+
valueTv =
415+
TranslatedValue.create(
416+
checkedValue, entryAst.value(), typeSystem, valueTv.isApproximate());
417+
value = checkedValue;
418+
402419
Expr<?> optRef = typeSystem.getOptionalRef(value);
403420
optionalHasValue = typeSystem.optHasValue(optRef);
404421
finalValue = typeSystem.getOptionalValue(optRef);
405422
}
423+
elementsTv.add(valueTv);
406424

407425
// Canonicalization Trick:
408426
//
@@ -438,6 +456,9 @@ private Expr<?> getDefaultValueForType(CelType type) {
438456
if (type instanceof NullableType) {
439457
return typeSystem.mkNull();
440458
}
459+
if (type instanceof OptionalType) {
460+
return typeSystem.mkOptionalNone();
461+
}
441462
if (type.equals(SimpleType.INT)) {
442463
return typeSystem.mkInt(0);
443464
}
@@ -819,36 +840,18 @@ private void applyBoundedMapBijection(
819840
}
820841
}
821842

822-
Expr<?> kVar = ctx.mkFreshConst(MAP_BIJECTION_PREFIX, typeSystem.celValueSort());
823-
BoolExpr isValidKey =
824-
ctx.mkOr(
825-
typeSystem.isInt(kVar), typeSystem.isUint(kVar),
826-
typeSystem.isBool(kVar), typeSystem.isString(kVar));
827-
BoolExpr inMap = (BoolExpr) ctx.mkSelect(mapPresence, kVar);
843+
BoolExpr isNotTruncated = ctx.mkLe(lengthExpr, ctx.mkInt(comprehensionUnrollLimit));
828844

829-
List<BoolExpr> inSeqMatches = new ArrayList<>();
845+
ArrayExpr seqMap = ctx.mkConstArray(typeSystem.celValueSort(), ctx.mkFalse());
830846
for (int i = 0; i < comprehensionUnrollLimit; i++) {
831-
BoolExpr match =
832-
ctx.mkAnd(
833-
ctx.mkLt(ctx.mkInt(i), lengthExpr), ctx.mkEq(kVar, ctx.mkNth(seq, ctx.mkInt(i))));
834-
inSeqMatches.add(match);
847+
seqMap =
848+
(ArrayExpr)
849+
ctx.mkITE(
850+
ctx.mkLt(ctx.mkInt(i), lengthExpr),
851+
ctx.mkStore(seqMap, ctx.mkNth(seq, ctx.mkInt(i)), ctx.mkTrue()),
852+
seqMap);
835853
}
836-
BoolExpr inSeq = CelZ3TypeSystem.mkOrFlattened(ctx, inSeqMatches);
837-
838-
BoolExpr isNotTruncated = ctx.mkLe(lengthExpr, ctx.mkInt(comprehensionUnrollLimit));
839-
840-
Pattern inMapPattern = ctx.mkPattern(inMap);
841-
842-
BoolExpr completeness =
843-
ctx.mkForall(
844-
new Expr<?>[] {kVar},
845-
ctx.mkImplies(ctx.mkAnd(isNotTruncated, isValidKey, inMap), inSeq),
846-
1,
847-
new Pattern[] {inMapPattern},
848-
null,
849-
null,
850-
null);
851-
typeConstraints.add(completeness);
854+
typeConstraints.add(ctx.mkImplies(isNotTruncated, ctx.mkEq(mapPresence, seqMap)));
852855
}
853856

854857
private TranslatedValue[] evaluateLoopCondAndStep(
@@ -1167,6 +1170,24 @@ private BoolExpr createTypeConstraint(Expr<?> val, long exprId, CelAbstractSynta
11671170
}
11681171

11691172
private BoolExpr createTypeConstraintForType(Expr<?> val, CelType type) {
1173+
if (type instanceof NullableType) {
1174+
NullableType nullableType = (NullableType) type;
1175+
return ctx.mkOr(
1176+
typeSystem.isNull(val),
1177+
createTypeConstraintForType(val, nullableType.targetType()));
1178+
}
1179+
if (type instanceof OptionalType) {
1180+
BoolExpr isOpt = typeSystem.isOptional(val);
1181+
CelType paramType = type.parameters().get(0);
1182+
if (paramType.kind().isDyn() || paramType.kind().isTypeParam()) {
1183+
return isOpt;
1184+
}
1185+
Expr<?> optRef = typeSystem.getOptionalRef(val);
1186+
BoolExpr hasValue = typeSystem.optHasValue(optRef);
1187+
BoolExpr valConstraint =
1188+
createTypeConstraintForType(typeSystem.getOptionalValue(optRef), paramType);
1189+
return ctx.mkAnd(isOpt, ctx.mkImplies(hasValue, valConstraint));
1190+
}
11701191
if (type.equals(SimpleType.BOOL)) {
11711192
return (BoolExpr) ctx.mkApp(typeSystem.boolCons().getTesterDecl(), val);
11721193
}

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

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import dev.cel.common.ast.CelExpr.CelCall;
4343
import dev.cel.common.types.ListType;
4444
import dev.cel.common.types.MapType;
45+
import dev.cel.common.types.NullableType;
4546
import dev.cel.common.types.OptionalType;
4647
import dev.cel.common.types.ProtoMessageTypeProvider;
4748
import dev.cel.common.types.SimpleType;
@@ -102,6 +103,8 @@ public final class CelVerifierZ3ImplTest {
102103
.addVar("dyn_var", SimpleType.DYN)
103104
.addVar("dyn_var2", SimpleType.DYN)
104105
.addVar("opt_var", OptionalType.create(SimpleType.INT))
106+
.addVar("opt_dyn_var", OptionalType.create(SimpleType.DYN))
107+
.addVar("nullable_int", NullableType.create(SimpleType.INT))
105108
.addVar("string_int_map", MapType.create(SimpleType.STRING, SimpleType.INT))
106109
.addVar("bytes_val", SimpleType.BYTES)
107110
.addVar(
@@ -147,7 +150,8 @@ private enum IsSatisfiableTestCase {
147150
"timestamp('2023-01-01T00:00:00Z') == timestamp('2023-01-01T00:00:00Z')"),
148151
CROSS_NUMERIC_EQUALITY_INT_DYN_EXACT("1 == request"),
149152
MACRO_LIMIT("dyn_list.all(x, x == 1)"),
150-
STRUCT_FIELD_MISSING_APPROXIMATE_SATISFIABLE("dyn_var.unknown_field");
153+
STRUCT_FIELD_MISSING_APPROXIMATE_SATISFIABLE("dyn_var.unknown_field"),
154+
NULLABLE_INT_SATISFIABLE("nullable_int == 123");
151155

152156
final String expr;
153157

@@ -463,6 +467,8 @@ private enum IsAlwaysTrueTestCase {
463467
OPTIONAL_OR_NONE_IS_NONE("optional.none().or(optional.none()) == optional.none()"),
464468
OPTIONAL_VALUE_VAR("optional.of(x).value() == x"),
465469
OPTIONAL_HAS_VALUE_VAR("optional.of(x).hasValue()"),
470+
OPTIONAL_VAR_HAS_VALUE_IMPLIES_INT("opt_var.hasValue() ? type(opt_var.value()) == int : true"),
471+
466472
IEEE_754_PROTO_NEG_ZERO_NEQ(
467473
"TestAllTypes{single_double: -0.0} != TestAllTypes{single_double: 0.0}"),
468474
IEEE_754_ROUND_NEAREST_TIES_TO_EVEN_DOWN("1.0 + 1.1102230246251565e-16 == 1.0"),
@@ -637,6 +643,7 @@ private enum IsAlwaysTrueTestCase {
637643
WRAPPER_SET_NULL_IS_NULL(
638644
"TestAllTypes{single_int64_wrapper: null}.single_int64_wrapper == null"),
639645
WRAPPER_SET_NULL_EQ_UNSET("TestAllTypes{single_int64_wrapper: null} == TestAllTypes{}"),
646+
WRAPPER_SET_NON_NULL_EQ("TestAllTypes{single_int64_wrapper: 123}.single_int64_wrapper == 123"),
640647
STRING_CONTAINS_EMPTY("role.contains('')"),
641648
STRING_STARTS_WITH_EMPTY("role.startsWith('')"),
642649
STRING_ENDS_WITH_EMPTY("role.endsWith('')"),
@@ -1031,6 +1038,26 @@ private enum IsAlwaysTrueViolationTestCase {
10311038
"!(request == unknown_var && type(request) == int && type(unknown_var) == double)",
10321039
"Condition is not always true.",
10331040
"Counterexample input:"),
1041+
OPTIONAL_DYN_VAR_HAS_VALUE_NOT_IMPLIES_INT(
1042+
"opt_dyn_var.hasValue() ? type(opt_dyn_var.value()) == int : true",
1043+
"Condition is not always true.",
1044+
"Counterexample input:",
1045+
"opt_dyn_var ="),
1046+
OPTIONAL_ENTRY_DYN_VAR_TYPE_MISMATCH(
1047+
"[?dyn_var] == [?dyn_var] ? true : true",
1048+
"Condition is not always true.",
1049+
"Counterexample input:",
1050+
"dyn_var ="),
1051+
OPTIONAL_MAP_ENTRY_DYN_VAR_TYPE_MISMATCH(
1052+
"{?1: dyn_var} == {?1: dyn_var} ? true : true",
1053+
"Condition is not always true.",
1054+
"Counterexample input:",
1055+
"dyn_var ="),
1056+
OPTIONAL_STRUCT_ENTRY_DYN_VAR_TYPE_MISMATCH(
1057+
"cel.expr.conformance.proto3.TestAllTypes{?single_int32: dyn_var} == cel.expr.conformance.proto3.TestAllTypes{?single_int32: dyn_var} ? true : true",
1058+
"Condition is not always true.",
1059+
"Counterexample input:",
1060+
"dyn_var ="),
10341061
DYNAMIC_MAP_ALL_VIOLATION(
10351062
"string_int_map == {'a': 1, 'b': 2} ? string_int_map.all(k, k == 'a') : true",
10361063
"Condition is not always true.",
@@ -1430,8 +1457,9 @@ private enum EquivalenceTestCase {
14301457
"TestAllTypes{?repeated_int32: optional.of([1, 2])}",
14311458
"cel.expr.conformance.proto3.TestAllTypes{repeated_int32: [1, 2]}"),
14321459
OPTIONAL_PRUNE_LIST_EQUALITY("[?optional.none(), 1] == [1]", "true"),
1433-
OPTIONAL_PRUNE_LIST_COMPREHENSION("[1, ?optional.none()].all(x, x > 0)", "true");
1434-
1460+
OPTIONAL_PRUNE_LIST_COMPREHENSION("[1, ?optional.none()].all(x, x > 0)", "true"),
1461+
MAP_COMPREHENSION(
1462+
"{'a': 1, 'b': 2}.exists(k, k == 'a')", "{'a': 1, 'b': 2}.exists(k, k == 'a')");
14351463
private final String exprA;
14361464
private final String exprB;
14371465

0 commit comments

Comments
 (0)