Skip to content

Commit 1ad562d

Browse files
l46kokcopybara-github
authored andcommitted
Give each uninterpreted overload its own solver symbol
PiperOrigin-RevId: 1003517315
1 parent 04050ab commit 1ad562d

3 files changed

Lines changed: 93 additions & 19 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: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import com.google.common.collect.ImmutableList;
1818
import com.google.common.collect.ImmutableSet;
19+
import com.google.common.collect.ImmutableSortedSet;
1920
import com.google.common.collect.Iterables;
2021
import com.google.common.collect.Streams;
2122
import com.microsoft.z3.ArithExpr;
@@ -83,6 +84,7 @@ final class CelAstToZ3Translator {
8384
private static final String EMPTY_MSG_REF_PREFIX = "!empty_msg_ref_";
8485
private static final String EMPTY_LIST_PREFIX = "!empty_list";
8586
private static final String EMPTY_MAP_PREFIX = "!empty_map";
87+
private static final String FUNCTION_PREFIX = "!fn_";
8688
private static final String NULL_VALUE_FIELD = "null_value";
8789
private static final String SECONDS_FIELD = "seconds";
8890
private static final String NANOS_FIELD = "nanos";
@@ -927,21 +929,40 @@ private TranslatedValue translateCall(CelExpr expr, CelAbstractSyntaxTree ast) {
927929
Sort[] argSorts = new Sort[args.size()];
928930
Arrays.fill(argSorts, 0, args.size(), typeSystem.celValueSort());
929931
FuncDecl<?> funcDecl =
930-
typeSystem.internFuncDecl(functionName, argSorts, typeSystem.celValueSort());
932+
typeSystem.internFuncDecl(
933+
uninterpretedFunctionSymbol(exprId, ast),
934+
argSorts,
935+
typeSystem.celValueSort());
931936

932937
Expr<?>[] exprArgs = args.stream().map(TranslatedValue::z3Expr).toArray(Expr[]::new);
933938
Expr<?> callRes = ctx.mkApp(funcDecl, exprArgs);
934939
typeConstraints.add(createTypeConstraint(callRes, exprId, ast));
935940
typeConstraints.add(ctx.mkNot(typeSystem.isUnknown(callRes)));
936941
typeConstraints.add(ctx.mkNot(typeSystem.isError(callRes)));
937942

938-
boolean isDynamic = ast.getTypeOrThrow(exprId).equals(SimpleType.DYN);
939-
BoolExpr isApprox = ctx.mkBool(!isDynamic);
943+
// The solver picks the result freely, so a model depending on it is never exact.
940944
return TranslatedValue.propagateStrict(
941-
ctx, typeSystem, functionName, callRes, Optional.of(expr), isApprox, args);
945+
ctx,
946+
typeSystem,
947+
functionName,
948+
callRes,
949+
Optional.of(expr),
950+
/* baseTaint= */ ctx.mkTrue(),
951+
args);
942952
});
943953
}
944954

955+
/**
956+
* Returns the Z3 symbol for an uninterpreted call. Each set of candidate overloads gets its own
957+
* symbol, since distinct overloads may have different implementations even on equal values (e.g.
958+
* empty lists of different element types). The prefix keeps it apart from variables and internal
959+
* symbols.
960+
*/
961+
private static String uninterpretedFunctionSymbol(long exprId, CelAbstractSyntaxTree ast) {
962+
ImmutableList<String> overloadIds = ast.getReferenceOrThrow(exprId).overloadIds();
963+
return FUNCTION_PREFIX + String.join("|", ImmutableSortedSet.copyOf(overloadIds));
964+
}
965+
945966
/**
946967
* Whether a call may construct a container that embeds its args (e.g. list concatenation, map
947968
* insertion or optional.of). Indexing and conditionals return existing values instead, so

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

Lines changed: 62 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,46 @@ 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 isAlwaysTrue_distinctFunctionSymbols_inconclusive(
2807+
@TestParameter({
2808+
// Member and global overloads of the same name.
2809+
"fx(role, country) == role.fx(country)",
2810+
// Overloads whose argument values coincide (empty lists of different element types).
2811+
"size(int_list) == 0 && size(string_list) == 0 ? f(int_list) == f(string_list) : true",
2812+
// A zero-arity function and a variable of the same name.
2813+
"g() == g"
2814+
})
2815+
String expression)
2816+
throws Exception {
2817+
CelCompiler celCompiler =
2818+
CEL.toCompilerBuilder()
2819+
.addVar("string_list", ListType.create(SimpleType.STRING))
2820+
.addVar("g", SimpleType.INT)
2821+
.addFunctionDeclarations(
2822+
CelFunctionDecl.newFunctionDeclaration(
2823+
"fx",
2824+
CelOverloadDecl.newGlobalOverload(
2825+
"fx_global", SimpleType.BOOL, SimpleType.STRING, SimpleType.STRING),
2826+
CelOverloadDecl.newMemberOverload(
2827+
"fx_member", SimpleType.BOOL, SimpleType.STRING, SimpleType.STRING)),
2828+
CelFunctionDecl.newFunctionDeclaration(
2829+
"f",
2830+
CelOverloadDecl.newGlobalOverload(
2831+
"f_list_int", SimpleType.INT, ListType.create(SimpleType.INT)),
2832+
CelOverloadDecl.newGlobalOverload(
2833+
"f_list_string", SimpleType.INT, ListType.create(SimpleType.STRING))),
2834+
CelFunctionDecl.newFunctionDeclaration(
2835+
"g", CelOverloadDecl.newGlobalOverload("g_zero", SimpleType.INT)))
2836+
.build();
2837+
CelAbstractSyntaxTree ast = celCompiler.compile(expression).getAst();
2838+
CelVerificationResult result = VERIFIER.isAlwaysTrue(ast);
2839+
2840+
assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE);
2841+
}
2842+
2843+
@Test
2844+
public void verifyEquivalence_identicalFunctionCalls_equivalent() throws Exception {
2845+
// Identical calls to an uninterpreted function always produce identical results.
27912846
CelCompiler celCompiler =
27922847
CEL.toCompilerBuilder()
27932848
.setContainer(CelContainer.ofName("cel.expr.conformance.proto3"))

0 commit comments

Comments
 (0)