Repository navigation
C2: a memory budget, counted as distinct nodes as they are built, and solver memory limits - #48
Merged
Merged
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
…ad of predicting their size as trees.
…ords, in obligation_size_limit.cpp.
…s and records checked as trees.
This was referenced Oct 27, 2026
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_11had modules of 41 to 248 million expression nodes, and the file took about 87 GB and 1655 s. verify: Share the subterms of logic terms instead of copying them #64 made the goals share their assumptions' terms, so a goal now adds three nodes per assumption, whatever the assumption's size. That is still quadratic in the function's length: a function of 800 checks took 13 GB, so one of 1,200 would take about 30 GB.--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)lowered in 267 s and peaked 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 4,000,000 distinct expression nodes;0disables it)NodeBudget(IR/NodeBudget.h) counts theVExprandLogicExprnodes built on its thread, against the limit the driver sets for the function.ObligationSizeErroronce they pass the limit.PassiveProgram::TooLarge, which makes the build anObligationSizeError.SpecAxiomsmaterializes are counted by their distinct nodes, from the module's own count.obligationModuleNodeCount, which the backends check for modules not built from source (archive replay), counts distinct nodes too.--obligation-out) write a shared subterm at each of its places, so they are larger than what is built. Each counts the module's complete query and definitions as trees, stopping once past the limit (obligationModuleTreeNodesUpTo). Over it, cvc5's verdict isUnresolvedwithquery.size-limit(its SMT-LIB script would state more than N expression nodes), and the record is an error. Z3 hash-conses terms, and the Lean printer names a shared subterm with alet, so the distinct count bounds them.Unresolvedwithquery.size-limit, at the function's declaration. The message says what was too large and what to change:iterative_fibonacciinsafe_fib.cpp, at 375,000 nodes with the definitions of the specs it applies (11.3 million as trees). The ULEB128 case study takes under 200,000 a function (22.6 million as trees), and the exhaustive decoder's largest function fits too. A node takes about 600 bytes, and Z3's encoding of it about five times that (massif sees the nodes,operator newin this static build, and RSS the rest). So the default lets one function take up to about 12 GB, with ten times the suite's largest function as headroom.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.ExecuteNoWaiton POSIX makes itforkrather than spawn.forkruns 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.(error "std::bad_alloc"), or aborts withstd::bad_allocor its SAT solver'sMinisat::OutOfMemoryException. Each of these issolver.memory-limit.--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.Results
The exhaustive decoder at the defaults (its own
--jobs=8 --timeout=30000) verifies all 68 functions, lengths 0 to 11 included, in 253 s with a peak of 9.4 GB. Before verify: Share the subterms of logic terms instead of copying them #64 the file took about 87 GB and 1655 s, with length 10 timing out, and the first version of this PR refused lengths 4 to 11 for their size. The test staysUNSUPPORTED(T10): at that memory it is too large for the CI runners.reveal_with_fuel(fibo, 40)stops at the default limit in 0.9 s with a peak of 0.44 GB; before this change, fuel 24 took more than 40 GB.The matching loop ends at 0.14 GB with
--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 1,938 it is refused;stepsis built and proved, though its archive record would state more and is refused;reveal_with_fuel(fibo, 40)stopping at the limit;--help.suite/cvc5_script_size.test(new, requires cvc5) checks that cvc5 refusesstepsat 3,000 nodes, where its script would state more.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 1,200-check function andreveal_with_fuel(fibo, 40)underulimit -v 3000000. Both end with their verdicts within the address space.suite/backend_execution.testnow expects the new messages; its--dump-ir=4run, which builds no module, reports the verdict.Mutants of the first version, each failing at least one of these tests:
Mutants of this design, each applied alone:
cvc5_script_size.test(and the boundary)obligation_size_limit.cppmemory_bounded.testobligation_size_limit.cppcvc5_script_size.testobligation_size_limit.cppcvc5_script_size.test,obligation_size_limit.cppobligation_size_limit.cpp(--help)obligation_size_limit.cppS9 survived its first run:
--helpstated the default as a literal. It is now built fromDefaultMaxQueryNodes. One mutant survives: S5, the definitions counted as trees. A definition is lowered fresh from its spec's body, and no definition in the suite shares a subterm, so its distinct count is its tree count.Checked
b782bf0473). The conflicts were:Obligation.cpp: main's assumption origins next to the size check;Passivize.cpp:finishreturns nothing now;CVC5Backend.cpp: includes;check-doc-examples.py: main's Lean rule with this PR's size rule;clang/test/Verifyon the merged tree with this design: 478 passed, 1 unsupported (darwin only), no failure, includingdocs_examples.testanddocs_coverage.testwith the new numbers.