Skip to content

Commit 00b529a

Browse files
l46kokcopybara-github
authored andcommitted
Expose the verifier's counterexamples as structured CelWitness values
PiperOrigin-RevId: 1004026905
1 parent 1675f38 commit 00b529a

18 files changed

Lines changed: 757 additions & 369 deletions

‎verifier/README.md‎

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,8 @@ public class PolicyVerifierExample {
224224
System.out.println(result.message());
225225
// Output:
226226
// Equivalence violation detected. Counterexample input:
227-
// country = "a"
228227
// role = "editor"
228+
// country = "a"
229229
// port = 443
230230
break;
231231
case INCONCLUSIVE:
@@ -241,7 +241,8 @@ public class PolicyVerifierExample {
241241
When checking whether an expression is satisfiable using `isSatisfiable`, the
242242
verifier returns `VERIFIED` if there exists at least one input combination
243243
where the expression evaluates to `true`. Furthermore, `result.message()`
244-
provides concrete satisfying model assignments (witness/test case generation).
244+
provides concrete satisfying model assignments (witness/test case generation),
245+
and `result.witness()` exposes them as CEL runtime values.
245246

246247
```java
247248
import dev.cel.common.CelAbstractSyntaxTree;
@@ -370,14 +371,12 @@ public class InvariantsExample {
370371

371372
### Limitations
372373

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.
374+
* **Uninterpreted Functions:** Custom functions, and standard library
375+
functions that don't have SMT axioms yet, are treated as uninterpreted:
376+
the solver only knows that identical inputs yield identical outputs.
377+
Results that depend on their return values are `INCONCLUSIVE`, and a
378+
`VERIFIED` result assumes they return a value of their declared type
379+
rather than an error.
381380

382381
### Best Practices & Performance
383382

‎verifier/src/main/java/dev/cel/verifier/BUILD.bazel‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ java_library(
1515
"CelVerificationResult.java",
1616
"CelVerifier.java",
1717
"CelVerifierBuilder.java",
18+
"CelWitness.java",
1819
],
1920
tags = [
2021
],
@@ -23,6 +24,7 @@ java_library(
2324
"//common:cel_ast",
2425
"//common/types:type_providers",
2526
"@maven//:com_google_errorprone_error_prone_annotations",
27+
"@maven//:com_google_guava_guava",
2628
],
2729
)
2830

@@ -179,10 +181,13 @@ java_library(
179181
"//common/ast",
180182
"//common/ast:cel_block",
181183
"//common/internal:cel_descriptor_pools",
184+
"//common/internal:proto_time_utils",
182185
"//common/internal:well_known_proto",
183186
"//common/types",
184187
"//common/types:cel_types",
185188
"//common/types:type_providers",
189+
"//common/values",
190+
"//common/values:cel_byte_string",
186191
"//optimizer",
187192
"//optimizer:optimization_exception",
188193
"//optimizer:optimizer_builder",

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

Lines changed: 32 additions & 5 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.microsoft.z3.ArithExpr;
2122
import com.microsoft.z3.ArrayExpr;
@@ -51,6 +52,7 @@
5152
import java.util.ArrayList;
5253
import java.util.Arrays;
5354
import java.util.HashMap;
55+
import java.util.LinkedHashMap;
5456
import java.util.LinkedHashSet;
5557
import java.util.List;
5658
import java.util.Map;
@@ -82,6 +84,7 @@ final class CelAstToZ3Translator {
8284
private static final String EMPTY_MSG_REF_PREFIX = "!empty_msg_ref_";
8385
private static final String EMPTY_LIST_PREFIX = "!empty_list";
8486
private static final String EMPTY_MAP_PREFIX = "!empty_map";
87+
private static final String FUNCTION_PREFIX = "!fn_";
8588
private static final String NULL_VALUE_FIELD = "null_value";
8689
private static final long MAX_UINT32 = 0xFFFFFFFFL;
8790
private final Context ctx;
@@ -114,6 +117,11 @@ CelZ3TypeSystem getTypeSystem() {
114117
return typeSystem;
115118
}
116119

120+
/** Returns the input variables, in order of first appearance. */
121+
Set<Expr<?>> getInputVariables() {
122+
return inputVariableTypes.keySet();
123+
}
124+
117125
/**
118126
* Returns a Z3 boolean expression asserting that the provided CEL value is a boolean and is true.
119127
*/
@@ -820,21 +828,40 @@ private TranslatedValue translateCall(CelExpr expr, CelAbstractSyntaxTree ast) {
820828
Sort[] argSorts = new Sort[args.size()];
821829
Arrays.fill(argSorts, 0, args.size(), typeSystem.celValueSort());
822830
FuncDecl<?> funcDecl =
823-
typeSystem.internFuncDecl(functionName, argSorts, typeSystem.celValueSort());
831+
typeSystem.internFuncDecl(
832+
uninterpretedFunctionSymbol(exprId, ast),
833+
argSorts,
834+
typeSystem.celValueSort());
824835

825836
Expr<?>[] exprArgs = args.stream().map(TranslatedValue::z3Expr).toArray(Expr[]::new);
826837
Expr<?> callRes = ctx.mkApp(funcDecl, exprArgs);
827838
typeConstraints.add(createTypeConstraint(callRes, exprId, ast));
828839
typeConstraints.add(ctx.mkNot(typeSystem.isUnknown(callRes)));
829840
typeConstraints.add(ctx.mkNot(typeSystem.isError(callRes)));
830841

831-
boolean isDynamic = ast.getTypeOrThrow(exprId).equals(SimpleType.DYN);
832-
BoolExpr isApprox = ctx.mkBool(!isDynamic);
842+
// The solver picks the result freely, so a model depending on it is never exact.
833843
return TranslatedValue.propagateStrict(
834-
ctx, typeSystem, functionName, callRes, Optional.of(expr), isApprox, args);
844+
ctx,
845+
typeSystem,
846+
functionName,
847+
callRes,
848+
Optional.of(expr),
849+
/* baseTaint= */ ctx.mkTrue(),
850+
args);
835851
});
836852
}
837853

854+
/**
855+
* Returns the Z3 symbol for an uninterpreted call. Each set of candidate overloads gets its own
856+
* symbol, since distinct overloads may have different implementations even on equal values (e.g.
857+
* empty lists of different element types). The prefix keeps it apart from variables and internal
858+
* symbols.
859+
*/
860+
private static String uninterpretedFunctionSymbol(long exprId, CelAbstractSyntaxTree ast) {
861+
ImmutableList<String> overloadIds = ast.getReferenceOrThrow(exprId).overloadIds();
862+
return FUNCTION_PREFIX + String.join("|", ImmutableSortedSet.copyOf(overloadIds));
863+
}
864+
838865
/**
839866
* Whether a call may construct a container that embeds its args (e.g. list concatenation, map
840867
* insertion or optional.of). Indexing and conditionals return existing values instead, so
@@ -1556,7 +1583,7 @@ private Expr<?> mkParameterizedUnknown(CelExpr expr, CelAbstractSyntaxTree ast)
15561583
this.comprehensionUnrollLimit = comprehensionUnrollLimit;
15571584
this.typeSystem = new CelZ3TypeSystem(ctx, parameterizeUnknowns);
15581585
this.typeConstraints = new LinkedHashSet<>();
1559-
this.inputVariableTypes = new HashMap<>();
1586+
this.inputVariableTypes = new LinkedHashMap<>();
15601587
this.operatorTranslator =
15611588
new CelZ3OperatorTranslator(
15621589
ctx,

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

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
package dev.cel.verifier;
1616

1717
import com.google.auto.value.AutoValue;
18+
import java.util.Map;
19+
import java.util.Optional;
1820

1921
/** Result object containing the outcome of a CEL AST verification check. */
2022
@AutoValue
@@ -43,6 +45,12 @@ public enum VerificationStatus {
4345
*/
4446
public abstract String counterexample();
4547

48+
/**
49+
* Returns the counterexample or satisfying input found by the solver, if any. Its variables are
50+
* empty when the outcome doesn't depend on any input.
51+
*/
52+
public abstract Optional<CelWitness> witness();
53+
4654
/**
4755
* Returns a message detailing the outcome of the verification check, such as a counterexample
4856
* input, satisfying model assignments, or truncation reason. May be empty if status is VERIFIED
@@ -53,28 +61,65 @@ public String message() {
5361
}
5462

5563
static CelVerificationResult verified() {
56-
return new AutoValue_CelVerificationResult(VerificationStatus.VERIFIED, "", "");
64+
return create(VerificationStatus.VERIFIED, "");
5765
}
5866

59-
static CelVerificationResult verified(String reason) {
60-
return new AutoValue_CelVerificationResult(VerificationStatus.VERIFIED, reason, "");
67+
static CelVerificationResult verified(String reason, CelWitness satisfyingInput) {
68+
return create(
69+
VerificationStatus.VERIFIED, reason, satisfyingInput, /* isCounterexample= */ false);
6170
}
6271

6372
static CelVerificationResult failed(String reason) {
64-
return new AutoValue_CelVerificationResult(VerificationStatus.VIOLATED, reason, "");
73+
return create(VerificationStatus.VIOLATED, reason);
6574
}
6675

67-
static CelVerificationResult failed(String reason, String counterexample) {
68-
return new AutoValue_CelVerificationResult(
69-
VerificationStatus.VIOLATED, reason, counterexample);
76+
static CelVerificationResult failed(String reason, CelWitness counterexample) {
77+
return create(
78+
VerificationStatus.VIOLATED, reason, counterexample, /* isCounterexample= */ true);
7079
}
7180

7281
static CelVerificationResult inconclusive(String reason) {
73-
return new AutoValue_CelVerificationResult(VerificationStatus.INCONCLUSIVE, reason, "");
82+
return create(VerificationStatus.INCONCLUSIVE, reason);
83+
}
84+
85+
static CelVerificationResult inconclusive(
86+
String reason, CelWitness witness, boolean isCounterexample) {
87+
return create(VerificationStatus.INCONCLUSIVE, reason, witness, isCounterexample);
7488
}
7589

76-
static CelVerificationResult inconclusive(String reason, String counterexample) {
90+
private static CelVerificationResult create(VerificationStatus status, String reason) {
91+
return new AutoValue_CelVerificationResult(status, reason, "", Optional.empty());
92+
}
93+
94+
private static CelVerificationResult create(
95+
VerificationStatus status, String reason, CelWitness witness, boolean isCounterexample) {
7796
return new AutoValue_CelVerificationResult(
78-
VerificationStatus.INCONCLUSIVE, reason, counterexample);
97+
status,
98+
reason,
99+
describe(witness, status == VerificationStatus.INCONCLUSIVE, isCounterexample),
100+
Optional.of(witness));
101+
}
102+
103+
private static String describe(
104+
CelWitness witness, boolean isApproximate, boolean isCounterexample) {
105+
if (witness.variables().isEmpty()) {
106+
return isCounterexample
107+
? " (The expression fails unconditionally, regardless of input state)"
108+
: " (The expression is satisfiable unconditionally, regardless of input state)";
109+
}
110+
111+
StringBuilder sb = new StringBuilder();
112+
if (isCounterexample) {
113+
sb.append(isApproximate ? " Potential counterexample input:" : " Counterexample input:");
114+
} else {
115+
sb.append(isApproximate ? " Potential satisfying input:" : " Satisfying input:");
116+
}
117+
for (Map.Entry<String, CelWitness.Variable> variable : witness.variables().entrySet()) {
118+
sb.append("\n ")
119+
.append(variable.getKey())
120+
.append(" = ")
121+
.append(variable.getValue().displayValue());
122+
}
123+
return sb.toString();
79124
}
80125
}

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

Lines changed: 17 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -255,23 +255,13 @@ public CelVerificationResult verifyEquivalence(
255255
switch (result.outcome) {
256256
case EXACT_MATCH:
257257
return CelVerificationResult.failed(
258-
"Equivalence violation detected.",
259-
getCounterexampleString(
260-
ctx,
261-
translator.getTypeSystem(),
262-
result.model,
263-
/* isApproximate= */ false,
264-
/* isCounterexample= */ true));
258+
"Equivalence violation detected.", extractWitness(ctx, translator, result.model));
265259
case APPROXIMATE_MATCH:
266260
return CelVerificationResult.inconclusive(
267261
"Inconclusive: a divergence may exist, but it depends on approximations, missing"
268262
+ " theories, or loop bounds.",
269-
getCounterexampleString(
270-
ctx,
271-
translator.getTypeSystem(),
272-
result.model,
273-
/* isApproximate= */ true,
274-
/* isCounterexample= */ true));
263+
extractWitness(ctx, translator, result.model),
264+
/* isCounterexample= */ true);
275265
case TRUNCATED:
276266
return CelVerificationResult.inconclusive(
277267
"Inconclusive: expressions are equivalent within the current loop unroll limit, but"
@@ -342,22 +332,13 @@ CelVerificationResult verifyImplication(
342332
case EXACT_MATCH:
343333
return CelVerificationResult.failed(
344334
String.format("%s violation detected.", subjectName),
345-
getCounterexampleString(
346-
ctx,
347-
translator.getTypeSystem(),
348-
result.model,
349-
/* isApproximate= */ false,
350-
/* isCounterexample= */ true));
335+
extractWitness(ctx, translator, result.model));
351336
case APPROXIMATE_MATCH:
352337
return CelVerificationResult.inconclusive(
353338
"Inconclusive: a counterexample may exist, but it depends on approximations, missing"
354339
+ " theories, or loop bounds.",
355-
getCounterexampleString(
356-
ctx,
357-
translator.getTypeSystem(),
358-
result.model,
359-
/* isApproximate= */ true,
360-
/* isCounterexample= */ true));
340+
extractWitness(ctx, translator, result.model),
341+
/* isCounterexample= */ true);
361342
case TRUNCATED:
362343
return CelVerificationResult.inconclusive(
363344
String.format(
@@ -402,23 +383,10 @@ private CelVerificationResult checkSatisfiability(
402383

403384
switch (result.outcome) {
404385
case EXACT_MATCH:
386+
CelWitness witness = extractWitness(ctx, translator, result.model);
405387
return searchForCounterexample
406-
? CelVerificationResult.failed(
407-
"Condition is not always true.",
408-
getCounterexampleString(
409-
ctx,
410-
translator.getTypeSystem(),
411-
result.model,
412-
/* isApproximate= */ false,
413-
/* isCounterexample= */ true))
414-
: CelVerificationResult.verified(
415-
"Condition is satisfiable."
416-
+ getCounterexampleString(
417-
ctx,
418-
translator.getTypeSystem(),
419-
result.model,
420-
/* isApproximate= */ false,
421-
/* isCounterexample= */ false));
388+
? CelVerificationResult.failed("Condition is not always true.", witness)
389+
: CelVerificationResult.verified("Condition is satisfiable.", witness);
422390

423391
case APPROXIMATE_MATCH:
424392
String prefix =
@@ -429,12 +397,8 @@ private CelVerificationResult checkSatisfiability(
429397
+ " approximations, missing theories, or loop bounds.";
430398
return CelVerificationResult.inconclusive(
431399
prefix,
432-
getCounterexampleString(
433-
ctx,
434-
translator.getTypeSystem(),
435-
result.model,
436-
/* isApproximate= */ true,
437-
/* isCounterexample= */ searchForCounterexample));
400+
extractWitness(ctx, translator, result.model),
401+
/* isCounterexample= */ searchForCounterexample);
438402

439403
case TRUNCATED:
440404
return CelVerificationResult.inconclusive(
@@ -537,14 +501,13 @@ private CelAstToZ3Translator newTranslator(Context ctx, boolean parameterizeUnkn
537501
parameterizeUnknowns);
538502
}
539503

540-
private static String getCounterexampleString(
541-
Context ctx,
542-
CelZ3TypeSystem typeSystem,
543-
Model model,
544-
boolean isApproximate,
545-
boolean isCounterexample) {
504+
private CelWitness extractWitness(Context ctx, CelAstToZ3Translator translator, Model model) {
546505
return CelZ3CounterexampleGenerator.generate(
547-
ctx, typeSystem, model, isApproximate, isCounterexample);
506+
ctx,
507+
translator.getTypeSystem(),
508+
model,
509+
comprehensionUnrollLimit,
510+
translator.getInputVariables());
548511
}
549512

550513
private enum SolverOutcome {

0 commit comments

Comments
 (0)