Skip to content

Commit 080363a

Browse files
l46kokcopybara-github
authored andcommitted
Prevent spurious counterexamples on maps by tightening its domain
PiperOrigin-RevId: 956295539
1 parent 8d150b2 commit 080363a

32 files changed

Lines changed: 3142 additions & 302 deletions

BUILD.bazel

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@ java_library(
9595
],
9696
)
9797

98+
java_library(
99+
name = "java_jline",
100+
exports = [
101+
"@maven//:org_jline_jline_reader",
102+
"@maven//:org_jline_jline_terminal",
103+
],
104+
)
105+
98106
default_java_toolchain(
99107
name = "repository_default_toolchain",
100108
configuration = DEFAULT_TOOLCHAIN_CONFIGURATION,

MODULE.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ maven.install(
9595
"info.picocli:picocli:4.7.7",
9696
"org.antlr:antlr4-runtime:4.13.2",
9797
"org.freemarker:freemarker:2.3.34",
98+
"org.jline:jline-reader:3.26.1",
99+
"org.jline:jline-terminal:3.26.1",
98100
"org.jspecify:jspecify:1.0.0",
99101
"org.threeten:threeten-extra:1.8.0",
100102
"org.yaml:snakeyaml:2.5",

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: 91 additions & 25 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.getTypeOrThrow(exprId).equals(SimpleType.DYN);
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

@@ -869,10 +877,6 @@ private TranslatedValue translateDynamicComprehension(
869877
ArrayExpr mapPresence =
870878
isMap ? (ArrayExpr) typeSystem.getMapPresence(typeSystem.getMapRef(iterRange)) : null;
871879

872-
if (isMap) {
873-
applyBoundedMapBijection(mapPresence, seq, lengthExpr);
874-
}
875-
876880
BoolExpr isTruncated = ctx.mkGt(lengthExpr, ctx.mkInt(comprehensionUnrollLimit));
877881
truncationConditions.add(isTruncated);
878882

@@ -885,14 +889,15 @@ private TranslatedValue translateDynamicComprehension(
885889
}
886890
}
887891

888-
private void applyBoundedMapBijection(
892+
private BoolExpr getBoundedMapBijection(
889893
ArrayExpr mapPresence, SeqExpr<?> seq, ArithExpr lengthExpr) {
894+
List<BoolExpr> constraints = new ArrayList<>();
890895
for (int i = 0; i < comprehensionUnrollLimit; i++) {
891896
for (int j = i + 1; j < comprehensionUnrollLimit; j++) {
892897
BoolExpr validPair = ctx.mkLt(ctx.mkInt(j), lengthExpr);
893898
BoolExpr notEqual =
894899
ctx.mkNot(ctx.mkEq(ctx.mkNth(seq, ctx.mkInt(i)), ctx.mkNth(seq, ctx.mkInt(j))));
895-
typeConstraints.add(ctx.mkImplies(validPair, notEqual));
900+
constraints.add(ctx.mkImplies(validPair, notEqual));
896901
}
897902
}
898903

@@ -907,7 +912,8 @@ private void applyBoundedMapBijection(
907912
ctx.mkStore(seqMap, ctx.mkNth(seq, ctx.mkInt(i)), ctx.mkTrue()),
908913
seqMap);
909914
}
910-
typeConstraints.add(ctx.mkImplies(isNotTruncated, ctx.mkEq(mapPresence, seqMap)));
915+
constraints.add(ctx.mkImplies(isNotTruncated, ctx.mkEq(mapPresence, seqMap)));
916+
return CelZ3TypeSystem.mkAndFlattened(ctx, constraints);
911917
}
912918

913919
private TranslatedValue[] evaluateLoopCondAndStep(
@@ -1239,9 +1245,10 @@ private BoolExpr createTypeConstraintForType(Expr<?> val, CelType type) {
12391245
}
12401246
Expr<?> optRef = typeSystem.getOptionalRef(val);
12411247
BoolExpr hasValue = typeSystem.optHasValue(optRef);
1242-
BoolExpr valConstraint =
1243-
createTypeConstraintForType(typeSystem.getOptionalValue(optRef), paramType);
1244-
return ctx.mkAnd(isOpt, ctx.mkImplies(hasValue, valConstraint));
1248+
Expr<?> optVal = typeSystem.getOptionalValue(optRef);
1249+
BoolExpr optValNotError = ctx.mkNot(typeSystem.isError(optVal));
1250+
BoolExpr valConstraint = createTypeConstraintForType(optVal, paramType);
1251+
return ctx.mkAnd(isOpt, ctx.mkImplies(hasValue, ctx.mkAnd(optValNotError, valConstraint)));
12451252
}
12461253
if (type.equals(SimpleType.BOOL)) {
12471254
return (BoolExpr) ctx.mkApp(typeSystem.boolCons().getTesterDecl(), val);
@@ -1269,16 +1276,25 @@ private BoolExpr createTypeConstraintForType(Expr<?> val, CelType type) {
12691276
if (type.equals(SimpleType.BYTES)) {
12701277
return (BoolExpr) ctx.mkApp(typeSystem.bytesCons().getTesterDecl(), val);
12711278
}
1279+
if (type.equals(SimpleType.TIMESTAMP)) {
1280+
IntExpr seconds = typeSystem.getTimestamp(val);
1281+
return ctx.mkAnd(
1282+
typeSystem.isTimestamp(val), ctx.mkNot(typeSystem.checkTimestampOverflow(seconds)));
1283+
}
1284+
if (type.equals(SimpleType.DURATION)) {
1285+
IntExpr seconds = typeSystem.getDuration(val);
1286+
return ctx.mkAnd(
1287+
typeSystem.isDuration(val), ctx.mkNot(typeSystem.checkDurationOverflow(seconds)));
1288+
}
1289+
12721290
if (type instanceof ListType) {
1273-
// Lists are explicitly bounded (sequence theory). We're safe in using for-all quantifiers
1274-
// here.
1291+
// Constrain list elements using bounded unrolling up to comprehensionUnrollLimit rather
1292+
// than Z3 forall quantifiers to prevent MBQI quantifier instantiation loops.
1293+
// Assert: isList(val) ∧ for all unrolled 0 <= i < length: ¬isError(seq[i]) ∧
1294+
// typeConstraint(seq[i])
12751295
BoolExpr isList = typeSystem.isList(val);
12761296
CelType elemType = ((ListType) type).elemType();
1277-
if (elemType.equals(SimpleType.DYN)) {
1278-
return isList;
1279-
}
12801297

1281-
// isList(val) ∧ ∀i. (0 <= i < length) ⇒ elemType(seq[i])
12821298
Expr<?> listRef = typeSystem.getListRef(val);
12831299
SeqExpr seq = typeSystem.getSeq(listRef);
12841300
Expr length = ctx.mkLength(seq);
@@ -1288,20 +1304,70 @@ private BoolExpr createTypeConstraintForType(Expr<?> val, CelType type) {
12881304
for (int i = 0; i < comprehensionUnrollLimit; i++) {
12891305
IntExpr idx = ctx.mkInt(i);
12901306
Expr elem = ctx.mkNth(seq, idx);
1291-
BoolExpr elemConstraint = createTypeConstraintForType(elem, elemType);
12921307
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())));
1308+
// Assert ¬isError(elem) as a domain invariant so Z3 never synthesizes an Error element in
1309+
// list(dyn). For concrete types, this is already implied by createTypeConstraintForType.
1310+
boundsAndTypes.add(ctx.mkImplies(validIndex, ctx.mkNot(typeSystem.isError(elem))));
1311+
// Short-circuit DYN element types to prevent generating redundant validIndex ⇒ TRUE
1312+
// clauses.
1313+
if (!elemType.equals(SimpleType.DYN)) {
1314+
BoolExpr elemConstraint = createTypeConstraintForType(elem, elemType);
1315+
boundsAndTypes.add(ctx.mkImplies(validIndex, elemConstraint));
1316+
}
12961317
}
12971318

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

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,10 @@ CelVerificationResult verifyImplication(
303303
/* isCounterexample= */ true));
304304
case TRUNCATED:
305305
return CelVerificationResult.inconclusive(
306-
String.format("Inconclusive: %s holds within the current loop unroll limit, but"
307-
+ " may be violated for larger collections.", subjectName.toLowerCase(Locale.US)));
306+
String.format(
307+
"Inconclusive: %s holds within the current loop unroll limit, but"
308+
+ " may be violated for larger collections.",
309+
subjectName.toLowerCase(Locale.US)));
308310
case NO_MATCH:
309311
return CelVerificationResult.verified();
310312
case SOLVER_UNKNOWN:

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

Lines changed: 38 additions & 19 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 =
@@ -173,14 +179,26 @@ private static String reconstructList(
173179

174180
private static String reconstructMap(
175181
Context ctx, CelZ3TypeSystem typeSystem, Model model, Expr<?> mapRef) {
176-
Expr<?> presenceArray =
182+
List<Expr<?>> keys = new ArrayList<>();
183+
Expr<?> lenExpr =
177184
evaluateStrict(
178185
model,
179-
typeSystem.getMapPresence(mapRef),
180-
String.format("Z3 failed to evaluate presence array natively for map %s", mapRef));
181-
182-
List<Expr<?>> keys = new ArrayList<>();
183-
extractKeys(presenceArray, keys);
186+
ctx.mkLength(typeSystem.getMapKeys(mapRef)),
187+
String.format("Z3 failed to evaluate length for map %s", mapRef));
188+
if (lenExpr instanceof IntNum) {
189+
int length = ((IntNum) lenExpr).getInt();
190+
int printLimit = Math.min(length, 100);
191+
for (int i = 0; i < printLimit; i++) {
192+
Expr<?> elem =
193+
evaluateStrict(
194+
model,
195+
ctx.mkNth(typeSystem.getMapKeys(mapRef), ctx.mkInt(i)),
196+
String.format("Z3 failed to evaluate map key at index %d for map %s", i, mapRef));
197+
if (!keys.contains(elem)) {
198+
keys.add(elem);
199+
}
200+
}
201+
}
184202

185203
List<String> entries = new ArrayList<>();
186204
for (Expr<?> key : keys) {
@@ -209,11 +227,11 @@ private static String reconstructMap(
209227

210228
private static String reconstructMessage(
211229
Context ctx, CelZ3TypeSystem typeSystem, Model model, Expr<?> msgRef) {
212-
Expr<?> valuesArray =
230+
Expr<?> presenceArray =
213231
evaluateStrict(
214232
model,
215-
typeSystem.getMsgValues(msgRef),
216-
String.format("Z3 failed to evaluate values array natively for msg %s", msgRef));
233+
typeSystem.getMsgPresence(msgRef),
234+
String.format("Z3 failed to evaluate presence array natively for msg %s", msgRef));
217235

218236
Expr<?> typeNameExpr =
219237
evaluateStrict(
@@ -224,7 +242,7 @@ private static String reconstructMessage(
224242
String typeName = formatExpr(ctx, typeSystem, model, typeNameExpr).replace("\"", "");
225243

226244
List<Expr<?>> keys = new ArrayList<>();
227-
extractKeys(valuesArray, keys);
245+
extractKeys(presenceArray, keys);
228246

229247
List<String> entries = new ArrayList<>();
230248
for (Expr<?> key : keys) {
@@ -262,16 +280,17 @@ private static void extractKeys(Expr<?> arrayExpr, List<Expr<?>> keys) {
262280
FuncDecl<?> decl = arrayExpr.getFuncDecl();
263281
String declName = decl.getName().toString();
264282

265-
if (!declName.equals("store")) {
266-
break;
283+
if (declName.equals("store")) {
284+
Expr<?>[] args = arrayExpr.getArgs();
285+
Preconditions.checkState(
286+
args.length == 3, "Z3 store array operation must have exactly 3 arguments");
287+
if (!keys.contains(args[1])) {
288+
keys.add(args[1]);
289+
}
290+
arrayExpr = args[0];
291+
continue;
267292
}
268-
269-
Expr<?>[] args = arrayExpr.getArgs();
270-
Preconditions.checkState(
271-
args.length == 3, "Z3 store array operation must have exactly 3 arguments");
272-
keys.add(args[1]);
273-
274-
arrayExpr = args[0];
293+
break;
275294
}
276295
}
277296

0 commit comments

Comments
 (0)