Skip to content

Commit 0be1068

Browse files
l46kokcopybara-github
authored andcommitted
Expose the verifier's counterexamples as structured CelWitness values
PiperOrigin-RevId: 1004026905
1 parent 2593150 commit 0be1068

12 files changed

Lines changed: 644 additions & 294 deletions

‎verifier/README.md‎

Lines changed: 3 additions & 2 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;

‎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

@@ -186,10 +188,13 @@ java_library(
186188
"//common/ast",
187189
"//common/ast:cel_block",
188190
"//common/internal:cel_descriptor_pools",
191+
"//common/internal:proto_time_utils",
189192
"//common/internal:well_known_proto",
190193
"//common/types",
191194
"//common/types:cel_types",
192195
"//common/types:type_providers",
196+
"//common/values",
197+
"//common/values:cel_byte_string",
193198
"//optimizer",
194199
"//optimizer:optimization_exception",
195200
"//optimizer:optimizer_builder",

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ CelZ3TypeSystem getTypeSystem() {
9292
return typeSystem;
9393
}
9494

95+
/** Returns the input variables, in order of first appearance. */
96+
Set<Expr<?>> getInputVariables() {
97+
return context.inputVariables();
98+
}
99+
95100
/**
96101
* Returns a Z3 boolean expression asserting that the provided CEL value is a boolean and is true.
97102
*/

‎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
@@ -256,23 +256,13 @@ public CelVerificationResult verifyEquivalence(
256256
switch (result.outcome) {
257257
case EXACT_MATCH:
258258
return CelVerificationResult.failed(
259-
"Equivalence violation detected.",
260-
getCounterexampleString(
261-
ctx,
262-
translator.getTypeSystem(),
263-
result.model,
264-
/* isApproximate= */ false,
265-
/* isCounterexample= */ true));
259+
"Equivalence violation detected.", extractWitness(ctx, translator, result.model));
266260
case APPROXIMATE_MATCH:
267261
return CelVerificationResult.inconclusive(
268262
"Inconclusive: a divergence may exist, but it depends on approximations, missing"
269263
+ " theories, or loop bounds.",
270-
getCounterexampleString(
271-
ctx,
272-
translator.getTypeSystem(),
273-
result.model,
274-
/* isApproximate= */ true,
275-
/* isCounterexample= */ true));
264+
extractWitness(ctx, translator, result.model),
265+
/* isCounterexample= */ true);
276266
case TRUNCATED:
277267
return CelVerificationResult.inconclusive(
278268
"Inconclusive: expressions are equivalent within the current loop unroll limit, but"
@@ -343,22 +333,13 @@ CelVerificationResult verifyImplication(
343333
case EXACT_MATCH:
344334
return CelVerificationResult.failed(
345335
String.format("%s violation detected.", subjectName),
346-
getCounterexampleString(
347-
ctx,
348-
translator.getTypeSystem(),
349-
result.model,
350-
/* isApproximate= */ false,
351-
/* isCounterexample= */ true));
336+
extractWitness(ctx, translator, result.model));
352337
case APPROXIMATE_MATCH:
353338
return CelVerificationResult.inconclusive(
354339
"Inconclusive: a counterexample may exist, but it depends on approximations, missing"
355340
+ " theories, or loop bounds.",
356-
getCounterexampleString(
357-
ctx,
358-
translator.getTypeSystem(),
359-
result.model,
360-
/* isApproximate= */ true,
361-
/* isCounterexample= */ true));
341+
extractWitness(ctx, translator, result.model),
342+
/* isCounterexample= */ true);
362343
case TRUNCATED:
363344
return CelVerificationResult.inconclusive(
364345
String.format(
@@ -403,23 +384,10 @@ private CelVerificationResult checkSatisfiability(
403384

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

424392
case APPROXIMATE_MATCH:
425393
String prefix =
@@ -430,12 +398,8 @@ private CelVerificationResult checkSatisfiability(
430398
+ " approximations, missing theories, or loop bounds.";
431399
return CelVerificationResult.inconclusive(
432400
prefix,
433-
getCounterexampleString(
434-
ctx,
435-
translator.getTypeSystem(),
436-
result.model,
437-
/* isApproximate= */ true,
438-
/* isCounterexample= */ searchForCounterexample));
401+
extractWitness(ctx, translator, result.model),
402+
/* isCounterexample= */ searchForCounterexample);
439403

440404
case TRUNCATED:
441405
return CelVerificationResult.inconclusive(
@@ -538,14 +502,13 @@ private CelAstToZ3Translator newTranslator(Context ctx, boolean parameterizeUnkn
538502
parameterizeUnknowns);
539503
}
540504

541-
private static String getCounterexampleString(
542-
Context ctx,
543-
CelZ3TypeSystem typeSystem,
544-
Model model,
545-
boolean isApproximate,
546-
boolean isCounterexample) {
505+
private CelWitness extractWitness(Context ctx, CelAstToZ3Translator translator, Model model) {
547506
return CelZ3CounterexampleGenerator.generate(
548-
ctx, typeSystem, model, isApproximate, isCounterexample);
507+
ctx,
508+
translator.getTypeSystem(),
509+
model,
510+
comprehensionUnrollLimit,
511+
translator.getInputVariables());
549512
}
550513

551514
private enum SolverOutcome {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.verifier;
16+
17+
import com.google.auto.value.AutoValue;
18+
import com.google.common.collect.ImmutableMap;
19+
import java.util.Optional;
20+
21+
/** An input assignment found by the solver: a counterexample or a satisfying input. */
22+
@AutoValue
23+
public abstract class CelWitness {
24+
25+
/** The value the solver assigned to one input variable. */
26+
@AutoValue
27+
public abstract static class Variable {
28+
29+
/**
30+
* Returns the value in its CEL runtime representation ({@code Long}, {@code UnsignedLong},
31+
* {@code Double}, {@code Boolean}, {@code String}, {@code CelByteString}, {@code Instant},
32+
* {@code Duration}, {@code NullValue}, {@code ImmutableList}, {@code ImmutableMap} or {@code
33+
* Optional}, nested accordingly), or empty if it has no exact runtime equivalent (e.g. an
34+
* unknown).
35+
*/
36+
public abstract Optional<Object> value();
37+
38+
// The value as a CEL literal where one exists, for display.
39+
abstract String displayValue();
40+
41+
static Variable create(Optional<Object> value, String displayValue) {
42+
return new AutoValue_CelWitness_Variable(value, displayValue);
43+
}
44+
}
45+
46+
/** Returns the variables keyed by name, in order of first appearance. */
47+
public abstract ImmutableMap<String, Variable> variables();
48+
49+
static CelWitness create(ImmutableMap<String, Variable> variables) {
50+
return new AutoValue_CelWitness(variables);
51+
}
52+
}

0 commit comments

Comments
 (0)