Skip to content

feat: def-site anchored copies + bitfield member recovery - #21

Merged
macabeus merged 6 commits into
mainfrom
match/UpdateHUDCounterDisplay-4
Aug 5, 2026
Merged

feat: def-site anchored copies + bitfield member recovery#21
macabeus merged 6 commits into
mainfrom
match/UpdateHUDCounterDisplay-4

Conversation

@macabeus

@macabeus macabeus commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Fourth round on kleod:UpdateHUDCounterDisplay:agbcc. diff:18MATCH (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, #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; the existing empty-then peephole then yields the single-armed positive if the 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 /preinit sticky-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 /defsite axis 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 — the v=0; if(...) v=1 boolean-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) >> b extract of a struct global's loaded bytes is spelled gSym.field when the map declares a bitfield at exactly those bits. The payoff is typing, not cosmetics: the declared u32 dreamStones : 7 promotes to signed int, so sdiv renders / and recompiles to __divsi3 where the raw-shift spelling stayed u32 (__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; declaredFields seats members by bit cursor and drops anything the u32 f : n no-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 via capabilities.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:

  • The fold moved a captured load past stores/callsg.field re-reads memory at the render position where the asm captured bits at the load's. Fixed with an ordering gate.
  • An anchored write in a discarded switch test block was silently dropped while its edge copy stayed suppressed. Fixed in PRE4.

Pass 2 then broke the first fix: fn.blocks is 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 own emitPos/memWriteBetween machinery (newly exported from analysis.ts) — the same cycle-aware discipline every other memory read already obeys. Also from the audits: the /no-bitfield fallback 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/16 CAST_PATTERNS shadowing is documented in both files and pinned.

Verification

pnpm bench run + regression0 lost, 0 missing, 6 gained, 0 other flips (743 rows); stale-check fresh, provenance 63f02b6, dirty: false. pnpm test:offline 697 passing (57 files). pnpm typecheck, pnpm lint (0 errors), pnpm format:check clean. Maps re-vendored: kleod +40 bitfield members, pokeemerald +5099, sa3 +5; the big-endian projects carry none by construction.

🤖 Generated with Claude Code

macabeus and others added 6 commits August 4, 2026 23:40
…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>
@macabeus macabeus changed the title feat: def-site anchored copies + bitfield member recovery (UpdateHUDCounterDisplay matches) feat: def-site anchored copies + bitfield member recovery Aug 5, 2026
@macabeus
macabeus merged commit cbe6b90 into main Aug 5, 2026
2 checks passed
@macabeus
macabeus deleted the match/UpdateHUDCounterDisplay-4 branch August 5, 2026 00:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant