feat: def-site anchored copies + bitfield member recovery - #21
Merged
Conversation
…fsite axis) SSA destruction places a merge copy where its CFG edge is, but the asm often materialized the constant earlier — movs r9,#0 at entry ahead of a single-armed overwrite, movs r5,#1 at the top of an arm ahead of a nested if. The new anchorConstCopies structuring option emits such a copy at the const op's original program position and suppresses the edge copies it replaces; where the surviving arm empties, the existing empty-then peephole yields the single-armed positive if the source wrote. Sound by refusal, never by approximation: only unnamed const args; the def's block must dominate every edge source; ANY in-loop shape declines outright (block dominance is not per-iteration precedence — the /preinit sticky-arm class from PR #13); the merge variable must name no other value; and a const whose anchored sibling could clobber it on a path to its edge keeps the edge placement. Off by default; rank.ts enumerates it as the differ-refereed /defsite axis crossed with branch sense, so it can never cost a match. kleod:UpdateHUDCounterDisplay:agbcc 18 -> 6 (the remaining 6 are the bitfield signedness family, built separately). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The symbol map now carries bitfield members (bitWidth + bitOffset, LSB-first), and three consumers learn what to do with them: - structure.ts recognizes the `(x << a) >> b` extract of a struct global's loaded bytes as the declared bitfield at exactly those bits and spells `gSym.field` — whose `u32 f : n` declaration makes C's own promotion reproduce the signedness every downstream operator compiled with (a 7-bit unsigned field promotes to signed int, so sdiv renders `/` and recompiles to __divsi3 where the raw-shift spelling stayed unsigned). A load whose every use is a spelled extract is absorbed — its materialized temp would recompile to a second load. Exact, never approximate: position, width and signedness must all match, the window must lie inside the loaded bytes, a signless field never matches, and a volatile container refuses the fold. - declaredFields seats members by BIT cursor (co-located bitfields are members, not union aliases; a plain member tied at one offset keeps winning as before), and drops any bitfield the `u32 f : n` no-straddle allocation model cannot reproduce. The exact (offset,size) scalar rules now exclude bitfields — a 7-bit field spans 2 bytes and would otherwise match a plain u16 read. - declare.ts renders bitfield runs with `u32 asmlift_pad_N : k` bit padding; verified byte-faithful against agbcc (extract shifts, __divsi3 promotion, and a cross-byte field all reproduce from the synthesized decl alone). The provider keeps bitfield facts only for LITTLE-ENDIAN ELFs — both the extract equation and the layout model are LE-GCC semantics, so a big-endian map carries no bitfield members at all (exactly today's behavior). A package reporting bitWidth without bitOffset refuses loudly (the same key-presence capability gate as every other fact). kleod:UpdateHUDCounterDisplay:agbcc 6 -> 0: with /defsite this row is now a MATCH (unsigned/defsite/scopebase-coalesce-v2-v4, exit 0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rojects kleod +40 bitfield members, pokeemerald +5099, sa3 +5; the big-endian projects (af, marioparty3, snowboardkids2) correctly carry none — the provider's EI_DATA gate at work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dd the /no-bitfield axis CRITICAL 1 — the bitfield fold moved a captured load past stores and calls: `g.field` re-reads memory at the render position, but the asm captured the bits at the load's. The fold now clears the same bar as every other memory read in the structurer: it refuses whenever a call, or a store that may alias the folded global, lies between the load and any position the member read renders at (linear program position, the analysis.ts liveAcrossCall idiom — a barrier on a disjoint path refuses harmlessly). A materialized shl refuses too (its temp would still read the load). CRITICAL 2 — an anchored const write in a switch-tree TEST block was discarded with the block while its edge copy stayed suppressed (s(1) returned 0). PRE4 now treats an anchored write as impurity: such a block is never consumed as a discarded test, so it re-roots the chain instead and its statements — the write included — are emitted before the dispatch. MAJOR — the fold was unconditional, and the named read recompiles at the DECLARATION's access width; where that diverges from the asm's load width the honest shifts are the spelling that matches. The OFF spelling is now the /no-bitfield axis, enumerated only when the map carries bitfield members. Also: TargetDescription.capabilities.endianness gains its first consumer — structureOptionsFor threads littleEndian into structure(), so a hand-built BE map cannot reach the LSB-first extract equation (the provider's EI_DATA gate, now enforced on core's side too). CAST_PATTERNS' width-8/16 shadowing of the recognizer is documented in both files and pinned by a test. kleod:UpdateHUDCounterDisplay:agbcc stays a MATCH (unsigned/defsite/scopebase-coalesce-v2-v4: 0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…k order is not path order The second adversarial pass broke the first fix: fn.blocks follows ADDRESS order, so a store block laid out after the render block but executing between the load and the render on the taken path slipped past the linear-position scan, and the fold emitted a member read of post-store bits. The gate now reuses the materialization model's own machinery, newly exported from analysis.ts: `emitPos` resolves where each extract actually renders (transitively through its inlining consumers; unresolvable refuses), and `memWriteBetween` walks every def-avoiding load-to-render PATH — the same cycle-aware discipline the materialize decisions use — for a call, an opaque, or a store not provably to a different named global. The kleod row's fold still clears the gate (its astores hit gBgTilemapBufs, a different global, on disjoint or later positions) and the row stays a MATCH. Regression pin: the non-topological layout shape from the audit is now a test; the linear scan fails it, the path walk refuses it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ost) kleod:UpdateHUDCounterDisplay:agbcc flips nonmatch(diff:18) -> MATCH on unsigned/defsite/scopebase-coalesce-v2-v4, and the /defsite axis also flips five synthetic agbcc rows carrying the same pre-init shape: fcmp, iszero, lor, notb, signum. regression: 0 lost, 0 missing, 6 gained, 0 other flips over 743 committed rows. Provenance 63f02b6, clean tree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Fourth round on
kleod:UpdateHUDCounterDisplay:agbcc.diff:18→MATCH(m2c:noncompile). Board: asmlift 360 vs m2c 342, 0 lost, 6 gained, 0 other flips over 743 rows.The 18 remaining rows split into exactly two missing capabilities, and the row falls out of building both — hand-edited variants attributed every row up front (pre-init/placement ~12, bitfield signedness ~6; all three edits combined compiled byte-exact before any code was written).
Capability 1 — def-site-anchored constant merge copies (
/defsite)SSA destruction places a merge copy where its CFG edge is, but the asm often materialized the constant earlier:
movs r9, #0at entry ahead of a single-armed overwrite,movs r5, #1at the top of an arm ahead of a nested if. The newanchorConstCopiesstructuring option emits such a copy at the const op's original program position and suppresses the edge copies it replaces; the existing empty-then peephole then yields the single-armed positiveifthe source wrote — polarity included.Sound by refusal: unnamed-const args only; the def's block must dominate every edge source; ANY in-loop shape declines outright (block dominance is not per-iteration precedence — the
/preinitsticky-arm class from #13); the merge variable must name no other value; a const whose anchored sibling could clobber it on a path to its edge keeps the edge placement; and a switch-tree test block carrying an anchored write is never consumed as a discarded test (it re-roots the chain, so the write is emitted before the dispatch). Off by default, enumerated as the differ-refereed/defsiteaxis crossed with branch sense — it can never cost a match.This axis alone flipped five synthetic rows carrying the same shape (
fcmp,iszero,lor,notb,signum— thev=0; if(...) v=1boolean-result idiom), on top of taking this row 18→6.Capability 2 — bitfield members from the symbol map
The map now carries bitfield members (
bitWidth+bitOffset, LSB-first), and the(x << a) >> bextract of a struct global's loaded bytes is spelledgSym.fieldwhen the map declares a bitfield at exactly those bits. The payoff is typing, not cosmetics: the declaredu32 dreamStones : 7promotes to signedint, sosdivrenders/and recompiles to__divsi3where the raw-shift spelling stayedu32(__udivsi3,bls-for-ble— the row's whole 6-point family).Exactness gates: position, width, and signedness must all match; the window must lie inside the loaded bytes; a signless field never matches; a volatile container refuses the fold;
declaredFieldsseats members by bit cursor and drops anything theu32 f : nno-straddle allocation model cannot reproduce — verified byte-faithful against agbcc (extract shifts, promotion-driven__divsi3, and a cross-byte field all reproduce from the synthesized declaration alone). The provider emits bitfield facts only for little-endian ELFs (EI_DATA), and core enforces the same boundary viacapabilities.endianness— its first consumer.Adversarial rounds (two passes, as this repo's history demands)
Pass 1 found two CRITICALs, both fixed and pinned by tests:
g.fieldre-reads memory at the render position where the asm captured bits at the load's. Fixed with an ordering gate.Pass 2 then broke the first fix:
fn.blocksis ADDRESS order, not topological order, so a store laid out after the render but executing between load and render slipped past the linear-position scan. The gate is now path-based, reusing the materialization model's ownemitPos/memWriteBetweenmachinery (newly exported fromanalysis.ts) — the same cycle-aware discipline every other memory read already obeys. Also from the audits: the/no-bitfieldfallback axis (the named read recompiles at the declaration's access width; where that diverges the honest shifts are what match, and the differ referees), and the width-8/16CAST_PATTERNSshadowing is documented in both files and pinned.Verification
pnpm bench run+regression→ 0 lost, 0 missing, 6 gained, 0 other flips (743 rows);stale-checkfresh, provenance63f02b6,dirty: false.pnpm test:offline697 passing (57 files).pnpm typecheck,pnpm lint(0 errors),pnpm format:checkclean. Maps re-vendored: kleod +40 bitfield members, pokeemerald +5099, sa3 +5; the big-endian projects carry none by construction.🤖 Generated with Claude Code