name: stack-layout
description: Diff stack-frame layouts between target and base for a function. Labels base-side slots with source variable names from a MSVC /Z7 CodeView recompile. Identifies SWAPPED pairs (decl-reorder candidates), SHIFTED slots, DIFFER (different variables in same slot), and TGT_ONLY / BASE_ONLY (extra/missing locals). Filters out callee-save slots.
argument-hint: " [-u ]"
allowed-tools: Bash(python3 scripts/analysis/stack_layout.py *), Bash(python3 scripts/analysis/codeview_locals.py *), Bash(python3 scripts/analysis/diff_inspect.py *), Read, Grep, Glob
Stack Layout Diff Skill (MSVC X360)
Compare stack-frame layouts between target and our build for a single function. Returns frame size and callee-save counts at the top, then a per-slot diff table with verdicts that point to specific source fixes.
Arguments
$ARGUMENTS — the symbol (MSVC mangled ?Name@Class@@QAEHXZ or demangled
Class::Method(args)). Append -u <unit> if ambiguous (e.g.
-u default/system/hamobj/HamCharacter).
Steps
Run the diff:
python3 scripts/analysis/stack_layout.py --symbol "$ARGUMENTS" --project-dir .Read the prologue summary at the top:
Frame ΔandCallee-saved GPR/FPR Δ- On MSVC X360, GPRs are 64-bit (
std) — 8 bytes per saved GPR. - If frame Δ is fully explained by callee-save counts → AT_LIMIT (not source-fixable).
- Otherwise the structural Δ remainder is the real lever.
Read the verdict table. Rows are sorted most-actionable first:
Verdict Meaning Action SWAPPED Two slots' fingerprints exchanged Reorder the two declarations DIFFER Same offset, different fingerprint Different variable lives there — decl-reorder SHIFTED Same fingerprint, offset differs by the dominant Δ One side has an extra local pushing the rest TGT_ONLY Slot exists only on target Target spills a temp we keep in a register (or vice versa) BASE_ONLY Slot exists only on our build Extra spill; usually a register-pressure symptom MATCH Hidden by default; pass --show-equalto seeFingerprint columns (
kind sz=N L=loads S=stores A=accesses [first..last]):float sz=4→floatfloat sz=8→double/ paired-single storeint sz=4→int, pointer,bool, or 32-bit memberint sz=8→ 64-bitstd/ld(frequent on Xenon)addr sz=0→ anaddi rN, r1, offtaking address-of
"base var" column — the source variable name our build allocates at that offset, extracted from a MSVC
/Z7CodeView recompile. Use it to identify exactly which declaration to reorder.
When to Use
run_objdiffoutput flagged**Stack:** frame Δ ... | N SWAPPED ...- Function diff shows many
[off:+N]annotations - Frame sizes don't match between target and our build
- You suspect a declaration reorder is the fix
Output knobs
--no-names— skip CodeView recompile + name extraction--show-equal— include MATCH rows--show-callee-save— include prologue/epilogue callee-save slots (hidden by default)--json-file <path>— skip objdiff invocation; load diff JSON from cached path
How name extraction works
The tool recompiles the function's source file with /Z7 to embed CodeView
records in the .debug$S COFF section, parses S_REGREL32 records, and maps
each register-relative variable to a frame-offset → name pair. Cached at
/tmp/claude/stack_codeview/<base>.<hash>.cv.obj by source mtime + cflags
hash; second runs are ~0.5s.
Frame-register handling: CodeView records reg=2 (= PPC r1) or reg=32
(= PPC r31). MSVC X360 commonly aliases r31 to new r1 via
subi r31, r1, FRAMESIZE before stwu, so r31-relative offsets equal
r1-relative offsets after frame allocation. The tool accepts both.
Limits:
- Base side only: there's no debug build oracle for the target. TGT_ONLY rows show no name.
- Compiler temps unnamed:
/O1strips many locals into registers — empty "base var" cell ≠ "unknown variable" — it's "no source declaration." - Same name in nested scopes: deeper scope wins via min-depth merging.
Detection limits
- Callee-save detection handles:
bl __savegprlr_NN/bl __savefpr_NNhelpers- Manual pre-stwu saves:
stw r12, -8, r1,std rN, -off, r1,stfd fN, -off, r1 - Post-stwu
stmw rN, off, r1(rare on X360)
- Unusual prologue shapes may under- or over-count; verify against the asm if the frame summary looks off.
MSVC X360 prologue cheat-sheet
mflr r12 ; LR -> r12 (not r0 like Wii MWCC!)
bl __savegprlr_NN ; r12->-8(r1), r31..rNN std at -16, -24, ...
stfd f31, -0x?, r1 ; manual FPR save (pre-stwu, NEGATIVE offset)
subi r31, r1, FRAMESIZE ; optional: r31 = new r1 (frame-ptr alias)
stwu r1, -FRAMESIZE, r1 ; allocate frame
... body code (uses r1 or r31 base) ...
addi r1, r1, FRAMESIZE ; deallocate
b __restgprlr_NN ; restore + return
After stwu, the saved-register slots end up at frame_size - 8 (LR) down to
frame_size - 8 - 8*(saved_count-1).
Tips
- After a declaration reorder, re-run to confirm SWAPPED rows resolve.
Dominant body-offset shiftis reported separately from frame Δ — the dominant shift is what SHIFTED rows are normalized against.- If verdicts are all DIFFER with no clean SHIFT/SWAP, the function is
mid-reflow; try
/compare-asmorrun_diff_inspect mode=diagnose.