Skip to content

Commit a2725f3

Browse files
Merge remote-tracking branch 'origin/main' into derive-states
2 parents ecc133a + 8805dab commit a2725f3

20 files changed

Lines changed: 424 additions & 86 deletions

CONTRIBUTING.md

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,40 @@ Thanks for your interest in contributing! This guide covers the essentials.
77
- Java 20+
88
- Maven 3.6+
99

10-
## Build & test
10+
## Build and Run
11+
12+
To build the project, run:
1113

1214
```bash
13-
mvn clean install # build everything
14-
mvn test # run the test suite
15+
mvn clean install
1516
```
1617

17-
Run a single test:
18+
Code formatting runs automatically via `formatter-maven-plugin` during the `validate` phase.
19+
20+
To verify a single file from the CLI, run:
1821

1922
```bash
20-
mvn -pl liquidjava-verifier -Dtest=TestExamples#testMultiplePaths test
23+
./liquidjava path/to/File.java
2124
```
2225

23-
Verify a single file from the CLI:
26+
This script recompiles `liquidjava-api` and `liquidjava-verifier` when local sources or Maven files have changed.
27+
28+
## Testing
29+
30+
To run all tests, run:
2431

2532
```bash
26-
./liquidjava path/to/File.java
33+
mvn test
2734
```
2835

29-
Code formatting runs automatically via `formatter-maven-plugin` during the `validate` phase.
36+
To run specific tests, run:
37+
38+
```bash
39+
mvn -pl liquidjava-verifier -Dtest=ExpressionFormatterTest test
40+
mvn -pl liquidjava-verifier -Dtest=ExpressionSimplifierTest test
41+
mvn -pl liquidjava-verifier -Dtest=RefinementsParserTest test
42+
mvn -pl liquidjava-verifier -Dtest=VariableResolverTest test
43+
```
3044

3145
## Release
3246

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,17 @@ mvn exec:java -pl liquidjava-verifier -Dexec.mainClass="liquidjava.api.CommandLi
8383
```
8484

8585
If you're on Linux/macOS, you can use the `liquidjava` script (from the repository root) to simplify the process.
86+
The script recompiles the verifier only when local sources or Maven files have changed.
87+
88+
The LiquidJava verifier can be run from the command line with the following options:
89+
90+
| Option | Description |
91+
| --- | --- |
92+
| `<...paths>` | Paths (files or directories) to be verified by LiquidJava |
93+
| `-h`, `--help` | Show the help message with available options |
94+
| `-v`, `--version` | Show the current version of the verifier |
95+
| `-d`, `--debug` | Enable debug logging and skip expression simplification for troubleshooting |
96+
| `-lsp`, `--language-server` | Enable language server mode for editor support |
8697

8798
**Test a correct case**:
8899
```bash

liquidjava

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
11
#!/bin/bash
2+
set -e
3+
4+
cd "$(dirname "$0")"
5+
6+
MARKER="liquidjava-verifier/target/.liquidjava-last-compile"
7+
8+
if [ ! -d liquidjava-api/target/classes ] || \
9+
[ ! -d liquidjava-verifier/target/classes ] || \
10+
[ ! -f "$MARKER" ] || \
11+
find pom.xml liquidjava-api/pom.xml liquidjava-api/src/main/java \
12+
liquidjava-verifier/pom.xml liquidjava-verifier/src/main/java liquidjava-verifier/src/main/antlr4 \
13+
-newer "$MARKER" -print -quit | grep -q .; then
14+
mvn compile -pl liquidjava-verifier -am -Dmaven.compiler.useIncrementalCompilation=false
15+
touch "$MARKER"
16+
fi
17+
218
mvn exec:java -pl liquidjava-verifier \
319
-Dexec.mainClass="liquidjava.api.CommandLineLauncher" \
420
-Dexec.args="$*"

liquidjava-verifier/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
<groupId>io.github.liquid-java</groupId>
1313
<artifactId>liquidjava-verifier</artifactId>
14-
<version>0.0.22</version>
14+
<version>0.0.25</version>
1515
<name>liquidjava-verifier</name>
1616
<description>LiquidJava Verifier</description>
1717
<url>https://github.com/liquid-java/liquidjava</url>

