Repository navigation
Loops over local storage: an invariant's definedness at the head, and initialization kept across iterations - #74
Merged
Merged
Conversation
…r does not represent.
…the release notes.
…the address passed to a callee.
…reseated, and members named whole in modifies as their bytes.
…nd name a class template for its members.
…ject at a member call.
…n the architecture.
… to report, so no header's is mangled.
… loop that stores to local storage keeps what was initialized.
…reads bound the head.
…dition reads what the loop changes.
…ion across loops, with a checked example.
This was referenced Oct 24, 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.
A loop that fills a local array could not be verified. On the base (#73):
Now:
Stacked on #73. Found while starting B3-2 (constructors), whose bodies initialize storage in loops.
Causes
collectModified) named only the value heap for a store, so the head's initialization heap was the one before the loop: every iteration after the first saw what the earlier ones wrote as uninitialized, and after the loop only the state before it remained.continueassert each invariant's definedness (its core checks and the memory checks recorded for it) before the invariant; the head assumed only the invariant. So nothing at the head said thata[k]fork < iwas initialized, which C++ contracts cannot state.Fixing (2) alone made
filledverify vacuously: the assumed initialization contradicted the stale heap of (1), forcingi == 0at the head. (1) is what makes (2) hold in the model.Design
forall a. init_before[a] -> init_head[a], triggered by reads of the head's version (initializationKept).startsLifetimewithholds it from a loop that starts a lifetime, which none can today, so that admitting one later cannot make the assumption false. No frame bounds the rest: no claim needs a cell to be uninitialized, so taking more cells as possibly initialized loses nothing.exprSafetyCheckscollects (shared withemitExprSafety, which asserts them), is assumed at the head before the invariant: every path into the head asserts both, so both hold there by the same induction.checkedCondition:!g || c, a quantified check's body), reads state the loop modifies (readsLoopState: a variable or field it assigns, a load or a heap-reading spec when it writes memory, validity or initialization when it changes those). A condition of nothing the loop changes holds at the head as it held before the loop; when it is false, the invariant's definedness would only constrain its guards, which the invariant can state. Assuming every check made the ULEB128 case study'sencode_uleb1284.7 times larger after simplification (1.79 to 8.45 million nodes) and its Lean export exceed its budget: its invariantscount <= k || buffer[k] == uleb_byte_machine(original, k)carry the helper's definedness at a constant index under a guard ofcount. With the rule it is 1.83 million nodes, and the export is 20.1 MB against 19.9 MB on the base.TrLoop:if (!w) { CheckWellformed(inv); assume inv; ... assume false }), and assumes itsCanCallAssumptionat the head. That self-framing rule works where storage is always initialized; C++ storage is not, and its initialization is not expressible in a contract (Frama-C asks for\initialized). Here definedness is proved where the invariant is, inductively, and holds at the head with it.Tests
suite/loop_initialization.cpp(Z3, with a check that no proof is vacuous, and BMC):filled: the loop above;incremented: a loop reads elements a loop before it initialized, which no invariant of its own states (the kept initialization);kept: a scalar initialized before a loop that writes an array stays initialized;partially_filled: an element the loop never writes is not initialized after it (initialization);read_first: nor one it reads before writing (initialization);last: an invariant readingp[i]boundsiat the head, soreturn p[i]is in bounds;log2_floor: a shift in an invariant bounds its amount at the head, so the condition's shift is defined;overrun: an invariant readingc[i]past the end still fails where it is preserved.Mutants, each built on an assertions build and run on
loop_initialization.cpp,out_parameters.cpp,loop_frames.cpp,old_loop_invariant_definedness_fail.cpp, andshared_logic_terms.test: 7 of 7 killed.loop_initialization(filled,incremented,last,log2_floor)loop_initialization(partially_filled;filledverifies vacuously)loop_initialization(incremented)loop_initialization(last)loop_initialization(log2_floor)loop_initializationshared_logic_terms(the Lean export exceeds its budget)On the base,
filled,incremented,last, andlog2_floorfail (initialization,overflow,shift), andpartially_filledfails at its invariant's preservation instead of at the read after the loop.startsLifetimecannot be exercised: the converter rejects every construct that starts a lifetime in a loop. It keeps the kept-initialization fact from becoming false if one is admitted.Docs
filled,partially_filled).Found alongside
loopWriteRegions, and the function's own frame is not used when the function owns storage. That is sound, but the loop forgets the values of every other object (keptwith a postcondition onx's value fails). Next PR.Checked
shared_logic_terms.test(the export above) andcounterexample_confirmation.cpp(which certifier application a message names, under load; it passes alone and has no loops).docs_examples.testanddocs_coverage.testpass.llvm_uleb128.cpp's Lean export: 20.1 MB (19.9 MB on the base);encode_uleb128's module 1.83 million nodes after simplification (1.79 million).