Skip to content

Commit 8805dab

Browse files
Add debug info for each simplification pass (#230)
## Description Added more information to the debug mode for the simplification passes. Each pass shows a diff with highlighted changes, and at the end we split expressions by `&&`. ## Example <img width="1930" height="1002" alt="image" src="https://github.com/user-attachments/assets/d0ffa4d5-469b-4831-8163-9d901f120659" /> ## Related Issue [Reference any related issue] ## Type of change - [ ] Bug fix - [x] New feature - [ ] Documentation update - [ ] Code refactoring ## Checklist - [ ] Added/updated tests under `liquidjava-example/src/main/java/testSuite/` (`Correct*` / `Error*`) - [ ] `mvn test` passes locally - [ ] Updated docs/README if behavior or API changed --------- Co-authored-by: Ricardo Costa <rcosta.ms358@gmail.com>
1 parent 5259fd4 commit 8805dab

2 files changed

Lines changed: 167 additions & 16 deletions

File tree

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

Lines changed: 141 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@
22

33
import java.util.ArrayList;
44
import java.util.List;
5-
import java.util.stream.Collectors;
6-
75
import liquidjava.api.CommandLineLauncher;
86
import liquidjava.processor.VCImplication;
97
import liquidjava.rj_language.Predicate;
108
import liquidjava.rj_language.ast.BinaryExpression;
119
import liquidjava.rj_language.ast.Expression;
1210
import liquidjava.rj_language.ast.GroupExpression;
13-
import liquidjava.smt.Counterexample;
1411
import liquidjava.utils.Utils;
1512
import spoon.reflect.cu.SourcePosition;
1613
import spoon.reflect.reference.CtTypeReference;
@@ -128,14 +125,152 @@ public static void smtStart(VCImplication chain, Predicate extraPremise, Predica
128125

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

141276
private static String plainLabel(VCImplication node) {

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

Lines changed: 26 additions & 10 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,10 +24,16 @@ 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) {
27+
DebugLog.simplificationStart(exp);
2628
ValDerivationNode fixedPoint = simplifyToFixedPoint(null, exp);
29+
DebugLog.simplificationPass("fixed-point reached", fixedPoint.getValue());
2730
ValDerivationNode simplified = simplifyValDerivationNode(fixedPoint);
31+
DebugLog.simplificationPass("remove redundant &&", simplified.getValue());
2832
ValDerivationNode unwrapped = unwrapBooleanLiterals(simplified);
29-
return AliasExpansion.expand(unwrapped, aliases);
33+
DebugLog.simplificationPass("unwrap boolean literals", unwrapped.getValue());
34+
ValDerivationNode expanded = AliasExpansion.expand(unwrapped, aliases);
35+
DebugLog.simplificationPass("expand aliases", expanded.getValue());
36+
return expanded;
3037
}
3138

3239
public static ValDerivationNode simplify(Expression exp) {
@@ -35,17 +42,15 @@ public static ValDerivationNode simplify(Expression exp) {
3542

3643
/**
3744
* 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
45+
* expression simplifies to a boolean literal, which means we've simplified too much.
3946
*/
4047
private static ValDerivationNode simplifyToFixedPoint(ValDerivationNode current, Expression prevExp) {
41-
// apply propagation and folding
42-
ValDerivationNode prop = VariablePropagation.propagate(prevExp, current);
43-
ValDerivationNode fold = ExpressionFolding.fold(prop);
44-
ValDerivationNode simplified = simplifyValDerivationNode(fold);
48+
ValDerivationNode simplified = simplifyOnce(current, prevExp);
4549
Expression currExp = simplified.getValue();
4650

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

@@ -58,6 +63,16 @@ private static ValDerivationNode simplifyToFixedPoint(ValDerivationNode current,
5863
return simplifyToFixedPoint(simplified, simplified.getValue());
5964
}
6065

66+
private static ValDerivationNode simplifyOnce(ValDerivationNode current, Expression prevExp) {
67+
ValDerivationNode prop = VariablePropagation.propagate(prevExp, current);
68+
DebugLog.simplificationPass("variable propagation", prop.getValue());
69+
ValDerivationNode fold = ExpressionFolding.fold(prop);
70+
DebugLog.simplificationPass("expression folding", fold.getValue());
71+
ValDerivationNode simplified = simplifyValDerivationNode(fold);
72+
DebugLog.simplificationPass("remove redundant && (loop)", simplified.getValue());
73+
return simplified;
74+
}
75+
6176
/**
6277
* Recursively simplifies the derivation tree by removing redundant conjuncts
6378
*/
@@ -84,8 +99,9 @@ private static ValDerivationNode simplifyValDerivationNode(ValDerivationNode nod
8499
if (isRedundant(rightSimplified.getValue()))
85100
return leftSimplified;
86101

87-
// collapse identical sides (x && x => x)
88-
if (leftSimplified.getValue().equals(rightSimplified.getValue())) {
102+
// collapse identical sides (x && x => x) — toString() avoids false positives when the two sides share a
103+
// mutated AST node and false negatives are harmless (we just keep the conjunction)
104+
if (leftSimplified.getValue().toString().equals(rightSimplified.getValue().toString())) {
89105
return leftSimplified;
90106
}
91107

0 commit comments

Comments
 (0)