Skip to content

Commit fbc69c3

Browse files
Provide more debug information (#208)
The previous debug information was not sufficient. Now we provide more, with the verification conditions used per each variable refinement we got, and with counter example when it fails. [SMT CHECK] [SMT] Verifying /Users/cgamboa/git/liquidjava-examples/examples/demo/src/main/java/com/soexamples/study-tasks/typestate-violations/ts-bufferedreader-01/Example.java:28 on 'reader.close();' [SMT] [SMT] #reader_50 : BufferedReader true ? state1(#reader_50) == 2 : state1(#reader_50) == 0 [SMT] #fresh_52 : int true [SMT] ──────────────────────────────────────── [SMT] !state1(#reader_50) == 2 [SMT] result: SAT (subtype fails) — counterexample: state1(#reader_50) = 2
1 parent 0240947 commit fbc69c3

8 files changed

Lines changed: 442 additions & 20 deletions

File tree

liquidjava-verifier/src/main/java/liquidjava/diagnostics/Colors.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,28 @@
44
public class Colors {
55
public static final String RESET = "\u001B[0m";
66
public static final String GREY = "\u001B[90m";
7+
public static final String RED = "\u001B[31m";
8+
public static final String GREEN = "\u001B[32m";
9+
public static final String YELLOW = "\u001B[33m";
10+
public static final String BLUE = "\u001B[34m";
11+
public static final String CYAN = "\u001B[36m";
712
public static final String BOLD_RED = "\u001B[1;31m";
813
public static final String BOLD_YELLOW = "\u001B[1;33m";
9-
}
14+
public static final String ORANGE = "\u001B[38;5;208m";
15+
public static final String PINK = "\u001B[38;5;205m";
16+
public static final String CORAL = "\u001B[38;5;203m";
17+
public static final String SALMON = "\u001B[38;5;209m";
18+
public static final String GOLD = "\u001B[38;5;220m";
19+
public static final String BRIGHT_YELLOW = "\u001B[38;5;226m";
20+
public static final String LIME = "\u001B[38;5;118m";
21+
public static final String BRIGHT_GREEN = "\u001B[38;5;46m";
22+
public static final String TEAL = "\u001B[38;5;37m";
23+
public static final String LIGHT_BLUE = "\u001B[38;5;39m";
24+
public static final String BRIGHT_CYAN = "\u001B[38;5;51m";
25+
public static final String PURPLE = "\u001B[38;5;129m";
26+
public static final String BRIGHT_MAGENTA = "\u001B[38;5;201m";
27+
public static final String BROWN = "\u001B[38;5;94m";
28+
public static final String NAVY = "\u001B[38;5;17m";
29+
public static final String LIGHT_GREY = "\u001B[38;5;250m";
30+
public static final String DARK_GREY = "\u001B[38;5;240m";
31+
}
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
package liquidjava.diagnostics;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
import liquidjava.api.CommandLineLauncher;
7+
import liquidjava.processor.VCImplication;
8+
import liquidjava.rj_language.Predicate;
9+
import liquidjava.rj_language.ast.BinaryExpression;
10+
import liquidjava.rj_language.ast.Expression;
11+
import liquidjava.rj_language.ast.GroupExpression;
12+
import liquidjava.utils.Utils;
13+
import spoon.reflect.cu.SourcePosition;
14+
import spoon.reflect.reference.CtTypeReference;
15+
16+
/**
17+
* Centralised debug-mode logging for verification activity. Output is gated on the global {@code --debug} / {@code -d}
18+
* flag and is purely informational.
19+
*
20+
* <p>
21+
* Layers of output, from outermost to innermost:
22+
* <ul>
23+
* <li>{@link #info} — verification context (caller-level predicates, source position).</li>
24+
* <li>{@link #smtStart} — premises and conclusion as fed to Z3.</li>
25+
* <li>{@link #smtUnsat} / {@link #smtSat} / {@link #smtUnknown} — solver outcome.</li>
26+
* </ul>
27+
*/
28+
public final class DebugLog {
29+
30+
private static final String SMT_TAG = Colors.BLUE + "[SMT]" + Colors.RESET;
31+
private static final String SMT_CHECK = Colors.SALMON + "[SMT CHECK]" + Colors.RESET;
32+
33+
private DebugLog() {
34+
}
35+
36+
public static boolean enabled() {
37+
return CommandLineLauncher.cmdArgs.debugMode;
38+
}
39+
40+
/**
41+
* One-line header for a verification check: emits the absolute file path + line so terminals (iTerm2, VS Code,
42+
* WezTerm, …) make it ⌘/Ctrl-clickable. Replaces the older two-line {@code info()} prints.
43+
*/
44+
public static void smtVerifying(SourcePosition position) {
45+
if (!enabled() || position == null) {
46+
return;
47+
}
48+
String where = position.getFile().getAbsolutePath() + ":" + position.getLine();
49+
String exp = Utils.getExpressionFromPosition(position);
50+
System.out.println(SMT_CHECK);
51+
System.out.println(SMT_TAG + " Verifying " + Colors.CYAN + where + Colors.RESET
52+
+ (exp != null ? " on '" + exp + "'" : ""));
53+
}
54+
55+
private static final String SEPARATOR = Colors.GREY + " ────────────────────────────────────────" + Colors.RESET;
56+
57+
/**
58+
* Flat-predicate fallback: prints top-level conjuncts in order with no per-variable grouping. Used by SMT entry
59+
* points that don't carry the structured per-variable {@link VCImplication} chain (e.g. ExpressionSimplifier).
60+
*/
61+
public static void smtStart(Predicate premises, Predicate conclusion) {
62+
if (!enabled()) {
63+
return;
64+
}
65+
List<Expression> conjuncts = new ArrayList<>();
66+
flattenConjunction(premises.getExpression(), conjuncts);
67+
System.out.println(SMT_TAG);
68+
for (Expression c : conjuncts) {
69+
System.out.println(SMT_TAG + " " + c);
70+
}
71+
System.out.println(SMT_TAG + SEPARATOR);
72+
System.out.println(SMT_TAG + " " + formatConclusion(conclusion));
73+
}
74+
75+
/**
76+
* Structured form: walks the {@link VCImplication} chain produced by {@code joinPredicates}, printing one line per
77+
* refined variable with all of its refinements together.
78+
*/
79+
public static void smtStart(VCImplication chain, Predicate conclusion) {
80+
smtStart(chain, null, conclusion);
81+
}
82+
83+
/**
84+
* Structured form with an extra unattributed premise (e.g. the "found" type appended in
85+
* {@code verifySMTSubtypeStates}).
86+
*/
87+
public static void smtStart(VCImplication chain, Predicate extraPremise, Predicate conclusion) {
88+
if (!enabled()) {
89+
return;
90+
}
91+
// Pre-compute label widths for column alignment across all printed rows.
92+
int labelWidth = 0;
93+
for (VCImplication node = chain; node != null; node = node.getNext()) {
94+
if (node.getName() == null && node.getType() == null) {
95+
continue;
96+
}
97+
labelWidth = Math.max(labelWidth, plainLabel(node).length());
98+
}
99+
if (extraPremise != null && !extraPremise.isBooleanTrue()) {
100+
labelWidth = Math.max(labelWidth, "found".length());
101+
}
102+
103+
System.out.println(SMT_TAG + " ");
104+
List<String> printedRefinements = new ArrayList<>();
105+
for (VCImplication node = chain; node != null; node = node.getNext()) {
106+
if (node.getName() == null && node.getType() == null) {
107+
continue; // skip the empty trailing tail node
108+
}
109+
String refinement = formatRefinement(node.getRefinement());
110+
printedRefinements.add(refinement);
111+
System.out.println(SMT_TAG + " " + paintLabel(node, labelWidth) + " " + refinement);
112+
}
113+
if (extraPremise != null && !extraPremise.isBooleanTrue()) {
114+
String extra = formatRefinement(extraPremise);
115+
// Skip when the appended "found" type is identical to a premise we just printed
116+
// (common in verifySMTSubtypeStates when `type` IS the variable's current refinement).
117+
if (!printedRefinements.contains(extra)) {
118+
String label = Colors.GREY + padRight("found", labelWidth) + Colors.RESET;
119+
System.out.println(SMT_TAG + " " + label + " " + extra);
120+
}
121+
}
122+
System.out.println(SMT_TAG + SEPARATOR);
123+
System.out.println(SMT_TAG + " " + formatConclusion(conclusion));
124+
}
125+
126+
private static String plainLabel(VCImplication node) {
127+
return node.getName() + " : " + simpleType(node.getType());
128+
}
129+
130+
private static String paintLabel(VCImplication node, int width) {
131+
String name = node.getName();
132+
String type = simpleType(node.getType());
133+
String padded = padRight(name + " : " + type, width);
134+
// Color only the name; keep alignment by computing padding on the plain string.
135+
String coloredName = Colors.CYAN + name + Colors.RESET;
136+
String coloredType = Colors.GREY + type + Colors.RESET;
137+
String tail = padded.substring((name + " : " + type).length()); // padding spaces
138+
return coloredName + Colors.GREY + " : " + Colors.RESET + coloredType + tail;
139+
}
140+
141+
private static String simpleType(CtTypeReference<?> type) {
142+
if (type == null) {
143+
return "?";
144+
}
145+
String qual = type.getQualifiedName();
146+
return qual.contains(".") ? Utils.getSimpleName(qual) : qual;
147+
}
148+
149+
private static String padRight(String s, int width) {
150+
if (s.length() >= width) {
151+
return s;
152+
}
153+
StringBuilder sb = new StringBuilder(s);
154+
for (int i = s.length(); i < width; i++) {
155+
sb.append(' ');
156+
}
157+
return sb.toString();
158+
}
159+
160+
/**
161+
* Render a refinement so multi-conjunct predicates are unambiguous on a single line: each top-level conjunct is
162+
* wrapped in parens and joined with ∧.
163+
*/
164+
private static String formatRefinement(Predicate p) {
165+
List<Expression> conjuncts = new ArrayList<>();
166+
flattenConjunction(p.getExpression(), conjuncts);
167+
if (conjuncts.size() <= 1) {
168+
return p.toString();
169+
}
170+
StringBuilder sb = new StringBuilder();
171+
for (int i = 0; i < conjuncts.size(); i++) {
172+
if (i > 0) {
173+
sb.append(Colors.GREY).append(" ∧ ").append(Colors.RESET);
174+
}
175+
sb.append('(').append(conjuncts.get(i)).append(')');
176+
}
177+
return sb.toString();
178+
}
179+
180+
/**
181+
* Conclusion needs its own painter: nesting {@link #formatRefinement} (which already emits ANSI {@code RESET}
182+
* around its operators) inside an outer color would clear the outer color after the first inner reset, leaving the
183+
* tail of the line uncoloured. Paint each conjunct individually instead.
184+
*/
185+
private static String formatConclusion(Predicate p) {
186+
List<Expression> conjuncts = new ArrayList<>();
187+
flattenConjunction(p.getExpression(), conjuncts);
188+
if (conjuncts.size() <= 1) {
189+
return Colors.BOLD_YELLOW + p + Colors.RESET;
190+
}
191+
StringBuilder sb = new StringBuilder();
192+
for (int i = 0; i < conjuncts.size(); i++) {
193+
if (i > 0) {
194+
sb.append(Colors.GREY).append(" ∧ ").append(Colors.RESET);
195+
}
196+
sb.append(Colors.BOLD_YELLOW).append('(').append(conjuncts.get(i)).append(')').append(Colors.RESET);
197+
}
198+
return sb.toString();
199+
}
200+
201+
private static void flattenConjunction(Expression e, List<Expression> out) {
202+
if (e instanceof GroupExpression g) {
203+
flattenConjunction(g.getExpression(), out);
204+
return;
205+
}
206+
if (e instanceof BinaryExpression b && "&&".equals(b.getOperator())) {
207+
flattenConjunction(b.getFirstOperand(), out);
208+
flattenConjunction(b.getSecondOperand(), out);
209+
return;
210+
}
211+
out.add(e);
212+
}
213+
214+
public static void smtUnsat() {
215+
if (!enabled()) {
216+
return;
217+
}
218+
System.out.println(SMT_TAG + " result: " + Colors.GREEN + "UNSAT (subtype holds)" + Colors.RESET);
219+
}
220+
221+
public static void smtSat(Object counterexample) {
222+
if (!enabled()) {
223+
return;
224+
}
225+
String header = SMT_TAG + " result: " + Colors.RED + "SAT (subtype fails)" + Colors.RESET;
226+
String pretty = formatCounterexample(counterexample);
227+
if (pretty == null) {
228+
System.out.println(header);
229+
} else if (pretty.contains("\n")) {
230+
System.out.println(header + Colors.GREY + " — counterexample:" + Colors.RESET);
231+
System.out.println(pretty);
232+
} else {
233+
System.out.println(header + Colors.GREY + " — counterexample: " + Colors.RESET + pretty);
234+
}
235+
}
236+
237+
/**
238+
* Render a {@link liquidjava.smt.Counterexample} as {@code lhs = value} pairs. Single assignment goes inline;
239+
* multiple assignments are listed one per indented line. Returns {@code null} when there is nothing useful to show
240+
* — caller prints just the SAT header.
241+
*/
242+
private static String formatCounterexample(Object counterexample) {
243+
if (!(counterexample instanceof liquidjava.smt.Counterexample ce)) {
244+
return counterexample == null ? null : counterexample.toString();
245+
}
246+
var pairs = ce.assignments();
247+
if (pairs == null || pairs.isEmpty()) {
248+
return null;
249+
}
250+
if (pairs.size() == 1) {
251+
var p = pairs.get(0);
252+
return p.first() + " = " + p.second();
253+
}
254+
StringBuilder sb = new StringBuilder();
255+
for (int i = 0; i < pairs.size(); i++) {
256+
var p = pairs.get(i);
257+
if (i > 0) {
258+
sb.append('\n');
259+
}
260+
sb.append(SMT_TAG).append(" ").append(p.first()).append(" = ").append(p.second());
261+
}
262+
return sb.toString();
263+
}
264+
265+
public static void smtUnknown() {
266+
if (!enabled()) {
267+
return;
268+
}
269+
System.out.println(SMT_TAG + " result: " + Colors.YELLOW + "UNKNOWN (treated as OK)" + Colors.RESET);
270+
}
271+
272+
/**
273+
* Print the result of an SMT check whose {@code smtStart} was emitted by the caller (e.g. VCChecker's structured
274+
* print). {@link liquidjava.smt.SMTResult} doesn't preserve UNKNOWN, so this maps OK → UNSAT and ERROR → SAT.
275+
*/
276+
public static void smtResult(liquidjava.smt.SMTResult result) {
277+
if (!enabled()) {
278+
return;
279+
}
280+
if (result.isError()) {
281+
smtSat(result.getCounterexample());
282+
} else {
283+
smtUnsat();
284+
}
285+
}
286+
287+
/**
288+
* Print an SMT-side failure (e.g. Z3 sort mismatch) so the trace doesn't end with a dangling header. Caller is
289+
* still responsible for surfacing the user-facing error.
290+
*/
291+
public static void smtError(String message) {
292+
if (!enabled()) {
293+
return;
294+
}
295+
System.out.println(SMT_TAG + " result: " + Colors.RED + "ERROR" + Colors.RESET + " — "
296+
+ (message == null ? "(no message)" : message));
297+
}
298+
}

