Skip to content

Commit d1c0010

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: 955537654
1 parent 01ac8a5 commit d1c0010

14 files changed

Lines changed: 389 additions & 87 deletions

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

Lines changed: 6 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);

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 & 13 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

@@ -397,7 +397,7 @@ private BoolExpr getNumericEqualityWithConstant(
397397
? ctx.mkEq(typeSystem.getUint(symVal), ctx.mkInt(uintVal))
398398
: ctx.mkFalse();
399399
BoolExpr doubleEq =
400-
ctx.mkFPEq((FPExpr) typeSystem.getDouble(symVal), typeSystem.mkFpDouble(doubleVal));
400+
ctx.mkFPEq(typeSystem.getDouble(symVal), typeSystem.mkFpDouble(doubleVal));
401401

402402
return (BoolExpr)
403403
CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx)
@@ -436,17 +436,17 @@ private BoolExpr getStaticallyKnownNumericEquality(
436436
return ctx.mkEq(typeSystem.getUint(z3Expr0), typeSystem.getUint(z3Expr1));
437437
case DOUBLE:
438438
return ctx.mkFPEq(
439-
(FPExpr) typeSystem.getDouble(z3Expr0), (FPExpr) typeSystem.getDouble(z3Expr1));
439+
typeSystem.getDouble(z3Expr0), typeSystem.getDouble(z3Expr1));
440440
default:
441441
return ctx.mkFalse();
442442
}
443443
}
444444

445445
private BoolExpr mkIsFiniteDouble(Expr<?> z3Expr) {
446-
Expr<?> fpVal = typeSystem.getDouble(z3Expr);
446+
FPExpr fpVal = typeSystem.getDouble(z3Expr);
447447
return ctx.mkAnd(
448448
typeSystem.isDouble(z3Expr),
449-
ctx.mkNot(ctx.mkOr(ctx.mkFPIsNaN((FPExpr) fpVal), ctx.mkFPIsInfinite((FPExpr) fpVal))));
449+
ctx.mkNot(ctx.mkOr(ctx.mkFPIsNaN(fpVal), ctx.mkFPIsInfinite(fpVal))));
450450
}
451451

452452
private BoolExpr getDynamicNumericEquality(Expr<?> z3Expr0, Expr<?> z3Expr1) {
@@ -475,25 +475,29 @@ private BoolExpr getDynamicNumericEquality(Expr<?> z3Expr0, Expr<?> z3Expr1) {
475475
BoolExpr isIntOrUintAndDouble = ctx.mkAnd(isIntOrUint0, typeSystem.isDouble(z3Expr1));
476476
BoolExpr isDoubleAndIntOrUint = ctx.mkAnd(typeSystem.isDouble(z3Expr0), isIntOrUint1);
477477

478-
Expr<?> fpVal1 = typeSystem.getDouble(z3Expr1);
478+
FPExpr fpVal1 = typeSystem.getDouble(z3Expr1);
479+
ArithExpr<?> realVal0 = ctx.mkInt2Real(val0);
479480
BoolExpr intDoubleEq =
480481
ctx.mkAnd(
481482
mkIsFiniteDouble(z3Expr1),
482-
ctx.mkEq(ctx.mkInt2Real(val0), ctx.mkFPToReal((FPExpr) fpVal1)));
483+
ctx.mkLe(realVal0, ctx.mkFPToReal(fpVal1)),
484+
ctx.mkLe(ctx.mkFPToReal(fpVal1), realVal0));
483485

484-
Expr<?> fpVal0 = typeSystem.getDouble(z3Expr0);
486+
FPExpr fpVal0 = typeSystem.getDouble(z3Expr0);
487+
ArithExpr<?> realVal1 = ctx.mkInt2Real(val1);
485488
BoolExpr doubleIntEq =
486489
ctx.mkAnd(
487490
mkIsFiniteDouble(z3Expr0),
488-
ctx.mkEq(ctx.mkFPToReal((FPExpr) fpVal0), ctx.mkInt2Real(val1)));
491+
ctx.mkLe(realVal1, ctx.mkFPToReal(fpVal0)),
492+
ctx.mkLe(ctx.mkFPToReal(fpVal0), realVal1));
489493

