Skip to content

Commit 42b34ac

Browse files
l46kokcopybara-github
authored andcommitted
Add CLI for Verifier
PiperOrigin-RevId: 956259271
1 parent 310d96f commit 42b34ac

27 files changed

Lines changed: 2304 additions & 237 deletions

common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import static com.google.common.math.LongMath.checkedMultiply;
1919
import static com.google.common.math.LongMath.checkedSubtract;
2020

21-
import com.google.common.annotations.VisibleForTesting;
2221
import com.google.common.base.Strings;
2322
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2423
import com.google.protobuf.Duration;
@@ -50,15 +49,11 @@
5049
public final class ProtoTimeUtils {
5150

5251
// Timestamp for "0001-01-01T00:00:00Z"
53-
@VisibleForTesting
54-
static final long TIMESTAMP_SECONDS_MIN = -62135596800L;
52+
public static final long TIMESTAMP_SECONDS_MIN = -62135596800L;
5553
// Timestamp for "9999-12-31T23:59:59Z"
56-
@VisibleForTesting
57-
static final long TIMESTAMP_SECONDS_MAX = 253402300799L;
58-
@VisibleForTesting
59-
static final long DURATION_SECONDS_MIN = -315576000000L;
60-
@VisibleForTesting
61-
static final long DURATION_SECONDS_MAX = 315576000000L;
54+
public static final long TIMESTAMP_SECONDS_MAX = 253402300799L;
55+
public static final long DURATION_SECONDS_MIN = -315576000000L;
56+
public static final long DURATION_SECONDS_MAX = 315576000000L;
6257

6358
private static final int MILLIS_PER_SECOND = 1000;
6459

verifier/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ public class InvariantsExample {
400400

401401
### Timeouts
402402

403-
SMT solving is NP-complete and can theoretically stop responding or take an
403+
SMT solving is NP-hard and can theoretically stop responding or take an
404404
exponential amount of time for complex formulas.
405405
The verifier uses a default timeout of 10 seconds. It is recommended to
406406
configure this to a reasonable duration for your specific use case using
@@ -433,3 +433,7 @@ What this means for verification:
433433
default unless you have a specific need and bounded inputs.
434434

435435
---
436+
437+
## Tools & CLI
438+
439+
For command-line verification and interactive execution, see the [CLI Tool documentation](tools/README.md).

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ java_library(
9898
tags = [
9999
],
100100
deps = [
101+
"//common/internal:proto_time_utils",
101102
"@maven//:com_google_errorprone_error_prone_annotations",
102103
"@maven//:com_google_guava_guava",
103104
"@maven//:tools_aqua_z3_turnkey",

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

Lines changed: 85 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,12 @@ private Expr<?> getDefaultValueForType(CelType type) {
533533
if (type.equals(SimpleType.UINT)) {
534534
return typeSystem.mkUint(0);
535535
}
536+
if (type.equals(SimpleType.TIMESTAMP)) {
537+
return typeSystem.wrapTimestamp(ctx.mkInt(0));
538+
}
539+
if (type.equals(SimpleType.DURATION)) {
540+
return typeSystem.wrapDuration(ctx.mkInt(0));
541+
}
536542
if (type instanceof ListType) {
537543
if (emptyListCache == null) {
538544
emptyListCache = typeSystem.mkListRefConst(EMPTY_LIST_PREFIX);
@@ -735,8 +741,10 @@ private TranslatedValue translateCall(CelExpr expr, CelAbstractSyntaxTree ast) {
735741
typeConstraints.add(ctx.mkNot(typeSystem.isUnknown(callRes)));
736742
typeConstraints.add(ctx.mkNot(typeSystem.isError(callRes)));
737743

744+
boolean isDynamic = ast.getType(exprId).map(SimpleType.DYN::equals).orElse(true);
745+
BoolExpr isApprox = ctx.mkBool(!isDynamic);
738746
return TranslatedValue.propagateStrict(
739-
ctx, typeSystem, callRes, Optional.of(expr), ctx.mkTrue(), args);
747+
ctx, typeSystem, callRes, Optional.of(expr), isApprox, args);
740748
});
741749
}
742750

@@ -1239,9 +1247,10 @@ private BoolExpr createTypeConstraintForType(Expr<?> val, CelType type) {
12391247
}
12401248
Expr<?> optRef = typeSystem.getOptionalRef(val);
12411249
BoolExpr hasValue = typeSystem.optHasValue(optRef);
1242-
BoolExpr valConstraint =
1243-
createTypeConstraintForType(typeSystem.getOptionalValue(optRef), paramType);
1244-
return ctx.mkAnd(isOpt, ctx.mkImplies(hasValue, valConstraint));
1250+
Expr<?> optVal = typeSystem.getOptionalValue(optRef);
1251+
BoolExpr optValNotError = ctx.mkNot(typeSystem.isError(optVal));
1252+
BoolExpr valConstraint = createTypeConstraintForType(optVal, paramType);
1253+
return ctx.mkAnd(isOpt, ctx.mkImplies(hasValue, ctx.mkAnd(optValNotError, valConstraint)));
12451254
}
12461255
if (type.equals(SimpleType.BOOL)) {
12471256
return (BoolExpr) ctx.mkApp(typeSystem.boolCons().getTesterDecl(), val);
@@ -1269,16 +1278,25 @@ private BoolExpr createTypeConstraintForType(Expr<?> val, CelType type) {
12691278
if (type.equals(SimpleType.BYTES)) {
12701279
return (BoolExpr) ctx.mkApp(typeSystem.bytesCons().getTesterDecl(), val);
12711280
}
1281+
if (type.equals(SimpleType.TIMESTAMP)) {
1282+
IntExpr seconds = typeSystem.getTimestamp(val);
1283+
return ctx.mkAnd(
1284+
typeSystem.isTimestamp(val), ctx.mkNot(typeSystem.checkTimestampOverflow(seconds)));
1285+
}
1286+
if (type.equals(SimpleType.DURATION)) {
1287+
IntExpr seconds = typeSystem.getDuration(val);
1288+
return ctx.mkAnd(
1289+
typeSystem.isDuration(val), ctx.mkNot(typeSystem.checkDurationOverflow(seconds)));
1290+
}
1291+
12721292
if (type instanceof ListType) {
1273-
// Lists are explicitly bounded (sequence theory). We're safe in using for-all quantifiers
1274-
// here.
1293+
// Constrain list elements using bounded unrolling up to comprehensionUnrollLimit rather
1294+
// than Z3 forall quantifiers to prevent MBQI quantifier instantiation loops.
1295+
// Assert: isList(val) ∧ for all unrolled 0 <= i < length: ¬isError(seq[i]) ∧
1296+
// typeConstraint(seq[i])
12751297
BoolExpr isList = typeSystem.isList(val);
12761298
CelType elemType = ((ListType) type).elemType();
1277-
if (elemType.equals(SimpleType.DYN)) {
1278-
return isList;
1279-
}
12801299

1281-
// isList(val) ∧ ∀i. (0 <= i < length) ⇒ elemType(seq[i])
12821300
Expr<?> listRef = typeSystem.getListRef(val);
12831301
SeqExpr seq = typeSystem.getSeq(listRef);
12841302
Expr length = ctx.mkLength(seq);
@@ -1288,20 +1306,69 @@ private BoolExpr createTypeConstraintForType(Expr<?> val, CelType type) {
12881306
for (int i = 0; i < comprehensionUnrollLimit; i++) {
12891307
IntExpr idx = ctx.mkInt(i);
12901308
Expr elem = ctx.mkNth(seq, idx);
1291-
BoolExpr elemConstraint = createTypeConstraintForType(elem, elemType);
12921309
BoolExpr validIndex = ctx.mkLt(idx, length);
1293-
boundsAndTypes.add(ctx.mkImplies(validIndex, elemConstraint));
1294-
BoolExpr outOfBounds = ctx.mkGe(idx, length);
1295-
boundsAndTypes.add(ctx.mkImplies(outOfBounds, ctx.mkEq(elem, typeSystem.mkUnknown())));
1310+
// Assert ¬isError(elem) as a domain invariant so Z3 never synthesizes an Error element in
1311+
// list(dyn). For concrete types, this is already implied by createTypeConstraintForType.
1312+
boundsAndTypes.add(ctx.mkImplies(validIndex, ctx.mkNot(typeSystem.isError(elem))));
1313+
// Short-circuit DYN element types to prevent generating redundant validIndex ⇒ TRUE
1314+
// clauses.
1315+
if (!elemType.equals(SimpleType.DYN)) {
1316+
BoolExpr elemConstraint = createTypeConstraintForType(elem, elemType);
1317+
boundsAndTypes.add(ctx.mkImplies(validIndex, elemConstraint));
1318+
}
12961319
}
12971320

12981321
return CelZ3TypeSystem.mkAndFlattened(ctx, boundsAndTypes);
12991322
}
13001323
if (type instanceof MapType) {
1301-
// Do NOT emit a for-all quantifier over map keys here.
1302-
// Doing so forces MBQI into an infinite loop. Structural equivalence of dynamic keys is
1303-
// naturally constrained by the primitive key assertions in getStructuralEquality().
1304-
return typeSystem.isMap(val);
1324+
// Do NOT emit a for-all quantifier over map keys or values here.
1325+
// Doing so forces MBQI into an infinite loop. Instead, constrain keys and values using
1326+
// bounded unrolling over the key sequence up to comprehensionUnrollLimit.
1327+
// Assert: isMap(val) ∧ for all unrolled 0 <= i < length: isPrimitiveKey(key) ∧ ¬isError(key)
1328+
// ∧ (presence(key) ⇒ ¬isError(val) ∧ typeConstraint(val))
1329+
BoolExpr isMap = typeSystem.isMap(val);
1330+
MapType mapType = (MapType) type;
1331+
CelType keyType = mapType.keyType();
1332+
CelType valType = mapType.valueType();
1333+
1334+
Expr<?> mapRef = typeSystem.getMapRef(val);
1335+
SeqExpr seq = typeSystem.getMapKeys(mapRef);
1336+
Expr length = ctx.mkLength(seq);
1337+
ArrayExpr mapValues = (ArrayExpr) typeSystem.getMapValues(mapRef);
1338+
ArrayExpr mapPresence = (ArrayExpr) typeSystem.getMapPresence(mapRef);
1339+
1340+
List<BoolExpr> boundsAndTypes = new ArrayList<>();
1341+
boundsAndTypes.add(isMap);
1342+
1343+
for (int i = 0; i < comprehensionUnrollLimit; i++) {
1344+
IntExpr idx = ctx.mkInt(i);
1345+
Expr key = ctx.mkNth(seq, idx);
1346+
BoolExpr validIndex = ctx.mkLt(idx, length);
1347+
1348+
BoolExpr isKeyPrim = typeSystem.isPrimitiveKey(key);
1349+
BoolExpr keyNotError = ctx.mkNot(typeSystem.isError(key));
1350+
// Assert isKeyPrim ∧ ¬isError(key) so Z3 never synthesizes a non-primitive or Error key in
1351+
// map(dyn, ...). For concrete map types, this is already implied by keyType constraints.
1352+
boundsAndTypes.add(ctx.mkImplies(validIndex, ctx.mkAnd(isKeyPrim, keyNotError)));
1353+
// Short-circuit DYN key types to prevent generating redundant validIndex ⇒ TRUE clauses.
1354+
if (!keyType.equals(SimpleType.DYN)) {
1355+
boundsAndTypes.add(ctx.mkImplies(validIndex, createTypeConstraintForType(key, keyType)));
1356+
}
1357+
1358+
BoolExpr presence = (BoolExpr) ctx.mkSelect(mapPresence, key);
1359+
BoolExpr validEntry = ctx.mkAnd(validIndex, presence);
1360+
1361+
Expr mapVal = ctx.mkSelect(mapValues, key);
1362+
BoolExpr valNotError = ctx.mkNot(typeSystem.isError(mapVal));
1363+
boundsAndTypes.add(ctx.mkImplies(validEntry, valNotError));
1364+
// Short-circuit DYN value types to prevent generating redundant validEntry ⇒ TRUE clauses.
1365+
if (!valType.equals(SimpleType.DYN)) {
1366+
boundsAndTypes.add(
1367+
ctx.mkImplies(validEntry, createTypeConstraintForType(mapVal, valType)));
1368+
}
1369+
}
1370+
1371+
return CelZ3TypeSystem.mkAndFlattened(ctx, boundsAndTypes);
13051372
}
13061373
if (type.kind() == CelKind.STRUCT) {
13071374
return ctx.mkAnd(

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ private static String formatExpr(
8282
// Handle CelType constructors wrapper unwrapping
8383
if (decl.equals(typeSystem.intCons().ConstructorDecl())) {
8484
return formatExpr(ctx, typeSystem, model, expr.getArgs()[0]);
85+
} else if (decl.equals(typeSystem.timestampCons().ConstructorDecl())) {
86+
return "timestamp(" + formatExpr(ctx, typeSystem, model, expr.getArgs()[0]) + ")";
87+
} else if (decl.equals(typeSystem.durationCons().ConstructorDecl())) {
88+
return "duration(" + formatExpr(ctx, typeSystem, model, expr.getArgs()[0]) + ")";
8589
} else if (decl.equals(typeSystem.uintCons().ConstructorDecl())) {
8690
return formatExpr(ctx, typeSystem, model, expr.getArgs()[0]) + "u";
8791
} else if (decl.equals(typeSystem.boolCons().ConstructorDecl())) {
@@ -123,6 +127,8 @@ private static String formatExpr(
123127
return "Error";
124128
} else if (decl.equals(typeSystem.unknownCons().ConstructorDecl())) {
125129
return "Unknown";
130+
} else if (decl.equals(typeSystem.nullCons().ConstructorDecl())) {
131+
return "null";
126132
} else if (decl.equals(typeSystem.optionalCons().ConstructorDecl())) {
127133
Expr<?> optRef = expr.getArgs()[0];
128134
Expr<?> hasValueExpr =

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

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,11 @@ private BoolExpr mkTypeGuard(Expr<?> arg, CelType expectedType) {
148148
// These match everything structurally type-wise, although we might refine this later.
149149
return ctx.mkTrue();
150150
case INT:
151+
return typeSystem.isInt(arg);
151152
case TIMESTAMP:
153+
return typeSystem.isTimestamp(arg);
152154
case DURATION:
153-
// Safe to map int, timestamp, and duration to IntSort because CEL's static checker prevents
154-
// invalid cross-type usage and their operator axioms translate to identical Z3 ASTs.
155-
return typeSystem.isInt(arg);
155+
return typeSystem.isDuration(arg);
156156
case UINT:
157157
return typeSystem.isUint(arg);
158158
case DOUBLE:
@@ -386,7 +386,7 @@ private BoolExpr getNumericEqualityWithConstant(
386386
? ctx.mkEq(typeSystem.getUint(symVal), ctx.mkInt(uintVal))
387387
: ctx.mkFalse();
388388
} else if (symType.kind() == CelKind.DOUBLE) {
389-
return ctx.mkFPEq((FPExpr) typeSystem.getDouble(symVal), typeSystem.mkFpDouble(doubleVal));
389+
return ctx.mkFPEq(typeSystem.getDouble(symVal), typeSystem.mkFpDouble(doubleVal));
390390
}
391391
}
392392

@@ -396,8 +396,7 @@ private BoolExpr getNumericEqualityWithConstant(
396396
(uintVal != null)
397397
? ctx.mkEq(typeSystem.getUint(symVal), ctx.mkInt(uintVal))
398398
: ctx.mkFalse();
399-
BoolExpr doubleEq =
400-
ctx.mkFPEq((FPExpr) typeSystem.getDouble(symVal), typeSystem.mkFpDouble(doubleVal));
399+
BoolExpr doubleEq = ctx.mkFPEq(typeSystem.getDouble(symVal), typeSystem.mkFpDouble(doubleVal));
401400

402401
return (BoolExpr)
403402
CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx)
@@ -435,18 +434,17 @@ private BoolExpr getStaticallyKnownNumericEquality(
435434
case UINT:
436435
return ctx.mkEq(typeSystem.getUint(z3Expr0), typeSystem.getUint(z3Expr1));
437436
case DOUBLE:
438-
return ctx.mkFPEq(
439-
(FPExpr) typeSystem.getDouble(z3Expr0), (FPExpr) typeSystem.getDouble(z3Expr1));
437+
return ctx.mkFPEq(typeSystem.getDouble(z3Expr0), typeSystem.getDouble(z3Expr1));
440438
default:
441439
return ctx.mkFalse();
442440
}
443441
}
444442

445443
private BoolExpr mkIsFiniteDouble(Expr<?> z3Expr) {
446-
Expr<?> fpVal = typeSystem.getDouble(z3Expr);
444+
FPExpr fpVal = typeSystem.getDouble(z3Expr);
447445
return ctx.mkAnd(
448446
typeSystem.isDouble(z3Expr),
449-
ctx.mkNot(ctx.mkOr(ctx.mkFPIsNaN((FPExpr) fpVal), ctx.mkFPIsInfinite((FPExpr) fpVal))));
447+
ctx.mkNot(ctx.mkOr(ctx.mkFPIsNaN(fpVal), ctx.mkFPIsInfinite(fpVal))));
450448
}
451449

452450
private BoolExpr getDynamicNumericEquality(Expr<?> z3Expr0, Expr<?> z3Expr1) {
@@ -475,25 +473,28 @@ private BoolExpr getDynamicNumericEquality(Expr<?> z3Expr0, Expr<?> z3Expr1) {
475473
BoolExpr isIntOrUintAndDouble = ctx.mkAnd(isIntOrUint0, typeSystem.isDouble(z3Expr1));
476474
BoolExpr isDoubleAndIntOrUint = ctx.mkAnd(typeSystem.isDouble(z3Expr0), isIntOrUint1);
477475

478-
Expr<?> fpVal1 = typeSystem.getDouble(z3Expr1);
476+
FPExpr fpVal1 = typeSystem.getDouble(z3Expr1);
477+
ArithExpr<?> realVal0 = ctx.mkInt2Real(val0);
479478
BoolExpr intDoubleEq =
480479
ctx.mkAnd(
481480
mkIsFiniteDouble(z3Expr1),
482-
ctx.mkEq(ctx.mkInt2Real(val0), ctx.mkFPToReal((FPExpr) fpVal1)));
481+
ctx.mkLe(realVal0, ctx.mkFPToReal(fpVal1)),
482+
ctx.mkLe(ctx.mkFPToReal(fpVal1), realVal0));
483483

484-
Expr<?> fpVal0 = typeSystem.getDouble(z3Expr0);
484+
FPExpr fpVal0 = typeSystem.getDouble(z3Expr0);
485+
ArithExpr<?> realVal1 = ctx.mkInt2Real(val1);
485486
BoolExpr doubleIntEq =
486487
ctx.mkAnd(
487488
mkIsFiniteDouble(z3Expr0),
488-
ctx.mkEq(ctx.mkFPToReal((FPExpr) fpVal0), ctx.mkInt2Real(val1)));
489+
ctx.mkLe(realVal1, ctx.mkFPToReal(fpVal0)),
490+
ctx.mkLe(ctx.mkFPToReal(fpVal0), realVal1));
489491

490492
return (BoolExpr)
491493
CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx)
492494
.addCase(bothIntOrUint, ctx.mkEq(val0, val1))
493495
.addCase(
494496
bothDouble,
495-
ctx.mkFPEq(
496-
(FPExpr) typeSystem.getDouble(z3Expr0), (FPExpr) typeSystem.getDouble(z3Expr1)))
497+
ctx.mkFPEq(typeSystem.getDouble(z3Expr0), typeSystem.getDouble(z3Expr1)))
497498
.addCase(isIntOrUintAndDouble, intDoubleEq)
498499
.addCase(isDoubleAndIntOrUint, doubleIntEq)
499500
.build(ctx.mkFalse());
@@ -593,7 +594,8 @@ private TranslatedValue translateEquality(
593594
// because X == X is a tautology (or propagates errors/unknowns exactly).
594595
if (z3Arg0.equals(z3Arg1)) {
595596
Expr<?> finalResult = typeSystem.propagateErrorAndUnknown(equalityExpr, z3Arg0);
596-
return TranslatedValue.create(finalResult, typeSystem, ctx.mkFalse());
597+
return TranslatedValue.create(
598+
finalResult, typeSystem, ctx.mkOr(arg0.isApproximate(), arg1.isApproximate()));
597599
}
598600

599601
return TranslatedValue.propagateStrict(ctx, typeSystem, equalityExpr, arg0, arg1)

0 commit comments

Comments
 (0)