liquidjava-verifier/src/main/java/liquidjava/processor/VCImplication.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,26 @@ public void setNext(VCImplication c) {
2727
next = c;
2828
}
2929

30+
public String getName() {
31+
return name;
32+
}
33+
34+
public CtTypeReference<?> getType() {
35+
return type;
36+
}
37+
38+
public Predicate getRefinement() {
39+
return refinement;
40+
}
41+
42+
public void setRefinement(Predicate refinement) {
43+
this.refinement = refinement;
44+
}
45+
46+
public VCImplication getNext() {
47+
return next;
48+
}
49+
3050
public String toString() {
3151
if (name != null && type != null) {
3252
String qualType = type.getQualifiedName();

liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/TypeChecker.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,13 @@ public void checkStateSMT(Predicate prevState, Predicate expectedState, CtElemen
373373
}
374374

375375
public boolean checkStateSMT(Predicate prevState, Predicate expectedState, SourcePosition p) throws LJError {
376+
return checkStateSMT(prevState, expectedState, p, false);
377+
}
378+
379+
public boolean checkStateSMT(Predicate prevState, Predicate expectedState, SourcePosition p, boolean silent)
380+
throws LJError {
376381
SMTResult result = vcChecker.verifySMTSubtypeStates(prevState, expectedState, context.getGhostStates(), p,
377-
factory);
382+
factory, silent);
378383
return result.isOk();
379384
}
380385

0 commit comments

Comments
 (0)