liquidjava-verifier/src/main/java/liquidjava/api/CommandLineArgs.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ public class CommandLineArgs {
2222
@Option(names = { "-d", "--debug" }, description = "Enable debug mode for more detailed output")
2323
public boolean debugMode;
2424

25+
@Option(names = { "-lsp", "--language-server" }, description = "Enable language server mode for editor support")
26+
public boolean lspMode;
27+
2528
@Parameters(arity = "1..*", paramLabel = "<...paths>", description = "Paths to be verified by LiquidJava")
2629
public List<String> paths;
2730

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

Lines changed: 155 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import java.util.ArrayList;
44
import java.util.List;
5-
65
import liquidjava.api.CommandLineLauncher;
76
import liquidjava.processor.VCImplication;
87
import liquidjava.rj_language.Predicate;
@@ -29,6 +28,7 @@ public final class DebugLog {
2928

3029
private static final String SMT_TAG = Colors.BLUE + "[SMT]" + Colors.RESET;
3130
private static final String SMT_CHECK = Colors.SALMON + "[SMT CHECK]" + Colors.RESET;
31+
private static final String SMP_TAG = Colors.YELLOW + "[SMP]" + Colors.RESET;
3232

3333
private DebugLog() {
3434
}
@@ -123,6 +123,156 @@ public static void smtStart(VCImplication chain, Predicate extraPremise, Predica
123123
System.out.println(SMT_TAG + " " + formatConclusion(conclusion));
124124
}
125125

126+
/**
127+
* Print the simplifier input and output side by side. This keeps the raw expression visible in debug traces while
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.
130+
*/
131+
public static void simplification(Expression input, Expression output) {
132+
if (!enabled()) {
133+
return;
134+
}
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();
274+
}
275+
126276
private static String plainLabel(VCImplication node) {
127277
return node.getName() + " : " + simpleType(node.getType());
128278
}
@@ -215,14 +365,14 @@ public static void smtUnsat() {
215365
if (!enabled()) {
216366
return;
217367
}
218-
System.out.println(SMT_TAG + " result: " + Colors.GREEN + "UNSAT (subtype holds)" + Colors.RESET);
368+
System.out.println(SMT_TAG + " Result: " + Colors.GREEN + "UNSAT (subtype holds)" + Colors.RESET);
219369
}
220370

221371
public static void smtSat(Object counterexample) {
222372
if (!enabled()) {
223373
return;
224374
}
225-
String header = SMT_TAG + " result: " + Colors.RED + "SAT (subtype fails)" + Colors.RESET;
375+
String header = SMT_TAG + " Result: " + Colors.RED + "SAT (subtype fails)" + Colors.RESET;
226376
String pretty = formatCounterexample(counterexample);
227377
if (pretty == null) {
228378
System.out.println(header);
@@ -266,7 +416,7 @@ public static void smtUnknown() {
266416
if (!enabled()) {
267417
return;
268418
}
269-
System.out.println(SMT_TAG + " result: " + Colors.YELLOW + "UNKNOWN (treated as OK)" + Colors.RESET);
419+
System.out.println(SMT_TAG + " Result: " + Colors.YELLOW + "UNKNOWN (treated as OK)" + Colors.RESET);
270420
}
271421

272422
/**
@@ -292,7 +442,7 @@ public static void smtError(String message) {
292442
if (!enabled()) {
293443
return;
294444
}
295-
System.out.println(SMT_TAG + " result: " + Colors.RED + "ERROR" + Colors.RESET + " — "
445+
System.out.println(SMT_TAG + " Result: " + Colors.RED + "ERROR" + Colors.RESET + " — "
296446
+ (message == null ? "(no message)" : message));
297447
}
298448
}

liquidjava-verifier/src/main/java/liquidjava/diagnostics/errors/RefinementError.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import java.util.List;
55
import java.util.stream.Collectors;
66

7-
import liquidjava.api.CommandLineLauncher;
87
import liquidjava.diagnostics.TranslationTable;
98
import liquidjava.rj_language.ast.Expression;
109
import liquidjava.rj_language.ast.formatter.VariableFormatter;
@@ -53,8 +52,8 @@ public String getCounterExampleString() {
5352
List<String> foundAssignments = found.getValue().getConjuncts().stream().map(Expression::toString).toList();
5453
String counterexampleString = counterexample.assignments().stream()
5554
// only include variables that appear in the found value and are not already fixed there
56-
.filter(a -> CommandLineLauncher.cmdArgs.debugMode || (foundVarNames.contains(a.first())
57-
&& !foundAssignments.contains(a.first() + " == " + a.second())))
55+
.filter(a -> foundVarNames.contains(a.first())
56+
&& !foundAssignments.contains(a.first() + " == " + a.second()))
5857
// format as "var == value"
5958
.map(a -> VariableFormatter.format(a.first()) + " == " + a.second())
6059
// join with "&&"
Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package liquidjava.diagnostics.errors;
22

33
import liquidjava.diagnostics.TranslationTable;
4-
import liquidjava.rj_language.ast.Expression;
4+
import liquidjava.rj_language.opt.derivation_node.ValDerivationNode;
55
import spoon.reflect.cu.SourcePosition;
66

77
/**
@@ -11,23 +11,23 @@
1111
*/
1212
public class StateRefinementError extends LJError {
1313

14-
private final String expected;
15-
private final String found;
14+
private final ValDerivationNode expected;
15+
private final ValDerivationNode found;
1616

17-
public StateRefinementError(SourcePosition position, Expression expected, Expression found,
17+
public StateRefinementError(SourcePosition position, ValDerivationNode expected, ValDerivationNode found,
1818
TranslationTable translationTable, String customMessage) {
19-
super("State Refinement Error",
20-
String.format("Expected state %s but found %s", expected.toDisplayString(), found.toDisplayString()),
21-
position, translationTable, customMessage);
22-
this.expected = expected.toDisplayString();
23-
this.found = found.toDisplayString();
19+
super("State Refinement Error", String.format("Expected state %s but found %s",
20+
expected.getValue().toDisplayString(), found.getValue().toDisplayString()), position, translationTable,
21+
customMessage);
22+
this.expected = expected;
23+
this.found = found;
2424
}
2525

26-
public String getExpected() {
26+
public ValDerivationNode getExpected() {
2727
return expected;
2828
}
2929

30-
public String getFound() {
30+
public ValDerivationNode getFound() {
3131
return found;
3232
}
3333
}

0 commit comments

Comments
 (0)