Repository navigation
verify: Bound what a run builds and what its solvers allocate - #48
Open
SwayamInSync wants to merge 36 commits into
Open
SwayamInSync wants to merge 36 commits into
SwayamInSync wants to merge 36 commits into
Conversation
This was referenced Oct 20, 2026
…odes with every backend.
SwayamInSync
force-pushed
the
fix/memory-budget
branch
from
October 21, 2026 00:51
0a3ce2d to
69da72a
Compare
This was referenced Oct 21, 2026
This branch has not been deployed
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.
Nothing bounded the memory of a run. Two constructions grow faster than the program, and the process only stopped when the system killed it:
suite/llvm_uleb128_decoder_exhaustive.cpp,decode_uleb128_length_4to_11have modules of 41 to 248 million expression nodes. On main the file takes about 87 GB and 1655 s;--lower-onlyalone, with one job, passed 57 GB on one function before I stopped it.--max-query-nodesexisted but was 0 by default, and it was checked only once the module had been built.cppverify::reveal_with_fuel(fibo, 18)lowers in 267 s and peaks at 22 GB, and fuel 24 passed 40 GB within 65 s.Z3 and cvc5 had no memory limit either.
This change bounds each of them. A function that reaches a limit is
Unresolved, never a proof or a counterexample, and every function whose proof relies on its contract isUnresolvedwithcallee.contract.--max-query-nodes: what is built for one function (default 32,000,000 expression nodes;0disables it)obligationModuleNodeCountcounts it, follows from the lowered statements: four times the goals' nodes (for the goals, their queries, the complete goal, and its query), plus three per obligation, less one.buildPassivecomputes it before cloning anything and returnsObligationSizeErrorwhen it is over the limit. In builds with assertions, the prediction is checked against the built module on every module of the suite.NodeBudget(IR/NodeBudget.h) counts theVExprandLogicExprnodes built on its thread, against the limit the driver sets for the function. The spec inliner unfolds nothing more once the budget is spent. Three constructions run inside a budget:PassiveProgram::TooLarge, which makes the build anObligationSizeError;SpecAxiomsmaterializes, counted level by level into the module's count;Unresolvedwithquery.size-limit, at the function's declaration. The message says what was too large and what to change:safe_fib.cpp, has 11.3 million (4.1 GB), and every other file stays under 1.2 million. The default keeps the case study with headroom and stops the decoder's functions above 32 million at once.the obligation module has N expression nodes, more than the limit of M (--max-query-nodes).--memory-limit=MB: the solvers (default 8192;0disables it)solver.memory-limit. Z3's own limits are not used:memory_max_sizefails whichever allocation passes it, in any context of the process. That includesZ3_mk_context's, which then returns no context, and z3++ dereferences it.smt-canceled, and the check freed its memory before returning, so a sample taken after the check could not tell why it stopped.memout,max. memory,out of memory, as from a failed allocation) is also classified assolver.memory-limit. Following the existing API-error rule, an encoder that ran out reports that and proves nothing.RLIMIT_DATA. The limit is set withprlimitonce the process is spawned; on Windows,ExecuteNoWait's job object sets it. Passing the limit toExecuteNoWaiton POSIX makes itforkrather than spawn, andforkruns Z3's fork handler (scoped_timer::finalize), which waits for the timers of every running check: the fidelity gate's portfolio run hung with four threads in it. When an allocation fails, cvc5 prints(error "std::bad_alloc"), or aborts withstd::bad_allocor its SAT solver'sMinisat::OutOfMemoryException; each of these issolver.memory-limit. macOS applies no such limit to mapped memory, so there cvc5 is not bounded.--int-encoding=bitvector, a Z3 query stopped for memory is retried in the integer encoding, as after a timeout.--memory-limitjoins verify: Reject the solver options with the Lean backend #41's list of solver options, which are errors with Lean export (error: --memory-limit requires a backend that runs an SMT solver).--max-query-nodesleaves that list: it now bounds what is built under every backend, Lean's included.suite/backend_options.cppchecks both, the first as an error and the second as an accepted--lean-outexport.Results
--jobs=8 --timeout=30000) finishes in 46 s with a peak of 21 GB, against 87 GB and 1655 s on main. Lengths 4 to 11 areUnresolvedwithquery.size-limit; every other function verifies, as on main.--max-query-nodes=0they behave as on main. Bringing them under the default is C3's job (hash-consed Layer 3, where the obligations share their prefixes).reveal_with_fuel(fibo, 24)ends in 7 s at 3.9 GB, against more than 40 GB on main.--memory-limit=100, against 2 GB at its 20 s timeout without a limit.Docs:
query.size-limit), since no obligations were built.Testing:
suite/obligation_size_limit.cppchecks:callee.contract, and a sibling that still verifies;--lower-onlyerror;stepsverifies, at 24,416 it is refused;reveal_with_fuel(fibo, 40)stopping at the limit;suite/solver_memory_limit.cppchecks that the matching loop stops at--memory-limit=100withsolver.memory-limit, in text and JSON, and that the next function verifies within the limit. It also checks that--memory-limitwith Lean is an error.suite/cvc5_memory_limit.cpp(Linux, cvc5) checks that a cvc5 process limited to 5 MB reportssolver.memory-limit.suite/memory_bounded.test(Linux) runs a 400-check function (45 million nodes predicted) andreveal_with_fuel(fibo, 40)underulimit -v 3000000. Both end with their verdicts. On main both abort withLLVM ERROR: out of memorywithin 5 s.suite/backend_execution.testnow expects the new messages; its--dump-ir=4run, which builds no module, reports the verdict.Checked:
-W.llvm::Errorsuccess value in the size error's path. With that fixed, the four pass, along with the other new tests. In that build an assertion compares the predicted size with the built module on every module the suite builds.mainafter verify: Reject valid(p, n) outside a precondition #40-verify: Turn off AssertMacros.h's names without underscores on Apple targets #42, resolving verify: Reject the solver options with the Lean backend #41's option list and its tooling paragraph as above. After the rebase the release Verify suite passes again (429, 1 unsupported). This merges cleanly with verify: Follow the body of a choose in a spec's reads and postcondition checks #43-verify: Report each conversion error at its source, with its line and caret #47 and docs: Publish the documentation of each release, and of main at /dev/ #49.