Repository navigation
RQ-71-STACKDEPTH (#1341): report a per-export maximum native stack depth — the number a --relocatable embedder has never had - #1351
Merged
Conversation
…pth — the number a --relocatable embedder has never had
cpetig's export returned a bit-identical-to-wasmtime answer EVERY time and then
corrupted an unrelated RTOS pointer: it overran its NuttX task stack while the
linear-memory guard bands stayed untouched. A --relocatable object says how much
LINEAR MEMORY to reserve and nothing about native stack, so the region is
guessed, and guessing low fails SILENTLY with a plausible result.
`--emit-stack-depth` now writes a `synth-stack-v1` sidecar with a per-export
maximum, or a NAMED decline. The rule is `own + MAX over the call tree`.
THE REPORTER'S PROPOSED MECHANISM WAS UNSOUND AS STATED, which is what shaped
this. "If --emit-wcet already walks the call graph, the same traversal would
yield the depth" — the TRAVERSAL is right, the ARITHMETIC is not.
`wcet_compose` weights each callee by its PROVEN EXECUTION COUNT, so a callee
invoked 1000x in a loop would be charged 1000x the stack it uses. Stack depth
is a MAX over the call tree. And the WCET DECLINE SET is not reusable either:
`Loop`, `LoopedExpansion`, `UnmodeledOp`, `UnresolvedBranch` do not affect stack
depth, because a loop body reuses one frame every trip. Riding on it would have
refused a bound for most real modules, the reporter's control loop included.
So this is a SIBLING pass sharing only the DFS shape.
THE THREE WAYS TO COUNT, measured on the committed branch fixture:
(a) the reporter's sum of `sub sp` only : 96 B UNDER by 24 (20%)
(b) a naive sum of every frame : 176 B OVER by 56 (46%)
(c) TRUTH: max over root-to-leaf paths : 120 B
(a) is the dangerous direction and misses three things: the callee-saved `push`
(24 B, before any `sub sp`), the conditional `vpush {d8-d15}` (64 B), and every
function whose frame_size is 0 — which emits NO `sub sp` at all while still
consuming 8 bytes. The immediate is also HEX: `#0x20` is 32, and a `#[0-9]+`
regex reads it as 0. The first measuring command written for this lane had
exactly that bug and reported "sum: 0".
THE FIGURE DEPENDS ON THE LINK MODE, AND THAT IS LEGITIMATE. The oracle's first
version asserted --relocatable and self-contained agree. They do not:
8/64/64/120 vs 16/72/72/128 on one fixture, because --relocatable forces the
DIRECT selector (#197) and the optimized prologue costs 8 more bytes per frame.
The number belongs to the build you ship; the oracle now asserts the two paths
agree on WHICH functions are boundable and lets the figures differ.
EVIDENCE, and the oracle found two of its author's errors:
- scripts/repro/stack_depth_1341.py, CI-wired, 6 emulations, RED-FIRST
against the pre-feature binary ("unexpected argument --emit-stack-depth").
- Both bounded fixtures: reported 128 B vs measured peak 128 B — slack ZERO.
- `call_indirect` reaches the stream as `ldr rN,[pc,#..]` + `blx rN`, not as a
CallIndirect pseudo-op, so the catch-all was declining it `unknown-sp-move`
— safe and WRONG: that tells an embedder the walker got confused rather
than that their call graph is unresolvable. The oracle demanded the
accurate reason.
- The oracle also wrongly required that NOTHING in an unboundable module be
bounded; that module's leaves are perfectly boundable and only the EXPORT
matters. Both corrections are recorded in the script, not quietly applied.
- 7 synth-backend unit tests, incl.
`composition_takes_the_max_over_branches_not_the_sum` — the one that catches
a regression to the WCET composer's arithmetic.
NOT CLAIMED: #1341 asked for TWO things and this ships ONE. The depth is
REPORTED (sidecar + compile line); it is NOT recorded in the object as an ELF
note, so a build step must read a file beside the .o rather than the .o itself.
Weaker than asked, and stated as the follow-up it is. The documentation half was
already met before this work.
MUTANT LEDGER RE-ANCHORED, proactively this time (the v0.70 lesson): this diff
inserts lines in arm_backend.rs ABOVE five pinned sites. `reanchor` moved 14
sites by exactly +8, 0 not found, with mutants/controls/ci_subset/UNTESTED all
unchanged — re-located, never re-baselined.
The unicorn job's `--min-oracles` floor moves 6 -> 7: it now runs 7 oracles, and
leaving the floor would let this one be deleted silently.
cargo fmt rc=0, clippy --workspace --all-targets -D warnings rc=0, cargo test
--workspace 168 suites / 0 failures; claim_check 75/75, status_evidence +
check_version_pins + oracle_wiring + artifact_citation all rc=0.
Refs #1341
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…7762 for the oracle this lane landed CI reddened `Claim Check` on #1351 and the gate was right. `--exact-emulation-floor` is EXACT, not a lower bound (RQ-63-FLOOREQ, v0.63) precisely so that an oracle landing without its same-PR bump cannot re-open slack silently — the v0.63 measurement found a +50 drift that left the wiring gate, claim_check AND status_evidence all at exit 0. `scripts/repro/stack_depth_1341.py` declares `# ci-checks: emulations >= 3`, so the derived total moves 367759 -> 367762. TWO places pin it and both had to move together, which is the pair doing its job: .github/workflows/ci.yml:603 --exact-emulation-floor 367762 claims.yaml:3756 text: "--exact-emulation-floor 367762" Verified both directions rather than one: `oracle_wiring_check.py --exact-emulation-floor 367762` exits 0 and `... 367759` exits 1, so the pin is load-bearing and not decorative. The other 367759 hits in the tree are HISTORICAL records of past releases (CHANGELOG's v0.66 entry, the v0.66 cold review) and are deliberately untouched — they describe the tree as it was, not as it is. claim_check 75/75, status_evidence + check_version_pins + oracle_wiring + artifact_citation all rc=0. No Rust changed. Refs #1341 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
… three new fixtures — and ASSERT the attributed-instruction count that had been narrated and went stale CI reddened the home-register write audit and the gate was right: the audit's corpus includes `scripts/repro/*.wat`, and this lane commits THREE fixtures there. Same bookkeeping RQ-70-ALIAS and RQ-70-NPA each got. functions audited: 7207 -> 7239 homes watched: 8220 -> 8228 PROVEN CORPUS GROWTH, NOT A CODEGEN CHANGE, by measurement rather than by the obvious argument. The stack-depth pass is a pure read of `final_instrs` and emits nothing, so it "cannot" move bytes — but the #1321 note set the precedent of proving it, so: holding the three new fixtures aside and re-running THIS BINARY over main's corpus reproduces 7207/8220 EXACTLY. AND THE ISOLATION RUN FOUND SOMETHING ELSE. It printed `attributed instructions: 217089` where the #1321 comment says "the 217081 attributed instructions are UNCHANGED". That figure is STALE and had been for two releases: RQ-70-NPA added a fixture (+4 functions, +6 homes), which necessarily added instructions, so 217081 could not still hold the moment NPA landed. NOTHING CAUGHT IT BECAUSE NO GREP EVER READ IT. The step asserts functions, homes, POTENCY, WIDTH-1226 and hits; the attributed count lived only in a comment. That is precisely the "a ratchet fixes the number and cannot fix the prose" shape recorded in v0.69's lessons, sitting inside the job whose whole purpose is pinning printed totals EXACTLY (the #1112 pattern). So it is now ASSERTED rather than narrated: grep -Eq '^attributed instructions: 217291 ' haa1189.txt and the stale sentence is corrected to say "the then-current attributed-instruction count" instead of quoting a number that had moved. The two remaining `217081` occurrences are deliberate quotations inside the new comment explaining the staleness. Every one of the step's six assertions was replayed against the derived output before committing; all six match. Local note for whoever hits this next: the audit FAILS in a fresh worktree with "found 0 top-level .wast, want 257" — `tests/spec-testsuite` is a submodule and is not populated by `git worktree add`. `git submodule update --init --depth 1 tests/spec-testsuite` fixes it; the submodule pointer is unchanged and is not part of this commit. claim_check 75/75, status_evidence + check_version_pins + oracle_wiring + artifact_citation all rc=0. No Rust changed. Refs #1341 Refs #1189 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
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.
Gives a
--relocatableembedder the one number the object has never carried: how much native stack a call into an export can consume.The failure this is about
cpetig's export returned an answer bit-identical to wasmtime, every time, and then corrupted an unrelated RTOS pointer. It overran its NuttX task stack while the linear-memory guard bands above and below stayed untouched. A
--relocatableobject tells the embedder how much linear memory to reserve and nothing at all about stack, so the region gets guessed — and guessing low fails silently, with a plausible result.The reporter's proposed mechanism was unsound as stated
The traversal is right — memoized DFS, cycle detection, honest declines. The arithmetic is not.
wcet_composeweights each callee by its proven execution count, so a callee invoked 1000× inside a loop would be charged 1000× the stack it uses. Stack depth is a MAX over the call tree, never a trip-weighted sum. And over-reporting isn't the safe direction: an embedder told 2 MB for a 2.3 KB requirement stops believing the number.The decline set isn't reusable either.
WcetDeclinecarriesLoop,LoopedExpansion,UnmodeledOp,UnresolvedBranch— none affects stack depth, because a loop body reuses one frame every trip. A data-dependent loop is WCET-unbounded and perfectly stack-bounded. Riding on it would have refused a bound for most real modules, the reporter's control loop included. So this is a sibling pass sharing only the DFS shape.Three ways to count, measured on a committed fixture
stack_depth_branch_1341.wat—topcallsaandb; both callleaf:sum(sub sp)(a) misses three things. The prologue moves SP in up to three steps —
push.w {r4-r8,lr}(24 B, always),vpush {d8-d15}(64 B, conditional),sub sp— andsub spis emitted only whenframe_size > 0, soleafconsumes 8 B while being invisible to that grep. The immediate is also hex:#0x20is 32, and a#[0-9]+regex reads it as0. That one isn't hypothetical — the first measuring command written for this lane had exactly that bug and printed "sum: 0".The figure depends on the link mode, and that's legitimate
The oracle's first version asserted
--relocatableand self-contained builds report the same number. They don't: 8/64/64/120 vs 16/72/72/128 on one fixture.--relocatableforces the direct selector (#197); the optimized prologue costs 8 more bytes per frame. The number belongs to the build you ship, so the oracle now asserts the two paths agree on which functions are boundable and lets the figures differ.Evidence — and the oracle found two of its author's errors
scripts/repro/stack_depth_1341.py, CI-wired, 6 emulations, proven red-first against the pre-feature binary (rc=1, "unexpected argument--emit-stack-depth").call_indirectwas declining with the wrong reason. It reaches the stream asldr rN,[pc,#..]+blx rN, not aCallIndirectpseudo-op, so the catch-all called itunknown-sp-move— safe, and wrong: that tells an embedder the walker got confused rather than that their call graph is unresolvable.synth-backendunit tests, includingcomposition_takes_the_max_over_branches_not_the_sum— the one that catches a regression to the WCET composer's arithmetic.What is NOT claimed
#1341 asked for two things and this ships one. The depth is reported (sidecar + compile line); it is not recorded in the object as an ELF note, so a build step must read a file beside the
.orather than the.oitself. That's weaker than asked, and it's stated as the follow-up it is. The documentation half was already met before this work —docs/embedder-abi-relocatable-arm.mdalready said the embedder owns stack placement and size; what was missing was a number.The reporter's own module is an issue attachment, so their 1984/4032 B bracketing stays [REPORTER-ONLY] and is not restated as re-derived.
Housekeeping done deliberately
arm_backend.rsabove five pinned sites.reanchormoved 14 sites by exactly +8, 0 not found,mutants/controls/ci_subset/UNTESTEDall unchanged — re-located, never re-baselined.--min-oraclesfloor moves 6 → 7. It now runs 7 oracles; leaving the floor would let this one be deleted silently — the RQ-70-WINDOWVAC class.Verification
cargo fmt --checkrc=0,clippy --workspace --all-targets -D warningsrc=0,cargo test --workspace168 suites / 0 failures;claim_check75/75,status_evidence/check_version_pins/oracle_wiring/artifact_citationall rc=0.Refs #1341
🤖 Generated with Claude Code
https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L