Skip to content

Commit eb6fc20

Browse files
l46kokcopybara-github
authored andcommitted
Introduce timestamp/duration as first class types in verifier. Add arithmetic axioms for the two types along with type conversions
PiperOrigin-RevId: 957414761
1 parent 8d150b2 commit eb6fc20

17 files changed

Lines changed: 397 additions & 135 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: 1 addition & 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

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: 17 additions & 0 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);
@@ -1269,6 +1275,17 @@ private BoolExpr createTypeConstraintForType(Expr<?> val, CelType type) {
12691275
if (type.equals(SimpleType.BYTES)) {
12701276
return (BoolExpr) ctx.mkApp(typeSystem.bytesCons().getTesterDecl(), val);
12711277
}
1278+
if (type.equals(SimpleType.TIMESTAMP)) {
1279+
IntExpr seconds = typeSystem.getTimestamp(val);
1280+
return ctx.mkAnd(
1281+
typeSystem.isTimestamp(val), ctx.mkNot(typeSystem.checkTimestampOverflow(seconds)));
1282+
}
1283+
if (type.equals(SimpleType.DURATION)) {
1284+
IntExpr seconds = typeSystem.getDuration(val);
1285+
return ctx.mkAnd(
1286+
typeSystem.isDuration(val), ctx.mkNot(typeSystem.checkDurationOverflow(seconds)));
1287+
}
1288+
12721289
if (type instanceof ListType) {
12731290
// Lists are explicitly bounded (sequence theory). We're safe in using for-all quantifiers
12741291
// here.

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

Lines changed: 4 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())) {

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

Lines changed: 17 additions & 16 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());

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