490494
return (BoolExpr)
491495
CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx)
492496
.addCase(bothIntOrUint, ctx.mkEq(val0, val1))
493497
.addCase(
494498
bothDouble,
495499
ctx.mkFPEq(
496-
(FPExpr) typeSystem.getDouble(z3Expr0), (FPExpr) typeSystem.getDouble(z3Expr1)))
500+
typeSystem.getDouble(z3Expr0), typeSystem.getDouble(z3Expr1)))
497501
.addCase(isIntOrUintAndDouble, intDoubleEq)
498502
.addCase(isDoubleAndIntOrUint, doubleIntEq)
499503
.build(ctx.mkFalse());

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

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ public final class CelZ3TypeSystem {
8282
private static final String IS_BYTES = "isBytes";
8383
private static final String GET_BYTES = "getBytes";
8484

85+
private static final String CONS_TIMESTAMP = "Timestamp";
86+
private static final String IS_TIMESTAMP = "isTimestamp";
87+
private static final String GET_TIMESTAMP = "getTimestamp";
88+
89+
private static final String CONS_DURATION = "Duration";
90+
private static final String IS_DURATION = "isDuration";
91+
private static final String GET_DURATION = "getDuration";
92+
8593
private static final String CONS_ERROR = "CelError";
8694
private static final String IS_ERROR = "isError";
8795

@@ -170,6 +178,8 @@ public int hashCode() {
170178
private final Constructor doubleCons;
171179
private final Constructor stringCons;
172180
private final Constructor bytesCons;
181+
private final Constructor timestampCons;
182+
private final Constructor durationCons;
173183
private final Constructor errorCons;
174184
private final Constructor unknownCons;
175185
private final Constructor nullCons;
@@ -233,30 +243,38 @@ public Sort listRefSort() {
233243
return listRefSort;
234244
}
235245

236-
Constructor boolCons() {
246+
public Constructor boolCons() {
237247
return boolCons;
238248
}
239249

240-
Constructor intCons() {
250+
public Constructor intCons() {
241251
return intCons;
242252
}
243253

244-
Constructor uintCons() {
254+
public Constructor uintCons() {
245255
return uintCons;
246256
}
247257

248-
Constructor doubleCons() {
258+
public Constructor doubleCons() {
249259
return doubleCons;
250260
}
251261

252-
Constructor stringCons() {
262+
public Constructor stringCons() {
253263
return stringCons;
254264
}
255265

256-
Constructor bytesCons() {
266+
public Constructor bytesCons() {
257267
return bytesCons;
258268
}
259269

270+
public Constructor timestampCons() {
271+
return timestampCons;
272+
}
273+
274+
public Constructor durationCons() {
275+
return durationCons;
276+
}
277+
260278
Constructor optionalCons() {
261279
return optionalCons;
262280
}
@@ -296,6 +314,16 @@ public Expr<?> wrapBytes(Expr<?> expr) {
296314
return ctx.mkApp(bytesCons.ConstructorDecl(), expr);
297315
}
298316

317+
/** Wraps a Z3 integer expression into a timestamp CelValue. */
318+
public Expr<?> wrapTimestamp(IntExpr expr) {
319+
return ctx.mkApp(timestampCons.ConstructorDecl(), expr);
320+
}
321+
322+
/** Wraps a Z3 integer expression into a duration CelValue. */
323+
public Expr<?> wrapDuration(IntExpr expr) {
324+
return ctx.mkApp(durationCons.ConstructorDecl(), expr);
325+
}
326+
299327
/** Creates a CelValue containing an integer. */
300328
public Expr<?> mkInt(long val) {
301329
return ctx.mkApp(intCons.ConstructorDecl(), ctx.mkInt(val));
@@ -326,8 +354,8 @@ public BoolExpr isDouble(Expr<?> val) {
326354
}
327355

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

333361
/**
@@ -372,7 +400,8 @@ public BoolExpr getStructuralEquality(Expr<?> arg0, Expr<?> arg1) {
372400
// Doubles must be compared using native floating-point equality to follow IEEE-754.
373401
// Z3's structural mkEq evaluates NaN == NaN as true and 0.0 == -0.0 as false.
374402
BoolExpr isDoubleEq = ctx.mkAnd(isDouble(arg0), isDouble(arg1));
375-
BoolExpr doubleEq = ctx.mkFPEq((FPExpr) getDouble(arg0), (FPExpr) getDouble(arg1));
403+
BoolExpr doubleEq = ctx.mkFPEq(getDouble(arg0), getDouble(arg1));
404+
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,19 @@ 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(253402300799L)), // 9999-12-31T23:59:59Z
779+
ctx.mkLt(result, ctx.mkInt(-62135596800L))); // 0001-01-01T00:00:00Z
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(315576000000L)), ctx.mkLt(result, ctx.mkInt(-315576000000L)));
786+
}
787+
722788
/** Checks if the given arithmetic expression overflows a 64-bit unsigned integer. */
723789
public BoolExpr checkUintOverflow(ArithExpr result) {
724790
return ctx.mkOr(ctx.mkGt(result, ctx.mkInt(MAX_UINT64)), ctx.mkLt(result, ctx.mkInt(0)));
@@ -890,6 +956,20 @@ public static BoolExpr mkNotFlattened(Context ctx, BoolExpr arg) {
890956
this.bytesCons =
891957
ctx.mkConstructor(
892958
CONS_BYTES, IS_BYTES, new String[] {GET_BYTES}, new Sort[] {ctx.getStringSort()}, null);
959+
this.timestampCons =
960+
ctx.mkConstructor(
961+
CONS_TIMESTAMP,
962+
IS_TIMESTAMP,
963+
new String[] {GET_TIMESTAMP},
964+
new Sort[] {ctx.getIntSort()},
965+
null);
966+
this.durationCons =
967+
ctx.mkConstructor(
968+
CONS_DURATION,
969+
IS_DURATION,
970+
new String[] {GET_DURATION},
971+
new Sort[] {ctx.getIntSort()},
972+
null);
893973
this.errorCons = ctx.mkConstructor(CONS_ERROR, IS_ERROR, null, null, null);
894974

895975
this.unknownIdSort = ctx.mkUninterpretedSort("UnknownId");
@@ -936,6 +1016,8 @@ public static BoolExpr mkNotFlattened(Context ctx, BoolExpr arg) {
9361016
this.doubleCons,
9371017
this.stringCons,
9381018
this.bytesCons,
1019+
this.timestampCons,
1020+
this.durationCons,
9391021
this.errorCons,
9401022
this.unknownCons,
9411023
this.optionalCons,

0 commit comments

Comments
 (0)