Skip to content

Commit 4b81ecf

Browse files
l46kokcopybara-github
authored andcommitted
Treat all uninterpreted function results as approximate
PiperOrigin-RevId: 1003496089
1 parent 04050ab commit 4b81ecf

3 files changed

Lines changed: 38 additions & 18 deletions

File tree

‎verifier/README.md‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -370,14 +370,12 @@ public class InvariantsExample {
370370

371371
### Limitations
372372

373-
* **Unsupported Standard Functions (Uninterpreted Functions):** Not all
374-
CEL standard library functions have SMT axioms defined yet. Unsupported
375-
functions are treated as *uninterpreted functions* by Z3 (the solver
376-
only guarantees that identical inputs yield identical outputs, but does
377-
not understand the function's internal logic or return types).
378-
Consequently, verifications that rely on the specific semantics of
379-
these functions may return `VerificationStatus.INCONCLUSIVE`. Support for
380-
more standard library functions will be added incrementally.
373+
* **Uninterpreted Functions:** Custom functions, and standard library
374+
functions that don't have SMT axioms yet, are treated as uninterpreted:
375+
the solver only knows that identical inputs yield identical outputs.
376+
Results that depend on their return values are `INCONCLUSIVE`, and a
377+
`VERIFIED` result assumes they return a value of their declared type
378+
rather than an error.
381379

382380
### Best Practices & Performance
383381

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -935,10 +935,15 @@ private TranslatedValue translateCall(CelExpr expr, CelAbstractSyntaxTree ast) {
935935
typeConstraints.add(ctx.mkNot(typeSystem.isUnknown(callRes)));
936936
typeConstraints.add(ctx.mkNot(typeSystem.isError(callRes)));
937937

938-
boolean isDynamic = ast.getTypeOrThrow(exprId).equals(SimpleType.DYN);
939-
BoolExpr isApprox = ctx.mkBool(!isDynamic);
938+
// The solver picks the result freely, so a model depending on it is never exact.
940939
return TranslatedValue.propagateStrict(
941-
ctx, typeSystem, functionName, callRes, Optional.of(expr), isApprox, args);
940+
ctx,
941+
typeSystem,
942+
functionName,
943+
callRes,
944+
Optional.of(expr),
945+
/* baseTaint= */ ctx.mkTrue(),
946+
args);
942947
});
943948
}
944949

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

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2750,9 +2750,9 @@ public void isAlwaysTrue_operationError_counterexampleFormat() throws Exception
27502750
}
27512751

27522752
@Test
2753-
public void isAlwaysTrue_dynamicFunction_nan_violated() throws Exception {
2754-
// request() == request() is a tautology only if request() does not evaluate to NaN.
2755-
// Since request() returns DYN, Z3 can assign NaN to its return value, and NaN == NaN is false.
2753+
public void isAlwaysTrue_dynamicFunction_inconclusive() throws Exception {
2754+
// request() == request() fails only if request() returns NaN. The solver may pick NaN for an
2755+
// uninterpreted function, but the actual function may never return it.
27562756
CelCompiler celCompiler =
27572757
CEL.toCompilerBuilder()
27582758
.setContainer(CelContainer.ofName("cel.expr.conformance.proto3"))
@@ -2764,8 +2764,26 @@ public void isAlwaysTrue_dynamicFunction_nan_violated() throws Exception {
27642764
CelAbstractSyntaxTree ast = celCompiler.compile("request() == request()").getAst();
27652765
CelVerificationResult result = VERIFIER.isAlwaysTrue(ast);
27662766

2767+
assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE);
2768+
assertThat(result.message()).contains("depends on approximations");
2769+
}
2770+
2771+
@Test
2772+
public void isAlwaysTrue_dynamicFunctionShortCircuited_violated() throws Exception {
2773+
// x == 0 falsifies the condition regardless of request()'s value.
2774+
CelCompiler celCompiler =
2775+
CEL.toCompilerBuilder()
2776+
.setContainer(CelContainer.ofName("cel.expr.conformance.proto3"))
2777+
.addFunctionDeclarations(
2778+
CelFunctionDecl.newFunctionDeclaration(
2779+
"request",
2780+
CelOverloadDecl.newGlobalOverload("request_overload", SimpleType.DYN)))
2781+
.build();
2782+
CelAbstractSyntaxTree ast = celCompiler.compile("x != 0 && request() == 1").getAst();
2783+
CelVerificationResult result = VERIFIER.isAlwaysTrue(ast);
2784+
27672785
assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED);
2768-
assertThat(result.message()).contains("request = NaN");
2786+
assertThat(result.message()).contains("x = 0");
27692787
}
27702788

27712789
@Test
@@ -2785,9 +2803,8 @@ public void isAlwaysTrue_boolFunction_verified() throws Exception {
27852803
}
27862804

27872805
@Test
2788-
public void verifyEquivalence_functionError_equivalent() throws Exception {
2789-
// request() and request() are equivalent, even if request() throws an Error,
2790-
// because Error == Error structurally in Z3 equivalence.
2806+
public void verifyEquivalence_identicalFunctionCalls_equivalent() throws Exception {
2807+
// Identical calls to an uninterpreted function always produce identical results.
27912808
CelCompiler celCompiler =
27922809
CEL.toCompilerBuilder()
27932810
.setContainer(CelContainer.ofName("cel.expr.conformance.proto3"))

0 commit comments

Comments
 (0)