Lines changed: 94 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.microsoft.z3.SeqExpr;
3232
import com.microsoft.z3.SeqSort;
3333
import com.microsoft.z3.Sort;
34+
import dev.cel.common.internal.ProtoTimeUtils;
3435
import java.util.ArrayList;
3536
import java.util.Arrays;
3637
import java.util.Collection;
@@ -82,6 +83,14 @@ public final class CelZ3TypeSystem {
8283
private static final String IS_BYTES = "isBytes";
8384
private static final String GET_BYTES = "getBytes";
8485

86+
private static final String CONS_TIMESTAMP = "Timestamp";
87+
private static final String IS_TIMESTAMP = "isTimestamp";
88+
private static final String GET_TIMESTAMP = "getTimestamp";
89+
90+
private static final String CONS_DURATION = "Duration";
91+
private static final String IS_DURATION = "isDuration";
92+
private static final String GET_DURATION = "getDuration";
93+
8594
private static final String CONS_ERROR = "CelError";
8695
private static final String IS_ERROR = "isError";
8796

@@ -170,6 +179,8 @@ public int hashCode() {
170179
private final Constructor doubleCons;
171180
private final Constructor stringCons;
172181
private final Constructor bytesCons;
182+
private final Constructor timestampCons;
183+
private final Constructor durationCons;
173184
private final Constructor errorCons;
174185
private final Constructor unknownCons;
175186
private final Constructor nullCons;
@@ -233,30 +244,38 @@ public Sort listRefSort() {
233244
return listRefSort;
234245
}
235246

236-
Constructor boolCons() {
247+
public Constructor boolCons() {
237248
return boolCons;
238249
}
239250

240-
Constructor intCons() {
251+
public Constructor intCons() {
241252
return intCons;
242253
}
243254

244-
Constructor uintCons() {
255+
public Constructor uintCons() {
245256
return uintCons;
246257
}
247258

248-
Constructor doubleCons() {
259+
public Constructor doubleCons() {
249260
return doubleCons;
250261
}
251262

252-
Constructor stringCons() {
263+
public Constructor stringCons() {
253264
return stringCons;
254265
}
255266

256-
Constructor bytesCons() {
267+
public Constructor bytesCons() {
257268
return bytesCons;
258269
}
259270

271+
public Constructor timestampCons() {
272+
return timestampCons;
273+
}
274+
275+
public Constructor durationCons() {
276+
return durationCons;
277+
}
278+
260279
Constructor optionalCons() {
261280
return optionalCons;
262281
}
@@ -296,6 +315,16 @@ public Expr<?> wrapBytes(Expr<?> expr) {
296315
return ctx.mkApp(bytesCons.ConstructorDecl(), expr);
297316
}
298317

318+
/** Wraps a Z3 integer expression into a timestamp CelValue. */
319+
public Expr<?> wrapTimestamp(IntExpr expr) {
320+
return ctx.mkApp(timestampCons.ConstructorDecl(), expr);
321+
}
322+
323+
/** Wraps a Z3 integer expression into a duration CelValue. */
324+
public Expr<?> wrapDuration(IntExpr expr) {
325+
return ctx.mkApp(durationCons.ConstructorDecl(), expr);
326+
}
327+
299328
/** Creates a CelValue containing an integer. */
300329
public Expr<?> mkInt(long val) {
301330
return ctx.mkApp(intCons.ConstructorDecl(), ctx.mkInt(val));
@@ -326,8 +355,8 @@ public BoolExpr isDouble(Expr<?> val) {
326355
}
327356

328357
/** Extracts the double reference from a double CelValue. */
329-
public Expr<?> getDouble(Expr<?> val) {
330-
return ctx.mkApp(doubleCons.getAccessorDecls()[0], val);
358+
public FPExpr getDouble(Expr<?> val) {
359+
return (FPExpr) ctx.mkApp(doubleCons.getAccessorDecls()[0], val);
331360
}
332361

333362
/**
@@ -372,7 +401,7 @@ public BoolExpr getStructuralEquality(Expr<?> arg0, Expr<?> arg1) {
372401
// Doubles must be compared using native floating-point equality to follow IEEE-754.
373402
// Z3's structural mkEq evaluates NaN == NaN as true and 0.0 == -0.0 as false.
374403
BoolExpr isDoubleEq = ctx.mkAnd(isDouble(arg0), isDouble(arg1));
375-
BoolExpr doubleEq = ctx.mkFPEq((FPExpr) getDouble(arg0), (FPExpr) getDouble(arg1));
404+
BoolExpr doubleEq = ctx.mkFPEq(getDouble(arg0), getDouble(arg1));
376405

377406
// For primitives, generic equality matches the direct Z3 datatype wrapper.
378407
BoolExpr genericEq = ctx.mkEq(arg0, arg1);
@@ -409,10 +438,14 @@ public Expr<?> mkNull() {
409438
return ctx.mkConst(nullCons.ConstructorDecl());
410439
}
411440

412-
Constructor errorCons() {
441+
public Constructor errorCons() {
413442
return errorCons;
414443
}
415444

445+
public Constructor nullCons() {
446+
return nullCons;
447+
}
448+
416449
/** Creates a CelValue representing an unknown value. */
417450
public Expr<?> mkUnknown() {
418451
return mkUnknown(ctx.mkConst(GENERIC_UNKNOWN_ID, unknownIdSort));
@@ -498,7 +531,7 @@ public Expr<?> withRuntimeError(
498531
return ctx.mkITE(condition, mkError(), result);
499532
}
500533

501-
Constructor unknownCons() {
534+
public Constructor unknownCons() {
502535
return unknownCons;
503536
}
504537

@@ -582,6 +615,26 @@ public IntExpr getUint(Expr<?> val) {
582615
return (IntExpr) ctx.mkApp(uintCons.getAccessorDecls()[0], val);
583616
}
584617

618+
/** Checks if the given CelValue is a timestamp. */
619+
public BoolExpr isTimestamp(Expr<?> val) {
620+
return (BoolExpr) ctx.mkApp(timestampCons.getTesterDecl(), val);
621+
}
622+
623+
/** Extracts the integer expression from a timestamp CelValue. */
624+
public IntExpr getTimestamp(Expr<?> val) {
625+
return (IntExpr) ctx.mkApp(timestampCons.getAccessorDecls()[0], val);
626+
}
627+
628+
/** Checks if the given CelValue is a duration. */
629+
public BoolExpr isDuration(Expr<?> val) {
630+
return (BoolExpr) ctx.mkApp(durationCons.getTesterDecl(), val);
631+
}
632+
633+
/** Extracts the integer expression from a duration CelValue. */
634+
public IntExpr getDuration(Expr<?> val) {
635+
return (IntExpr) ctx.mkApp(durationCons.getAccessorDecls()[0], val);
636+
}
637+
585638
/** Checks if the given CelValue is a string. */
586639
public BoolExpr isString(Expr<?> val) {
587640
return (BoolExpr) ctx.mkApp(stringCons.getTesterDecl(), val);
@@ -719,6 +772,20 @@ public BoolExpr checkIntOverflow(ArithExpr result) {
719772
return ctx.mkOr(ctx.mkGt(result, ctx.mkInt(MAX_INT64)), ctx.mkLt(result, ctx.mkInt(MIN_INT64)));
720773
}
721774

775+
/** Checks if the given arithmetic expression overflows CEL Timestamp bounds. */
776+
public BoolExpr checkTimestampOverflow(ArithExpr result) {
777+
return ctx.mkOr(
778+
ctx.mkGt(result, ctx.mkInt(ProtoTimeUtils.TIMESTAMP_SECONDS_MAX)),
779+
ctx.mkLt(result, ctx.mkInt(ProtoTimeUtils.TIMESTAMP_SECONDS_MIN)));
780+
}
781+
782+
/** Checks if the given arithmetic expression overflows CEL Duration bounds. */
783+
public BoolExpr checkDurationOverflow(ArithExpr result) {
784+
return ctx.mkOr(
785+
ctx.mkGt(result, ctx.mkInt(ProtoTimeUtils.DURATION_SECONDS_MAX)),
786+
ctx.mkLt(result, ctx.mkInt(ProtoTimeUtils.DURATION_SECONDS_MIN)));
787+
}
788+
722789
/** Checks if the given arithmetic expression overflows a 64-bit unsigned integer. */
723790
public BoolExpr checkUintOverflow(ArithExpr result) {
724791
return ctx.mkOr(ctx.mkGt(result, ctx.mkInt(MAX_UINT64)), ctx.mkLt(result, ctx.mkInt(0)));
@@ -890,6 +957,20 @@ public static BoolExpr mkNotFlattened(Context ctx, BoolExpr arg) {
890957
this.bytesCons =
891958
ctx.mkConstructor(
892959
CONS_BYTES, IS_BYTES, new String[] {GET_BYTES}, new Sort[] {ctx.getStringSort()}, null);
960+
this.timestampCons =
961+
ctx.mkConstructor(
962+
CONS_TIMESTAMP,
963+
IS_TIMESTAMP,
964+
new String[] {GET_TIMESTAMP},
965+
new Sort[] {ctx.getIntSort()},
966+
null);
967+
this.durationCons =
968+
ctx.mkConstructor(
969+
CONS_DURATION,
970+
IS_DURATION,
971+
new String[] {GET_DURATION},
972+
new Sort[] {ctx.getIntSort()},
973+
null);
893974
this.errorCons = ctx.mkConstructor(CONS_ERROR, IS_ERROR, null, null, null);
894975

895976
this.unknownIdSort = ctx.mkUninterpretedSort("UnknownId");
@@ -936,6 +1017,8 @@ public static BoolExpr mkNotFlattened(Context ctx, BoolExpr arg) {
9361017
this.doubleCons,
9371018
this.stringCons,
9381019
this.bytesCons,
1020+
this.timestampCons,
1021+
this.durationCons,
9391022
this.errorCons,
9401023
this.unknownCons,
9411024
this.optionalCons,

0 commit comments

Comments
 (0)