Skip to content

Commit dc4ef43

Browse files
Build each class's state machine once per checker (#414)
Stacked on #413. ## Description Building a state machine takes about 25 SMT checks for a small class, and each check creates its own Z3 context. The machine only depends on specifications, which are all registered in the first pass, so `TypeChecker` now builds it on the first state error and reuses it for every later error in the same checker. Finding the receiver's current states still runs at each error. Other options considered: - **Eagerly, after the first pass, for every class with `@StateSet`:** correct programs would pay the SMT cost too. Only worth it once something needs machines without an error, e.g. the fsm diagram. - **Once per run, cached in `Context`:** `Context` would depend on `state_machine`, which depends on `context` (a package cycle). The other way is a static cache that tracks when specifications change. Neither seemed worth it for the extra hits across classes. ## Cost Extra time of `TestExamples` on the 269 cases that exist on `main` (summed per case, one run each; `main` takes 80.3 s): - #413, without the cache: +29.5 s - with this PR: +10.6 s, spread over the 54 files with state errors ## Type of change - [x] Code refactoring ## Checklist - [x] `mvn test` passes locally 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c1f53c5 commit dc4ef43

2 files changed

Lines changed: 20 additions & 4 deletions

File tree

‎liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/TypeChecker.java‎

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

33
import java.lang.annotation.Annotation;
44
import java.util.Collections;
5+
import java.util.HashMap;
56
import java.util.List;
67
import java.util.Map;
78
import java.util.Optional;
@@ -17,6 +18,9 @@
1718
import liquidjava.processor.context.RefinedVariable;
1819
import liquidjava.processor.facade.AliasDTO;
1920
import liquidjava.processor.facade.GhostDTO;
21+
import liquidjava.processor.state_machine.StateMachine;
22+
import liquidjava.processor.state_machine.StateMachineBuilder;
23+
import liquidjava.processor.state_machine.StateSolver;
2024
import liquidjava.rj_language.Predicate;
2125
import liquidjava.rj_language.parsing.RefinementsParser;
2226
import liquidjava.smt.SMTEvaluator;
@@ -49,6 +53,7 @@ public abstract class TypeChecker extends CtScanner {
4953
protected final Factory factory;
5054
protected final VCChecker vcChecker;
5155
private final Diagnostics diagnostics = Diagnostics.getInstance();
56+
private final Map<String, Optional<StateMachine>> stateMachines = new HashMap<>();
5257

5358
public TypeChecker(Context context, Factory factory) {
5459
this.context = context;
@@ -64,6 +69,18 @@ public Factory getFactory() {
6469
return factory;
6570
}
6671

72+
/**
73+
* The state machine of {@code className}, built once per checker: the specifications it is built from are all known
74+
* before a checker starts
75+
*/
76+
public Optional<StateMachine> getStateMachine(String className, CtElement element) throws LJError {
77+
if (!stateMachines.containsKey(className)) {
78+
StateSolver solver = new StateSolver(this, element.getPosition());
79+
stateMachines.put(className, StateMachineBuilder.build(this, solver, className, element));
80+
}
81+
return stateMachines.get(className);
82+
}
83+
6784
@Override
6885
public void scan(CtElement element) {
6986
try {

‎liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/object_checkers/AuxStateHandler.java‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -659,12 +659,11 @@ private static List<Hint> stateHints(TypeChecker tc, RefinedFunction function, S
659659
if (receiver.isEmpty() || !StateMachineBuilder.canModel(function))
660660
return List.of();
661661
try {
662-
StateSolver solver = new StateSolver(tc, invocation.getPosition());
663-
Optional<StateMachine> machine = StateMachineBuilder.build(tc, solver, function.getTargetClass(),
664-
invocation);
662+
Optional<StateMachine> machine = tc.getStateMachine(function.getTargetClass(), invocation);
665663
if (machine.isEmpty())
666664
return List.of();
667-
Set<State> current = solver.possibleStates(machine.get().states(), state, instanceName);
665+
Set<State> current = new StateSolver(tc, invocation.getPosition()).possibleStates(machine.get().states(),
666+
state, instanceName);
668667
return StateHints.forCall(machine.get(), function, current, receiver.get());
669668
} catch (RuntimeException e) {
670669
// a failure to compute hints must not replace the error being reported

0 commit comments

Comments
 (0)