-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathDebugLog.java
More file actions
448 lines (414 loc) · 18.4 KB
/
Copy pathDebugLog.java
File metadata and controls
448 lines (414 loc) · 18.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
package liquidjava.diagnostics;
import java.util.ArrayList;
import java.util.List;
import liquidjava.api.CommandLineLauncher;
import liquidjava.processor.VCImplication;
import liquidjava.rj_language.Predicate;
import liquidjava.rj_language.ast.BinaryExpression;
import liquidjava.rj_language.ast.Expression;
import liquidjava.rj_language.ast.GroupExpression;
import liquidjava.utils.Utils;
import spoon.reflect.cu.SourcePosition;
import spoon.reflect.reference.CtTypeReference;
/**
* Centralised debug-mode logging for verification activity. Output is gated on the global {@code --debug} / {@code -d}
* flag and is purely informational.
*
* <p>
* Layers of output, from outermost to innermost:
* <ul>
* <li>{@link #info} — verification context (caller-level predicates, source position).</li>
* <li>{@link #smtStart} — premises and conclusion as fed to Z3.</li>
* <li>{@link #smtUnsat} / {@link #smtSat} / {@link #smtUnknown} — solver outcome.</li>
* </ul>
*/
public final class DebugLog {
private static final String SMT_TAG = Colors.BLUE + "[SMT]" + Colors.RESET;
private static final String SMT_CHECK = Colors.SALMON + "[SMT CHECK]" + Colors.RESET;
private static final String SMP_TAG = Colors.YELLOW + "[SMP]" + Colors.RESET;
private DebugLog() {
}
public static boolean enabled() {
return CommandLineLauncher.cmdArgs.debugMode;
}
/**
* One-line header for a verification check: emits the absolute file path + line so terminals (iTerm2, VS Code,
* WezTerm, …) make it ⌘/Ctrl-clickable. Replaces the older two-line {@code info()} prints.
*/
public static void smtVerifying(SourcePosition position) {
if (!enabled() || position == null) {
return;
}
String where = position.getFile().getAbsolutePath() + ":" + position.getLine();
String exp = Utils.getExpressionFromPosition(position);
System.out.println(SMT_CHECK);
System.out.println(SMT_TAG + " Verifying " + Colors.CYAN + where + Colors.RESET
+ (exp != null ? " on '" + exp + "'" : ""));
}
private static final String SEPARATOR = Colors.GREY + " ────────────────────────────────────────" + Colors.RESET;
/**
* Flat-predicate fallback: prints top-level conjuncts in order with no per-variable grouping. Used by SMT entry
* points that don't carry the structured per-variable {@link VCImplication} chain.
*/
public static void smtStart(Predicate premises, Predicate conclusion) {
if (!enabled()) {
return;
}
List<Expression> conjuncts = new ArrayList<>();
flattenConjunction(premises.getExpression(), conjuncts);
System.out.println(SMT_TAG);
for (Expression c : conjuncts) {
System.out.println(SMT_TAG + " " + c);
}
System.out.println(SMT_TAG + SEPARATOR);
System.out.println(SMT_TAG + " " + formatConclusion(conclusion));
}
/**
* Structured form: walks the {@link VCImplication} chain produced by {@code joinPredicates}, printing one line per
* refined variable with all of its refinements together.
*/
public static void smtStart(VCImplication chain, Predicate conclusion) {
smtStart(chain, null, conclusion);
}
/**
* Structured form with an extra unattributed premise (e.g. the "found" type appended in
* {@code verifySMTSubtypeStates}).
*/
public static void smtStart(VCImplication chain, Predicate extraPremise, Predicate conclusion) {
if (!enabled()) {
return;
}
// Pre-compute label widths for column alignment across all printed rows.
int labelWidth = 0;
for (VCImplication node = chain; node != null; node = node.getNext()) {
if (node.getName() == null && node.getType() == null) {
continue;
}
labelWidth = Math.max(labelWidth, plainLabel(node).length());
}
if (extraPremise != null && !extraPremise.isBooleanTrue()) {
labelWidth = Math.max(labelWidth, "found".length());
}
System.out.println(SMT_TAG + " ");
List<String> printedRefinements = new ArrayList<>();
for (VCImplication node = chain; node != null; node = node.getNext()) {
if (node.getName() == null && node.getType() == null) {
continue; // skip the empty trailing tail node
}
String refinement = formatRefinement(node.getRefinement());
printedRefinements.add(refinement);
System.out.println(SMT_TAG + " " + paintLabel(node, labelWidth) + " " + refinement);
}
if (extraPremise != null && !extraPremise.isBooleanTrue()) {
String extra = formatRefinement(extraPremise);
// Skip when the appended "found" type is identical to a premise we just printed
// (common in verifySMTSubtypeStates when `type` IS the variable's current refinement).
if (!printedRefinements.contains(extra)) {
String label = Colors.GREY + padRight("found", labelWidth) + Colors.RESET;
System.out.println(SMT_TAG + " " + label + " " + extra);
}
}
System.out.println(SMT_TAG + SEPARATOR);
System.out.println(SMT_TAG + " " + formatConclusion(conclusion));
}
/**
* Print the simplifier input and output side by side. This keeps the raw expression visible in debug traces while
* callers continue using the simplified expression for user-facing diagnostics. Long predicates are split on
* top-level {@code &&} so each conjunct lands on its own line.
*/
public static void simplification(Expression input, Expression output) {
if (!enabled()) {
return;
}
printSplitConjunction("Before simplification:", Colors.YELLOW, input);
printSplitConjunction("After simplification: ", Colors.BOLD_YELLOW, output);
}
private static void printSplitConjunction(String header, String color, Expression exp) {
List<Expression> conjuncts = new ArrayList<>();
flattenConjunction(exp, conjuncts);
if (conjuncts.size() <= 1) {
System.out.println(SMP_TAG + " " + header + " " + color + exp + Colors.RESET);
return;
}
System.out.println(SMP_TAG + " " + header);
String joiner = " " + Colors.GREY + "&&" + Colors.RESET;
for (int i = 0; i < conjuncts.size(); i++) {
String suffix = (i < conjuncts.size() - 1) ? joiner : "";
System.out.println(SMP_TAG + " " + color + conjuncts.get(i) + Colors.RESET + suffix);
}
}
private static final String PASS_NAME_COLOR = Colors.GOLD;
private static final int PASS_NAME_WIDTH = 28;
private static int simplificationPass;
private static String previousSimplification;
/**
* Start a simplification log. DebugLog owns the running pass number and the previous expression snapshot because
* both are only needed for debug output.
*/
public static void simplificationStart(Expression input) {
if (!enabled()) {
return;
}
previousSimplification = input.toString();
simplificationPass = 0;
printSimplificationPass(simplificationPass, "initial expression", previousSimplification);
}
/**
* One line per simplifier phase.
*/
public static void simplificationPass(String name, Expression result) {
if (!enabled()) {
return;
}
String resultStr = result.toString();
printSimplificationPass(++simplificationPass, name, previousSimplification, resultStr);
previousSimplification = resultStr;
}
/**
* Prints {@code (no change)} when the step left the expression unchanged, and otherwise emits a unified-diff-style
* pair (red {@code -} for the previous expression with removed tokens highlighted, green {@code +} for the new one
* with added tokens highlighted), so substitutions inside a long predicate are obvious at a glance.
*
* <p>
* {@code previous} is taken as a string rather than an {@link Expression} because the simplifier mutates the AST in
* place: caching an {@code Expression} reference and re-stringifying it after a later pass would yield the
* already-mutated form, masking real changes as "no change".
*/
private static void printSimplificationPass(int pass, String name, String previous, String result) {
if (previous != null && previous.equals(result)) {
System.out.printf("%s pass %02d: %s %s(no change)%s%n", SMP_TAG, pass, paintPassName(name), Colors.GREY,
Colors.RESET);
return;
}
System.out.printf("%s pass %02d: %s%s%s%n", SMP_TAG, pass, PASS_NAME_COLOR, name, Colors.RESET);
if (previous == null) {
System.out.printf("%s %s%n", SMP_TAG, result);
return;
}
String[] diff = wordDiff(previous, result);
System.out.printf("%s %s-%s %s%n", SMP_TAG, Colors.RED, Colors.RESET, diff[0]);
System.out.printf("%s %s+%s %s%n", SMP_TAG, Colors.GREEN, Colors.RESET, diff[1]);
}
private static void printSimplificationPass(int pass, String name, String result) {
System.out.printf("%s pass %02d: %s%n %s%n", SMP_TAG, pass, paintPassName(name), result);
}
/**
* Color the pass name without breaking column alignment: pad to {@link #PASS_NAME_WIDTH} first, then wrap only the
* visible characters in {@link #PASS_NAME_COLOR}. The trailing spaces stay uncolored so {@code printf}'s width
* accounting stays correct.
*/
private static String paintPassName(String name) {
int pad = Math.max(0, PASS_NAME_WIDTH - name.length());
return PASS_NAME_COLOR + name + Colors.RESET + " ".repeat(pad);
}
/**
* Word-level LCS diff. Returns {@code [previousColored, currentColored]} where tokens that don't appear in the LCS
* are wrapped in red (for the previous string) and green (for the current string). Splitting on a single space is
* intentional — the {@link Expression#toString()} output spaces operators and operands.
*/
private static String[] wordDiff(String previous, String current) {
String[] prev = previous.split(" ");
String[] curr = current.split(" ");
int[][] dp = new int[prev.length + 1][curr.length + 1];
for (int i = 1; i <= prev.length; i++) {
for (int j = 1; j <= curr.length; j++) {
if (prev[i - 1].equals(curr[j - 1])) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
boolean[] prevKept = new boolean[prev.length];
boolean[] currKept = new boolean[curr.length];
int i = prev.length;
int j = curr.length;
while (i > 0 && j > 0) {
if (prev[i - 1].equals(curr[j - 1])) {
prevKept[i - 1] = true;
currKept[j - 1] = true;
i--;
j--;
} else if (dp[i - 1][j] >= dp[i][j - 1]) {
i--;
} else {
j--;
}
}
return new String[] { colorizeDiff(prev, prevKept, Colors.RED), colorizeDiff(curr, currKept, Colors.GREEN) };
}
private static String colorizeDiff(String[] tokens, boolean[] kept, String color) {
StringBuilder sb = new StringBuilder();
for (int k = 0; k < tokens.length; k++) {
if (k > 0) {
sb.append(' ');
}
if (kept[k]) {
sb.append(tokens[k]);
} else {
sb.append(color).append(tokens[k]).append(Colors.RESET);
}
}
return sb.toString();
}
private static String plainLabel(VCImplication node) {
return node.getName() + " : " + simpleType(node.getType());
}
private static String paintLabel(VCImplication node, int width) {
String name = node.getName();
String type = simpleType(node.getType());
String padded = padRight(name + " : " + type, width);
// Color only the name; keep alignment by computing padding on the plain string.
String coloredName = Colors.CYAN + name + Colors.RESET;
String coloredType = Colors.GREY + type + Colors.RESET;
String tail = padded.substring((name + " : " + type).length()); // padding spaces
return coloredName + Colors.GREY + " : " + Colors.RESET + coloredType + tail;
}
private static String simpleType(CtTypeReference<?> type) {
if (type == null) {
return "?";
}
String qual = type.getQualifiedName();
return qual.contains(".") ? Utils.getSimpleName(qual) : qual;
}
private static String padRight(String s, int width) {
if (s.length() >= width) {
return s;
}
StringBuilder sb = new StringBuilder(s);
for (int i = s.length(); i < width; i++) {
sb.append(' ');
}
return sb.toString();
}
/**
* Render a refinement so multi-conjunct predicates are unambiguous on a single line: each top-level conjunct is
* wrapped in parens and joined with ∧.
*/
private static String formatRefinement(Predicate p) {
List<Expression> conjuncts = new ArrayList<>();
flattenConjunction(p.getExpression(), conjuncts);
if (conjuncts.size() <= 1) {
return p.toString();
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < conjuncts.size(); i++) {
if (i > 0) {
sb.append(Colors.GREY).append(" ∧ ").append(Colors.RESET);
}
sb.append('(').append(conjuncts.get(i)).append(')');
}
return sb.toString();
}
/**
* Conclusion needs its own painter: nesting {@link #formatRefinement} (which already emits ANSI {@code RESET}
* around its operators) inside an outer color would clear the outer color after the first inner reset, leaving the
* tail of the line uncoloured. Paint each conjunct individually instead.
*/
private static String formatConclusion(Predicate p) {
List<Expression> conjuncts = new ArrayList<>();
flattenConjunction(p.getExpression(), conjuncts);
if (conjuncts.size() <= 1) {
return Colors.BOLD_YELLOW + p + Colors.RESET;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < conjuncts.size(); i++) {
if (i > 0) {
sb.append(Colors.GREY).append(" ∧ ").append(Colors.RESET);
}
sb.append(Colors.BOLD_YELLOW).append('(').append(conjuncts.get(i)).append(')').append(Colors.RESET);
}
return sb.toString();
}
private static void flattenConjunction(Expression e, List<Expression> out) {
if (e instanceof GroupExpression g) {
flattenConjunction(g.getExpression(), out);
return;
}
if (e instanceof BinaryExpression b && "&&".equals(b.getOperator())) {
flattenConjunction(b.getFirstOperand(), out);
flattenConjunction(b.getSecondOperand(), out);
return;
}
out.add(e);
}
public static void smtUnsat() {
if (!enabled()) {
return;
}
System.out.println(SMT_TAG + " Result: " + Colors.GREEN + "UNSAT (subtype holds)" + Colors.RESET);
}
public static void smtSat(Object counterexample) {
if (!enabled()) {
return;
}
String header = SMT_TAG + " Result: " + Colors.RED + "SAT (subtype fails)" + Colors.RESET;
String pretty = formatCounterexample(counterexample);
if (pretty == null) {
System.out.println(header);
} else if (pretty.contains("\n")) {
System.out.println(header + Colors.GREY + " — counterexample:" + Colors.RESET);
System.out.println(pretty);
} else {
System.out.println(header + Colors.GREY + " — counterexample: " + Colors.RESET + pretty);
}
}
/**
* Render a {@link liquidjava.smt.Counterexample} as {@code lhs = value} pairs. Single assignment goes inline;
* multiple assignments are listed one per indented line. Returns {@code null} when there is nothing useful to show
* — caller prints just the SAT header.
*/
private static String formatCounterexample(Object counterexample) {
if (!(counterexample instanceof liquidjava.smt.Counterexample ce)) {
return counterexample == null ? null : counterexample.toString();
}
var pairs = ce.assignments();
if (pairs == null || pairs.isEmpty()) {
return null;
}
if (pairs.size() == 1) {
var p = pairs.get(0);
return p.first() + " = " + p.second();
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < pairs.size(); i++) {
var p = pairs.get(i);
if (i > 0) {
sb.append('\n');
}
sb.append(SMT_TAG).append(" ").append(p.first()).append(" = ").append(p.second());
}
return sb.toString();
}
public static void smtUnknown() {
if (!enabled()) {
return;
}
System.out.println(SMT_TAG + " Result: " + Colors.YELLOW + "UNKNOWN (treated as OK)" + Colors.RESET);
}
/**
* Print the result of an SMT check whose {@code smtStart} was emitted by the caller (e.g. VCChecker's structured
* print). {@link liquidjava.smt.SMTResult} doesn't preserve UNKNOWN, so this maps OK → UNSAT and ERROR → SAT.
*/
public static void smtResult(liquidjava.smt.SMTResult result) {
if (!enabled()) {
return;
}
if (result.isError()) {
smtSat(result.getCounterexample());
} else {
smtUnsat();
}
}
/**
* Print an SMT-side failure (e.g. Z3 sort mismatch) so the trace doesn't end with a dangling header. Caller is
* still responsible for surfacing the user-facing error.
*/
public static void smtError(String message) {
if (!enabled()) {
return;
}
System.out.println(SMT_TAG + " Result: " + Colors.RED + "ERROR" + Colors.RESET + " — "
+ (message == null ? "(no message)" : message));
}
}