Repository navigation
Reuse solver results across verification runs - #430
Merged
Merged
Conversation
A long-lived process such as the language server verifies the whole program again after every save, asking the solver mostly the same queries each time. Their results are now kept, identified by everything the translation to Z3 reads: the expression node by node, the types and supertypes of its variables (renamed in order of appearance), the enums, ghosts and ghost states of the context, and the solver configuration. Unknown results are never kept, and a counterexample is only reused when the variables have the same names. The system property liquidjava.caches turns the cache off, or makes it recompute every result it finds and fail on a difference, which the tests do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s on Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Enum constants reach the translator as names it binds to fixed values, so the cache must keep them: renaming them like variables gave m == Mode.Photo and m == Mode.Video the same key. Found by the check mode; tested by enumConstantsAreValuesNotVariables. Also, from review: drop the solver description from the key (results only live in memory, so the solver cannot change under them), leave the cache mode to SMTEvaluator, store the SMTResult itself, report the key on a mismatch, rename the property to liquidjava.cache, keep pom.xml's line endings, test the ghost type with double (long is not a ghost type), and check that each edited program reaches the solver. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
In VS Code, the language server verifies the whole source tree again on every save, in the same JVM, and asks Z3 mostly the same queries each time. On the study workspace (
proposal-C-stars, 15 files) a save makes about 1,100 queries. About 96% of them depend only on the library specs: the typestate state machines built for state hints, and the satisfiability check of each@StateRefinement.This PR keeps solver results across verification runs, in memory, so a save after the first one only asks Z3 the queries that changed.
How a query is identified
The cache sits in
SMTEvaluator, around the only place the verifier calls Z3. The key (smt/QueryCache.java) is everythingTranslatorToZ3reads to build the query:ExpressionVisitor, so a new AST node does not compile until the key handles it), withstatic finalconstants including their resolved values;Variables are renamed in order of first appearance. A query whose fresh variables were numbered differently because of an edit earlier in the file is still found. Names that denote enum constants (
Mode.Photo) are values, not variables, so they are never renamed. The translator and the cache share the helper that names them.Results only live in memory, for as long as the process runs, so the verifier and the solver cannot change under them.
What is never reused:
checkStateSMT, which builds the state machines and checks state refinements, and the constraint simplifier) say so and get the cached answer.Keeping it correct
Switch:
-Dliquidjava.cache=on|off|check(defaulton).checkrecomputes every result it finds in the cache and throws anAssertionErroron any difference. AnAssertionErroris not caught by the code that recovers from exceptions, so a mismatch cannot be hidden.The whole test suite runs in
checkmode (surefiresystemPropertyVariables). Every cache hit duringmvn testis verified against a fresh solver call.TestQueryCacheverifies a program, edits one thing the queries depend on without changing their text, and verifies it again in the same JVM, as the language server does after a save. The cached result must equal an uncached run, and the edit must change the result. The edits:int→long:v > 0impliesv >= 1only for integers);int→double).Each test also checks that both versions of the program reach the solver, so a test cannot pass because the edited program fails to parse.
enumConstantsAreValuesNotVariables:photo(Mode.Photo)andphoto(Mode.Video)differ only in the enum constant, and the second call must be reported. A first version of this PR renamed enum constants like variables, so both calls shared one key and the second could have reused the first's "ok". The check mode caught this while I was reviewing the code, and this test fails on that version.QueryCacheTestchecks the renaming, and that variable types and supertypes are part of the key.Mutation check: I removed each part of the key in turn. Removing any of these makes a test fail:
Same results
I compared every diagnostic, with its full rendered message, hints and counterexamples included, against
main. These results are for the final version. All cases ran in a single JVM, so specs are shared between cases and the cache gets many hits:checkmode: mismatches / crashesonmode: verdict / message diffs vsmaintestSuiteexercises,exercises-oss)* The variable number in one message (
transactionCommitted²⁷vs²⁸). The messages are otherwise identical. This is the existing determinism issue described below.mvn test: 205 tests, 0 failures.Timings
Study workspace, the same 15 files verified 5 times in one JVM, like 5 saves. The machine was shared, so main-thread CPU time is shown too:
Corpus totals (each corpus in one JVM, on a shared machine): Barista 9,667 s → 349 s, exercises 207 s → 77 s.
The CLI verifies once and exits. It still benefits within a run, from repeated queries, and otherwise behaves as before.
Not changed / known
SMTEvaluatorlost acatch (SyntaxException)that nothing could throw. It only compiled becauseTranslatorToZ3.close()declaresthrows Exception, and with the solving moved into its own method it no longer compiles.TranslatorToZ3starts reading something new from the context, the key must include it. The class comment says so, andcheckmode in the tests is the safety net.main: the numbering of fresh variables in one message (transactionCommitted²⁷vs²⁸inE_OSS_A1_50) depends on what was verified earlier in the same JVM. It shows with the cache off as well, and does not affect verdicts. I'll look into it separately.🤖 Generated with Claude Code