Skip to content

Commit 82c5603

Browse files
add debug info for each simplification pass
1 parent 0d6f57b commit 82c5603

3 files changed

Lines changed: 162 additions & 14 deletions

File tree

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

Lines changed: 125 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,136 @@ public static void smtStart(VCImplication chain, Predicate extraPremise, Predica
128128

129129
/**
130130
* Print the simplifier input and output side by side. This keeps the raw expression visible in debug traces while
131-
* callers continue using the simplified expression for user-facing diagnostics.
131+
* callers continue using the simplified expression for user-facing diagnostics. Long predicates are split on
132+
* top-level {@code &&} so each conjunct lands on its own line.
132133
*/
133134
public static void simplification(Expression input, Expression output) {
134135
if (!enabled()) {
135136
return;
136137
}
137-
System.out.println(SMP_TAG + " Before simplification: " + Colors.YELLOW + input + Colors.RESET);
138-
System.out.println(SMP_TAG + " After simplification: " + Colors.BOLD_YELLOW + output + Colors.RESET);
138+
printSplitConjunction("Before simplification:", Colors.YELLOW, input);
139+
printSplitConjunction("After simplification: ", Colors.BOLD_YELLOW, output);
140+
}
141+
142+
private static void printSplitConjunction(String header, String color, Expression exp) {
143+
List<Expression> conjuncts = new ArrayList<>();
144+
flattenConjunction(exp, conjuncts);
145+
if (conjuncts.size() <= 1) {
146+
System.out.println(SMP_TAG + " " + header + " " + color + exp + Colors.RESET);
147+
return;
148+
}
149+
System.out.println(SMP_TAG + " " + header);
150+
String joiner = " " + Colors.GREY + "&&" + Colors.RESET;
151+
for (int i = 0; i < conjuncts.size(); i++) {
152+
String suffix = (i < conjuncts.size() - 1) ? joiner : "";
153+
System.out.println(SMP_TAG + " " + color + conjuncts.get(i) + Colors.RESET + suffix);
154+
}
155+
}
156+
157+
private static final String PASS_NAME_COLOR = Colors.GOLD;
158+
private static final int PASS_NAME_WIDTH = 28;
159+
160+
/**
161+
* One line per simplifier phase. {@code pass} is a running counter inside a single {@code simplify()} call.
162+
*/
163+
public static void simplificationPass(int pass, String name, Expression result) {
164+
if (!enabled()) {
165+
return;
166+
}
167+
System.out.printf("%s pass %02d: %s%n %s%n", SMP_TAG, pass, paintPassName(name), result);
168+
}
169+
170+
/**
171+
* Same as {@link #simplificationPass(int, String, Expression)} but prints {@code (no change)} when the step left
172+
* the expression unchanged, and otherwise emits a unified-diff-style pair (red {@code -} for the previous
173+
* expression with removed tokens highlighted, green {@code +} for the new one with added tokens highlighted), so
174+
* substitutions inside a long predicate are obvious at a glance.
175+
*
176+
* <p>
177+
* {@code previous} is taken as a string rather than an {@link Expression} because the simplifier mutates the AST in
178+
* place: caching an {@code Expression} reference and re-stringifying it after a later pass would yield the
179+
* already-mutated form, masking real changes as "no change". The caller is expected to snapshot the printed form at
180+
* the moment the previous pass ran.
181+
*/
182+
public static void simplificationPass(int pass, String name, String previous, String result) {
183+
if (!enabled()) {
184+
return;
185+
}
186+
if (previous != null && previous.equals(result)) {
187+
System.out.printf("%s pass %02d: %s %s(no change)%s%n", SMP_TAG, pass, paintPassName(name), Colors.GREY,
188+
Colors.RESET);
189+
return;
190+
}
191+
System.out.printf("%s pass %02d: %s%s%s%n", SMP_TAG, pass, PASS_NAME_COLOR, name, Colors.RESET);
192+
if (previous == null) {
193+
System.out.printf("%s %s%n", SMP_TAG, result);
194+
return;
195+
}
196+
String[] diff = wordDiff(previous, result);
197+
System.out.printf("%s %s-%s %s%n", SMP_TAG, Colors.RED, Colors.RESET, diff[0]);
198+
System.out.printf("%s %s+%s %s%n", SMP_TAG, Colors.GREEN, Colors.RESET, diff[1]);
199+
}
200+
201+
/**
202+
* Color the pass name without breaking column alignment: pad to {@link #PASS_NAME_WIDTH} first, then wrap only the
203+
* visible characters in {@link #PASS_NAME_COLOR}. The trailing spaces stay uncolored so {@code printf}'s width
204+
* accounting stays correct.
205+
*/
206+
private static String paintPassName(String name) {
207+
int pad = Math.max(0, PASS_NAME_WIDTH - name.length());
208+
return PASS_NAME_COLOR + name + Colors.RESET + " ".repeat(pad);
209+
}
210+
211+
/**
212+
* Word-level LCS diff. Returns {@code [previousColored, currentColored]} where tokens that don't appear in the LCS
213+
* are wrapped in red (for the previous string) and green (for the current string). Splitting on a single space is
214+
* intentional — the {@link Expression#toString()} output spaces operators and operands.
215+
*/
216+
private static String[] wordDiff(String previous, String current) {
217+
String[] prev = previous.split(" ");
218+
String[] curr = current.split(" ");
219+
int[][] dp = new int[prev.length + 1][curr.length + 1];
220+
for (int i = 1; i <= prev.length; i++) {
221+
for (int j = 1; j <= curr.length; j++) {
222+
if (prev[i - 1].equals(curr[j - 1])) {
223+
dp[i][j] = dp[i - 1][j - 1] + 1;
224+
} else {
225+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
226+
}
227+
}
228+
}
229+
boolean[] prevKept = new boolean[prev.length];
230+
boolean[] currKept = new boolean[curr.length];
231+
int i = prev.length;
232+
int j = curr.length;
233+
while (i > 0 && j > 0) {
234+
if (prev[i - 1].equals(curr[j - 1])) {
235+
prevKept[i - 1] = true;
236+
currKept[j - 1] = true;
237+
i--;
238+
j--;
239+
} else if (dp[i - 1][j] >= dp[i][j - 1]) {
240+
i--;
241+
} else {
242+
j--;
243+
}
244+
}
245+
return new String[] { colorizeDiff(prev, prevKept, Colors.RED), colorizeDiff(curr, currKept, Colors.GREEN) };
246+
}
247+
248+
private static String colorizeDiff(String[] tokens, boolean[] kept, String color) {
249+
StringBuilder sb = new StringBuilder();
250+
for (int k = 0; k < tokens.length; k++) {
251+
if (k > 0) {
252+
sb.append(' ');
253+
}
254+
if (kept[k]) {
255+
sb.append(tokens[k]);
256+
} else {
257+
sb.append(color).append(tokens[k]).append(Colors.RESET);
258+
}
259+
}
260+
return sb.toString();
139261
}
140262

141263
private static String plainLabel(VCImplication node) {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ public <R> void visitCtMethod(CtMethod<R> method) {
7575
String signature = method.getSignature();
7676
String message = String.format("Could not find constructor '%s' for '%s'", signature, prefix);
7777
String[] overloads = getOverloads(targetType, method);
78-
diagnostics.add(new ExternalMethodNotFoundWarning(method.getPosition(), message, signature,
79-
prefix, overloads));
78+
diagnostics.add(
79+
new ExternalMethodNotFoundWarning(method.getPosition(), message, signature, prefix, overloads));
8080
}
8181
} else {
8282
if (!methodExists(targetType, method)) {

liquidjava-verifier/src/main/java/liquidjava/rj_language/opt/ExpressionSimplifier.java

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package liquidjava.rj_language.opt;
22

3+
import liquidjava.diagnostics.DebugLog;
34
import liquidjava.processor.context.Context;
45
import liquidjava.rj_language.Predicate;
56
import java.util.Map;
@@ -23,29 +24,53 @@ public class ExpressionSimplifier {
2324
* expanding aliases Returns a derivation node representing the tree of simplifications applied
2425
*/
2526
public static ValDerivationNode simplify(Expression exp, Map<String, AliasDTO> aliases) {
26-
ValDerivationNode fixedPoint = simplifyToFixedPoint(null, exp);
27+
int[] pass = { 0 };
28+
// String, not Expression: the simplification passes mutate the AST in place, so storing an Expression
29+
// reference would always compare equal to itself after the next pass runs. Snapshot the printed form instead.
30+
String[] prev = { null };
31+
DebugLog.simplificationPass(0, "initial expression", exp);
32+
prev[0] = exp.toString();
33+
ValDerivationNode fixedPoint = simplifyToFixedPoint(null, exp, pass, prev);
34+
logStep(pass, prev, "fixed-point reached", fixedPoint.getValue());
2735
ValDerivationNode simplified = simplifyValDerivationNode(fixedPoint);
36+
logStep(pass, prev, "remove redundant &&", simplified.getValue());
2837
ValDerivationNode unwrapped = unwrapBooleanLiterals(simplified);
29-
return AliasExpansion.expand(unwrapped, aliases);
38+
logStep(pass, prev, "unwrap boolean literals", unwrapped.getValue());
39+
ValDerivationNode expanded = AliasExpansion.expand(unwrapped, aliases);
40+
logStep(pass, prev, "expand aliases", expanded.getValue());
41+
return expanded;
3042
}
3143

3244
public static ValDerivationNode simplify(Expression exp) {
3345
return simplify(exp, Map.of());
3446
}
3547

48+
private static void logStep(int[] pass, String[] prev, String name, Expression result) {
49+
String resultStr = result.toString();
50+
DebugLog.simplificationPass(++pass[0], name, prev[0], resultStr);
51+
prev[0] = resultStr;
52+
}
53+
3654
/**
3755
* Recursively applies propagation and folding until the expression stops changing (fixed point) Stops early if the
38-
* expression simplifies to a boolean literal, which means we've simplified too much
56+
* expression simplifies to a boolean literal, which means we've simplified too much. {@code pass} and {@code prev}
57+
* are running counters shared with {@link #simplify} so debug output keeps a single monotonic numbering and can
58+
* detect no-op steps.
3959
*/
40-
private static ValDerivationNode simplifyToFixedPoint(ValDerivationNode current, Expression prevExp) {
60+
private static ValDerivationNode simplifyToFixedPoint(ValDerivationNode current, Expression prevExp, int[] pass,
61+
String[] prev) {
4162
// apply propagation and folding
4263
ValDerivationNode prop = VariablePropagation.propagate(prevExp, current);
64+
logStep(pass, prev, "constant propagation", prop.getValue());
4365
ValDerivationNode fold = ExpressionFolding.fold(prop);
66+
logStep(pass, prev, "constant folding", fold.getValue());
4467
ValDerivationNode simplified = simplifyValDerivationNode(fold);
68+
logStep(pass, prev, "remove redundant && (loop)", simplified.getValue());
4569
Expression currExp = simplified.getValue();
4670

47-
// fixed point reached
48-
if (current != null && currExp.equals(current.getValue())) {
71+
// fixed point reached — compare on toString() because propagate/fold/reduce mutate the AST in place, so a
72+
// reference-level .equals() can trivially be true on shared (mutated) nodes
73+
if (current != null && currExp.toString().equals(current.getValue().toString())) {
4974
return current;
5075
}
5176

@@ -55,7 +80,7 @@ private static ValDerivationNode simplifyToFixedPoint(ValDerivationNode current,
5580
}
5681

5782
// continue simplifying
58-
return simplifyToFixedPoint(simplified, simplified.getValue());
83+
return simplifyToFixedPoint(simplified, simplified.getValue(), pass, prev);
5984
}
6085

6186
/**
@@ -84,8 +109,9 @@ private static ValDerivationNode simplifyValDerivationNode(ValDerivationNode nod
84109
if (isRedundant(rightSimplified.getValue()))
85110
return leftSimplified;
86111

87-
// collapse identical sides (x && x => x)
88-
if (leftSimplified.getValue().equals(rightSimplified.getValue())) {
112+
// collapse identical sides (x && x => x) — toString() avoids false positives when the two sides share a
113+
// mutated AST node and false negatives are harmless (we just keep the conjunction)
114+
if (leftSimplified.getValue().toString().equals(rightSimplified.getValue().toString())) {
89115
return leftSimplified;
90116
}
91117

0 commit comments

Comments
 (0)