|
| 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 | +} |
0 commit comments