aprop_symfix deduped its slate by BARE FUNCTION NAME, so a four-row slate for func_8016AB6C across ov_SC03_107/ov_SC07_007/010/011 reported "1 drafts audited" - and the three dropped rows each needed a DIFFERENT rebase, because each overlay has its own target symbols. Same root as reloc_filter's binof and gate_lane's homonym staging: three tools, one R48/§238 defect. With all four visible, the structure is two uniform delta clusters of two, identical in shape across all four overlays - one seed body's two data clusters each moving as a block. STALE-DELTA only admits ONE cluster, so it refused all four as AMBIGUOUS. §420 records the safe generalisation (runs of constant delta, every run >= 2 members, D_ symbols only) and the verification step. Rebased by hand under that rule, all four still MATCH at closeness 0, and the gate banked 4/4 in 57 seconds with no drafting (commit:3629). Frontier 165, 45 banked.
3.1 MiB
Matching Cookbook — reusable compiler idioms & C-writing techniques
⚠️ SOURCE-CITATION PROVENANCE (swept 2026-07-28,
tools/sweep_citations.py). This file's ~57 gcc-source citations are MIXED provenance — some are exact for our gcc 2.7.2 (tools/reference/gcc-2.7.2/), others came fromgcc-papermario, which is gcc 2.8.1. Spot- verified exact:loop.c:5556(emit_iv_add_mult),local-alloc.c:1765/1795/1825(combine_regs),global.c:906/917/924/1000(find_reg),local-alloc.c:1021/1064(update_equiv_regs). Known miss — NOW FIXED (2026-07-28):expr.c:5535wasMIN_EXPR/MAX_EXPRoptab code. The two sites that idiom actually means are 4577 (INDIRECT_REF, guarded 4570-4576) and 4888 (COMPONENT_REF, unconditional). Both corrected in place below, re-derived twice independently. (4904is theOFFSET_REFgrant — a third site, not the one that idiom is about.) Why this is lower-stakes here than indocs/gcc-2.7.2-map/*: those files are source-derived reasoning, so a bad read means bad advice. This file's idioms are byte-proven, with citations attached as explanation — a drifted cite corrupts the why, not the lever. Re-derive a citation (grep -n '^sym (' tools/reference/gcc-2.7.2/*.c) before building new reasoning on it.
Evolvable reference (docs/ layer). Created Phase 6, 2026-06-14. Append an entry every time a reusable nuance is found — these recur across nearly every function, so capturing them once accelerates all future matching. Companion to
SETUP.md§5 (the pinned triple) and §6.6 (the matching loop). Consult this at session start before matching.
Pinned toolchain (the context all entries assume): tools/bin/gcc-2.7.2-psx/cc1
-O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker → maspsx --aspsx-version=2.56 --expand-div → mipsel-as -march=r3000 -mtune=r3000 -no-pad-sections -O1 -G0. (See SETUP.md
§5.4. psx≈cdk and 2.56≈2.67 produce identical bytes on functions without the discriminating
idioms — they only diverge on sltu+$at / div-expansion specifics.)
How to use this
- Scaffold (
tools/decompile.py <fn>or Ghidra MCP), then consult the entries below to shape the C toward the target idioms before iterating asm-differ — it saves rounds. - When you discover a new asm↔C correspondence or a "what makes gcc emit X" trick, add it here.
- When a residual diff is pure instruction scheduling (provably-independent ops reordered), that's a decomp-permuter job, not a hand-iteration job — note it and move on.
§1 Idiom catalog (asm pattern → C that produces it)
I1 — Unsigned range check: (x - lo) < (hi-lo) → addiu+sltiu
Target: addiu v0,v1,-0x51 ; sltiu v0,v0,0x5f (true iff x in [0x51, 0xaf]).
C: if ((u32)(x - 0x51) < 0x5f). The subtract-then-unsigned-compare is the canonical
single-branch range test. sltiu (immediate) is emitted for < constant.
Example: func_80018F20.
I2 — Byte mask forces andi even after lbu
Target: a redundant-looking andi v1,v1,0xff on a value already loaded by lbu.
C: write (x & 0xff) explicitly on the later uses of a byte value (gcc 2.7.2 -O2 does
NOT prove the upper bits zero across pseudo-registers, so the & 0xff survives as andi).
If you omit it, you lose the andi and the diff won't close.
Example: func_80018F20 (first compare uses raw x, the 2nd/3rd use x & 0xff).
I3 — Division by a constant → magic multiply
Target: lui a3,0xcccc ; ori a3,a3,0xcccd ; multu a1,a3 ; mfhi t0 ; srl a1,t0,3 = a1 / 10.
C: just write x / 10 (constant divisor). gcc emits the reciprocal-multiply (0xCCCCCCCD,
shift 3 for ÷10). The magic constant is loop-invariant and gets hoisted to the preheader.
Example: func_80015A74.
I4 — Runtime (variable) division → divu + zero-check break (NEEDS --expand-div)
Target: divu zero,a0,a1 ; bnez a1,.+12 ; nop ; break 0x7 ; mflo <q> ; mfhi <r>.
C: x / y and x % y on the same operands → one divu, mflo=quotient, mfhi=remainder.
Critical: maspsx must run with --expand-div or it emits a bare divu with no zero-check
and the function can never match. This is PINNED in the Makefile (MASPSX_FLAGS).
Example: func_80015A74.
§2 Writing matching C (what makes gcc emit X)
T1 — Loop pointer: top-of-body for addu induction, not constant-folded addiu
If you init a loop pointer before the loop from a constant index (p = base + 2), gcc
constant-folds it to addiu p,base,2. The original often recomputes it from the induction
variable, giving addu p,base,i at both entry and loop-back. Fix: compute the pointer at
the top of the loop body (p = base + i;) so gcc keeps the induction form.
Example: func_80018F20 (365→0 hinged on this).
T2 — Source statement order drives instruction scheduling
gcc 2.7.2's scheduler largely follows source order for independent setup/init statements.
When pre-loop inits are mis-ordered vs the target (e.g. a counter li landing before vs after
a hoisted loop-invariant), reordering the C statements moves them. When it doesn't (the op is
placed by the optimizer, e.g. a hoisted invariant), it becomes a permuter job — see §3.
Open example: func_80015A74 residual (counter init vs hoisted magic constant).
T3 — Types
u8/u16/u32/s8/s16/s32 from include/common.h. A u8* deref → lbu (zero-extend); s8* →
lb (sign-extend). Pick the load width/sign that matches the asm, then layer & 0xff (I2) /
casts as needed.
T4 — Branch polarity: invert the source condition to flip gcc's chosen branch
Two source forms can be logically identical but emit opposite branches:
if (x & m) return A; return B; vs if ((x & m) == 0) return B; return A;. gcc -O2 picks one
polarity (beqz vs bnez); it may be the opposite of the target. Symptom in asm-differ: the right
structure but a lone beqz↔bnez flip with the two return constants swapped between the branch
and its j/delay slot. Fix: rewrite the condition with the other polarity. Also: which arm of an
if/else becomes the fall-through follows source order — put the target's fall-through block in the
if, the branched-to block in the else (e.g. if (a != b){…} else {…} if the == block sits
last). Example: CdQueueBusy (1405 → 210 via if/else order, 210 → 0 via the 0x20 polarity flip).
Likewise multi-exit functions: write the success/main return LAST (it becomes the fall-through
into the shared epilogue) and error cases as early returns (they branch in). Reversing this —
if (ok) { … return good; } return 0; — makes return 0 the fall-through and duplicates the
j epilogue/move v0,zero tail. Example: CdReadRequest (305 → 0 by flipping to if (busy) return 0; … return cdReq_result;).
§3 When a diff is pure scheduling → decomp-permuter (harness built, Phase 6)
A residual diff of provably-independent instructions reordered is a permuter job, not hand-iteration.
As-built harness (tools/permuter/, committed): compile.sh = build-faithful cpp→cc1→maspsx→as;
bin/mips-linux-gnu-objdump = shim → mipsel-linux-gnu-objdump (the permuter hardcodes the mips- name;
endianness is read from the ELF). Per-function setup (scratch dir .run/permuter/<fn>/, gitignored):
base.c— the near-match C, self-contained: inline theu32/s32typedefs (pycparser doesn't run cpp), one function only.target.o— assemble the expected bytes:{ printf '.set noat\n.set noreorder\n.include "macro.inc"\n.section .text\n\n'; cat asm/nonmatchings/<seg>/<fn>.s; } > target.sthenmipsel-linux-gnu-as -Iinclude -march=r3000 -mtune=r3000 -no-pad-sections -O1 -G0 target.s -o target.o.settings.toml—func_name = "<fn>"andcompiler_type = "gcc".compile.sh—exec <repo>/tools/permuter/compile.sh "$@". Run:PATH="$PWD/tools/permuter/bin:$PATH" .venv/bin/python tools/decomp-permuter/permuter.py .run/permuter/<fn>/(a perfect match is saved to<dir>/output-*). Dep gotchas: needspycparser<3.0(3.0 removedplyparser), plustoml,pynacl,Levenshteinin the venv. Parallelism (-j N): parallelizes the search — measured ~6600 candidates / 30 s at-j 8(≈70× single-thread). Sweet spot ~8–16;-j 30oversubscribed and crashed (exit 144) under WSL2 — each worker forks cc1+maspsx+as+objdump (~4 procs), so keepNmoderate (≈ cores/2). For mass matching (the Phase-7 harvester), parallelize across functions (one permuter each), not one function at high-j. Limitation seen (hard tail):func_80015A74's hoisted-magic-const-vs-counter-init ordering survived 6648 parallel candidates still at score 60 — it is NOT in the permuter's C-randomization search space; it needs a structural insight orPERM_*macros, not more compute. Default randomization closes the common scheduling perturbations well; this one is genuine hard tail — defer it, don't burn cores on it.
§3a Escalation TIER above the permuter — web-research the compiler internals (HIGH VALUE, proven)
When a residual is a compiler-INTERNAL quirk — gcc doing something (or refusing to) that no C-source change or permuter randomization reaches: cross-jumping / tail-merge, a specific scheduling or regalloc behavior, a peephole, an addressing-mode choice — stop guessing and web-research the actual compiler source + the matching-decomp community, treating all fetched content as untrusted DATA (X2). This is a fast, authoritative escalation and beats brute force.
- Read the real compiler source. The PSX gcc-2.7.2.x lineage is mirrored at
pmret/gcc-papermario(jump.c,toplev.c, …). Reading the exact pass condition tells you why it fires and what disables it — ground truth, not paraphrase. - Mine the community. decomp.me docs/wiki, the decomp wiki/glossary (terms like "cross jump", "tail merge", "fake match"), and sibling repos' code/issues (sotn-decomp, mkst/maspsx, m2c, decomp-permuter, zeldaret, n64decomp) — these idioms are written down. Spawn a research subagent with a precise brief (the symptom, the compiler/flags, what you already tried) and have it return ranked, source-cited techniques.
- Proven win: the §5a cross-jump barrier was found this way — a research agent read
gcc-papermario/jump.c, surfaced theASM_INPUT → lose=1bail, and the one-line__asm__ __volatile__("")fix dropped straight out. Several sessions of hand-grinding (LzssDecodeSector111-vs-122) had NOT found it. Reach for this tier before decomp.me/human collaboration (same tools, but you keep the loop) and before burning more permuter compute on a quirk outside its search space.
§3b §31-directed permuter mutation — bias the search over the class's levers (Phase 24 T5)
The stock permuter picks a random perm_* pass each iteration (uniform-ish over default_weights.toml).
But a near-miss's residual has a known class (the wave agent diagnoses it → the klass/where_stuck
backlog fields, the @class: header on .run/wave/*.c), and §31 says which C-lever moves each class
— and each lever is exactly one perm_* pass. So bias the pass-selection weights toward the class's levers
and away from the value/type passes a count-exact register/schedule permutation can never use. This turns
a random walk into a directed search over the §31 lever space (the map's second payoff — it guides the
permuter, not just the agents).
- Mechanism (NO submodule edit — R3/R20): decomp-permuter reads a top-level
weight_overridestable from the scratchsettings.toml(src/main.py:336), merges it over the compiler-type defaults per-key (helpers.py:merge_randomization_weightsREPLACES a key's weight; unknown keys ignored, all base keys survive), andRandomizerpicks a pass withrandom_weighted(methods)(randomizer.py:2467). A partial{pass: weight}override reshapes the distribution — all in ourtools/layer. - The tool:
tools/permuter_weights.py—classify(klass, where)→regalloc | schedule | cse | None(the klass TAG is the primary bucket; a cse residual overrides — func_80148094 is taggedregalloc-orderbut its residual is a cse mult-order, so it wants the commutative-heavy profile; a generic WAVE/GIANT tag falls back to thewheretext).render_settings_toml()emits the[weight_overrides]block.p16_permute.setup(fn, draft, asm_subdir, klass=…, where=…)writes it;grinder.pyauto-threadsklass/where_stuckfrom the backlog record.klass=None→ no table → the plain gcc defaults (identical to the pre-T5 undirected search: a safe superset). - The three profiles → §31 levers (keys are the exact
perm_*names): regalloc (RC-1/2/3, S7, S11 register-permutation) up-weightsperm_reorder_decls(40, RC-1 slot / RC-3 tie-order) ·perm_reorder_stmts(40, RC-2 range / S11 LUID) ·perm_temp_for_expr(60, S2 boost) ·perm_split_assignment/perm_duplicate_assignment(set-count → RC-2 / defeat RC-7 equiv); schedule (S1–S5, D1–D4) leads withperm_reorder_stmts(60, LUID) +perm_temp_for_expr(60, S2) +perm_ins_block/perm_empty_stmt(S4 filler); cse leads withperm_commutative(40, operand order) +perm_expand_expr/perm_split_assignment(re-decompose). All three push the value/type noise (perm_add_mask/xor_zero/mult_zero/randomize_*_type/…) to ~0.1. - Validated: on
func_8014E048(S11 LUID⊗alloc, 143-ins, base masked-36) the regalloc profile found a better score in <30 s (36→34→33) where the undirected search had stalled — proof the biased distribution explores the class's territory. The whole-binary byte-gate (harvest_verify) stays the sole arbiter (G3/P9): a permuteroutput-0-*is a strong CANDIDATE to gate, never a bank. - The grinder's companion fix (T5): the old idle path did a blind
tried.clear()→ re-permuted every floor-victim on every idle tick (churn, R14). Replaced with input-changed gating (grinder.py draft_sig=(best_draft mtime, closeness)): a fn is re-opened only when the worker actually improved its draft; the permuter is deterministic givenbase.c+target.o, so an unchanged input can never newly win. - Scope note: the four flagship count-exact seeds (
func_8014E04835 ·func_80176D9452 ·func_8014809472 ·func_801412A8110) are the project's worst-case intrinsic walls (RC-6/S11, "the C-space around the target is discontinuous" — §31 regalloc RC-6). Directed mutation is the right tool but the map's "two probes, don't grind" applies. The real payoff is the broader reach-134 near-miss tail (T8: ~30 schedule / ~84 regalloc), most of which is far less extreme — there the directed profiles raise the per-batch close-rate.
§4 Flag/toolchain gotchas
--expand-divis required for any div/rem (§I4). Pinned globally inMASPSX_FLAGS.-O2 -G0(no$gp-relative).-G0confirmed (zero%gp_relin the disassembly).- psx-vs-cdk cc1 and aspsx 2.56-vs-2.67 are byte-equivalent on functions without the
discriminating idioms — don't expect a probe to distinguish them unless it uses
sltu+$ator div-expansion specifics. The evidence-based pin is psx + 2.56 (PsyQ 4.0 stamps). - Per-module mixing (§5.5) is expected: if a whole module's div is bare (no
--expand-div) or flags differ, that's a per-file override — record it here and add the Makefile mechanism.
§5 Known hard-residual classes (instruction-identical, one byte-exact blocker)
These are functions where every instruction matches but a final whole-function artifact blocks score 0. They still confirm the compiler (instruction selection + regalloc match); defer them as decomp-permuter candidates rather than hand-grinding.
- Phantom empty stack frame — gcc 2.7.2 -O2 sometimes wraps a frameless leaf in an unused
16-byte frame (
addiu sp,sp,-0x10/+0x10, no saves/spills); the target is frameless. The one extra instruction shifts the whole function → large score from a trivial cause. Structural variation / the permuter can flip frame allocation. Example:func_80016714(bzero). - Hoisted-invariant vs IV-init ordering — a loop-invariant load scheduled before/after the
counter init; not reachable by C-source changes (permuter stuck at base). Needs
PERM_*or insight. Example:func_80015A74(uint→BCD). See §3.
§5a Cross-jump tail-merge — gcc collapses two byte-identical blocks the original kept separate (FIX FOUND)
FINDABILITY ADDENDUM (P31 S65, measured — this section's law was RE-DERIVED from scratch by a
drafting agent that never found it). Two of three "new lever" claims distilled from the t5s/t5t
waves were rediscoveries of laws already in this book, and the reason is the SYMPTOM WORDING, not a
missing law. The index entry reads "gcc stole an instruction into a branch delay slot that the
target leaves as nop" — but that is not what the agent SAW. What it saw, on func_8017E044, was:
match_one reporting LENGTH-DRIFT/1? at closeness 45, an extra unconditional j present in
its build and absent from the target, and a store duplicated at the jump's destination. The
nop never appeared in its diff at all — the target's load-delay nop was DISPLACED, not matched
against. So search this section for any of these instead:
- an extra
jyour build emits that the target does not have, whose destination block's FIRST instruction is a store shared by two converging if/else arms — reorg fills thej's delay slot by COPYING that store, thejdegenerates to jump-to-next, and a later pass deletes it; LENGTH-DRIFT+1 with the cascade beginning exactly at aj/label pair;- a store that appears twice in your build and once in the target.
The fix is the §34 zero-byte
__asm__ __volatile__("")placed immediately BEFORE the shared store (reorg.c:675 stop_search_phalts the eager scan on ANY asm insn) — byte-proven here: that one line, nothing else changed, tookfunc_8017E044fromnear 45tomatch 0 174 MATCH.
Symptom: your function is N instructions SHORTER than the target, because the original binary has two
(or more) byte-identical tail blocks (classically a "save K globals then return c" epilogue reached from
different states) but gcc merges them into one. asm-differ shows a big cascade; the instruction COUNT is
short by exactly one copy of the tail. Example: LzssDecodeSector — the original keeps block_14 (the
state-3/4 save, ending j epilogue) SEPARATE from the state-2 reload save (which falls through to the
epilogue); gcc merged them → 111 vs the original 122 instructions.
Root cause (ground-truthed against gcc-2.7.2.3 jump.c): the find_cross_jump/do_cross_jump pass
walks two blocks backward and merges them while the instruction suffix is identical (rtx_renumbered_equal_p).
It is hardcoded ON at any optimize > 0 (fires at -O1 too; no -fno-crossjumping exists before gcc 3.3),
and do_cross_jump explicitly rewrites RETURN insns — so identical save/return epilogues are exactly what
it targets. Shared-goto, explicit-epilogue, and three-inline-copy C forms all produce RTL-identical tails →
gcc re-merges every time. cdk cc1 merges too. The permuter's default randomization does NOT defeat it.
THE FIX — a zero-byte volatile-asm barrier. find_cross_jump sets lose = 1 (bails) on ANY volatile asm
node (ASM_INPUT/MEM_VOLATILE_P). Put one empty volatile asm in ONE of the twin blocks (after the last
store, before the return):
/* ...the K stores... */
__asm__ __volatile__("" ::: "memory"); /* zero-byte cross-jump barrier */
return c;
It emits no machine code but makes the block's RTL non-identical to its twin, so gcc keeps BOTH copies →
correct instruction count. Document it as load-bearing (a future reader will "clean it up" and lose 11 bytes).
This is a standard decomp idiom (sotn writes duplicate funcs explicitly; the "" ::: "memory" clobber also
pins store ordering — drop the clobber to plain __asm__ __volatile__("") if it perturbs scheduling).
Permuter caveat: pycparser rejects __asm__ __volatile__(... ::: ...). To still permute the residual
regalloc, put a placeholder call (CJBARRIER(); + an extern void CJBARRIER(void);) in base.c and have the
per-function compile.sh sed it to the real asm before compiling. Note the asm-differ object-mode score then
floats on a cosmetic .rodata-vs-jtbl_<addr> symbol floor (the migrated jump table links identically), so
verify candidates with the linked make check, not the permuter score.
The full close of this exact function — the ~4 regalloc/scheduling slots this barrier leaves behind, and the
floor-free .text metric that finally measured them — is §10 (LZSS matched byte-for-byte, Phase 7 session F).
§6 Per-module optimization mixing — the -O0 boot module (Phase 7)
Finding (2026-06-14): the EXE mixes optimization levels per original translation unit (the
§5.5 Xenogears-style mixing, now concrete). The boot/main/game-mode-dispatch module — a clean
contiguous block at vram 0x80010000–0x800123F0 (~50 funcs: start, main, GameModeDispatch,
DebugMenuHandler, the game-mode handlers) — was compiled at -O0. Everything from 0x800123F0
onward (every match so far + the file-loader cluster) is -O2. Always opt-fingerprint a new
function before writing C — the pinned -O2 is NOT global.
Detecting the opt level (do this first)
gcc 2.7.2 -O0 keeps a frame pointer: addu $fp,$sp,$zero (21F0A003) in the prologue +
addu $sp,$fp,$zero in the epilogue; -O2 omits it. Grep the target .s:
grep -l 21F0A003 asm/nonmatchings/<seg>/<fn>.s → hit = -O0, miss = -O2. Module scan: classify
every .s by that signature, sort by address; the contiguous -O0 run is the module (the boot block
is the one early -O0 run). Other -O0 tells: redundant move/addu rd,rs,$zero copies; a nop after
every load (no load-delay scheduling); single-use values parked in callee-saved s0..; large
constant member offsets left unfolded (la $reg,sym + lhu off($reg)), where -O2 folds
sym+off into one load.
-O0 idiom — far struct member via a register base pointer
Target: lui s0,%hi(BASE); addiu s0,s0,%lo(BASE); lui at,1; addu at,s0,at; lhu v0,-0x5c52(at) =
load a u16 at BASE + 0xA3AE. The lui 1 / addu / -0x5c52 is just as expanding a register-relative
load whose offset (0xA3AE) exceeds 0x7FFF (%hi=1, %lo=-0x5C52). C — a register-qualified pointer
to the base, then offset-deref:
extern u8 BASE[];
register u8 *p = BASE;
... *(u16 *)(p + 0xA3AE) ... /* base stays in a callee-saved reg; offset left unfolded */
register is load-bearing: drop it and -O0 spills the pointer (extra sw/lw, bigger frame); and
-O1/-O2 fold it all back to lhu sym+off (no base reg, no frame pointer). Plain BASE[idx] or
((struct*)BASE)->m also fold at -O0 → wrong. Example: GameModeDispatch (0x80010B40) =
gameModeHandlerTable[*(u16*)(p+0xA3AE)]() — byte-exact (asm-differ 0).
-O0 idiom — a reserved (unstored) local sets the frame size
A named local the original declares but our toolchain wouldn't store (e.g. a call result the
original checks directly) still reserves its 8-byte stack slot at -O0, enlarging the frame.
If a near-match differs only by frame size + a uniform save-offset shift (every instruction
identical), add the missing local as a declaration-only int x; + (void)x; — no store, no
load, no -Wall noise, no code, just the slot. (Assigning the result to the local instead emits a
sw/lw pair the target lacks.) Example: DebugMenuHandler (0x80011144) — if (CdReadRequest(...) != 0) with a reserved int iVar1; → frame 0x20 (score 42 → 0).
Build mechanism — per-file opt override (splat resegmentation)
One original .c = one opt level; you can't mix within a compile unit, and gcc 2.7.2 has no per-function optimize pragma. Split the module into its own splat c-subsegment and give that object its flags:
config/splat.us.exe.yaml: split the text subseg at the module boundary (a function start; file off = vram − 0x8000F800). Boot module =[0x800, c, boot] → src/boot.c; the rest stays[0x2BF0, c, 800] → src/800.c(name kept to avoid migrating matched C).Makefile: target-specific override —build/src/boot.o: CC1FLAGS := …-O0…(the pattern recipe reads$(CC1FLAGS), so this overrides just that object).- Regression gate: the split must rebuild byte-identical at 100% INCLUDE_ASM before any -O0 C
is added (opt level only affects matched C, not stubs). Verified for the boot split.
Reuse this hook for any future module whose flags differ (another opt level, bare
divu, etc.).
§7 PsyQ SDK types & symbols (the library-call prerequisite)
A function that calls PsyQ library routines needs both the SDK types and the library symbols:
- Types: pull the EXACT layout from Ghidra's imported
.gdt(mcp__ghidra__types get <Name>, category e.g./LIBCD.H) — never guess offsets (G1). Declare ininclude/psyq/<lib>.h(it#include "common.h"for u8/u32, guard-safe). Verify sizes with a compile-time assert:typedef char a[sizeof(T)==N ? 1 : -1];. - Symbols: PsyQ fns are already named in the Ghidra DB but absent from our exported
config/symbols.us.txt, so the build shows them asfunc_<addr>. AddName = 0xADDR; // functosymbols.us.txt(R15) AND rename the matchingINCLUDE_ASM("…", func_<addr>)stub(s) insrc/to the canonical name (splat won't rewrite a committed.c). Re-extract → byte-identical (label-only change). No matched C may already reference the old name. - Done Phase 7:
include/psyq/libcd.h(CdlLOC 4B, CdlFILE 24B + CdSearchFile/CdPosToInt/CdIntToPos protos) + the 4 libcd/libetc symbols — unlocks the file-loader cluster. Same pattern for libgpu/libgte/libspu as they come up.
§8 rodata island (compiler jump tables) — the .data→.rodata→.data sandwich (Phase 7)
GCC emits each switch jump table into .rodata; in this EXE all compiler rodata is ONE island at
0x80072A38–0x80074750, sitting BETWEEN the front .data (globals @0x800629DC) and the tail .data
(@0x80074750). No single splat section_order expresses data→rodata→data. Proven mechanism (session C):
- Migrate, don't standalone. A jtbl
.words reference function-internal.L/jlabeltargets, so a separate rodata object can't link — the table MUST co-locate in its function's object. Use a dotted.rodatasubseg whose NAME matches the code subseg ([<off>, .rodata, 800]):extract=False, spimdisasm migrates each single-ref jtbl/const intoasm/nonmatchings/<seg>/<fn>.sas.section .rodata. The INCLUDE_ASM stub already.includes that.s, so it flows into the object for free. Multi-ref rodata can't migrate → splat emitsINCLUDE_RODATA(...)lines (in a FRESH.c). H5: don't regen-fresh the curated.c(drops comments) — surgically INSERT just the INCLUDE_RODATA lines. - Place explicitly. splat is section-major (floats all
.rodatato the front).tools/ld_interleave.py(wired intomake extract) rewrites the.main {}body to text → front-.data→.rodata→ tail-.data→ bss, splitting front/tail by object basename. Sizes then land byte-exact. - Carve data-in-text. A trailing non-code table inside the text range (here 0x80062998–0x800629DC) must be
its own
datasubseg, or jumptable analysis mis-extends the last function across it (the +24main_TEXT_ENDoverrun's first cause). - The
.align 3file-split trap: GCC 8-aligns jtbls; concatenating many functions into one object injects padding nops the original (separate TUs) lacked → image grows. spimdisasm PRINTS file-split suggestions at the misaligned jtbls. Fix = per-file split at those boundaries (sotn-style) — OR link the real library object (§9) when the owning function is SDK code.
§8a rodata island in a flat OVERLAY — the tail sandwich, per matched jr-function (Phase 26 — PoC PROVEN)
The EXE's §8 was one central island. The overlays are different: gcc's switch jtbls sit in ONE contiguous
.rodata block at the TAIL of the flat blob — between the .data globals and a tiny .data remnant
(ov_SC01_077: island vram 0x801D7F9C..~0x801D9460, right after .data global D_801D7F94; layout = text →
data-globals → rodata-jtbls → data-tiny). While every jr-function is INCLUDE_ASM the jtbls emit as in-place
.data and the build is byte-fine. The moment you MATCH a jr-function, its C emits the jtbl into .rodata
(which the overlay section_order:[.rodata,.text,.data,.bss] floats to the FRONT @0x80128158) and the raw
copy is still in the data tail → duplicate + wrong address. The proven fix (byte-identical on func_8012ACE0,
a 25-ins single-jtbl jr-function in ov_SC01_077):
- Carve per matched fn. Split the
[…, data, tail]subseg around that function's jtbl(s) into[…, data, tail](globals + pre-carve jtbls, still raw.data) +[<off>, .rodata, <code-subseg-name>](the fn's jtbl → migrates into itsasm/nonmatchings/<subseg>/<fn>.sas.section .rodata; the name MUST match the code subseg the fn lives in, e.g.ov_SC01_077_a) +[<off2>, data, tail2](post-carve jtbls + tail, raw). Other functions' jtbls STAY raw.datauntil they too are matched (per-fn carve, not whole-island). - Place via the parameterized
ld_interleave(Phase-26: added--section .<binary>→ derives the<binary>_TEXT/DATA/RODATA/DATA2/BSSsymbol prefix; default.main= the EXE, byte-identical): it rewrites the overlay's output section to text → data(tail,--front tail.data.o) → rodata → data(tail2+trailing,--tail tail2.data.o --tail trailing.o) → bss. Wired intomake extractvia a per-binary<bin>_JTBL_INTERLEAVEvar inconfig/overlays.mk(holds the--front/--tailbasenames) + anifneq ($(strip $(JTBL_INTERLEAVE)),)branch. GOTCHA: put NO trailing#commenton theJTBL_INTERLEAVE :=line and$(strip)it — a trailing comment leaves whitespace → non-empty → the branch misfires on EVERY binary (ld_interleave then runs with EXE defaults → "front data object not found" on resident). - The C body needs
canon_sig_reconcilebefore it will compile in the real TU (the raw draft hitsconflicting types for <fn>vs the TU's forward decl +conflicting types for <typedef>vs a sibling; reconcile rewrites the def sig to canonical + uniquifies the draft's typedefs + block-scopes externs). Placement is orthogonal — reconcile first, then the carved jtbl lands byte-exact. - Alignment: gcc emits the jtbl
.rdata .align 3(8-byte). If the original jtbl address is 8-aligned (jtbl_801D8078, 0x…078) there is no pad and it lands exact. A 4-aligned original address (jtbl_801D8AFC) would force a 4-byte align pad → HANDLED (Phase 29): the §8eJTBL_PADSpad-spec filter (hit for real byjtbl_801D8144in the func_80131340 bank). - rtu_match is NOT a whole-binary gate for jr-functions — it masks relocs AND excludes the §8 jtbl rodata, so it MATCHes a body whose switch is subtly wrong (e.g. func_80159C84's 2nd jtbl was 5 words vs the real 6 — a false-MATCH). Always confirm jr-function cracks with the whole-binary gate (which now works, via this carve).
- ×134 automation (NEXT): each overlay sibling has the SAME jr-function at a per-overlay address with its own
jtbl in its own tail → the carve config + the
<bin>_JTBL_INTERLEAVEvar must be generated per overlay from the sibling's jtbl address (a tool overfamily_sweep), then reconcile+template the body per sibling. The PoC proves the per-binary mechanism; the fleet rollout is the mechanical generator.
§8a-pad — a trailing .word 0x00000000 under a jtbl dlabel is .align PAD, not an entry (Phase 26 session 6, byte-proven)
⚠ CORRECTED by §8e (Phase 29): the "maspsx drops all
.align" rationale below is FALSE (thatcontinueis in an inventory-only pass; the output path passes.alignverbatim). The TRIM itself remains correct — the pad belongs to the NEXT table's.align 3, absent when that owner isn't compiled in the same object. Multi-table spans now reproduce interior pads via the §8eJTBL_PADSspec filter.
This retroactively explains the §8a func_80159C84 "5 words vs the real 6" false-MATCH.
The raw dlabel jtbl_XXXXXXXX in asm/<ov>/data/*.data.s can span one word MORE than the switch has
cases. That last .word 0x00000000 is the ORIGINAL TU's intra-rdata .align 3 padding — emitted when a
jump table's entries end ≡4 mod 8 and another jtbl of the same TU follows. It cannot be a table entry:
0x00000000 is not a jump target.
- The true entry count is the function's
sltiu <n>range check, not the dlabel span. Byte-confirmed:func_8015AE2C→sltiu $v0, $v1, 0x7= 7 entries, yet its raw dlabel spans 8 words. - maspsx drops all
.align(maspsx.py:435), so a C-emitted jump table can NEVER reproduce the pad. - Therefore
jtbl_carvemust TRIM trailing zero words from the carve range, leaving the pad in the raw post-carvedatapiece. Carving to the next dlabel reserves 8 words while the compiled object supplies only 7 → the.rodatapiece under-fills by 4 bytes → every later symbol shifts +4 (the same image corruption class as §41d: ~271k differing bytes from one missing word). Trimming is always safe. - Existing carves are parsed from the CONFIG (their
end= the next piece's offset), not re-derived from the data asm, so the trim only affects NEW carves — committed banks are unaffected.
§8b MULTI-jtbl per overlay — the ld_interleave --order sandwich + the same-subseg cases (Phase 26 session 4)
Once ONE jr-function is banked in an overlay, banking a SECOND makes it multi-jtbl (§8a's single-carve breaks:
jtbl_family_bank.revert() restores the committed config = already has carve #1). The generalization:
ld_interleave.py --order <leaf1,leaf2,…>— an explicit, ADDRESS-ORDERED list of the data-region pieces forming the sandwich (text → [these] → bss). A*.data.o/trailing.oleaf contributes its.data; any other (code) object leaf contributes its.rodatacarve. Unlisted.data/.rodatalines must be empty code-object sections → parked byte-neutrally with.text. Generalises the 3-piece single sandwich to N pieces. Legacy--front/--tailpath is byte-untouched (main EXE + single-carve siblings unaffected).jtbl_carve.pyis additive / regenerate-from-config — parse the tail data-region + the existing.rodatacarves, add the new fn's jtbl (split its containing data piece), re-emit the address-ordered pieces + the--orderarg. Idempotent. BOUND-FIX (subtle, cost a false "non-contiguous"): a new jtbl's end is bounded by the next raw dlabel OR the next EXISTING carve start — an already-carved adjacent jtbl is GONE from the data asm, so the raw dlabels alone over-extend the new jtbl past it.jtbl_family_bank.bank()mustmake extractBEFORE the carve (the sibling asm must match the reverted committed config so the carve finds the new fn's RAW jtbl; the old error-string retry was fragile).- PROVEN cross-subseg (fleet-scale): func_801734BC (34-ins PURE jr, clean shared-tail switch
case N:t=-N;break; default:goto after;) inov_SC01_077_after+ func_8012ACE0 in_a= 2 carves / 2 subsegs → banked ×134, R22 136/136 byte-identical. - A code object emits its jtbls CONTIGUOUS (gcc source order), so two matched jr-fns in ONE subseg are
byte-correct only if their jtbls are ADJACENT in the island. Two flavors:
- (a) adjacent → MERGE into one spanning
.rodatacarve (jtbl_carvedoes this; config-proven on func_80171B4C801D8C48+ func_801734BC801D8C68). Byte-proof needs a matched adjacent pair. - (b) non-adjacent (unmatched jtbl between) → ISOLATE one fn into its own code subseg (whale
_o0bprecedent) so each object holds ONE contiguous rodata run.jtbl_carvederives the carve subseg fromfunc_subseg; isolating F preserves carves BELOW F (trim keeps<F) → bank same-subseg families ASCENDING. RESOLVED (session 6, byte-proven):tools/overlay_src_split.py(overlay-.c-aware partition, 404/404 round-trip) +tools/jr_isolate_all.py(multi-cut resegment) + the declaration-environment reconstruction of §8c. Full 54-jr isolate-all on ov_SC01_077 →d19c9580byte-identical, R22 clean-fleet 136/136. Applied LAZILY (isolate only the cores we actually bank — upfront-×134 would add ~7,200 region files):jtbl_family_bankcatchesjtbl_carve'sNON-CONTIGUOUSfail-loud →jr_isolate_all --only <core>→ re-extract → re-carve. Proven onfunc_80178D40(the 890×134 heaviest core): carve blocked → isolated (byte-neutrald19c9580) → carve lands in its own subseg.
- (a) adjacent → MERGE into one spanning
§8c Splitting a TU means rebuilding its DECLARATION ENVIRONMENT, not moving text (Phase 26 session 6)
The §8b isolation wall. A mechanical source split is not mechanical: this C is written against gcc-2.7.2's lenient scoping, and a cut silently strands declarations. Four file-scope decl sources must be carried forward into each new region (regions are address-ordered and file order == address order, so ambient flows strictly FORWARD — every carried decl already preceded every item of the receiving region in the original file):
- col-0 decls in the
.c— the obvious one (the only one the first attempt handled). DEFINE_func_*macro LEADING externs. The macro expands at file scope toextern <type> <sym>; … <definition>, so its externs ARE part of the invoking TU's file-scope environment — but they live inengine_core.h, so no col-0 text scan of the.ccan ever see them (1,377 macros / 3,929 extern lines / 1,462 symbols). This strandedfunc_801734BCfromextern s16 D_80126B3E;. (Correcting the session-5 hypothesis: this is NOT a "block-scope extern persists to file scope" gcc quirk — the externs are genuinely file-scope, just textually invisible. The 148 externs INSIDE macro bodies are real block-scope shadows and must never be hoisted.)- A function DEFINITION is itself a declaration for everything below it in its TU. Cut the definition into
an earlier region and every later caller that took its address breaks (
func_8012B2CC undeclared). Synthesize its prototype — and a K&R definition declares an unprototyped function, so it must renderextern T f();, neverf(void)or the K&R param names. - File-local typedefs used by a carried prototype (
extern s32 f(Vec3s *a0)→parse error before '*'). Legal to re-emit because each region becomes its OWN TU. Emit types before decls.
Why NOT "declare every used symbol from a global symbol→type map" (the intuitive design): this codebase is
loosely typed, so a symbol legally carries contradictory decls — func_80173544 is defined at file scope as
s32 f(void *) yet declared extern void f(void); inside func_801734BC's body. Hoisting "every used
symbol" lifts that block-scope shadow to file scope, where it collides with the definition — so the design then
needs a heuristic "type-shadowed set" to dodge a problem it created. Carrying forward only what was already
file-scope is conflict-free by construction: every carried decl already coexisted with every definition in
the one original TU, and decl compatibility is order-symmetric. Shadows stay in bodies and travel with them.
- Dedup by exact decl TEXT, not by symbol. One symbol legitimately has several distinct file-scope decls
(the baseline build emits 87
type mismatch with previous external declwarnings and is byte-identical). Collapsing to the first drops a decl the original had. - Baseline-parity is the warning oracle: diff the isolated build's warnings against the baseline's. New classes mean you changed decl visibility; identical classes mean you reproduced it.
- TRAP —
func_subsegfrom the asm tree is stale-prone.make extractdoes not prune stale subseg dirs, so after an isolation BOTHnonmatchings/<ov>_after/<f>.sandnonmatchings/<ov>_jr_<A>/<f>.sexist; anos.listdirscan returns the STALE owner and silently re-creates the very collision the isolation removed. Derive the owning subseg from the CONFIG (address → containing code piece). - TRAP — a sweep's revert must restore, not delete.
overlays.mkis SHARED by all 134 overlays and every one now has a committed<ov>_JTBL_INTERLEAVE; the old revert dropped the line unconditionally, destroying a banked carve on any failed sibling. Restore it to its committed value (git show HEAD:), splice per-overlay (nevergit checkoutthe shared file mid-sweep), and delete only the region files this attempt created.jtbl_family_banknow refuses to start on a dirtyconfig/+src/(an uncommitted prior family would be silently reverted) — commit each family before sweeping the next.
§8d Templating a body INTO a TU must not CHANGE its declaration environment — demote the carried data externs (Phase 26 session 8, byte-proven on func_8015AE2C ×133)
The mirror of §8c. There, splitting a TU meant carrying its decl environment forward. Here, templating a cracked body into a sibling TU means not disturbing the environment that is already there — and the ×N family sweep was doing exactly that, silently.
family_remap.gather_externs carries the exemplar's decl for every symbol the body references and prepends
them at FILE scope. For a per-location DATA symbol that the sibling declares only at BLOCK scope —
inside its own later functions, loosely typed — that carried decl establishes a global declaration the TU
never had, and every later block-scope extern of that symbol must now agree with it. In loosely-typed
engine code they never do. The whole-binary gate proved both halves:
BLOCK(int) -> BLOCK(struct Ent *) -> FILE(void *) ... builds [ OK ] (the region, stub state)
FILE(void *) -> BLOCK(int) -> ... conflicting types for `D_801812A4' (ERROR)
D_801812A4 (ov_SC01_000's entity dispatch table) is declared four incompatible ways in one region —
(int) and (struct Ent_8015CD20 *) at block scope inside func_8015C128 / func_8015CD20, then (void *)
at file scope ×12. That compiles. Prepend the body's extern void (*D_801812A4[])(void *); above them and it
does not. It was the only hard error in the build; all 27 carried function externs were fine raw.
THE FIX (tools/scope_data_externs.py, a pure draft-text transform): emit a carried D_ extern at
block scope inside the function body whenever the target TU has no file-scope decl of that symbol above
the insertion point. It then declares no global, nothing below it can conflict, and the TU's environment is
preserved exactly. Byte-neutral — an extern emits no code, and moving it changes neither the symbol nor
the declared type, so every access keeps its opcode. It also restores fidelity: the original source declares
these symbols at block scope in precisely this way (m2c/Ghidra emit per-function externs there).
Wired into jtbl_family_bank as the scoped stage (raw → scoped → recovered → reconciled) and used as
the base for the later recovery stages. First sibling byte-identical; 562-ins core ×133.
- Never worse than raw, which is why it can be applied unconditionally: a symbol that does have a file-scope decl above is left alone (an identical spelling is a legal duplicate; a differing one is the §41 reconcile class and errors at file scope either way, so demoting could not have saved it).
reconcile_declsis the WRONG instrument for this class, twice over. (a) Its oracle is fleet-majority (engine_core.h first-seen, else a plurality vote across all overlays) — but the question is not "what does the fleet call this symbol", it is "what can this TU see". (b) ItsDATA_DECL_LINE_REcannot even parse the fn-ptr-array formextern void (*D_x[])(void *);, so it silently skipped the very symbols that were failing. A tool that no-ops on the failing input reads exactly like a tool that had nothing to fix.- TRIAGE RULE (R17 boundary, Drew 2026-07-13). "The compiler produced the wrong BYTES" → read the gcc
source (regalloc / sched / cross-jump / CSE — things no C change reaches). "The compiler refused to
compile" → read our Python. This was
conflicting types: a C front-end diagnostic, gcc correctly rejecting plain C89. Readingcse.c/global.cwould have taught nothing; the bug was ours. - Diagnostics gotcha: gcc-2.7.2 does not prefix errors with
error:— grepping a build log forerrorfinds only make'sError 33. Grep for the diagnostic text (conflicting types,undeclared,parse error,redefinition) instead, and rememberwarning: conflicting types for built-in function 'memcpy'is benign.
§8e The jtbl ALIGNMENT LAW + the pad-spec filter — multi-table .rodata spans (Phase 29, byte-proven; .run/probe_jtbl/verdict.md)
Two prior cookbook claims are CORRECTED here (both were instrument errors, R35):
- §8a-pad's "maspsx drops all
.align" is FALSE. Thecontinueatmaspsx/__init__.py:435is inpreprocess_lines— an inventory-only pass (sbss/bss/sdata dicts) whose skips produce no output; the real output path (process_line, L872-873 catch-all) re-emits.alignverbatim. The §8a-pad trim is still right, for a different reason: the trailing pad word belongs to the NEXT table's.align 3, which is only emitted when that next owner is compiled in the same object. - The Phase-29 session-2 "half-pin" ("cc1 AND maspsx emit the jtbl
.align 2") was inverted, measured off VACUOUS probes (an emptyj $31fn — no jtbl in them at all; the only.align 2was the function-entry.textalign). Lesson: a probe whose output contains no instance of the thing being probed pins nothing.
The law (each link byte-verified in .run/probe_jtbl/):
- cc1 (Sony gcc 2.7.2) emits
.rdata+.align 3+ label before every switch jump table (probe: 2 tables in one TU → 2×.align 3; the clean single-table object's.rodatash_addralign=8). - maspsx passes
.alignthrough;asbakes the pad into the section SECTION-RELATIVE — the linker can never remove an intra-object pad. (ascontrol: bare.wordsection → Al=4; with.align 3→ Al=8.) - Link placement is always TIGHT:
SUBALIGN(2)(splatsubalign: 2, fleet-wide) +ld_interleave's. = ALIGN(., 4)override input-section alignment — a 4-mod-8 carve start places exactly (the banked0xb07dccarve is the byte proof). So only intra-object pads can diverge from the original. - ORIGINAL layout semantics: originally-separate TUs pack TIGHT (PSX linker, 4-aligned placement:
jtbl_801D8078's 51 entries end 0x801D8144 exactly where the next TU's table begins, %8==4); intra-TU
consecutive tables carry a REAL zero-word pad wherever the previous table ends ≡4 mod 8
(tail2.data.s: jtbl_801D8158/8170/8188/81A0 = 5 entries + one
.word 0each). - ⇒ Merging originally-separate TUs into one decomp TU makes cc1's intra-TU
.align 3fire where the original had a tight TU boundary: a non-first table at original vram ≡4 mod 8 gains a +4 interior pad → every downstream data symbol shifts →%lorelocs break image-wide (func_80131340: pad at rodata 0xCC, SHA1 fail from a clean build). Conversely a genuine intra-TU pad must be REPRODUCED. - Isolation does NOT fix this in general:
.alignis section-relative, so an isolated object whose first table starts at vram ≡4 mod 8 flips the parity of every INTERNAL align — a multi-table function with a 4-mod-8 first table would mis-pad inside its own object. Only explicit pad control is general.
The mechanism (tools/jtbl_rodata_pads.py + jtbl_carve + Makefile JTBL_PADS): for a multi-table
span, jtbl_carve derives each boundary's pad by interval arithmetic (pad[K] = start[K] − end[K−1]
∈ {0,4}; a 4-gap must be a verifiably-zero payload word, else NON-CONTIGUOUS→isolate) and writes a
per-object build/src/<ov>/<sub>.o: JTBL_PADS := 0,4,… target var into config/overlays.mk; the Makefile
pipes that object through the filter, which REPLACES each rodata .align with the spec'd pad bytes
(.word 0 or nothing). Per-sibling self-adapting (each overlay's own addresses), fail-loud on drift
(spec count ≠ align count; non-.align 3; non-jtbl rodata content). Committed spec values are CARRIED,
never re-derived (a committed span's interior boundaries are unrecoverable from its interval).
Single-table carves get no var — their pipeline is byte-identical to pre-§8e (their lone .align 3 at
section offset 0 pads nothing and SUBALIGN neutralizes the sh_addralign).
- Object-layer proof before image-layer claims: verbatim vs filtered objdump — 0xE4/pad-at-0xCC vs 0xE0/tight — settled the mechanism before any carve landed. Cheap, decisive, reusable probe shape.
§8e-2 — the spec-derivation law (learned banking the 4 giants, Phase 29):
- The ZERO-WORD rule is the whole derivation: pad before table K ⟺ payload word at
start[K]−4is0x00000000(a zero can never be a table ENTRY — the §8a-pad axiom). No entry counts, no interval bookkeeping.spec_from_startsneeds only the span's TABLE STARTS + the payload. - Table starts are PERISHABLE — persist them.
make extractprunes a matched owner's stub.s, so a span's interior structure becomes unrecoverable (thefunc_8013F350lesson: the existing0xb0740carve was a Phase-26 merged double (8+5 tight) that the red-team's "all single-table" sample missed — R14; its structure had to be re-derived from the compiled stream + span length). TheJTBL_PADSline now persists starts as span-relative offsets (tables=+0x0,+0x20,…); source priority per span: untouched+line → reuse verbatim · untouched+no-line → skip (its natural.align 3s are committed-green) · touched → union{new fn's.s, surviving stub.s, linetables=rebased,--span-tablesoverride,--like <exemplar>role-transfer} → zero-word rule. Sibling sweeps get structure via--like(same family ⇒ same span shape; pads still derived from the LOCAL payload). - 4-mod-8 FIRST tables are placement-only (SUBALIGN packs them tight —
jtbl_801D836C/0xb07dcproofs); only NON-first tables need the spec. A single-table carve never needs a line. - Splice-reconcile carry-overs (the 59C84/F350 banks; all §56-class, byte-neutral): the draft's
standalone scalar typedefs must be STRIPPED in-TU; a conflicting draft decl is replaced by the
macro-canonical redecl (NOT just dropped — C89 rejects file-scope use-before-declaration, so the
decl must exist above the use) + §18 width-preserving store casts (
*(u16 *)&D_x = …under a canonicalu8) + §17a-1 fn-ptr call casts where the canonical prototype differs from the matched call shape (((s32 (*)(s32))func_801416D4)(…)under a(s16)def; a prototyped(void)canon needs it too). §30#2 def-side widen (extern void→s32in every DISCARDING caller decl, incl. engine_core macros). - Crash-ordering gotcha:
jtbl_carve.apply()writes the splat yaml BEFORE the overlays.mk vars — a crash between the two leaves a half-applied config vs stale asm (symptom: phantom trim counts / shifted NON-CONTIGUOUS offsets). Recover:git checkoutboth configs + re-extract, then re-carve. - The CLEAN-DRAFT law (the 59C84 sweep lesson, 3/8→100%): a family sweep's
--rawdraft must carry the exemplar's reconciles as canonical-form decls + call casts IN THE DRAFT TEXT (typedefs stripped; decls in the fleet-canonical spelling; §17a-1/§18 casts baked in). The per-sibling ladder absorbs some of a dirty draft on some siblings (3/8), which disguises the class as random — diagnose by re-running ONE failed sibling with the cleaned draft (BANKED ⇒ the class is the draft, not the siblings). Symbol names remap per sibling, so textual canonical fixes transfer. - Named deferral classes from the 4-giant campaign (fail-loud, zero corruption, burn-down items):
(i) the 4 SC07
files=1overlays compose a new span against a PRE-EXISTING_o2bcarve whose object emits no tables at raw stage (the count-guard refuses — needs a per-overlay look); (ii) an -O0 family member cannot bank until its overlay has the -O0 cluster carve (rollout_o0_cluster, the Arm-A splat wall) — func_8013C414 banked ×1 (ov_SC01_077 has_o0) but its 137 siblings uniformly gate-fail at -O2; the family rides the -O0-rollout dependency, not a codegen wall.
§9 Link real PsyQ library objects byte-exact (Phase 7 — GO proven)
~350 of BFM's functions are unmodified PsyQ 4.0 SDK code. They are byte-identical to the real PsyQ library
objects, so link them directly instead of hand-decompiling — and each library .o brings its own correct
alignment (dissolving the library-half of §8's .align 3 problem). Validated: CdPosToInt/CdIntToPos EXACT
vs PsyQ libcd; PRESET_OBJ_* ∈ LIBGS.LIB. Workflow (the decomp-standard psyq-obj-parser path):
- Tools (gitignored
tools/psyq/):psyq-obj-parser(decompme prebuilt —.OBJ→ELF; rejects.LIB),lib40/*.LIB= PsyQ 4.0 USA libraries (DTL-S2002 R2.0 = BFM's version; extracted from the redump ISO viatools/bfm_extract/iso9660.py). Identify a function's library by searching the.LIBfor a NON-relocated instruction run from its EXE bytes (relocated runs false-negative — use leaves or interior runs). - Integration: split
.LIB(LIB\x01 archive) →.OBJ→psyq-obj-parser→arper lib → link the.ofor each library function and drop its INCLUDE_ASM. BFM mixes 4.0+4.2 library stamps, so a few objects may need 4.2/4.3 libs — determine per-object by the byte test. - Proven full-object link recipe (SYS.o byte-identical to BFM, Phase 7):
- Placement —
tools/psyq_identify.py <elf_dir>: relocation-masked search finds each object's.textvram in the EXE. Per library the used objects are CONTIGUOUS in object order → place the first at the region base, link the rest in order. - Recover externals — symbols the object references but doesn't define are usually absent from
symbols.us.txt; read them straight out of the EXE's RESOLVED relocations: for each reloc,R_MIPS_26→target = ((word&0x3FFFFFF)<<2)|(pc&0xF0000000); anHI16+LO16pair →(hi<<16)+signext(lo). Feed asld --defsym NAME=0xADDR. - Alignment — psyq-obj-parser emits
.text/.rdata/.dataat align 2**3; the original is 4-aligned, so an 8-align bumps the section +4 (the tell: everyLO16to that section is off by +4). Fix:objcopy --set-section-alignment '.rdata=4' --set-section-alignment '.data=4' obj.o obj_a.obefore linking. - Link + verify —
ld -T <SECTIONS: . = <text vram>; .text:{*(.text)} . = <island>; .rdata:{*(.rodata) *(.rdata)} . = <data vram>; .data:{*(.data)}> --defsym … obj_a.o→objcopy -O binary --only-section .text→ byte-compare to the EXE..rdata/.datavrams are found by searching the EXE for the section bytes (objcopy --only-section). Tools:tools/psyq_lib_split.py,tools/psyq_build_libs.sh,tools/psyq_identify.py.
- Placement —
§9.1 Generalised per-object linker — tools/psyq_link.py (+ psyq_link_lib.py), 18/18 libcd byte-exact
Session-D generalised the SYS.o recipe into a tool that links every used object of a library byte-identical. Two gotchas the one-object recipe didn't surface, both now handled:
- psyq-obj-parser MISLABELS common-style globals. Uninitialised globals (PSYLINK
.comm) get packed into each object's.bsswith sequentialst_values, but the original linker SCATTERED them (e.g. libcd CDROM'sStRingAddr→0x800c7c94andStRingSize→0x800c7f00are 0x26c apart, the ELF claims 8). Trust nost_valuefor placement. - Robust model = recover-everything + selective override. Place
.textat its vram and the real initialised sections at their bases (byte-search; or, for.bss/reloc-bearing.data, the address the section symbol itself resolves to in the EXE). Resolve every symbol the.textreferences by the address read out of the EXE's already- linked relocations (R_MIPS_26jump field;HI16+LO16immediates; object addend subtracted, but PsyQ addends are 0). A symbol that is a genuine member of a placed section (recovered == base+st_value) is left told; a mislabelled one is--weaken-symbol'd then--defsym'd to its recovered address (a strong defsym beats the weak section def —--strip-symbolis refused on reloc-referenced symbols, weaken isn't)..textbyte-compare is the check (G3). - Tells:
ld: 'X' referenced … defined in discarded section= you discarded a section whose section-symbol the.textneeds → place it instead. A 1–4 word residual inlui/lw/swimmediates (3c0480xx) = a mislabelled.bsscommon → weaken+defsym it. - Externals split intra/extra-library. Per-object "externals" (UND) include symbols defined in sibling objects
(resolve internally in a whole-library link) vs truly external ones (other libs' funcs like
VSync/memcpy, and module data globals likeSt*) — the latter feed--defsym/symbols.us.txt(R15). libcd: 82 union = 48 intra + 34 extra.tools/psyq_link_lib.py <elf_dir>links all located objects, flags address conflicts, writes.run/psyq_link.<lib>.json. Same tooling will serve libgs/libspu/… (the +24 culprits).
§9.2 Wire a library region into the build with NOLOAD — no data carving (tools/psyq_link_region.py)
To replace the asm stubs of a library's functions with the real objects in the byte-identical build WITHOUT
carving the flat data subsegment:
- Place
.textLOADED at each object's exact vram; place.data/.rdata/.bssas NOLOAD at their vrams. A NOLOAD section contributes its symbol addresses but zero bytes toobjcopy -O binary, so the build's existing flat data subsegment still emits those bytes (no double-emit, no carve) while the hundreds of section-relative.textrefs resolve via the NOLOAD placement. Set.data/.rdata/.bssalign=4 first or a 4-but-not-8-aligned vram bumps +4 (same tell as §9). Needsld --no-check-sections(NOLOAD overlaps the loaded flat blob's VMA). - Weaken every
.bss/.sbss-defined named symbol, then--defsymit to its recovered address: the common-style globals are scattered (genuineCD_*and mislabelledSt*alike, and a.bsssymbol of object A may be referenced by object B), so a uniform strong-defsym-beats-weak-def resolves them all. Truly-undefined externals (other libs' funcs) surface from a probe link'sundefined referencelines → defsym from the recovered map. - Place each object at its EXACT vram, not by concatenation — a library's objects are mostly contiguous but a
non-library function can sit between them (libcd: a 76-B gap of non-libcd code between C_003 and C_004), so naive
*(.text)concatenation drifts past the gap. The gap stays an asm stub in the build (split the splat code subseg into [pre][lib block 1][gap stub][lib block 2][post]).tools/psyq_link_region.py <elf_dir> --emit <p>verifies the region byte-identical per-object and emits<p>.ld(text + NOLOAD lines) +<p>.syms. libcd: 18 objects byte-exact, 36 externals.
§9.3 Make it the build: resegment + swap + resolve (tools/psyq_integrate.py, libcd DONE)
Wiring a library region into make build byte-identical (libcd: 58 SDK funcs, full pipeline green):
- Resegment the splat text subseg into [pre][block1][gap][block2…][post] at the library blocks
(one
csubseg per block + per non-library gap; vram→file = −0x8000F800).make extractregenerates the gap/post stubs;tools/split_src_region.py trimrewrites the curated pre-file (keeps items <lo, preserving real C and#ifdef NON_MATCHINGblocks by brace/#endifmatching) — splat will NOT overwrite an existing.c, so a stale one mis-places everything. splat-auto-empties (void f(void){}) ≥hi regenerate identically — no move needed. psyq_integrate.py(run in the$(OUT)recipe, after objects compile — the externals discovery trial-links the whole image): (1) prep objects (align=4 + weaken every.bsssymbol) →build/psyq/<lib>/; (2) rewrite the splat.ld— replace eachbuild/src/<stub>.o(.text);with the block's real<obj>.o(.text);(concatenation places them at their vrams since the pre-file ends exactly at the block start) AND delete the stub object's other(.rodata/.data/.bss)lines (else its stub symbols multiply-define the real ones); add per-object NOLOAD data sections sorted by vram (unsorted → "dot moved backwards"); (3) resolve externals via a full trial link (symbols still defined elsewhere never appear, so no double-def — no blanket exclude needed).- External resolution, in order:
func_<addr>(external code/data calls a libcd fn by its splat address-name; the real object exports a PsyQ name) → that address; asymbols.us.txtname (jump-table / dispatch pointer in the flat.data, e.g.BIOS_OBJ_3B8) → itssymbols.us.txtaddress; a recovered data/extern global (St*/CD_*) → recovered. Always also defsym EVERY weakened.bsscommon — the ones whose object's.bssis NOLOAD-placed resolve to that weak placement and never show as undefined (the 8-wordStModemiss). Capture BOTHundefined referenceANDdefined in discarded sectionfrom the trial link. - Byte-identical with OR without the SDK objects (the stubs reproduce the same bytes), so gate the
whole thing on
[ -d <elf_dir> ]— a fresh clone withouttools/psyq/builds via stubs. Idempotent (build/psyq/<lib>in the.ld⇒ re-derive syms only).
§9.4 Integrating a SECOND library (libgs block 6 after libcd) — multi-library gotchas
Wiring a 2nd psyq_integrate call into the same build (libcd, then a libgs block) surfaced bugs the
single-library path never hit. All fixed in tools/psyq_integrate.py; reuse for libspu/libsnd/…:
- Namespace the NOLOAD section names per library (
.nl_<objdir-basename>_<i>, e.g..nl_libgs6_0). The idempotency guard was a globalif ".nl_0" not in ld— so the 2nd integration saw.nl_0(from libcd) and SKIPPED adding its own NOLOAD lines, discarding that library's.data/.rdata/.bss(symptom:'.bss'/'.rdata' referenced … defined in discarded sectionfor the 2nd lib's objects, §9.1 tell). - Globally re-sort ALL
.nl_*NOLOAD lines by vram across libraries at the end of each integrate. Two separately-sorted groups whose vram ranges interleave make ld's location counter jump back (warning: dot moved backwards) — harmless (NOLOAD emits no bytes) but noisy. Pure reordering. - Pass already-emitted sibling
*_externals.ldto the trial link. The 2nd library's trial link sees the FIRST library's real objects (already in the.ld) referencing symbols defined only in the first library's externals (e.g. libcd objects callDMACallback/DeliverEvent); without the sibling syms the trial reports them as spurious!! UNRESOLVED. Globbuild/psyq/*_externals.ldminus the current one. - A resegment can shift spimdisasm's auto-detected function/data boundaries in the UNCHANGED regions
around the new subseg (shrinking 800b re-merged
func_80052FCC/053050and re-typed theD_80062998data table as afunc_). Two deterministic fixes, both from the real artifacts (G1): declare the affected REAL functions insymbols.us.txt(verify via the PsyQ object's symbol table — these wereGsMulCoord2/3in MATRIX.o), and carve any trailing data-in-text table as its owndatasubseg (§8; here extend the front-data subseg back to the table start, updateld_interleaveFRONT_DATA basename). Always regenerate the splitsrc/*.cfrom the fresh extract so stubs match the generated.s. - Block selection is by EXE-placement + disambiguation, not the full library. A library's objects span
several non-contiguous blocks (libgs: 6 blocks + gaps incl. the GS_001 scattered-
.bssgap, §9.1).psyq_identify <full-lib-dir> <lo> <hi>over ONE block's range reports the right objects PLUS byte- identical-.textaliases (GS_131≡RVWUNIT, PRESET2≡PRESET3, OBJT2≡OBJT3); keep the one that also matches.data/.rdata(psyq_link_region --verifyconfirms per-object). Hardcode the disambiguated object list in a committed regen script (tools/make_libgs.sh) — SDK-derived dir, gitignored.
§9.5 Integrating a WHOLE multi-block library in one call (full libgs — Phase 7 session G)
Block 6 alone (§9.4) proved the pattern; the full library is then ONE psyq_integrate call over all of
its used objects — no separate integration per block. integrate auto-splits the elf dir's vram-ordered
objects into contiguous runs (contiguous_blocks) and maps the i-th run to the i-th stub: pass one
block stub per contiguous run, and a plain asm stub per non-library gap (gaps keep their own subseg).
- Derive the block/gap structure empirically (G1), don't trust the notes.
psyq_identify <used-dir>prints each object's vram; a new block starts wherever the next object's vram ≠ the previous object's end (vram + nins*4). libgs = 6 blocks, gaps 80/48/1536/48/304 B (the 1536 is the excluded GS_001). Resegment the splat code subseg into[game-pre][block1][gap1]…[blockN][gapN]…(vram→file = −0x8000F800); block subsegs become thelibgsNintegrate stubs, gap subsegs staygsgapNasm stubs (incl. the GS_001 gap). Onesplit_src_region.py trimdrops the old single-subseg's stubs from the pre-file — but verify FIRST that no matched C lives in [lo,hi) (trim drops that range unconditionally; here all matched fns were ≤0x8002Axxx, far below the 0x80051804 libgs base). - Short objects need the placement WINDOW (new
psyq_integrate <stubs> <lo> <hi>arg). An object whose.textis too short to anchor uniquely over the whole EXE — libgs GS_106 (8 ins; its pattern recurs in game code) — isambiguousin the default 0x80010000..0x800629DC scan, so it drops from the placement map, its block splits, and the block↔stub count breaks. Pass the library's text window (wired in the Makefile integrate call:… libgs1,…,libgs6 0x80051804 0x80057928); the object anchors uniquely there.tools/make_libgs.sh --verify(psyq_link_region over the same window) is the preflight. - Result + checks: 31 libgs objects / 6 blocks linked byte-identical in one integrate (≈49 named SDK functions — count OBJECTS, not stubs: splat over-segments library code into ~5× more INCLUDE_ASM stubs than real functions). The finer resegmentation (10 new subsegs) was split-deterministic across two clean extracts (no new §9.4 boundary re-detection — the GsMulCoord2/3 declarations + the 53198 data carve from §9.4 already cover it) and byte-identical WITH or WITHOUT the SDK objects (stub fallback, fresh-clone-safe). The unified call cleanly supersedes the §9.4 block-6-only integration.
§9.6 Scaling library linking to the whole EXE (Phase 8 — 8 libs linked, 20%→50% byte-identical)
Phase 8 linked the remaining footprint libraries (libetc/libgpu/libmcrd/libc2/libgte/libspu/libsnd/libapi/ libcard). New patterns + tools that make a fragmented, multi-library EXE tractable:
- Survey first (
psyq_identifyover every built.run/obj40/*). Produces the byte-confirmed footprint map (docs/psyq-worklist.md): which libs place, how many blocks, regions, aliases. 3 libs (libmath/libc/ libsn) place 0 objects — BFM links libc2 not libc. Replaces guesswork (R14). tools/gen_lib_subsegs.pyautomates the multi-block resegment. A library scatters across a game-code region in many contiguous blocks (libgte = 22, libsnd/libspu combined = 9). The tool places the objects, groups blocks, and emits the splat subseg lines (libNblocks + game-code gap frags) + the integrate stub list. Boundary gotcha (was a real bug): a block's end = the last object's.textSECTION size (readelf, 8-aligned), NOTpsyq_identify's instruction count × 4 — the count omits trailing align pad (libc2 SETJMP.o: 30 ins = 0x78, but.text= 0x80). A too-low boundary overlaps the object's padded tail and the relink inserts +N padding, shifting the WHOLE downstream image (pervasive 1-byte reloc diffs + a grown file). The tool bakes the section-size rule in.- Pass the stub list via a make var (
LIBGTE_STUBS := libgte1,…) for the long ones;progress.py'slinked_subsegs()resolves$(VAR)from the Makefile's:=defs so LINKED still counts them. - Interleaved libraries → ONE combined region. libspu+libsnd interleave object-by-object in 0x3A444..
0x4239C, so two independent passes tangle (each lib's objects span the other's gaps). Instead build a
combined curated dir (
tools/make_snd_used.py): merge both libs' objects by vram, and for an aliased address (>1 object, same masked.text) pick the one whose linked.textbyte-matches the EXE (psyq_link.link_object). libapi+libcard share the same trick (make_apicard_used.py, C112 dedup). - Scattered-
.bssexclusion = the GS_001 class, now also cross-object. An object whose.bsscommons the original linker scattered (referenced as.bss+offset via one section symbol, but resolving to >1 base in the EXE) can't be reproduced by a single NOLOAD base → EXCLUDE it (stays a byte-identical stub). Detect:psyq_link_regionshows N words differ in that object andconflicts>0. Exclude by address (the alias twin fails identically). Cases: libgpu SYS.o, sound S_R/S_GRMDT/VM_F; plus a false placement (S_IH @0x3D94C is inside libsnd SSSTART.o). Excluding a few objects banks the other 60. - CLEAN-REBUILD gotcha (verification).
psyq_integraterewrites the.ldin place; after a src change an incrementalmake buildcan re-run integrate on an already-rewritten.ldand transiently mis-resolve a sibling library's externals (a harvest falsely diffed in libmcrd). The canonical verify is alwaysmake clean && make extract && make build— never trust an incremental build for a byte check. - Honest metric: linked objects stay INCLUDE_ASM stubs in their
.c(the fresh-clone fallback), soprogress.pycounts them in a distinct LINKED bucket (not REAL, not stub). REAL = hand-written C only.
§9.7 Binary-agnostic toolchain refactor (Phase 9) — the reusable pattern for Gen2
Gen2 builds many binaries (resident blob, location overlays) with these same tools. The refactor that got there, and the technique to land it safely without ever breaking the byte-locked EXE:
- Required params, no defaults. Every binary-specific value —
--vram-base(the fileoff→vram delta),--exe,--symbols— is a REQUIRED argparse/function parameter. No module-levelEXE/VRAM_BASEdefault an overlay could silently inherit; a miss fails loud (argparse error / NameError). The roadmap's #1 risk was a hidden EXE default surfacing as a wrong overlay address only at Phase 10 — required params make that impossible by construction. --vram-baseis a single scalar (EXE0x8000F800=0x80010000 − 0x800), sufficient for any flat-loaded PS1 image. The text-scan window[lo hi]is a SEPARATE, orthogonal param (scan-narrowing only, for short/ambiguous objects) — never conflate the two.- Transitional-default technique (keeps every per-tool commit green despite in-process coupling).
psyq_integrate/psyq_link_regionimportrecover_sym_addrs/VRAM_BASEfrompsyq_linkIN-PROCESS, so deleting the global in one tool's commit breaks the build mid-sequence. Instead: each tool first gains the param DEFAULTING to the kept EXE global (build stays byte-identical as callers are updated one commit at a time); a FINAL commit removes the globals + all defaults → required, once every caller passes explicitly. Refactor leaf-first (psyq_link→psyq_identify→psyq_link_region→psyq_integrate) so a missed call site fails loud, not silently on a stale global that happens to hold the EXE value. - Negative control proves threading. A pure no-op (
143dbb89…unchanged) can pass for the wrong reason (param accepted-but-ignored). Always ALSO pass a deliberately wrong--vram-baseand confirm the build/link DIVERGES (cae22f7e…≠ target) — that proves the value is load-bearing. Per-tool:psyq_link.py … --vram-base 0x8000F900must FAIL where0x8000F800PASSes. - Makefile shape.
BINARIES := main(alias keys) +main_*vars +$(BINARY)-resolved aliases (OUT/LD_SCRIPT/VRAM_BASE/…). EXE artifact paths preserved verbatim (no rename churn against the oracle). EXE-only SDK-integration +ld_interleaveblocks gated underifeq ($(BINARY),main). Lockstep gotcha:progress.pyparses the Makefile'spsyq_integratecalls for the LINKED subseg list, so when you add leading--flag valuepairs to those calls, its stub-list regex must consume them (psyq_integrate\.py(?:\s+--\S+\s+\S+)*\s+\S+\s+\S+\s+\S+\s+\S+\s+(\S+)) — land the regex change in the SAME commit, gated onmake reportreproducing the LINKED count.
§10 Closing the regalloc/scheduling hard tail by hand (LZSS, Phase 7 session F — the full close)
LzssDecodeSector (0x80018730) was the last-mile case the §5a barrier set up but did not finish: with the
cross-jump barrier the instruction COUNT was correct (122) but ~4 register-allocation / scheduling slots were
wrong. The decomp-permuter could NOT measure progress (its object score floats on the .rodata-vs-jtbl
floor — §5a caveat), and its random search diverged. Hand-solving with the §3a research tier won — every
fix below was ground-truthed against the pinned gcc-2.7.2 source (reorg.c, jump.c, local-alloc.c).
These idioms are general; reach for them whenever a function is instruction-count-correct but off by a few
regalloc/schedule slots and the permuter can't score it.
The clean object-level metric (use this, not the permuter score, for jtbl/rodata functions)
The permuter/asm-differ object score is polluted by the migrated-jtbl symbol name (.rodata vs
jtbl_<addr>), so it can't see real .text progress. Two floor-free checks (no link needed):
- Normalized instruction diff —
objdump -dr --no-show-raw-insn -j .text, strip theR_MIPS_*lines, the<sym>operands and the branch-target hex, thendiffcandidate vstarget.o. Shows ONLY real register/opcode/order differences. (.run/permuter/LzssDecodeSector/try.shis the reference impl.) - Raw
.textbyte compare —objcopy -O binary --only-section=.text cand.o c.textand… target.o t.text, thencmp -l. In a relocatable object the%hi/%loimmediates of an unresolved symbol are BOTH 0 (the reloc fills them at link), so a migrated jtbl reference is byte-identical here and does NOT show — the only diffs that remain are genuine (e.g. a wrong local branch offset). This is the fast, authoritative iterate-on-.textoracle; finish with the linkedmake check(G3) for the whole-binary truth.
Residual A — commutative |/&/+ result lands in the wrong source-operand register
local-alloc.c combine_regs (≈line 1855) ties a commutative op's result to the first RTL operand that
dies at the insn (RTL operand order = source order; gcc 2.7.2 has NO swap_commutative_operands, so the
source order survives). Target or $v0,$v0,$v1 ⇒ result tied to the code&0xFF operand ($v0).
- Fix A1 — operand order: write the operand whose register you want the result in FIRST:
code = (code & 0xFF) | (nh << 8);(not(nh<<8)|(code&0xFF)). - Fix A1-companion — DECOUPLE shared inputs (load-bearing): if a variable feeds two expressions
(
nbfed both the low- and high-byte ORs), reshaping one OR re-allocates that variable in BOTH paths (it jumped$v1→$a0). Give the second use its own variable (nh) so the operand-order change is local. This was the unlock — A1 alone "didn't work" only because of the coupling.
Residual B — a return <const> materialised late / merged instead of distributed per-site
reorg.c fill_simple_delay_slots backward-scan (≈line 2907) pulls the common li $v0,K out of the
predecessors into the shared epilogue's branch-delay slot, and redundant_insn then collapses the other
copies — so one li $v0,1 ends up in the j <epilogue> slot instead of one per return site. Three levers,
applied where each fits:
- B-distribute (shared tail reached by ≥2 predecessors): carry the value in a plain local set in each
PREDECESSOR block (
result = 1; newState = N; goto save;…save: …stores…; return result;). Because the value is live-in from two defs, gcc emits a distinctli $v0,1per predecessor and leaves the tail's delay slotnop. (A barrier is NOT needed for this half; the predecessor structure is.) - B-schedule-early (single-path block whose
li $v0,K's only use is the sharedjr ra): the sched list-scheduler gives an independentli $v0,Kpriority 0 (its use is in another block) and the tie-break drops it to just beforejr ra; the target schedules it first. Force it with an explicit$v0register local pinned by a read-only input-asm BEFORE the stores:A plainregister s32 r __asm__("$2"); r = 1; __asm__ __volatile__("" : : "r"(r)); /* materialise li $v0,1 here, ahead of the stores */ …stores… __asm__ __volatile__("" ::: "memory"); /* the §5a cross-jump barrier, still required */ return r;resultlocal does NOT work here (gcc rematerialises the constant at the return); the+r/"=r":"0"read-write pins put it in$v1; only the explicit-$2local + early read-only input pin landsli $v0,1first, in$v0. (Scoperto the one block so$v0stays free as scratch elsewhere.) - B3-reuse-the-compare (a
return 0whose 0 already sits in a reg): aswitch(x){case…}range check issltiu $v0,x,N; beqz $v0,<dft>; on the out-of-range path$v0==0already equals the wantedreturn 0. Give gcc nothing else to do: NOdefault:and NO statement after the switch → thebeqzthreads straight tojr ra, reusing thesltiuresult (targetbeqz $v0,.epilogue). An explicitif(x>=N)return 0;, adefault: return 0;, or a trailingreturn 0;each forces a separatemove $v0,$zero(+1 insn / wrong branch target). Falling off the end of the non-void function is deliberate here and matches the original (gcc warns under-Wall; harmless). Cross-refs cookbook §3 (T4 branch-polarity / fall-through) — same family.
Method note (reinforces §3a + R16)
The permuter is the wrong tool when (a) its score can't see the residual (rodata/jtbl floor) or (b) the
residual is a specific compiler-internal placement rather than a randomizable C perturbation. For those,
web-research the exact pinned compiler source (§3a) to name the pass and its bail/tie condition, then
express the lever in C. Here a research agent reading reorg.c/jump.c/local-alloc.c produced all four
levers directly; hand-iteration with the clean .text metric closed it in a few compiles. Pin every such
construct with a LOAD-BEARING comment naming the pass — a future reader WILL try to "simplify" them.
§11 Cross-binary dedup & code-sharing (Phase 11 — "one match unlocks many")
BFM is overlay-heavy: 134 location overlays all load to the SAME vram 0x80128158 and run on the same engine,
so they share enormous amounts of code (a 770-instruction engine fn is byte-identical in all 134). Match a
shared fn ONCE, credit every binary it lives in. The pipeline (all Ghidra-free except the EXE/resident sigs):
1. Sign every binary → .run/sig.<bin>.jsonl. make sig-refresh (Ghidra, EXE/resident) + make sig-overlays (the 134 0.4.dec via tools/sig_image.py, no Ghidra). Each fn gets h_exact (SHA1 of raw
instruction bytes), h_norm (structural), h_seq, nins, calls.
2. Group across binaries → docs/duplicates.cross.md. tools/dup_report.py --cross (run by make report,
gated BINARY=main) buckets ALL sigs by h_exact then h_norm, splits cross-binary (members in >1 binary —
the Phase-12/13 work queue) vs intra-binary, ranks by collapsible bytes (count−1)×nins×4, top-200 capped.
3. Register a share → config/dedup.us.yaml. group → {id, tier, hash, source, func, members:[{binary, vram, name}]}. tools/dedup_integrate.py --check is the byte-honesty gate (fail-closed if a member's live
sig hash drifts from the recorded hash); wired into make report so a stale share fails the report (P9).
The mechanism: game-code dedup is SOURCE-LEVEL, not an object swap (R-D1, the key lesson)
psyq_integrate's stub-object swap works only for separate library subsegments. Game-code functions are
interior to one compiled object per binary (build/src/800.o, build/resident/resident.o, each overlay's
one object) — the linker can't excise interior bytes. So you share at the SOURCE level: author the matched body
ONCE as a macro in src/shared/<fn>.h and instantiate it at each member site in each binary's .c:
// src/shared/clearTbl40.h
#define CLEAR_TBL40(name) void name(void) { s32 i; for (i=0x40; i>=0; i-=0x10) (&D_80076251)[i]=0; }
// src/800.c: CLEAR_TBL40(func_80037004) ... CLEAR_TBL40(func_80037334)
Same bytes land at each vram. The byte-gate is the existing per-binary make check — the image is identical
or it is not. h_exact shares are risk-free; h_norm shares are CANDIDATES, accepted only if every claiming
binary stays byte-identical (a wrong h_norm group wastes a build, never poisons an image). A shared .h is
skipped by the find src -name '*.c' OBJS glob automatically (no exclusion needed). progress.py counts dedup
members as REAL via the registry (the macro form isn't a parseable function def).
sig_image.py (Ghidra-free signer) — notes for reuse on overlays
h_exactis the workhorse: SHA1 of raw bytes → format-independent → byte-matches the Ghidra dumper with no normalization. Validated 100% on the resident's contiguous/non-GTE functions. Use it as the cross-tool tier.h_normis self-consistent, NOT Ghidra-byte-exact (R-D2): masks j/jal targets, lui highs, hi/lo-paired address-los (a consistent lui→reg tracker); keeps registers / true constants / PC-relative branch offsets. Uniform within the overlay fleet (catches different-offset structural dups); does not cross-compare with the Ghidra-signed EXE/residenth_norm(low value — overlays call, don't embed, the resident). Full normToken byte-match is a deferred refinement.- Boundary detection: (a) seeded (pass
--seeds <sig.jsonl>when boundaries are known, e.g. the resident); (b)--bootstrapfor overlays = linear partition (split contiguous code at the firstjr $ra(+delay) that lies at/after all forward branch targets — handles early-return + double-epilogue) bounded bydetect_code_end(first run of ≥3 invalid instrs = the code→data transition; overlay code decodes ~100% valid). Call-graph BFS FAILS on overlays (they dispatch via function-pointer tables, notjal). Residual: jump-table-only fns + non-contiguous Ghidra bodies (D5) are missed — conservative, fixed when splat configs land (Phase 13).
Per-binary toolchain provenance (R24)
Verify the toolchain per binary before linking its library code: the EXE is PsyQ 4.0, the resident is 4.7
(tools/psyq/conv47/, sha-recorded in tools/psyq/CHECKSUMS.sha256). Never assume one binary's SDK applies to
another — the 4.0 libs won't byte-match the resident's 4.7 objects.
§12 Ultracode harvest — parallel-draft + byte-gate at scale (Phase 12, resident: 1.4%→71.7% in one session)
When a binary has many independent small/medium functions to hand-match (the resident: ~143 game-code fns, the overlays later), fan the drafting out to a swarm of agents and let an incorruptible byte-gate filter — a wrong match is structurally impossible to accept (G3/P9), so blind/semi-blind drafting is safe to mass-produce. This drove REAL 1→102/145 (71.7% byte-identical) on the resident in one Ultracode session (5 passes). Reusable verbatim for the Phase-13 overlays.
The loop (each pass = a Workflow + a deterministic gate; loop-until-dry):
- Draft (parallel, Workflow). N agents (round-robin a size-sorted fn list into ~13–16 batches), each reads
asm/<bin>/nonmatchings/.../<fn>.s+ this cookbook + the already-matched fns (the style/extern conventions) and writes ONE self-contained.cper fn to.run/drafts/<fn>.c(externs + body) + a.conf(high/med/low). No builds, no Ghidra inside the draft pass (the asm IS the target; Ghidra contention flakes under fan-out). Distinct files per fn → no write races (no worktree isolation needed). - Byte-gate (deterministic,
tools/harvest_verify.py). Substitutes each draft for itsINCLUDE_ASMstub,make build BINARY=<bin>, keeps it ONLY if the image stays byte-identical, else reverts to the stub. Chunk-with-bisection (apply K, build; if the SHA holds keep all, else isolate one-at-a-time). The build is the sole arbiter — agent over-claims cost nothing. (src/<bin>/*.cis git-committed → alwaysgit checkout-able.) - Loop (redraft passes). Re-run the Workflow on the residual stubs, each agent seeded by its prior failed draft(s) + a debugging checklist (the high-yield miss-modes — see below). Gate again. Resident yield per pass: +62, +8, +9, +2.
- Iterate pass (the strongest —
tools/match_one.py). Gives each agent a real per-function asm-differ loop: compile ONE fn's C standalone (the pinned triple), mask relocations (jal/HI16/LO16, exactlypsyq_identify's mask), compare to the target bytes in its.s→MATCHor a per-instructionidx | MINE | TARGETdiff. Fully isolated (own.run/match/<fn>/temp dir) → parallel-safe. Agentswrite C → run match_one → read diff → fixuntil MATCH. This cracks scheduling/regalloc near-misses blind drafting can't (resident +13 on the hard tail).
Workflow resilience: wrap the per-batch agent() in retry waves — a transient server 500/"rate limited"
returns null; re-run only the null batches up to 3× (pending/okResults pattern). One un-retried pass lost
10/14 agents to a server throttle; the retry-wave pass recovered all 13.
Under a SUSTAINED server-side throttle, throttle the FAN-OUT, not just retry it — process in SEQUENTIAL
WAVES of ≤10 agents. A big concurrent burst (40+ batches submitted at once) hammers the shared
Server is temporarily limiting requests (not your usage limit) rate limit, and even retry waves keep
failing because every wave re-bursts. Phase-15 T6 v3: a 48-batch burst harvest crawled at ~1 draft / 45 s
and finished with 33 dead batches; re-run as for (g of chunks(batches, 10)) await parallel(waveOf10)
(with intra-wave retry), the SAME 375 targets drafted at ~42 drafts / 60 s, 0 dead batches. Keeping
≤10 requests in flight stays under the per-window limit; sequential waves space the load so each window
resets between them. Pair with leaf-first / easy-first ordering so the early waves bank the high-yield
functions even if later waves get throttled. This is the rate-limit-gentle default for large harvests.
MANDATORY GAP-FILL after every multi-agent run you expect to be complete (the retry-wave is NOT enough).
Retry waves only re-run agents that returned null. But an "API Error: Connection closed mid-response. The response above may be incomplete." failure returns a truncated-but-non-null result — the workflow reports
dead_batches: 0 and a clean exit, yet that batch's agent silently wrote only SOME of its assigned drafts (or
none). The summary count looks fine; work is missing. So the gate alone would silently skip those functions
(a leftover stub is itself byte-identical — the byte-gate can't see an un-attempted target). Always
reconcile the produced artifacts against the expected work-list before gating (this generalizes to any
fan-out, not just harvests — diff produced-vs-expected whenever "expected complete"):
- Missing: every manifest target with no draft file on disk.
- Truncated/malformed: every present draft that is brace-imbalanced, has no function def, or doesn't close — a partial write. (These would fail the gate harmlessly, but re-drafting recovers them.)
- Re-draft the union with a focused gap-fill agent (same prompt, the gap names), THEN gate the full set. This is the byte-honest closure of "exhaustive" (P9/R14): the disk, not the workflow's success summary, is ground truth. Phase-15 T6 v2: workflow reported 585 drafted / 0 dead after the retry-wave re-ran 2 connection-closed batches, but a produced-vs-manifest diff still found 6 un-drafted targets (individual skips inside completed batches) — re-drafted before gating, none lost.
Two TU-level gotchas (both bit, both have a fix):
- Inline scalar-typedef redefinition. Agents told "self-contained" sometimes inline
typedef unsigned char u8;— in a.cthat already#includescommon.h, gcc-2.7.2 (C89) errors on the dup → a compile fail, NOT a byte miss.harvest_verify.py/match_one.pySTRIP^\s*typedef\b.*\b(u8|u16|…|f64)\s*;lines (common.h provides them). (Recovers false-failures: re-gate after the strip.) match_oneMATCH but whole-build FAIL = extern-type conflict.match_onecompiles standalone (one fn's externs); the real build is ONE TU (resident.c) where all fns coexist. A caller that declares a shared symbol to suit ITS call site (e.g.extern s32 func_800D1714(void);to drop anandi v0,0xffff) conflicts with that symbol's decl/def elsewhere (u16) →conflicting types for …→ gate fails. Resident pass-4: 34 standalone MATCH → 13 whole-build (21 conflict casualties on ~25 shared symbols). Fix: unify the extern types in the.c— usually widen the definition's return type where byte-identical (u16 f(){return u16g;}↔s32 f(){…}are the samelhu;jr), so all callers agree. NOT separate.cper fn: splat places the binary as ONE address-ordered object, so a second TU's.oisn't interleaved at the right vram.
Honest tail (P9). What survives the iterate pass is real compiler-internal residual — cross-jump tail-merge /
block-reorder / regalloc that no C-source shape steers (the agents document each in the draft header). Those go to
decomp-permuter (tools/permuter/) + the §3a/§5a/§10 research tier, or stay honest stubs — never forced.
Idioms the swarm surfaced (fold the asm patterns into §1/§2/§10):
- A "void-looking" dispatcher that ends in
jalrwith NO trailingmove v0,zerobut HASmove v0,zeroin its early-return delay slots is actuallys32-returning withreturn 0;early-exits andreturn fp(...)at the tail (the jalr'sv0is the return) — declaring itvoidmis-schedules the constant. - Local = global for a small fixed table: a
lw 0/4/8(base); sw …prologue copying a 3-word global into a stack array is a struct copy (Foo local = D_global;), not element-wise assignment (forced when a runtime index makes gcc materialise the whole table on the stack). *10(and small-const multiplies) decompose as(x<<1)+(x<<3), not((x<<2)+x)<<1; if a div/mul-by-const diffs by one shift/add arrangement, hand-write the explicitx*2 + x*8form.- Masked compare
andisurvives only if the value's range is unprovable.(a0 & 0xff) == kkeeps itsandi v1,a0,0xffonly when gcc can't provea0's range; a cleanu8load lets gcc-2.7.2 provea0∈[0,255]and DROP the andi (shifting the whole tail). An(s8)/(u8)cast on a wider load is the lever to restore-or-drop it. - Callee return type forces the cast at the call site:
jal f; andi v0,0xffffmeansfis declared returning a type WIDER than u16 (so the(u16)cast emits theandi); declaringfasu16lets gcc trust it and drop the andi.
§13 Add a location overlay — the canonical runbook (Phase 13; the Phase-15 fleet recipe)
All ~134 location overlays are flat LZSS-decompressed 0.4.dec payloads that stream into the SAME slot
vram 0x80128158 (position-locked, Phase 3) and chain into the resident engine. Each is its own build
binary ov_<SCxx>_<nnn>. The whole pipeline is one command + the §12 harvest; this is the reusable recipe.
One-command onboarding — tools/new_overlay.sh <SCxx> <FILE_nnn>
Computes sha1 / size / code_end (from sig_image.py --bootstrap, the last function's end), instantiates
config/splat.<ov>.yaml from config/splat.us.overlay.template.yaml, writes config/check.<ov>.sha + an empty
config/symbols.<ov>.txt, appends the <ov>_* block to the generated config/overlays.mk (the Makefile
-includes it, so the hand-maintained Makefile body is NEVER edited) + the alias to OVERLAY_BINARIES,
sentinel-inserts the entry into the 4 report/diff dicts (diff_settings.py, tools/{progress,difficulty, dup_report}.py — grep-guarded + ast.parse syntax-checked), then make extract && make build to byte-verify
at 100% INCLUDE_ASM. Idempotent (re-run = clean no-op). Proven on SC01/005, /006, SC03/001.
The flat-blob overlay config (what the template encodes)
- No header, no
gp_value(-G0), singlecodesegment @vram 0x80128158;build_path: build; per-binary nestedasm/<ov>+src/<ov>+build/<ov>;asset_path: assets/<ov>; stacked symbols[symbols.us.txt, symbols.resident.txt, symbols.<ov>.txt](overlays call the resident engine). - Overlays open with code at file 0x0 (a prologue), UNLIKE the resident's leading data word — first subseg
[0x0, c, <ov>], no leading-rodata trick. Subsegs:[0x0, c]+[<code_end>, data, tail];code_end= the last sig_image function's end (file offset). Byte-match is robust to the exact split (splat round-trips bytes).
THE NON-4-ALIGNED-OVERLAY GOTCHA (≈75% of the fleet; fixed in the template + Makefile, automatic)
A 0.4.dec whose size isn't a multiple of 4 (SC01/077 = 0xB29D7, mod 4 = 3) loses its final 1–3 bytes three ways:
- spimdisasm drops the trailing partial word (won't emit < 4 leftover bytes; a
datacarve of those emits nothing). → Carve them as abinsubseg[<word_floor>, bin, trailing](raw.incbin).new_overlay.shinjects this whensize % 4 != 0. - splat's
binasset needs a build rule —.ldreferencesbuild/assets/<ov>/trailing.o. The Makefilebuild/assets/%.o: assets/%.binrule assembles a one-line.incbinstub +objcopy --set-section-alignment .data=1(elseasdefaults.datato 16-align → ld pads the image, adds a stray byte). - splat's
.lddoes. = ALIGN(., 4)at the segment end → up to 3 zero pad bytes. The Makefile objcopy step TRIMs it: shrink-only, capped at 3 bytes, gated onsize(build) > size($(EXE)) && delta ≤ 3— can never hide a shortfall or touch the 4-aligned EXE/resident (which never hit any of this).
The A→B→C per-overlay workflow
- A. Onboard + all-asm byte-match (
new_overlay.sh) →make check BINARY=<ov>byte-identical. The milestone bar (splat round-trips bytes regardless of carve quality). - B. Seed boundaries + Ghidra import (to draft harder fns): splat carves ~all functions itself (SC01/077:
2504 cleanly), so seeding
symbols.<ov>.txtonly fixes conservative carves (§8/§11) + attaches names — the byte-match doesn't depend on it. Ghidra:ghidra_import_raw.sh <0.4.dec> 0x80128158 <ov>(MCP stopped, R23) +DefineFunctions.javaover splat's func list (auto-analysis finds only thejal-reachable subset — overlays dispatch via fn-pointer tables; SC01/077: 1237 → 2661). The overlay Ghidra DB is script-reproducible → DB-commit optional (skip to avoid ~14 MB bloat unless doing manual RE). Restarting MCP to serve<ov>drops the client SSE → pause + ask Drew to run/mcp(memorymcp-reconnect-after-restart). - C. Dedup-credit FIRST, then the §12 harvest. Credit the high-leverage shared engine functions (top
docs/duplicates.cross.mdgroups — byte-identical at fixed vrams in all 134 overlays) so they leave the queue; thendifficulty.py --binary <ov>ranks the unique remainder for the §12 parallel-draft + byte-gate. Use Ultracode (T7 evidence: xHigh agents = Max-agent yield on blind drafting — no reason to spend Max depth; seedocs/effort-map.md). SC01/077: 704 matched (yield 92%→79%→54% as difficulty rose; the hard tail → permuter/§3a or honest stubs).
Dedup-credit (§11) for overlays — two shapes
- Per-function share (general/fleet shape): a matched body lives once as a macro in
src/shared/<h>.h, instantiated in place (preserve address order — overlays link functions in source order!) at each member site, registered inconfig/dedup.us.yaml(members in >1 binary), byte-gated bydedup_integrate --check(the recordedh_exactmust equal each member'ssig_imagehash, keyed by addr-int — sig_image names lowercase, splat uppercase, validator case-moot). The nested overlay.cresolves quoted includes relative to ITS dir → use#include "../shared/<h>.h". Proven: SC01/005 ≡ 006 share 3 accessors fromsrc/shared/ov_setters.h(SETTER/RETCONST), both byte-identical from one source.progress.pycredits shared members as REAL. - Whole-overlay collapse (special case for byte-identical pairs): for two overlays with the SAME
0.4.decsha1,src/ov_B/ov_B.ccan#include "../ov_A/ov_A.c"— B inherits ALL of A's matches from one source. Maximal but doesn't generalize to partial sharing, so the per-function share is the fleet default.
Fleet build — make build-all / make check-all
Recursive $(MAKE) BINARY=<b> over $(BINARIES) (NOT foreach — the OBJS glob is parse-time per $(BINARY)),
one PASS/FAIL + per-binary .run/check.<b>.log. Serial across binaries (shared build/asm|src/** outputs make
binary-level -j racy). Day-to-day incremental; milestone fleet proof = a CLEAN run (R22): make clean && for b in $(BINARIES); do make extract BINARY=$$b; done && make check-all.
§14 Propagate a matched function across the fleet — tools/dedup_propagate.py (Phase 15)
The economics that drives Phase 15. Overlays are position-locked at 0x80128158, so a shared engine
function has the SAME vaddr (hence the SAME func_<ADDR> symbol) and a BYTE-IDENTICAL body in every overlay
that contains it. Measured on the fleet: 577 of ov_SC01_077's 785 matched functions are h_exact-identical
across ALL 134 overlays (~2.19 MB collapsible) — already matched, just needing propagation. So the rule is
match once → propagate, do NOT re-harvest each overlay. (tools/dedup_propagate.py --auto-from ov_SC01_077
enumerates exactly this set: matched-as-an-inline-def in the source AND h_exact-shared across ≥--min-reach.)
The tool. tools/dedup_propagate.py --addr 0x..[,..] --source-overlay <ov> (or --auto-from <ov> for the
whole shared set; --check-only for a dry-run plan):
- extract the matched body (preceding
externs + the def, brace-matched) from the source overlay's.c; - author it ONCE as a tool-generated
#define DEFINE_func_<ADDR>() \-continued macro insrc/shared/engine_core.h(idempotent; refuses//line comments — they break line-splicing; block/* */is fine); - at every onboarded overlay whose sig shows that
h_exact(lead withh_exact— guaranteed byte-identity), replace that function'sINCLUDE_ASMstub (or, in the source overlay, its inline def) in place withDEFINE_func_<ADDR>()— address order preserved;#include "../shared/engine_core.h"added once aftercommon.h; - byte-gate each touched overlay (
make build BINARY=<ov>== itscheck.sha); on ANY miss, restore EVERY file from an in-memory snapshot and abort (fail-closed; nothing wrong lands); - register the group in
config/dedup.us.yaml, validated bydedup_integrate --check.
Key gotchas (each cost a real bug or false pass during the Phase-15 proof):
- Key by addr-int, never the string.
sig_imagewrites lowercase hex (0x80144b9c); splat's symbol is uppercase (func_80144B9C). Compareint(addr,16); render the symbol asfunc_%08X. - Accumulate edits from the on-disk text, not a snapshot cache. When a batch propagates several functions into the same overlay, re-read the file before each edit (the snapshot dict holds the ORIGINAL for restore, not the running state) — else each function's edit clobbers the previous and only the last lands.
- The byte-gate can't catch under-application. A leftover
INCLUDE_ASMstub is itself byte-identical (it just uses the asm), somake checkpasses even if a function wasn't actually converted. Add a STRUCTURAL self-check: after editing, assert each member's.ccontainsDEFINE_func_<ADDR>()and NO leftover stub line. - Header-dependency tracking is mandatory once shared headers are build inputs (R22). The Makefile C rule
originally made
build/src/%.odepend only on the.c, so editingengine_core.h/common.hdid NOT trigger a recompile → an incrementalmake checkafter a header-only edit was STALE (a wrong shared body falsely passed). Fixed: thecppstage now emits a.d(-MMD -MP -MT $@ -MF $(@:.o=.d)) and the Makefile-includes$(C_SRCS:%.c=build/%.d). Side-effect only — output bytes unchanged. With it, the negative test (corrupt a macro body →make checkrebuilds via the.d→ SHA mismatch → fail) behaves correctly. - Idempotent + resumable. A propagated source function becomes a
DEFINE_…macro (no longer an inline def), so--auto-fromre-runs skip the done ones;--addrre-runs no-op (source is now a macro, plan is empty). - Scale note (deferred until it bites):
dedup.us.yamlmembers are listed verbose ({binary,vram,name}). For the full 134-overlay × hundreds-of-functions bulk, switch to avram + binaries:[...]shorthand (expanded bydedup_integrate/progress.py) before it becomes a 5-figure-line file.
§14a The fleet bulk run (Phase 15 — --auto-from, 947 REAL → 74,527; 3.82% → 22.14% in one pass)
A measured 577 of ov_SC01_077's matches are h_exact across all 134 overlays; dedup_propagate --auto-from ov_SC01_077 propagated 553 of them fleet-wide (each overlay byte-gated) in ~2 min. The 5 things that made
the bulk work (each cost a real failure first):
- Only self-contained bodies are mechanically liftable. A 077 match whose body names an overlay-LOCAL struct
type (
SrcB964 *a0— a harvest-invented type defined in 077.c, not common.h) compiles in 077 but FAILS in every other overlay (parse error before '*',a0 undeclared). The first bulk attempt died on one and reverted all 134 (fail-closed, correct but wasteful). Fix: a compile pre-filter —compiles_standalone(body)builds the body withcommon.honly (cpp→cc1); skip if it fails. 9/562 were local-typed → skipped honestly (P9). These need their types shared too (a future enhancement); they are NOT a byte regression, just deferred. - The registry shorthand is mandatory at this scale. 553 groups × ~134 members verbose ≈ 77k lines; the
vram + binaries:[...]shorthand keepsdedup.us.yamlat ~4k.group_members()(indedup_integrate) is the single expander used bydedup_integrate --checkandprogress.py. - Per-overlay apply, not per-(function,overlay). Group targets by overlay → one read/write per file; stub lines replace 1:1 (no shift); the source overlay's inline defs splice by range in REVERSE line order. (Naive per-pair editing is 77k file-ops and minutes slower.)
- Skip already-registered + gate-only-changed.
--auto-fromexcludes addrs already in the registry (additive, resumable, never collides with an existing SETTER/engine_core share); only overlays that actually changed are rebuilt. Re-running tops up as more overlays onboard. progress.py --fleetparse cache.dedup_membersparsed the (now-large) registry once per binary (136×) — cache it once (_DEDUP_CACHE): fleet report 6m+ → ~7s.
§14b Harvesting the UNMATCHED shared core — the match_one wall (Phase 15)
After the bulk (already-matched) propagation, the remaining shared functions are the hard residual the
original per-overlay harvest already failed on. A 25-agent Ultracode pass (§12) drafted + match_one-verified
300 of the smallest unmatched-shared functions (8–16 ins); the whole-binary byte-gate (harvest_verify in
ov_SC01_077) verified only 49 (16%) despite 292 agent-claimed "high". The gap is structural, not agent error:
match_onemasks relocations (jal 26-bit, HI16/LO16), so it CANNOT verify call/data targets. A draft that calls the wrong function — or the right function with the wrong extern signature — still printsMATCH. It is a true gate only for leaf functions (no calls, no global refs). The 49 that passed were essentially the leaves.- The real misses are the extern-type-conflict wall (§12's gotcha, now the dominant failure): a wrapper
func_Acallsfunc_B; the draft declaresextern void func_B(s32), butfunc_Bis already defined IN THE SAME TU (anengine_core.hmacro from the bulk, or an inline def) with a different signature →conflicting types→ compile fail → the byte-gate reverts it.match_one(standalone, no other defs) never sees the conflict. - Implications for future passes (the open-ended tail): (1) leaf-first — filter targets to functions whose
.shas nojal/%hi/%lofor a high-yield pass; (2) callee-signature-aware drafting — when a draft callsfunc_Xthat's already C-defined (grepengine_core.h/ the overlay.c), it must reusefunc_X's EXACT signature; a STUB callee (stillINCLUDE_ASM) takes any consistent extern (asm provides the symbol, no conflict); (3) a real per-function gate would need to link (resolve relocations), i.e. the whole-binaryharvest_verify, not the maskedmatch_one; (4) the call-heavy residual is genuine decomp-permuter / hand-iteration work — the open-ended Phase 15 continuation, not a milestone gate.
§14c Callee-signature-aware harvest — breaking the extern-type-conflict wall (Phase 15, T6)
§14b named the wall; this is how it falls. The call-heavy shared residual fails the whole-binary
byte-gate (NOT match_one, which compiles standalone and masks jal) because a draft declares a
callee or data symbol with a type that conflicts with that symbol's canonical declaration
elsewhere in the single overlay TU (an engine_core.h DEFINE macro, another banked function's
inline extern). The fix is to stop letting agents guess: pre-resolve every callee's EXACT signature
deterministically and hand it to the agent to reuse verbatim.
tools/gen_harvest_targets.py builds the callee-sig-aware target manifest (.run/t6_*.json).
For each still-INCLUDE_ASM shared function it records {name, addr, nins, reach, callees:[{sym, status, signature}]}, resolving each callee three ways (priority order):
- defined — a body exists:
engine_core.hDEFINE_func_Xmacro body, or an inline def in the overlay.c. The signature is authoritative; the draft MUST reuse it verbatim. - declared — no body, but the symbol is already
extern-declared somewhere (another macro/draft). That extern is authoritative too (a stub callee with an established signature — reuse it). - stub / extern — undeclared (a bare overlay stub, or a resident/EXE symbol). The agent infers a
minimal consistent extern from the
.sarg-setup; resident/EXE callees are conflict-free (not defined in this TU).
Result on the small band (nins 8-30, 616 fns): 855/1019 callees resolved to exact sigs, and the
whole-binary gate yield jumped 16% → 60-67%. The agents draft callee-sig-aware C (reuse exact sigs
for defined/declared callees; infer for stubs), self-check with match_one, and the whole-binary
harvest_verify is the sole arbiter (match_one cannot see TU-level conflicts).
The four conflict flavors (all real this phase):
- (a) defined-callee return/param mismatch — draft guessed
s32/wrong params; canonical body says otherwise. (func_8012A100isvoid f(s8), not(s32).) → reuse the resolved sig. - (b) declared-stub-callee — a still-
INCLUDE_ASMcallee already extern-declaredint func_X(int, int,int)by other macros; a draft declaring itvoidconflicts. (func_80150BA4→func_80151184.) → harvest the extern declarations too, reuse them. - (c) DATA-symbol type conflict — the dominant residual after (a)/(b):
D_XXXXglobals declared with different types by different drafts (u8 D[]array vss32 Dscalar) →conflicting types. The manifest resolves FUNCTION sigs but not yet DATA-symbol types; this is the next yield-limiter. Structural fix for a future pass: a single canonical decls header (all data symbols asextern u8 D_X[];byte-arrays, used via explicit casts) that the overlay.cincludes and drafts never redeclare → zero possible conflict. - (d) narrow-return-widening — a callee defined returning a NARROW type (
s8/s16/u8/u16, bodyreturn <load>) forces asll/sra(orandi) sign/zero-extension at EVERY call site; if the target asm lacks it, the original declared that functionint/s32. Widen the DEFINITION's return type tos32/u32— byte-identical because the body'slb/lbu/lh/lhualready extends to 32 bits — and all call sites match. (func_8017AE08→func_80174764.)
Operational gotcha (cost real work): NEVER git checkout src/<overlay>.c during an active harvest.
It silently reverts banked matches while the propagation artifacts (engine_core.h macros, other
overlays' DEFINE_func_X() instantiations, dedup.us.yaml groups) survive — an inconsistent (though
byte-recoverable: the next gate re-verifies) state. Use a .run/_bak.c copy for diagnostic
substitute/build/revert, never git checkout.
Resilient incremental loop (proven under server-side API rate limiting): the draft Workflow is the
only rate-limited part. Gate whatever drafts have landed (harvest_verify), dedup_propagate --auto-from (deterministic, immune to throttling), make report, repeat as more land. Quarantine
gate-failures to .run/drafts-<x>-fail/ after each round so re-gates stay fast. Partial harvests bank
cleanly; the residual resumes next pass (loop-until-dry). T6 banked 123 fns / +4.6% fleet this way with
the Workflow only ~40% through its batches.
§14d Deterministic recovery beats agent waves on the hard tail (Phase 15, the final-session finding)
Once the EASY shared core is banked, the residual is permuter-class and agent harvesting is poor ROI: a
50-agent Ultracode wave on 300 hard-tail stubs verified only 27 (~9%) for ~4.1M tokens (+0.36% fleet), while
a deterministic recovery pass added +2.67% for ~0 agent tokens the same session. Reach for these BEFORE
mass-drafting the hard tail (this is a cost rule, codified in docs/effort-map.md):
tools/sig_unify.py— unify the draft's OWN definition signature, not just callee externs. The dominant hard-tail gate failure is a TU-level signature conflict on a draft whose body is already byte-correct (it passesmatch_one). A probe is the tell: substitute each gate-failing standalone-MATCH draft alone, build, and classify the error — 30/30 sampled wereconflicting types, 0 false-positives (built-but-SHA-diff) → they are byte-correct C blocked only by declaration unification.canon_draft_declsrewrites callee externs but never the function's own def signature (definedvoid/s16here, extern-declareds32by banked callers) —sig_unifyadds that (canonical return + param types, draft's param names kept; arity-mismatch → return-only fallback). The byte-gate stays the arbiter. 191 conflict-blocked → 32 recovered deterministically.- The silent-under-propagation class (
find_sitebrace bug). A GREEN byte-gate proves what landed is correct; it does NOT prove everything that should have propagated did.dedup_propagate's inline-def detector required the opening brace on the same line as the signature, so every next-line-brace def (whichsig_unify/permuter emit) was silently skipped from propagation — a whole session's matches capped invisibly. Always sanity-check the OUTCOME metric (make reportfleet %, or--auto-from --check-onlyplan size), not just the gate. Fix:find_siteaccepts brace on the same OR the next non-blank line. (One fix unlocked a 61-function backlog.) Generalizes: any "match→register" detector must accept all draft formats. build_engine_types.pymust be ADDITIVE. A 2nd--striprun regenerating the shared types header from the source's current inline defs DROPS the already-migrated types (they were stripped last run) → build breaks. Merge with the existing header; the header is the cumulative record.- Probe-before-investing. Size every recovery lever on a ~20–30 sample (build-classify the failures) before building the full pass — it told us sig_unify was worth it (30/30 conflicts) AND that the next tier was a dead-end (below) before we spent on it.
§14e Two hard-tail dead-ends (Phase 15 — documented so they aren't re-attempted)
()no-prototype externs DON'T resolve arity/param conflicts here. The textbook PS1-decomp escape (declare inter-function externs param-less) FAILS under gcc-2.7.2: "An argument type that has a default promotion can't match an empty parameter name list declaration" — C forbids()matching a prototype with a default-promotion param (s8/s16/u8/u16/float), which these engine fns have. The remaining ~159 arity conflicts (def needs N params, callers declare M) have no clean deterministic fix.- m2c/Ghidra output is a scaffold, NOT byte-matching C — the struct is the wall. On the remaining shared band,
20/20 sampled m2c outputs are struct-heavy (
arg0->unkXXXinferred field accesses) and won't compile without the engine struct defined, plus?-typed values. A decompiler gives structure + offsets for free; it does not give the struct definition or the byte-match. The path forward (Phase 16): infer the one engine actor struct layout from the union of m2c field-accesses (offsets + widths) → feed as m2c--context→ compilable C →sig_unify→ decomp-permuter brute-force (compute-bound, low-token — the "set-it-and-go" pipeline). Validate on a 10-fn medium sample before scaling.
§15 Struct-heavy shared-core pipeline (Phase 16) — empirical determinations (S0)
Established by running m2c on real ov_SC01_077 stubs (R14) — it corrects §14e's framing on two key points and the corrections are favorable.
(1) m2c output COMPILES via tools/m2c/m2c_macros.h — the struct is NOT a hard compile prerequisite.
--valid-syntax emits M2C_FIELD(p, type, off) ≡ *(type)((s8 *)p + off) (defined in m2c_macros.h) — a byte-faithful cast (same lw/sw/lh/sh as p->field). §14e's "won't compile without the struct" was without m2c_macros.h. 30/30 sampled m2c-targets use only byte-faithful macros (M2C_FIELD/M2C_BITWISE/M2C_UNK) → they compile. The non-faithful macros — M2C_ERROR/M2C_BREAK/MULT_HI/MULTU_HI/CLZ/GLUE_F64/BSWAP/M2C_TRAP — emit (0) (discard the real op) → a function using any of them cannot byte-match (GTE/handwritten/special). Their presence = "defer, not m2c-matchable."
(2) Compiling ≠ byte-matching; the residual is regalloc/scheduling → decomp-permuter is the byte-closer (essential, not optional). Pure-leaf example func_8012CB64: macro-compiled output = 16/16 ins, identical control flow, only v0↔v1 regalloc + a trailing move/nop. That is permuter-class. Some functions m2c gets structurally wrong (e.g. 8 vs 18 ins) → permuter cannot fix → struct types / hand / defer.
The byte-match path: m2c --valid-syntax (+ macros + context) → sig_unify → decomp-permuter (regalloc/schedule) → harvest_verify whole-binary byte-gate → dedup_propagate. Struct types (struct_infer, S1) are an ENHANCER — readability, nudging gcc's regalloc toward the original's struct-based codegen, fixing structural misses, and typing function-pointer tables — not the sole gate. S3/GATE-B measures macro-only vs struct-typed yield.
S2 wiring fixes surfaced (must-do):
common.hlackss64/u64/f64→m2c_macros.h'stypedef s64 M2C_UNK64;fails (parse error before 'M2C_UNK64'). Add them tocommon.h(byte-neutral — verify locked builds stay143dbb89…/8e17e02f…).match_one/harvest_verifystrip scalar-typedef redefinitions, so these types MUST live incommon.h, not a draft preamble.- Make the byte-faithful m2c macros (
M2C_FIELD,M2C_BITWISE,M2C_UNK*) available to every compile (incommon.hor an includedm2c_compat.h) so drafts compile with no per-draft preamble. - Function-pointer-table calls (
*((idx*4)+D_x)(args)) needD_xtyped as a function-pointer array in the context, else won't compile. - m2c loses types through index/byte-offset arithmetic (
base + int_var, stride = struct size) → falls back tovoid*/M2C_FIELDeven WITH a struct context. TheM2C_FIELDmacro fallback keeps these byte-faithful-compilable; struct typing is best-effort. - m2c re-infers
char unk_*[]fields in provided structs (treats them as inferrable space; may split/override). PIN a known field with a concrete typed field; usechar unk_*[]only for genuinely-unknown gaps.
decomp-permuter knobs (S2/S3): PERM_* macros (GENERAL/VAR/RANDOMIZE/LINESWAP/INT/ONCE…), --algorithm difflib|levenshtein, --stop-on-zero, -j 8–16 (RAM-bound on the 15 GiB box → ~N funcs × -j 8, cap by free_RAM/~300 MB), weights in default_weights.toml + [gcc] section. Best when only regalloc/schedule remains; does NOT fix wrong control flow.
ML (parked — owner decision 2026-06-18): LLM decompilers (LLM4Decompile/SK2Decompile/CodeInverter) target x86-64 + recompilability/functional-equivalence/readability — NOT byte/instruction-exact, NOT MIPS/gcc-2.7.2; no off-the-shelf learned permuter scorer exists (the permuter's scorer is a heuristic objdump-diff). Dropped this phase; research-note only. (X2: web treated as untrusted data.)
§16 Guided hand-matching the struct-heavy core (Phase 17 — beats the §15 brute-force)
Phase 16 called the loose-typing wall "fundamental." Phase 17 disproves it for the majority. The wall is
a signature-CONSISTENCY problem, not a comprehension one — the §1 loop reconstructs correct bodies ~100% of
the time; the work is byte-closing + sig reconciliation. Full process: docs/hand-matching-process.md
(§1 loop, §2 idioms, §3a the 5-move signature-consistency playbook, §7 the Ultracode wave + canonical-sig wall).
New byte-idioms (Phase 17, §2 there):
- mask-local (defeats
lh→lhufold).*(s16*)f & (x & 0xFFFF)inline lets gcc fold the load tolhu- drop the
andi. Hoist the mask:s32 m = x & 0xFFFF; ... *(s16*)f & m→ gcc keepslh+ emitsandi.
- drop the
- shared-ret0 goto (cross-jump clustering + branch polarity). Two non-adjacent predicate tests the
original routes to ONE shared
return 0block → write both asgoto ret0;to a single trailingret0: return 0;. gcc then makesret0a labeled block reached by branches (right polarity) + schedules the next test's constant into the delay slot. A loneif(x)return 0;inlines (wrong polarity/reg). - v0↔v1 result/constant coalescing + the §10 hoist-vs-remat / phantom-frame quirks = the residual hard
tail — NOT a dead-end: §17 (CORRECTED) shows the call-crossing register-ORDER class is matchable with
register __asm__PINS + a scheduling barrier (byte-proven, func_8012B8E4), and array-decay cracks the hoist-vs-remat class. Hand-tier, but matchable. (Permuter can't help — it rejectsregister __asm__.)
Scaling = Ultracode wave (§12 pattern + §7): Ghidra pre-pass (DecompileFunctions.java, headless batch,
no /mcp) → parallel draft agents (m2c+Ghidra-C+asm+actor-struct+§3a, self-validate match_one) → whole-binary
gate (harvest_verify --chunk 1) → sig_unify recover → dedup_propagate --auto-from. Calibration (top-30):
60% match_one MATCH, 33% whole-binary (+0.47% fleet), 136/136.
THE CANONICAL-SIG LAYER (built Phase-17 session-4; NOT the ~2× lever it first looked like). The
match_one→whole-binary gap on the calibration's top-30 was SIG CONFLICTS (parallel agents declare shared
callees inconsistently → conflicting types in the one-big-TU; 100% compile-errors, 0 codegen). Fix = a
SURGICAL per-callee canonical-sig layer: tools/census_conflict_callees.py (the conflict predicate:
undeclared-stub callee with decl_sources = n_callers + is_target >= 2) + tools/derive_canonical_sigs.py
(byte-neutral s32 func_X(s32...), arity from Ghidra-C + asm read-before-write $a0-$a3) → a 20-extern
block at the TOP of ov_SC01_077.c (LOCAL, not engine_core.h — reach-1 names differ across overlays). (Both census_conflict_callees.py and derive_canonical_sigs.py were DELETED in Phase 26-A — R33; the fleet-canonical-sig approach was superseded by reconcile_tu's per-TU oracle. Historical record.)
gen_harvest_targets + sig_unify auto-read it; the gate pipeline is now draft → sig_unify (MANDATORY)
→ harvest_verify --chunk 1 (the accumulating baseline now carries the file-top block, so a raw draft's
guessed extern would clash without sig_unify). SIZING CORRECTION (R14): for the remaining 270, the
conflict wall is only 20 callees / 24 targets / 7% of wave reach — the "~2×" was the top-30's in-flight
conflicts, since resolved by banking. The real wall is the gcc-quirk tail, not sig conflicts — the 4
highest-reach circular targets are ALL §10-hoist / regalloc / layout-bound (0 closed by hand or permuter).
The layer makes a wave sig-clean; it does NOT unlock the quirk tail. → The match-% lever is understanding
gcc-2.7.2 (R17 compiler-source research, Phase 18), not more brute waves. Wave deferred; infra staged
(.run/harvest_wave_s4.js, 40 tractable reach-134 targets). See docs/hand-matching-process.md §8.
§17 (CORRECTED) answers it: the call-crossing register-ORDER class IS matchable — with register __asm__
pins + a scheduling barrier (byte-proven). The "brute waves won't help" point stands; HAND levers (pins) do.
§17 The compiler-quirk wall — the matching TOOLKIT (Phase 18; gcc-2.7.2 source + byte-gated)
Phase 18 read the real gcc-2.7.2 source (tools/reference/gcc-papermario, SETUP §5.6) + mined Xenogears (our
EXACT compiler). Verdict (CORRECTED — an earlier draft of this section wrongly called the register-order
class "unsteerable"; it is NOT): every quirk class met so far is matchable from C — the register-order tail
needs register __asm__ PINS, which I'd skipped. The wall was a missing lever, not an impossibility. Triage
each residual with match_one (the floor-free oracle — NEVER the permuter score on jtbl/rodata fns, §10), then
pick the tool. R14 caveat: don't conclude "unsteerable" until you've tried the PINS.
Register-allocation ORDER (call-crossing $s0/$s1 swap) → FORCE it with register pins (byte-proven)
Mechanism (why the swap happens): two pseudos live across a call → both are global allocnos (NOT
local-alloc) competing for callee-saved $s0/$s1; global.c:allocno_compare sorts by density
floor_log2(n_refs)*n_refs/live_length so the short-lived value wins $s0 and the whole-function value gets $s1
— often the reverse of the original. Clean-C reshapes / flags / cc1-swaps do NOT flip it (all tested).
The lever (DON'T skip this): pin each call-crossing value to the register the TARGET uses —
register s32 d __asm__("$16"); /* $s0 */ register s32 s1ang __asm__("$17"); /* $s1 */
gcc honors the pin and forces the allocation. Read the target .s, map each call-surviving value → its
callee-saved reg ($s0=$16, $s1=$17, $s2=$18 …), pin it. Then the residue is usually small: fix it with the
branch-polarity invert (§3-T4), explicit temps for any reassociation (t = u6+0x1000; iVar4 = u5-t;),
and a scheduling barrier for a last stuck instruction (__asm__ __volatile__("" : : "r"(u5)); emits zero
code, anchors u5 ahead of the next op). WORKED EXAMPLE — func_8012B8E4 (the flagship "unsteerable" fn):
21 → MATCH via pins + branch-polarity (24→21) + clamp temps (7→3) + the u5 barrier (3→MATCH); byte-gated +
propagated ×134. NOTE the permuter can't help here — pycparser rejects register __asm__/__asm__ (§5a), so
this residue is a HAND lever, not a permuter job. Labor: ~5-10 min/fn, but each circular fn is reach-134 → ×134.
(Xenogears ships this class as INCLUDE_ASM only because they hadn't found the pin lever — not because it's
impossible. We did.)
The STEERABLE idioms (byte-confirmed this phase)
- array-decay forces rematerialization (NEW). A stack buffer passed to a callee as
&struct/mtx.w/*(T*)arr(any address-taken form) is HOISTED into a callee-saved reg (needs an extra callee-saved → bigger frame, more spills). Declare it a local arrayT buf[N]and pass it asbuf(array-decay, never address-taken) → gcc rematerializesaddiu $reg,$sp,offper call instead (matches the original, frees the reg). func_8012B4B8: 88→52 (the hard regalloc+remat half fixed). CAVEAT: an array can't take a struct block-copy (arr = STRUCTneeds a struct; element-copy constant-folds each global addr to its ownlui, +ins), so a fn that ALSO needs a load-base-once struct-copy has an unavoidable tension. - for-loop vs do-while controls delay-slot scheduling. A counted scan as a
for(init/cond/update) lets gcc schedule the branch-taken return value into the loop test's delay slot; ado-whilewith increments in the body fills that slot with an increment instead (+1 ins, wrong schedule). func_801399A8: do-while 7 mismatch → for-loop 2 → MATCH. - statement order in the for-update = instruction order (§2-T2 extended). Independent updates in
for(...; ...; A, B)emit in source order; swap to match. (func_801399A8 final 2.) - (existing, reconfirmed) §3-T4 branch-polarity invert (func_8012B8E4 24→21), §16 mask-local, §16 shared-ret0 goto.
The pipeline gotcha — match_one ≠ the gate; sig_unify is MANDATORY
match_one masks relocations → it MATCHES even when the draft's own def-signature or a data-extern TYPE
conflicts with the canonical decl in engine_core.h (u8 *func(void) vs canonical s32 func(void);
extern u8 D_x vs extern s32 D_x). The whole-binary gate then fails conflicting types. ALWAYS retype the
draft to the canonical set (return + data-extern types; use integer address arithmetic (s32)&sym,
codegen-neutral): draft → sig_unify/canonical-retype → harvest_verify.
The LOOSE-TYPING wall is real for narrow params (Phase 16, reconfirmed)
Some STRUCTURAL_MISS fns are blocked by it: func_80146A6C needs an incoming arg as lhu (s16), but the shared
canonical sig declares it s32 (→ lw); the byte-match needs s16, another call site needs s32, no single C
type satisfies both. No clean fix (the documented narrow-param dead-end). Stub it. (Reconciles §16's
"Phase 17 disproves the loose-typing wall": disproven for pure-structure fns like func_801399A8; REAL for
narrow-param fns like func_80146A6C and inseparable from the regalloc tail.)
Strategic conclusion — the match-% lever post-research
The high-reach circular regalloc-order tail IS matchable — by register pins + barriers (above), labor-
intensive but ×134 per match. So the lever is BOTH: (a) the non-walled STRUCTURAL_MISS fns (clean
reconstruction + the structural idioms + mandatory sig_unify — proven on func_801399A8), the cheap bulk; and
(b) the circular tail hand-matched with pins (proven on func_8012B8E4), the high-value-per-fn work. Triage
each with match_one: pure structure → reconstruct; stack-buffer-to-callee → array-decay; call-crossing
register swap → PINS; last-instruction schedule → barrier. The ONLY genuine dead-end left is the narrow-param
loose-typing conflict (func_80146A6C: an arg that must be s16 here and s32 at another call site — no single C
type) → stub THAT and move on; everything else is matchable with enough hand effort.
§17a The TOOLKIT at WAVE scale (Phase-18 Step-3b/Step-1 — measured) + the pipeline-integration gotchas
The §17 toolkit was taught to a parallel Ultracode harvest wave (agent prompt = the triage above + the two worked-example templates). Measured on ov_SC01_077 tractable reach-134 residuals:
- Step-3b calibration (16 targets, prompt v1):
match_one12/16, whole-binary 9/16 = 56% close-rate (vs the Phase-17 prompt's 33%). The pins/array-decay moves landed real matches the old "stub the quirk" prompt would have lost. - Step-1 (31 targets, prompt v2 = +embedded canonical callee sigs +call-site-cast/re-validate):
match_one28/31 = 90%, whole-binary 22 verified. The big lift was embedding each callee's canonical signature per target (fromgen_harvest_targets.py) so agents declare callees right instead of guessing.
THE match_one→gate GAP is mostly DECLARATION plumbing, not codegen (so it's cheap to recover):
-
Call-site casts, NOT redeclaration (the #1 recurring miss).
match_onemasks jal/%hi/%lo, so a draft that declares a callee with the WRONG arity/return still "MATCHES" — thensig_unifyrewrites that extern to the SHARED canonical sig (fewer args /void), and the whole-binary gate fails (too many arguments/void value not ignored). FIX = keep the canonical extern, cast at the call site (codegen-neutral):((void(*)(s32,s32,s32))func_X)(a,b,c)for an over-arity call;x = ((s32(*)(s32,s32))func_X)(a,b)when avoid-canonical callee's$v0is used;(u16)/(s16)at the use site per the asmlhu/lh. Then RE-RUNmatch_oneon the canonical-typed draft — applying canonical sigs can change codegen, so gate the draft that still MATCHes WITH the canonical decls. (sig_unify can also, rarely, regress a match by forcing a canonical that's wrong for the byte-match — e.g.void/s32over a neededs32/u32def-sig; that's the narrow-param wall on the def itself → stub.) -
Stale sibling forward-decl (
M2C_UNK func_X();). A function you're matching is sometimes forward-declared by an already-matched SIBLING in the same overlay.c(m2c scaffolding:M2C_UNK func_X(); /* extern */), which conflicts with your real def. FIX = reconcile that one line to the real sig (void func_X(void);). (Proven: func_8012A418.) A standalonematch_oneMATCH that fails the whole-binary gate withconflicting types for func_X / previous declarationis this class — grep the overlay.cforfunc_X. -
Implicit-int caller plumbing→ CORRECTED (Phase 19 / T2, R14): two distinct real classes, NOT implicit-int, and propagate-first does NOT fix them. The 3 "deferred" fns (func_80147514, func_80168F40, func_8017209C) were reproduced through the gate to find the ACTUAL blocker (R14 — verify the framing against the bytes). Neither is implicit-int;dedup_propagate(def →engine_core.h) would NOT have resolved either (the def still lands at its address-order site, after any file-scope caller extern). The two classes:- (3a) Resident-callee LINK-miss (func_8017209C). The draft calls a resident EXE/engine function by
func_<ADDR>, but that address carries a curated name inconfig/symbols.us.txt(e.g.0x8004CFEC = ratan2). The linker resolves the curated name, notfunc_<ADDR>→ the draft compiles but fails to link (undefined reference to func_8004CFEC) → the gate (correctly) reverts. FIX =tools/canon_resident_calls.pyrewrites everyfunc_<ADDR>whose address has a curated// funcname to that name (extern + call), a pure draft-text transform (body bytes unchanged) → run it FIRST in the recovery pipeline. Proven: func_8017209C byte-identical afterfunc_8004CFEC→ratan2. - (3b) Shared-caller ARITY conflict (func_80147514 =
s32, func_80168F40 =void *). An already-banked shared caller macro inengine_core.h(e.g.DEFINE_func_80147478, instantiated at a lower address) declares the callee file-scope asextern void func_X(void);(void = no args) then callsfunc_X();— but func_X's real def takes an argument →conflicting types for func_X / previous declaration(NOT implicit-int; it's an explicit(void)proto). FIX = change that caller macro's extern to no-prototypeextern void func_X();(K&R): byte-neutral for the caller (the empty call is identical), and a no-proto decl is compatible with a def whose params are default-promotion-safe (int/s32/long/pointer — NOTchar/short/float; the narrow-param wall §3 of the toolkit still applies there). The macro lives in the shared header, so fleet-re-gate (make check-all). Proven: func_80147514 + func_80168F40, all 136/136 byte-identical, byte-neutral on every overlay (only ov_SC01_077 carries the def; the other 133 keep the stub, where a declaration-only change emits no code).
The recovery pipeline is therefore:
draft → canon_resident_calls → sig_unify → harvest_verify --chunk 1(canon_resident_calls first so link-miss names are fixed before any signature unification; sig_unify still mandatory for def/callee-sig canonicalization). The 3b no-proto move is a separate, one-time edit per caller/callee pair (it touches the shared header) — apply it when the gate reportsconflicting types … (void)from a shared-caller macro and the def's params are promotion-safe. - (3a) Resident-callee LINK-miss (func_8017209C). The draft calls a resident EXE/engine function by
Two NEW residual classes found at scale (beyond the §17 quirks):
- Per-file
-O0class. ~18 functions in ov_SC01_077 were built-O0(prologue sig21F0A003=addu $fp,$sp,$zero, args spilled to frame, load-delay nops, redundantaddu rd,rs,$zero). The correct C is byte-exact at -O0 but the overlay TU compiles-O2, and gcc-2.7.2 has no per-function optimize pragma (opt is per-file, Makefile). So these need their own-O0split file (thesrc/boot.cprecedent, per-fileCC1FLAGS := -O0). HIGH ROI: ~18 fns × reach-134. Members incl. func_8013C360, func_8013B568/B598/B6A0/B7AC/B7F4/B83C/BC7C/BCDC/BD34/BD74, func_8013C08C/C0F8/C360/C414/C938/C964, func_80144B9C, func_801457A4. (→ Phase 19 build-infra task.) - gcc-2.7.2 loop-guard (func_8012C2D0). [Phase-20 R14 CORRECTION — see §20] the real residual is gcc
STRENGTH-REDUCTION / IV-final-value ADDRESSING, NOT operand-order: the loop end is formed as
&D_80120194(the array base)+ 0x658C— the base materialized SEPARATELY then offset (an induction-variable final value), while the start is its own symbolD_801202A0. The right C is base-relative end +lhu/u16(gets structurally close), but gcc -O2 CONSTANT-FOLDSbase + Ninto one address (lui %hi; addiu %lo), so no clean C form (pointer-var, struct-array index) reproduces the separate base materialization. Still a genuine residual — stub; the (uncracked) lever direction is forcing the unfolded IV-final-value, not operand order. - (also: func_8014F2E0 4-off = §10 store-vs-load schedule placement, base-preservation-vs-load-order mutually exclusive — a real §10 residual.)
§18 Per-file -O0 split inside an overlay/blob (Phase 19 T1)
§18-P29 — the multi-stub cluster carve is splat-integration-fragile (defer, or use the whale's shape). P29 Arm A generalized the whale rollout (§38) to the 0x13410..0x14834
-O0cluster viatools/rollout_o0_cluster.py(3-way<ov>/<ov>_o0/<ov>_o2bcarve +O0_CLUSTER_OBJSMakefile wildcard). It banked 9/9 -O0 members on ov_SC07_010 (whole-binary, R22 140/140 — the Task-1 swing verdict is a BANKED FACT, not just masked-MATCH), but the SAME carve byte-shifts 006/007/011 (+0x20%lodata-symbol shift, 34% of bytes, from a CLEAN build; boundaries verified as real fn-starts). Root cause is splat re-disassembly: 3-way splitting a code subseg that still containsINCLUDE_ASMstubs makes spimdisasm resolve some%lorefs to a different auto-symbol — NOT the compiler, NOT a boundary bug. The whale carve (§38) never hits this because its_o0bsplit is a thin#include "../shared/<fn>.h"wrapper with no INCLUDE_ASM to re-disassemble. Lesson: a per-overlay-O0cluster rollout should route each member through a shared-per-member header (whale shape), not leave stubs in the split. Until then the fleet-O0harvest (~1,233 members / ~0.6pp) is DEFERRED;docs/decision-log.md(2026-07-16) has the full byte-evidence. This is the "-O0 cluster split infra" Phase 20 "built + reverted", now root-caused.
A cluster of functions compiled -O0 inside an otherwise--O2 binary needs its own -O0-compiled .c (the
src/boot.c precedent — gcc-2.7.2 has no per-function optimize pragma, opt is per-file via a target-specific
build/src/<path>.o: CC1FLAGS := -O0 …). Detect -O0 by the prologue 21F0A003 (addu $fp,$sp,$zero) + param
spill/reload + load-delay nops.
The mid-blob constraint (the non-obvious part). src/boot.c worked because boot is a PREFIX. When the -O0
cluster is in the MIDDLE of the address space, you CANNOT keep before+after in one .c: a single object's .text
is atomic, so the .ld references main.o(.text) twice but GNU ld consumes it on first match → the after-region
stays concatenated with before and lands at the wrong address (byte divergence appears EARLY, at the first
jal/%hi to a moved callee, not at the cluster). Fix = THREE distinct objects (before / _o0 / after), each
its own .c/subseg so each .text is independent and the .ld orders them by address. Minimize migration by
keeping the bulky side as the original name (asm paths unchanged) and moving the smaller side to a new subseg
(rewrite its INCLUDE_ASM paths with sed nonmatchings/<old>" → nonmatchings/<new>"). Keep the file-top header
(includes + canonical-sig externs) in BOTH halves. R22 clean-rebuild is a 100%-INCLUDE_ASM no-op gate before
adding bodies. NOTE: tools/split_src_region.py's naive item-parser GLOMS a file-top extern block onto a
high-address function name → mis-addresses the split; do the cut by explicit line/offset instead.
-O0 gating ≠ match_one. match_one compiles -O2, so it's WRONG for -O0 functions — gate them only via
the whole-binary build (which honors the Makefile -O0 override). harvest_verify works IF it builds via make;
direct substitute → make build → SHA + per-fn bisection is the reliable loop.
-O0 matched-idiom notes. Hoist data externs to the file top, ONE canonical type per symbol (parallel drafts
disagree: u8 vs s32 on the same D_* → conflicting types). Scalar global store (D_x = k) and
pointer-loops match cleanly.
%lo-folding indexed global — CRACKED (Phase 20, the array-of-STRUCT idiom). The residual: for indexed
global access the original FOLDS %lo into the store (lui %hi(sym); addu $at,idx; sw val,%lo(sym)($at)),
1 ins shorter than our cc1's MATERIALIZE (lui; addiu %lo; addu idx; sw 0($at)). The lever: declare the
global as extern Struct base[] where sizeof(Struct) == the array stride, and write base[index].field.
This keeps sym a symbol_ref through gcc's array-index addressing → it folds %lo(sym+field_off) into the
store, byte-matching. The forms that FAIL (and why §18 first called it irreducible): *(T*)(&sym + index*stride)
and *(T*)((char*)&sym + off) — &sym forces the symbol's full address to be MATERIALIZED as a value (lui+
addiu) before the index add, so %lo can't fold. Worked example (byte-gated, func_8013B7AC):
*(s32*)(&D_801DAA08 + a0*0x1C)=0 (materializes, FAILS) → typedef struct{s32 f0; u8 pad[0x18];} E; extern E D_801DAA08[]; D_801DAA08[a0].f0 = 0; (folds, MATCHES). Pick the struct so the accessed field's address == the
target symbol (put the field at offset 0 and base the array at the field's symbol, OR base at the real array
symbol and use the real field offset — same bytes either way; the %hi/%lo immediates encode the address).
The constant-multiply for the stride (e.g. ×0x1C → sll 3; subu; sll 2) is gcc's synth_mult, emitted even at
-O0. Gate via the WHOLE-BINARY -O0 build (match_one is -O2, wrong for -O0). NB: the -O0 cluster is
overlay-LOCAL (per the correction below) so this banks ×1 per overlay — but the idiom is reusable for ANY
-O0 (or -O2) indexed-global access, fleet-wide.
-O0 reach-134 propagation is NOT free. -O0 functions can't go through engine_core.h (it's included by
each overlay's -O2 main .c → would compile -O2 → not match). To bank the ×134, each overlay needs its OWN
-O0 split. Uniform across the fleet (all overlays share vram base 0x80128158, so the cluster offsets are
identical) → scriptable, but it's per-overlay infra + 134 gates, not the "free ×134" a dedup report implies.
Phase-20 R14 CORRECTION — the -O0 cluster is NOT reach-134 byte-identical; the rollout is INVALID.
Byte-proven: of the 134 overlays that span the cluster (vram 0x8013B568, file 0x13410), only 1 — ov_SC01_077
itself — has cluster bytes identical to the matched C (even func_8013B568's first 0x20 B match in just 1).
The cluster functions reference per-overlay data addresses (func_8013B568 stores to D_80187270/%lo 0x7270
in ov_SC01_077 but to 0x80182B04/%lo 0x2b04 in ov_SC01_005 — same instruction shape, different overlay-local
global), so each overlay's bytes differ → they are overlay-LOCAL code, not shared engine code. The
Phase-18/19 "6/16 matched (reach-134)" label conflated function-present-at-this-vram with byte-identical
(the precise R14 failure mode: trust the bytes, not the reach label). So the matched C banks ONLY ov_SC01_077
(×1); a fleet rollout via shared C is impossible. Banking these elsewhere = per-overlay RE-matching with each
overlay's own data addresses (extract %lo per overlay → template the C) — ~6 tiny fns × per-overlay, the
×1 per-overlay-unique bucket (low priority), ~0.2–0.3% for the whole-cluster, NOT the "+0.6% free ×134" the
backlog projected. The §18 split machinery (3-object before/_o0/after split + a build/src/%_o0.o: CC1FLAGS:=-O0
pattern rule + a PROVIDE-based .ld for the out-of-range data syms) was built and byte-validated to compile/
link a second overlay, then the gate exposed the per-overlay-data wall and it was reverted (the finding, not
the infra, is the deliverable). Net: skip the -O0 rollout; it's low-ROI per-overlay-unique work, not a shared win.
§19 Scaling the toolkit waves — the recovery PIPELINE + the propagation CAP (Phase 19 T3)
Two §17/§17a waves over the tractable reach-134 tail, measured. match_one close-rate is high and rising (batch-1 44/50 = 88%, batch-2 35/38 = 92% after the fixes below); the work is in the match_one→whole-binary gap (declaration plumbing) and then in propagation (not matching).
The gate PIPELINE order matters — canon-only FIRST, sig_unify FALLBACK. tools/sig_unify.py can REGRESS a
draft the agent already wrote canonically (it re-canonicalizes the def/callee sigs and occasionally forces a sig
that perturbs codegen, or mangles a line into a parse error). So gate in two stages (.run/t3_gate2.sh):
(1) canon_resident_calls → whole-binary gate ALL drafts; (2) sig_unify ONLY the stage-1 failures → gate again.
Measured: unconditional sig_unify lost ~5/batch vs canon-first.
The dominant gate failure at scale = the shared-caller ARITY class → tools/fix_arity_callers.py (automated
§17a-3b). Once garbled hints and sig_unify-regressions are removed, ~100% of the residual gate failures are:
a banked SHARED caller in engine_core.h declares the callee extern <ret> func_X(void); (or a different
arity), conflicting with the real def that takes args. FIX = rewrite the caller decl to no-prototype
extern <ret> func_X(); — byte-neutral, compatible with promotion-safe params (int/s32/u32/long/ptr; NOT
char/short/float — narrow-param wall). fix_arity_callers.py --apply --from-file <fails> --drafts <dir> does it
(skips narrow-param defs), then re-gate; --revert to undo. Batch-2: recovered 8/18 this way; the rest were
genuine loose-typing conflicts (caller sig ≠ def sig with call-site casts already in play — Phase-16 wall).
Garbled callee hints → fixed at the source. gen_harvest_targets.py INLINE_DEF_RE was matching an
indented if (func_X(...) == ...) { call-expression as a "definition" → a garbled callee "signature" agents
paste verbatim as an extern → PARSE error at the gate. Fix: column-0 + type-only prefix (^[A-Za-z_][\w \t*]*?).
0 garbled of 749 after; batch-2 close-rate 88%→92%.
THE CAP: propagation, not matching. dedup_propagate only lifts a body whose types resolve from
common.h + engine_types.h (its compiles_standalone filter) — so a matched function whose body uses a
typedef'd / anonymous / sibling local type stays ov_SC01_077-LOCAL (no ×134, no fleet %). Batch-1 propagated
25/30, batch-2 only 10/25 (struct-heavier). tools/build_engine_types.py --strip lifts NAMED structs (0
same-name-different-layout collisions historically) but NOT typedefs → ~16 banked matches still can't propagate.
Lever (Phase 20): extend the type-lift to typedefs/local types → recovers those ×134 for ~0 agent tokens AND
raises every future batch's realized yield. The harvest's bottleneck has moved from "can we match it" to "can we
share it."
§20 The wave-at-scale GATE CAP + residual-class verdicts (Phase 20)
Phase 20 closed the §19 propagation cap (T1 below) then ran the §17/§19 wave + recovery over the reach-134 ≤90-ins tail — and hit a HARDER gate cap than Phase 19's. All byte-verified.
THE CAP: at this tail the match_one→gate gap is the LOOSE-TYPING CALL-GRAPH wall (not cheap plumbing)
Batch numbers: 41/48 match_one MATCH (85%), but only 8/41 survived the whole-binary gate (~80% gap — vs
Phase 19's ~30%). The gap is NOT the §17a/§19 cheap declaration-plumbing (link-miss/arity recovered ~0 here).
Byte-verified cause: a callee (e.g. func_80153C74) is declared with CONFLICTING types at different overlay
sites, so the draft's extern hits in-TU conflicting types — a COMPILE error, not a byte miss. match_one
OVER-PREDICTS because it compiles STANDALONE with the draft's own externs and masks jal/%hi/%lo — it never
sees the overlay's conflicting decls. Every recovery lever fails at this tail (all 0): sig_unify /
canon_draft_decls impose a single "canonical" that's wrong for some sites (loose-typing); fix_arity_callers
(not arity); no-proto externs (incompatible with the overlay's NARROW existing decls — void f() can't
co-exist with void f(s16)); strip-externs (implicit-int ≠ target). The ONLY fix is the §17a-1 per-site
function-pointer cast ((ret(*)(args))func_X)(…) — it takes the callee's ADDRESS and calls with the draft's
intended sig, so there's no global decl and no conflict. Agents don't apply it reliably → AUTOMATE it: a
recovery pass that, per draft, DROPS the conflicting callee extern + CASTS the call to the draft's sig, then
gates. That is the Phase-21 cap lever (recovers a batch's lost ~33 reach-134 AND lifts every wave's gate-pass
from ~20% toward ~80%+). The bottleneck has moved again: matching ✓ (85%) → sharing ✓ (§19 type-lift) → now
in-TU declaration reconciliation under loose typing.
Diagnostic lesson: a failed in-TU build leaves a STALE .o
When you substitute a draft and the in-TU build FAILS to compile, build/src/<…>.o retains the PREVIOUS
(target/stub) bytes → objdump of that .o shows a FALSE "byte-match." Always trust the whole-binary SHA
gate, not a per-function objdump (cost a real detour: func_80153C44's stale .o looked identical while the
true failure was a conflicting types compile error).
NEW / CONFIRMED residual classes (this session)
- §10 store-vs-load scheduling (func_8014F2E0, func_80150528) — CONFIRMED unsteerable. Wave agents tried
for-init / barrier / precompute / volatile;
sched.ctie-break: the IV-init lands before the loop guard not in the preheader, and theD_x=0store schedules BETWEEN two arg-loads instead of after both — mutually exclusive with base-preservation. Stub. - §10 hoist-vs-remat regalloc tie-break (func_80149374, func_801493D0) — CONFIRMED. gcc caches a
sp+offbuffer address in a freed callee-saved reg + moves (cheaper by its count) where the target REMATERIALIZESaddiu $a,$sp,offper call. Array-decay / pins / barriers / permuter all fail. Stub. - IV-combine divergence (func_80177AD4) — NEW. Our cc1's
combine_givswon't fold a halfword RMW (lhu;sh -2(p)) into the byte biv (sb 0(p)) the way the target does (one IV atp+0x20); it spawns a dedicated 2nd IV → wrong base constant (p+0x1e). Probe-confirmed (a non-RMW*p/*(p-2)pair combines fine; the RMW spawns the 2nd IV). Genuine codegen divergence, not source-typeable. Stub. - Hoisted-invariant PROLOGUE ORDER (func_80177F84). 3 prologue insns in the wrong order — gcc emits the
pinned-
$a2pointer init before the two hoisted loop-invariant constants; the target emits the constants first. No prologue permutation is < 3-off; the permuter can't run (theregister __asm__pins are rejected by pycparser). §3/§5-class — stub. - The -O1 class (func_80161A90) — NEW build-infra (extends §18). A function built
-O1(frame 0x18,lhureload, unfolded base, load-delay nops) inside an otherwise--O2overlay.match_one(hardcoded-O2) CANNOT match it. Like the §18-O0class but-O1→ needs its own-O1split file (target-specificCC1FLAGS := -O1). Detect: prologue/scheduling between-O0(21F0A003) and-O2.
Operational gotchas (cost real time)
- Workflow
args: pass the target list as a JSON ARRAY, not a JSON string — a stringified array reaches the script as one string andargs.mapthrows (names.map is not a function). Defensive:const names = Array.isArray(args) ? args : JSON.parse(args). harvest_verify --chunk 1for wave batches. With--chunk >1, ONE draft that fails to COMPILE (a loose-typing conflict) fails the whole chunk's build and the bisection mis-attributes the innocent neighbors as failures. Gate wave drafts one at a time when the failure mode is compile-conflicts.
What WORKED — the Phase-20 reusable wins
- T1 — the typedef type-lift (
tools/build_engine_types.pyextended). Addedfind_typedefs()(brace-aware: anon-structtypedef struct{…}N;, fn-ptrtypedef r(*N)();, alias) + same-name-different-layout collision + tagged-struct-typedef overlap guards; emits typedefs in source order AFTER the named structs (deps likeA→Spreserved). Closed the §19 type-blocked propagation cap — 9 reach-134 fns ×134 for ~0 agent tokens, byte-neutral (--stripremoves the defs; type decls emit no code). - T2 — the residual router (
tools/exemplar_miner.py). Consumeswall_taxonomy.json+ per-overlay reach (dedup_propagate's computation) → routes every residual to a lever (WAVE / STRUCT / PINS / STUB) →docs/exemplar_curriculum.md+ the reach-134 wave-target list +.run/exemplar_routing.json. The "scan all residuals, pick the teachers / size the pools" router. Caveat: itsmismatchis the M2C-DRAFT mismatch, NOT the hand-match floor (a loop-guard buckets STRUCTURAL_MISS at mismatch-16 yet hand-floors to 1).
Phase-20 RESOLUTION — tools/cast_call_sites.py BUILT + the cap re-diagnosed (R14, byte-proven)
The §17a-1 per-site cast is now AUTOMATED. tools/cast_call_sites.py (pure --in/--out transform, sibling
of sig_unify/canon_resident_calls): per draft, for every callee whose canonical TU sig differs from the
draft's intended sig (exactly the conflict set), it (1) rewrites the callee's decl line → the canonical
declaration (kills the in-TU conflicting types, keeps the symbol in scope) and (2) casts every call site to
the draft's INTENDED sig ((ret(*)(args))func_X)(args) (decl lines never cast). The cast is codegen-neutral —
confirmed: gcc-2.7.2 folds (cast)func_symbol back to a direct jal func_X with the draft's calling
convention. Pipeline: canon_resident_calls → cast_call_sites → sig_unify (def-sig) → harvest_verify --chunk 1.
RESULT on T6 batch-1's 33 gate-fails: the callee-cast recovered 6 byte-identical (func_80153C44/ 8015CF58/801711FC/80161BE0/801683D8/8015F948); 5 propagated ×134 + 1 local; fleet 58.63%→58.82%, 136/136.
THE §20 "~33" PROJECTION WAS WRONG (R14 — the earlier diagnosis was incomplete). Byte-classifying all 33
gate-fails: the batch is NOT dominated by the callee-conflict class. It splits into THREE directions, ALL
declaration-driven (0 pure codegen-quirk survives match_one):
- Callee-conflict (~6) — the draft CALLS a shared callee declared inconsistently →
cast_call_sites✓. - DEF-conflict (dominant, ~18) — the GENUINE loose-typing wall, byte-proven unrecoverable by text transform.
A banked caller (an
engine_core.hDEFINE macro, invoked in this overlay) declares the draft's OWN function with a sig the matchable def can't satisfy (e.g. func_80161208: callerss32 f(void)0-arg vs defvoid f(void*)1-arg; func_80146A6C: callerss32 f(s32,void*,…)vs defvoid f(short,int,…)). Both ways fail: keep the draft's def-sig →conflicting types; canonicalize the def-sig (sig_unify) → 11/26 compile clean but BYTE-MISMATCH (the body genuinely needs the draft's sig). The symmetric "caller-side cast" fix is BLOCKED:INCLUDE_ASMemits only an__asm__(".include …")block — it declares NO C symbol — so a shared macro's internalextern func_Xis the ONLY declaration of func_X in the 133 STUB overlays; dropping it (to cast the caller) breaks them, and it can't be edited per-overlay (it's in the shared header). The only path for these is RE-DRAFTING the body under the caller-canonical sig (a future wave with the canonical pinned), and the arity-mismatch subset (0-arg callers vs N-arg def) has NO compatible C sig at all → hard stub. - DATA-conflict — byte-proven MOOT on this tail (R14, do not build the data-cast for it). A data-analog
cast (
*(T*)&D_xagainst the canonical decl) sounded like the naturalcast_call_sitesextension, but the bytes say there's nothing to cast: 0 drafts declare anextern struct/uniondata conflict, and the lone apparent "DATA-conflict" (func_8016A8FC /D_800AE620) was a typedef-REDEFINITION — the draft re-definestypedef struct{s32 w[8];} Blk20;inline whileBlk20is already inengine_types.h(both sides useextern Blk20 D_800AE620— SAME type, no data conflict). Stripping the redundant inline typedef compiles but STILL byte-mismatches (the underlying blocker is the def-sig loose-typing wall, class 2). So the data-cast would be code for 0 real cases; skip it. (A general "strip inline named-type defs already in engine_types.h" pass is a 1-line harvest_verify-style cleanup if a future wave needs it — but it recovered 0 here.)
Net: the cast tool is the real, reusable cap-lever for the callee-conflict fraction of every wave (it
recovers what §17a-1 casting can, byte-gated) — but it does NOT lift gate-pass to ~80% here, because THIS tail
is dominated by the def-side loose-typing wall, not callee-conflicts. The honest bottleneck for the reach-134
residual: a matched body whose required signature is incompatible with the established caller-canonical, with no
caller-side escape (INCLUDE_ASM declares nothing). Diagnostic discipline that found this: classify EVERY
gate-fail by build-error class (callee conflicting types / DEF conflicting types for the draft's own fn /
DATA / clean-build BYTE-MISMATCH) before assuming a single cause — the §20 first pass saw one callee example and
generalized it; the bytes said otherwise.
T3a %lo-folding -O0 — CRACKED (Phase 20). Not irreducible after all: the array-of-STRUCT idiom folds
%lo (declare extern Struct base[], sizeof == stride, access base[i].field; NOT *(T*)(&sym+i*stride),
which materializes). Full write-up + worked example in §18 ("%lo-folding indexed global — CRACKED").
Banks ×1 (the cluster is overlay-local) but the idiom is reusable fleet-wide for any indexed-global access.
§21 — wave-distilled idioms (Phase 21)
Byte-gated wins from the class-grouped waves. Each is a generalizable C shape (not a one-off), tied to its byte-matched evidence fn. (Pins/array-decay/statement-order/shared-ret0/for-vs-do-while are §17–§18 — not re-listed.)
- mem→mem unaligned N-byte copy →
memcpy(dst, src, N): when the target copies a contiguous byte region withlwl/lwr+swl/swrpairs (unaligned 8-byte block, no field math), writememcpy((void*)dst,(void*)src,8)withextern void *memcpy(void*,const void*,u32);— gcc-2.7.2 inlines the small fixed-size copy to exactly that lwl/lwr/swl/swr sequence (element-wise*(T*)dst=*(T*)srcinstead pickslw/swfor the aligned case or splits wrong for unaligned). fixes inlined-unaligned-block-copy codegen; evidence func_80153800 (sibling func_80146FC4 same shape). - unaligned-SOURCE word load (then byte-read it) →
__attribute__((packed,aligned(1)))struct in a union, plain assignment: when the source is at an UNALIGNED address (e.g. an odd global) so the target reads itlwl/lwrinto an aligned stack slot (swl/swr) and then reads individual bytes off$sp(lbu 0/1/2($sp)), the memcpy form above copies it but gives no typed handle to the bytes; declaring the source a plainint/structMISSES (gcc assumes alignment →lw). Mark the source typealigned(1)and wrap it with a byte view in a union, then ASSIGN it to a stack union and index the bytes:struct W{int w;}__attribute__((packed,aligned(1))); union U{struct W w; u8 b[4];}; extern struct W G; union U t; t.w = G; … t.b[0] … t.b[1] …. Thepacked,aligned(1)makes gcc emitlwl/lwrfor the bare struct assignment (the unaligned read), theswl/swrlands it on the aligned stack union, andt.b[i]becomes the$sp-directlbu. fixes alw-vs-lwl/lwrunaligned-source load where you also need per-byte access; evidence func_80142A80 (D_801BD644readlwl 3 / lwr 0, byte-readlbu 0/1/2($sp)). - aligned 16-byte field-block copy → one struct assignment: a contiguous aligned 4-word copy between two memory
locations is
typedef struct{u32 a,b,c,d;} Blk16; *(Blk16*)(dst)=*(Blk16*)(src);→ gcc emits its a0–a3 4-register block load/store (1 ins shorter than four separatelw/sw, and fixes the v1/v0 load order + the load-delay nop). Four element-wise word copies constant-fold each base to its ownlui(+ins, wrong order). fixes word-by-word vs block-move + load-delay; evidence func_80163A94 (0x34..0x50 = two Blk16 assigns). - partially-read out-param region → ONE stack struct, not separate scalars: when a callee fills several fields of
a stack buffer via
&buf/&buf.fieldbut the caller reads only SOME of them, declaring the slots as separate locals lets gcc DCE the unread ones and overlap/shrink the frame (wrong frame size). Make the whole region ONEstruct buf;and pass(s32)&buf/(s32)&buf.field— the struct keeps every slot live at its true frame offset. fixes DCE-driven frame-size/overlap divergence; evidence func_801749C8. - (refinement of §16 mask-local — the inverse direction) raw
lhu+(s16)at each use: to force the target'slhu;sll;srainstead of a foldedlh, load the global as a RAWu16(iVar=(u16)D_x;) and apply(s16)at every USE site — this defeats gcc'slhu+sext→lhcombine fold (the opposite goal to §16's mask-hoist, which keepslh+andi). evidence func_801749C8. - (refinement of the §5a/§17 zero-byte barrier) in-place re-tie variant:
__asm__ __volatile__("":"=r"(x):"0"(x))(output tied to input via constraint"0") forces x to be re-materialized into a register at that point, pinning where a following store schedules — distinct from the input-only anchor__asm__ __volatile__("":: "r"(x))(which only anchors x ahead of the next op). Use when interleaved stores need a value freshly re-tied mid-sequence. evidence func_80165CA0 (SHB(x)macro, combined with$v0/$v1pins). - disjoint-bits
x + CONSTemitsori, notaddiu→ break it with a re-tie barrier: when x's low bits are provably zero where CONST has bits (e.g.x = (v & 0x7F00) >> 5;thenx + 0xC00— masked-then-shifted value can't overlap 0xC00), gcc-2.7.2 proves the add is disjoint and folds it toori x,x,CONST; but the target usedaddiu. Insert the §21 re-tie barrier__asm__ __volatile__("":"=r"(x):"0"(x));BETWEEN the mask/shift and the+ CONST— re-materializing x there erases the known-zero-bits range, so gcc can no longer prove disjointness and emitsaddiu x,x,CONST. (Same barrier syntax as the bullet above, but the effect here is ARITHMETIC OPCODE SELECTION, not store scheduling.) fixes theadd→oridisjoint-bit fold; evidence func_80169058 (the+0xC00no-bit-overlap add). - steer WHICH giv becomes the loop IV anchor → make that store LAST in source order: when a loop writes several
fields off one moving record pointer (
*(T*)(p+k)=…for several k, thenp += stride), gcc-2.7.2 combines those givs into ONE induction reg, andloop.c:record_givPREPENDS each new giv tobl->givsocombine_givspicks the LAST-recorded (= last in PROGRAM ORDER) store asgiv_array[0], the anchor base. The target picks a particular offset as anchor (visible in the.sas the IV reg =addiu $iv,$base,Kand every field store reaching off it with displacements of one sign, e.g. all<=0). To match, write the field whose offset is that anchor (K) as the LAST store before the pointer bump; the others (earlier in source) then reach it with the matching-sign displacements. (This is the RECOVERABLE counterpart to §20's IV-combine failure class — there combine refuses to fold a halfword RMW and you stub; here combine DOES fold and you steer the anchor by store order.) fixes wrong IV base-constant / all-positive-vs-all-negative displacement set; evidence func_80178298 (anchor at p+0x12, all stores<=0off it; the +0x12 store moved last took it 15-mismatch→3). - register-resident
shorttruthiness test →sll rX,16+ branch (NOT a barebnez): when a flag/counter the target keeps in a register (never spilled) is tested for!= 0viasll $v0,$reg,16; beqz/bnez $v0instead of branching on the value directly, the original local was ashort/s16— gcc-2.7.2 lowers ashortrvalue's truthiness by left-shifting 16 (dropping the upper half) then branching. Reproduce it on anintlocal by writing the test explicitly asif ((flag << 0x10) != 0)(the shift is computed without storing back, so the counter stays live in its reg across the increments); equivalently declare the locals16. A plainif (flag != 0)on anintemitsbnez $regwith noslland the diff won't close. fixes the missingsll _,16before a truthiness branch; evidence func_8013F244 ((iVar2 << 0x10) != 0, the twosll $v0,$v1,16merge sites). - force a global RE-LOAD across a store WITHOUT pinning the frame/prologue → NON-volatile
__asm__("":::"memory")(NOT volatile): when the target reads a global N times with intervening pointer-stores (each section gets its ownlhu sym/reload), gcc-2.7.2 -O2 instead CSEs the derived index (e.g.idx*4survives in a reg across the store, so no reload). A memory clobber forces the reload — but the volatile form (__asm__ __volatile__("":::"memory")) is a hard scheduling fence: it (a) pins the prologue stack-adjust (addiu sp,-N) to the top of the function (cc1 emitssubu spbefore the asm because the clobber may touch the stack), so a target whose frame alloc is scheduled mid-function (into a load-delay slot) can't match, and (b) co-schedules large-constant builds (li reg,HI; ori reg,LO) as an adjacent unit. Dropping__volatile__keeps the memory clobber (→ the reload still fires) but makes it a soft dependency the scheduler reorders around: thesubu spthen floats down into the load-delay slot, AND cc1 splits the constant build around an independent address materialization (li t0; la a2; ori t0instead ofli t0; ori t0; la a2). So: use the non-volatile memory clobber when you need the reload but the target has a scheduler-mobile frame and/or a split constant build; use the volatile form only when you also want the hard fence. (The §5a cross-jump barrier still needs volatile — that's a correctness fence, different goal.) fixes simultaneous {global-reload, mid-function phantom-frame placement, split %hi/%lo-constant prologue order}; evidence func_801758FC (3 sections each reloadlhu D_800B9A02; phantom 0x10 frame alloc lands in the F7D0 pointer-load's delay slot; closed 4-/3-mismatch tensions → MATCH 55 ins). - induce a phantom (unused, no-spill) stack frame the target has but your codegen omits → an address-taken local
array
s32 frame_pad[N]; (void)&frame_pad;: when the target reservesaddiu sp,-0x10/+0x10with NO register saves and NO spills (a leaf whose original had a stack local the optimizer later kept in regs), gcc'scompute_frame_sizeroundsget_frame_size()up to the next 8 → declare a local whose address is taken (so it's not DCE'd and reservesvar_size) but never stored through.(void)&frame_pad;escapes the address with zero emitted code at -O2. Size N×4 picks the frame: 1–2 words → 8, 3–4 → 0x10. (This is the INDUCE direction; §5's phantom-frame note is the REMOVE direction where gcc adds a frame the target lacks.) evidence func_801758FC (0x10). - store a high-bit (≥0x8000) 16-bit constant to a halfword →
unsigned short*/u16*, NOTshort*/s16*: the STORE issheither way, but the constant materialization differs by signedness of the pointee. Au16store zero-extends →ori reg,$zero,K(opcode0x34, e.g.3403c040=ori v1,$zero,0xc040); ans16store sign-extends →addiu reg,$zero,K(opcode0x24, e.g.2403c040=addiu v1,$zero,-16320, value 0xFFFFC040). objdump pretty-prints BOTH asli v1,0xc040/li v1,-16320, so read the opcode (34xx=ori vs24xx=addiu), not the mnemonic. Only matters when bit 15 of the constant is set (0x8000–0xFFFF: 0xc040, 0xaa10, …); for K<0x8000 both forms emit the sameori. Picku16*to getori,s16*to get the sign-extendingaddiu. fixes theaddiu(sign-ext)↔ori(zero-ext) constant-build before a halfword store; evidence func_8017E924 + func_8017E974 (0xc040), func_80182E30 (0xaa10). $sp-manipulating scratchpad-stack-switch trampoline → a FULL__asm__ __volatile__body (clobber"memory"), copied from the byte-proven sibling with ONLY thejaltarget + theD_801D961xsymbol swapped — plus three maspsx rules that are byte-load-bearing. This 22-ins idiom (repoint$spinto the D-cache scratchpad stack at*(0x1F8003FC), call one engine fn, stash$v0throughD_801D961x, restore$sp) is NOT expressible in C (it rewrites$sparound a call), so write the whole body as inline asm with.set noreorder. It is a 15-member reach-134 duplicate family (canonicalfunc_8014CCB4), so each match is high-value. The three rules the wave proved (each a +1-ins byte-miss if violated): (1) do NOT write an explicitnopafterjal— maspsx--aspsx-version=2.56auto-fills the delay slot; putlui $at,%%hi(D_801D961x)directly after thejaland its auto-nop becomes the slot (→jal/nop/luiexactly); (2) no trailing.set reorder— it emits a stray epilogue nop; (3) escape%hi/%loas%%hi/%%loinside the__asm__string (a bare%is read as an operand placeholder → assembler error). (General beyond this trampoline: rules 1–3 apply to ANY hand-written inline- asm body carrying ajal+%hi/%lorelocations.) fixes double-delay-nop / stray-epilogue-nop /%-placeholder byte-misses in a hand-asm$sp-switch wrapper; evidence func_8014D04C, func_8014D738, func_8014DF3C, func_8014E434, func_8014E6A0, func_8014E934, func_8014ED28, func_8014F1F4, func_8014F468, func_8014F6F4, func_8014FCFC, func_80150480 (12 banked this wave, family of func_8014CCB4).- byte/
u8in-place pre-decrement that tests the OLD value → write the subtract as+ 0xFF, NOT- 1: for au8field decremented in place where the original tests the pre-decrement value (c = p[i]; p[i] = c-1; if (c==0)…, emittinglbu;addiu $v0,$v1,0xFF;bnez $v1;sb $v0), gcc-2.7.2 does NOT canonicalizec + 0xFFandc - 1to the same immediate even though they are equal mod 256 and thesbtruncates either way — it materializes whatever signed-representable literal you wrote. Empirically (this toolchain):c + 0xFF→addiu …,0xFF(2462…00FF, matches),c - 1(andp[i]-1) →addiu …,-1(2462…FFFF, MISSES); the distinction holds even on a plainint. So when the target's decrement immediate is0xFF(or any positive wrap-literal) rather than-1, write the wrapping formp[i] = c + 0xFF;explicitly. fixes theaddiu …,-1↔addiu …,0xFFimmediate-literal mismatch on a byte predecrement; evidence func_8016EBA8 (param_1[2] = c + 0xFFwith pre-decrementif (c==0)). - store a call result AND test/reuse it in one expression → combined assignment
*(T*)(p+k) = local = f();(NOT a store of a re-read local): when the target doesjal f; sw $v0,k(base)(the store in the call's delay slot, or immediately after) then BRANCHES on$v0(beqz/bnez $v0) and/or copies it to a callee-saved reg (addu $sX,$v0,$zero) for use in the success arm, write the store, the test, and the reuse off ONE local that is assigned the call result in the SAME statement as the store:*(int*)(p+0x20) = v0 = f(); if (v0==0){…} else {…use v0…}. gcc-2.7.2 then keeps the result in$v0so thesw $v0(delay slot) and thebeqz $v0both read it directly, and emits theaddu $sX,$v0copy ONLY because the success arm reusesv0across later calls. The naive two-statement form (v0 = f(); *(int*)(p+0x20) = v0;) instead emits the copy-to-$sXBEFORE the store and loses the baresw $v0/delay-slot schedule. (General: any "store an allocator/constructor result to a struct field, null-check it, then use it" pattern.) fixes the call-result store/test landing in the wrong register + lost delay-slot store; evidence func_80142DC4 (*(int*)(param_1+0x20)=v0=func_8012C1B8(); if(v0==0)… else …v0…). - force a memory-operand RELOAD (two identical
lws) with NO barrier → place the intervening store between the two reads in SOURCE ORDER: when the target reads the same field twice (lw $v0,K(base)… thenlw $v0,K(base)again) with a store to a different field in between — i.e. gcc-2.7.2 -O2 did NOT keep*(p+K)cached across that store — reproduce the reload purely by statement order: write the dependent store as a statement that uses*(p+K)and place it BEFORE the second use, so an unrelated write (*(p+J)=…) sits between the two reads and defeats the load-CSE. No__asm__clobber and no pin needed (this is the barrier-free counterpart of the §21 memory-clobber reload bullet, which is for a derived-index CSE; here the CSE'd thing is the memory load itself). Hoisting the field into one local (int t = *(p+K); …; use t;) instead keeps it in a reg → a singlelw, misses. fixes a missing 2ndlw <field>reload across an unrelated field store; evidence func_80168430 (*(p+0x30)=D[*(p+0x2C)];placed beforeiVar1=(*(p+0x2C)<<17)>>16;→ thesw 0x30between the twolw 0x2Cforces the reload, 33/33 ins). - EXPLOIT cross-jumping (the inverse of the §5a barrier) — duplicate the SAME call into BOTH if/else arms to merge
the
jalwhile keeping per-arm operand setup: when anif/elseends by calling ONE function with the same argument-register layout but DIFFERENT constant/value args per arm, and the target emits ONE sharedjalsite reached by ajfrom one arm + fall-through from the other (each arm doing its ownaddu $a0,$base+addiu $a1,Kbeforehand, with anopjoin delay slot), write the call LITERALLY IN BOTH ARMS:if(c){ g(p,5); h(p,0x1C); } else { g(p,4); h(p,0x11); }. gcc-2.7.2's cross-jump pass (§5a) finds the twojal hinsns RTL-identical → merges them into one shared site, but the per-arm arg setup differs → stays duplicated → exactly the[arm1:setup; j join][arm2:setup; fall][join: jal; nop]layout. The naive single-call form (compute K in theif, thenh(p,K)once after the join) instead schedules the arg setup differently and loses the dual-setup/j/nopshape. (This is the deliberate-MERGE direction; §5a's__asm__ __volatile__("")barrier is the deliberate-KEEP-SEPARATE direction — same pass, opposite goal. Combine with the §3-T4 branch-polarity invert to pick which arm is thebeqzfall-through.) fixes a duplicated-vs-sharedjal+ missing per-arm arg setup / joinnop; evidence func_80159BE4 (func_80154A74(arg0,0x1C)in the if-arm +func_80154A74(arg0,0x11)in the else-arm → one mergedjalat the join, 40 ins). - read-modify-write of a SCALAR global where the target materializes the address ONCE and reuses it for both the
lwand thesw→ access it through a pointer VARIABLE (T *p = &D_x; *p += 1; if (*p >= K)), NOT the bare global (D_x += 1): when the target doeslui $r,%hi(sym); addiu $r,$r,%lo(sym); lw $v,0($r); … sw $v,0($r)(one address reg,0($r)displacement on BOTH accesses), gcc-2.7.2 -O2 reaches that by CSE-ing the&D_xaddress expression across the read and the write. Writing the bare global RMW (D_x += 1) instead lets gcc fold%lointo each access INDEPENDENTLY → two separatelui … ; lw/sw %lo(sym)($at)materializations (the §18 single-store fold, applied twice; wrong reg/ins for an RMW). A pointer var to the global forces the shared base reg. (This is the GLOBAL-RMW counterpart of §18's indexed-global fold and §17's stack-buffer remat — distinct trigger: a scalar global read AND written in the same region.) NB unrelated to the same fn: read a+0x34halfword field withlhu(u16), notlh, when the target zero-extends. fixes two-separate-%lo-folds vs one-materialized-base-reg on a scalar global RMW; evidence func_80186938 (s32 *p=&D_801270C8; *p+=1; if(*p>=4)…→lui;addiu %lo;lw 0($v1);…;sw 0($v1), byte-gated). - force a RELOAD between two CONSECUTIVE stores to the SAME global (store a const, then read it back to OR/RMW) →
qualify the global
volatile, NOT an__asm__barrier: when the target doessh K,%lo(sym); lhu %lo(sym); ori …; sh %lo(sym)— i.e. it stores a known constant to a global and then re-reads it from memory before the next store — gcc-2.7.2 -O2 normally store-to-load-FORWARDS the just-stored constant and CONSTANT-FOLDS the two writes into ONE (D_x = K | 0x4000;→ a singleshwithli K|0x4000), so the reloadlhuand the secondshvanish. Declaring the globalvolatile(extern volatile u16 D_x;) forbids the forward/fold: eachD_x = …is emitted verbatim and the read between them becomes thelhureload, reproducing both stores. This is the TYPE-QUALIFIER reload lever — distinct from the §21__asm__memory-clobber (derived-index CSE across pointer stores) and the statement-order reload (two identical loads across an unrelated field store): here the value is a constant gcc KNOWS, so onlyvolatiledefeats the store-forwarding; statement order and clobbers do not. Use it whenever the target re-reads a global it just stored a literal into. fixes a folded-awaylhureload + merged doubleshto one global; evidence func_801806D8 (volatile u16 D_80126B96; D_x=2; D_x=D_x|0x4000;→sh 2; lhu; ori 0x4000; sh, byte-gated 56 ins). - force ONE specific field/pointer load to RELOAD (target re-emits
lw K(base)where gcc -O2 CSEs it across intervening calls/non-field stores) → cast THAT access*(volatile T*), NOT a global__asm__fence: when the target reads a struct field through a pointer twice (lw 0x64($s0)… other code (calls, stores to unrelated addresses) …lw 0x64($s0)again) but gcc -O2 keeps the first load cached in a register (a singlelw, the field reused via regalloc — so neither statement-order nor a constant store-forward defeats it), wrap each reload site in a volatile pointer cast:iVar = *(volatile int *)(param_1 + 0x32);(and reuse it for the follow-on field reads offiVar). Thevolatilemakes that ONE access a non-CSE-able memory reference, so gcc re-emitslw 0x64($s0)there, while leaving all other scheduling untouched. This is the SURGICAL reload lever — distinct from the §21 non-volatile__asm__("":::"memory")clobber (a function-wide fence, for a derived-index CSE), the statement-order reload (needs an unrelated field store to fall between the two reads), and the §22volatile-qualified GLOBAL (defeats constant store-forwarding of a known literal): here the CSE'd thing is a runtime field load reused via register, the intervening ops are calls/foreign stores (not a usable field-store), and you must NOT add a global barrier (it perturbs the rest of the schedule). fixes a folded-away 2nd/3rdlw <field>reload of a pointer-reached struct field across intervening calls; evidence func_801424E4 (*(volatile int*)(param_1+0x32)×2 →lw 0x64($s0)re-emitted at each use, byte-gated 58 ins). - fill a load-delay slot in a tail-store sequence → read the field into an explicit TEMP one statement EARLY
(refinement of §2-T2): when a tail reads several fields from an out-param/struct and then writes several
constant stores, and the target schedules ONE of those field loads early so its load-delay slot is filled by a
following
li(the value held in a reg and stored LAST), pull that load up with a named temp:c = out.c; *(int*)(p+0x1c)=0x5a; *(short*)(p+0x34)=1; *(short*)(p+0xe)=c;→ gcc emits thelhuforout.cahead of theli 0x5a(delay-slot filled) and thesh clast from the held register. Leaving it inline as the last store (*(short*)(p+0xe)=out.c;) instead lets gcc defer a different field's store and the schedule diverges. The temp only relocates the load in source order; §2-T2 names the mechanism but punts the load-delay case to the permuter — this is the concrete C lever for it. fixes a deferred-field-store / unfilled load-delay schedule in a const-store tail; evidence func_801856F8 (c = out.c;hoisted before the0x5a/1stores, byte-gated 71 ins). -
- **handwritten GTE
sqrbody: WRITE the two cop2-latencynops and VERIFY ON RAW BYTES — the assembler keeps - them; objdump (and
match_one) only HIDE them (false "stripped" diff):** for a handwritten GTE squared-distance - fn (
lwc2 $9/$10/$11, two latencynops,sqr 0,swc2 $25/$26/$27), write the whole GTE op as an __asm__ __volatile__body with the"nop\n" "nop\n"literally between the lastlwc2andsqr(`: : "r"(&in[0])- "$9","$10","$11","memory"
, store block pinning$2).mipsel-asdoes **NOT** strip those cop2 nops — they are in the object. The trap that made a prior session wrongly mark this whole family "not C-source reachable": **objdump ELIDES runs of zero words** (prints\t...for the two00000000nops), andmatch_onediffs via objdump → it reports a spurious mismatch (e.g. "22 mismatched") for bytes that are actually identical. **VERIFY GTE/cop2 fns on RAW bytes** (objcopy -O binary --only-section=.textthen compare words), never on the objdump listing. (Independently byte-confirmed here:+0x054/+0x058=00000000/00000000precede4AA00428/sqr 0in the built.o.) This unblocks the entire handwritten GTE-sqr` family (siblings func_8013E064/_8013E0FC/_8013E194/_8013E22C/_8013E298/ _8013E370/_8013E410). fixes the false "cop2 nops stripped" residual (an objdump zero-run-elision artifact, not a codegen miss); evidence func_8013E2C4 (43/43 raw bytes, banked + propagated ×13).
- **handwritten GTE
- store ONE value to several memory locations → write them as a CHAINED assignment
*a=*b=*c=v;, NOT separate statements: when the target copies a (callee-saved) value into a SCRATCH reg once (addu $v0,$sX,$zero) and thensh/sw $v0to several locations (often one store in a delay slot), the original wrote the stores chained — the chain's intermediate rvalue becomes that single reused scratch temp. Separate statements (*a=v; *b=v; *c=v;) instead store the source reg directly (nomove) and miss by ≥1 ins. (Distinct from §21'sfield=local=f(), which keeps a call result live for a test; here it's one value fanned out to N stores.) fixes a missingmove $v0,$sX+from-scratch store run; evidence func_80143D28 (*(s16*)(iVar3+0x18)=*(s16*)(iVar3+0x1a)=*(s16*)(iVar3+0x1c)=sVar4).
⚠ CANDIDATE (unverified) — the next two bullets are from
func_801775E0, a NEAR-MISS (closeness 2, NOT byte-banked), appended directly by a wave-13 drafter (that drafter→cookbook path is now blocked,commit:0219). They are plausible gcc-2.7.2 observations but were NOT confirmed by a byte-match. A drafter MAY try them — the byte-gate (G3/P9) is the sole arbiter, so a wrong idiom can never bank — but VERIFY before trusting. Drew to keep / refine / drop.
lbuvalue with SIGNED compares (bltz+slti, NOTsltu) → write each range bound as a SEPARATEif (…) gotostatement, never a chained||: when the target loads au8global withlbu(zero-extend → known 0..255) yet the comparisons are signed (bltz $v1;slti $v0,$v1,K) — including a provably-deadbltzon a 0..255 value — gcc-2.7.2 has emitted the conditions as INDEPENDENT signedsltbranches. A chainedif (v<0 || v>0xf6 || v<0xf3)CANONICALIZES: gcc provesv<0impossible (drops thebltz) and foldsv>0xf6 || v<0xf3into the unsigned range tricksltu $2,$v1,247/addiu -0xf3; sltiu 4(wrong:sltu, nobltz, fewer ins). Splitting intoif (v==0) goto a; if (v<0) goto b; if (v>=0xf7) goto b; if (v<0xf3) goto c;keeps each as its own signedslt+branch (thev<0becomes a realbltz, the bounds becomeslti), reproducing the target exactly. The launder/barrier tricks do NOT help (gcc re-derives the u8 range across an__asm__move); only the per-conditionif-gotosplit does. fixes the unsigned-range-collapse → signed-separate-compare divergence; evidence func_801775E0 (bltz;slti 0xF7;slti 0xF3).- shared join-block placement (call/store block reached by ≥2 paths) is steered by which exit the LAST range test
BRANCHES to vs FALLS THROUGH: gcc-2.7.2's jump pass lays the join block (e.g. a
jalreached by both an earlybeqand the range-chain fall-through) right after whichever predecessor it processes to fall through. Writing the chain's terminal asif (v>=K) goto join;(branch TO the join) puts the join AFTER the sibling block (matches a target whose join sits between the early-eq block and the tail); writingif (v<K) goto other; goto join;(branch AWAY, fall to join) FLIPS the entire layout (the early-eq test invertsbeqz↔bneztoo). The two are COUPLED — you cannot independently pick the terminal branch polarity AND the join placement; pick the form whose join placement matches and accept the terminal-branch polarity it implies (a residual the permuter can't touch underregister __asm__pins). evidence func_801775E0 (theif(v>=0xf3)goto callform gives correct [chain][eq-block][call][else] layout but leaves the terminal asbeqz→callwhere the target hasbnez→.L674; the negative form flips to a 50-mismatch layout).
§22 — DEF-side loose-typing recovery + grinder blacklist (Phase 21)
The wide self-MATCH → whole-binary-gate gap (a draft that match_one-MATCHes but the gate rejects) is dominated
by the DEF-side loose-typing wall, NOT cheap data plumbing (Phase-20 was right that data-cast is mostly moot —
the lone data exception is a D_x whose draft extern type disagrees with engine_core.h, fixed by rewriting the
draft extern to canonical and assigning through it). The DEF-side wall: the draft's byte-correct definition has
FEWER params than the canonical cross-overlay decl in engine_core.h (e.g. def void f(void) vs decl
void f(s32,s32,s32) because callers across overlays pass args the body ignores) → conflicting types for f in the
full TU → gate reject. Diagnose before building (R14): reproduce the whole-binary error (substitute the draft,
make build, grep error:/conflicting types) — harvest_verify discards it. The warnings at fixed line numbers
(incompatible pointer / built-in memcpy) are pre-existing and harmless; the real error is the conflicting types.
- Recovery = adopt the canonical param list on arity mismatch (
sig_unify.rewrite_def, Phase-21 fix). Previously it rewrote params only when arity MATCHED (return-type-only fallback otherwise — leaving the conflict). Now: when the canonical decl has MORE params, take the canonical param TYPES + COUNT, keep the draft's names where they exist, and synth_argNfor the unused extras. Unused params sit in$a0–$a3→ free at -O2 → byte-identical, conflict gone. Sincesig_unifyis in thegate_stagepipeline, this auto-recovers DEF-side near-misses on every wave + the grinder (compounding, like the Phase-20 type-lift). evidence: func_8016EDEC, func_8016EE40 banked via this. Still-hard residual (diminishing returns): narrow params (s8/s16/u8/u16/float) where gcc's default-promotion rule blocks the no-proto/wider escape (§15/§18), and multi-way loose typing (the same fn called with contradictory arities → no single ANSI prototype). - Grinder blacklist (
tools/grinder.py): a permuter "win" the whole-binary gate STILL rejects is plumbing-bound (the masked-0 doesn't survive the real link) — re-permuting can NEVER bank it. Record won-but-gate-rejected fns to a persistent.run/auto/grinder_blacklist.jsonand skip them forever (the daemon'stried.clear()-after-idle would otherwise churn the lowest-closeplumbing fns endlessly — it banked 0 in ~8h doing exactly that). Frees the permuter for genuine regalloc/schedule near-misses (the only class it can actually close).
§23 — Giant func_80153E00 cracked (scalar-data CAST); BUT most giant drafts are STALE+INCOMPLETE, and a diag MUST remove artifacts (Phase 21 — byte-proven + a self-correction)
func_80153E00 (195 ins, ×134) cracked — a clean 1-instruction near-miss. Substituting its draft compiles
AND links CLEAN and was 1/195 off. match_one over-predicted (compiles STANDALONE with the draft's own externs
AND masks jal/%hi/%lo) → it never saw the residual. Banked ×134 (fleet 62.27→62.31%).
⚠ DO NOT GENERALIZE "giants are near-misses" (a same-session R14 self-correction). After func_80153E00, I
diagnosed the other 14 MAIN-file giant drafts and a buggy diag reported 10 byte-MATCHes — a STALE-ARTIFACT
MIRAGE. The real byte-gate (harvest_verify, which os.removes the output first) banked 0 of 10: every
draft FAILS TO COMPILE. They are prior-wave drafts gone stale + incomplete vs the grown engine_core.h —
conflicting types for D_x (the draft declares u8 D_80126B58 vs canonical s32, u8 D_80126948 vs u8[]),
undeclared data symbols the draft never externs (D_800A5E8C, D_800B9A08 → link undefined reference),
and incomplete types. These are NOT bankable near-misses — they need RE-DRAFTING fresh against the current
canonical (a worker wave), not a cheap fix. func_80153E00 banked only because its draft was complete + clean.
⚠ THE STALE-.o TRAP BIT THE DIAGNOSTIC ITSELF (§20, the hard way). A per-function objdump diff that
builds WITHOUT removing build/src/<ov>.o + <ov>.elf first shows a FALSE byte-match when the spliced draft
fails to compile: make leaves the prior (stub) artifacts, so objdump disassembles the TARGET and reports
"0 diffs." This faked 10 giant MATCHes. A diag MUST os.remove the .o/.elf/binary before each build (then
a compile failure → empty disasm → honest "BUILD-FAIL", not a false match). .run/diag_funcdiff.py was fixed to
do this. The whole-binary SHA gate (harvest_verify) is the SOLE arbiter (G3/P9) — it removes the output, so
it was right while the diag lied. When a diag and the gate disagree, the gate wins; suspect a stale artifact.
THE DIAGNOSTIC (right tool, artifact-safe): .run/diag_funcdiff.py <fn> <draftdir> — build STUB →
objdump -d the fn (target); splice draft + build → objdump -d (candidate); diff, address column normalized
(s/80[0-9a-f]{6}/ADDR/). NOW removes artifacts before each build (else false match). Shows relocation vs
codegen vs type residual in seconds — but a 0-diff here is only trustworthy because of the artifact-removal +
a confirming harvest_verify. Run ONLY when no ov_SC01_077 build is in flight (concurrent builds clobber build/).
THE FIX for func_80153E00 — the scalar-data-signedness CAST (extends §22, corrects §20's "data-cast moot").
The 1 diff: target lhu D_8011DB0C (unsigned halfword) vs candidate lh (signed). The global D_8011DB0C is
declared extern s16 canonically (in engine_core.h + a banked DEFINE_* macro that writes it), but
func_80153E00 needs a u16 read (lhu). Two non-fixes: declaring the draft extern u16 D_8011DB0C; →
sig_unify reverts it to the canonical s16 (canonical wins, by design) → lh again; §22's "assign through
the canonical decl" → also lh (the canonical type IS s16). The fix is the DATA analog of cast_call_sites's
per-site cast: keep the canonical extern s16 decl, cast the READ site —
db0c = *(u16 *)&D_8011DB0C; /* canonical decl stays `extern s16`; gcc folds &sym+deref → a single lhu */
gcc-2.7.2 folds *(u16*)&sym to one lhu sym (no extra address insn), identical to declaring it u16 — but with
NO decl conflict, so it survives the gate pipeline (sig_unify leaves the canonical decl alone). Writes
(D_x = 0 → sh) are signedness-agnostic, so only READ sites need casting. This refutes §20's "data-cast is
MOOT" — that finding only checked extern struct/union conflicts; scalar signedness/width conflicts
(s16↔u16, and by extension s8↔u8 lb/lbu, s16↔s32 lh/lw) ARE real and recoverable by this
read-site cast. The same class is hinted across the other giants (e.g. func_80129CF8's note: "D_80126DB8
fields s32-store/(s16)-read, lh vs lw"). evidence: func_80153E00 — 1/195 → 0/195, banked ×134, fleet 62.27→62.31%.
TOOLING LEVER (probe-before-build, R14): if this scalar-data-conflict class recurs across the remaining
giants, build cast_data_sites.py (the data sibling of cast_call_sites.py): for each D_x read whose required
load width/signedness differs from the canonical decl, rewrite the read site → *(T*)&D_x (leave the decl
canonical; never cast a write). Folds into gate_stage → auto-recovers the class on every wave for ~0 tokens.
Gather evidence on ≥1–2 more giants first (func_80153E00 is N=1).
§24 — The ov_SC01_077_a.c split-file vein: split-aware propagation, but loose-typing-gated (Phase 21)
The opportunity (real): the Phase-19 -O0 work split ov_SC01_077 into 3 TUs (ov_SC01_077.c + _a.c +
_o0.c). The harvest pipeline defaults to src/{ov}/{ov}.c, so the 66+ fresh, cached, reach-134 fns in
ov_SC01_077_a.c were never waved — a tooling gap, not a difficulty wall. The main-.c reach-134 fuel is
byte-exhausted (1 fresh fn); _a.c is where the fresh fuel is. (tools/build_fuel_manifest.py + cross-ref the
3 split files' stub sets to enumerate it; .run/diag_a.py is the artifact-safe per-fn diff for _a.c.)
What's BUILT (validated):
dedup_propagate.pyis split-aware —overlay_files(ov)returns[(main.c, ov), (_a.c, ov_a), (_o0.c, ov_o0)]; source def-finding scans all (source_text), the member loop edits whichever split file holds each target's stub/def (per-file asm-subdir regex), the structural check spans all files. Single-file overlays are unchanged (default path). Validated: source-find locates_a.cdefs (--check-only→ 134 members); fail-path reverts correctly. (Success-path ×134 of an_afn still pending a clean propagatable+matchable_afn — the wave.)cast_call_sites.py --src-file <file>— canonicalize callee decls against the file the draft LANDS in, not main.c. Why it's needed: a callee can be declared differently in main vs_a.c(cross-file loose typing, e.g.RotTransSV), so canonicalizing against main injects a decl that conflicts with_a.c's. Pass--src-file src/ov_SC01_077/ov_SC01_077_a.cfor_adrafts. (Default = main, unchanged.)
The WALL (honest — the _a vein is NOT a clean win): propagation/matching of _a fns hits the SAME §16/§20
loose-typing wall as everywhere, in fresh form:
- Cross-overlay def-conflict → ×1, not ×134.
func_8012C098matched in_a.c(cast-recovered: a calleefunc_8012C218(void*)called 0-arg via((void(*)(void))…)()), but a banked caller in another overlay (engine_core.h DEFINE) declares itextern void func_8012C098(void)while the body USESparam_1→ the propagated macro's def conflicts there →dedup_propagate(all-or-nothing) reverts. Irreducible (no single C sig fits a param-using body + a 0-arg caller decl). Banks ×1 only. - Within-
_aloose-typed callee.func_8012F274(decls=0, would propagate freely) won't even match:RotTransSVis declared inconsistently within_a.c→ any single extern conflicts; needs the per-site cast, whichcast_call_sitesonly applies when the canonical differs from the draft's intent (here it picked one and it still clashed with another site). A genuine multi-sig callee. sig_unifystill drops_afns (itscur_stubs/decls read main.conly) → for_awaves either give it the same--src-file/split-awareness or runcanon → cast --src-file → gate(skip sig_unify; the cast class banks, the def-side class is a wall anyway). NOT yet done.
Net / next: the enabler tooling is built + safe (fail-closed); the _a vein is matchable but its ×134 yield
is loose-typing-limited (unknown fraction are ×1 walls). The proper measurement is a worker wave over the
_a pool (parallel agents match bodies; the byte-gate + split-aware propagate sort ×134 vs ×1) — needs
wave_targets _a support + sig_unify _a-awareness (or skip it). Decide whether the uncertain yield
justifies the wave vs pivoting (the wall is the same as the main vein's).
UPDATE (cont.6): sig_unify --src-file is now BUILT (closes the open item above). sig_unify.py --src-file src/ov_SC01_077/ov_SC01_077_a.c reads cur_stubs + inline/extern canonical sigs from the split file (not the
main .c), so _a/_o0 drafts are no longer dropped and DO get the def-side arity-adopt recovery. Validated:
21 _a drafts unified WITH the flag vs 0 without; the _a close=0 recovery wave then banked func_8012F568 ×134
(commit:0277). gate_stage passes --src-file to BOTH cast_call_sites and sig_unify now. See §25 for the
canon-first two-stage gate that makes this safe (sig_unify must be a FALLBACK, not unconditional).
§25 — The "schedule" class is mostly COALESCING (pin-crackable), not scheduling; + the gate two-stage + h_exact over-counts ×134 (Phase 21, cont.6)
⚠ COMPLETED BY §162j (P30 S48). This section's triage rule prescribes pins for "a copy emitted
before its source's other use". There is a sibling mechanism with the same symptom that pins
provably cannot reach: local-alloc's optimize_reg_copy_1 rewrites the later use to the copy's
destination, and its hard-register escape is compiled out on this target (SMALL_REGISTER_CLASSES
is never defined in config/mips/mips.h). The lever there is an in-place SET, not a pin. Read §162j
before spending a pin sweep on a one-register sll/copy diff.
idiom_loop.py --assess named schedule (50 "reach-134", median 15 ins off) as the next idiom. Cracking its
lowest-closeness reach-134 exemplar func_80128ED8 (close=3) surfaced three durable lessons.
The crack: the residual was gcc-2.7.2 COPY-COALESCING, not the instruction scheduler
match_one diff (3-off): MINE move $a3,$v1 / sll $v0,$a3,3 / addu $v0,$v0,$t0 vs TARGET sll $v0,$v1,3 / addu $v0,$v0,$t0 / addu $a3,$v1,$zero. The "schedule" label was misleading: gcc COALESCED the index-preserve
copy (idc = idx) into the multiply operand (sll on the copy $a3) instead of multiplying $v1 directly,
and routed idc+1 back through the copy's reg. Two §17 register pins fixed it (byte-gated, match_one
MATCH 53/53):
- Pin the preserve-copy to its TARGET register (
register s32 idc __asm__("$7")=$a3): gcc can no longer fold it into the multiply operand → the multiply uses the original$v1directly and the copy emits SEPARATELY after it. (3-off → 2-off.) - Route the copy's dependent arithmetic through a dead, target-scratch-pinned temp (reuse an already-dead
register __asm__("$2")=$v0pseudo:cnt = idc + 1; store cnt;) → the result lands in$v0(the target's scratch), not back in the copy's reg. (2-off → MATCH.) Triage rule: on a small "schedule" residual, read thematch_onediff FIRST. A copy emitted before its source's other use, with that use reading the copy's register = COALESCING → pins (cookbook §17), crackable. Don't assume the scheduler.
The genuine scheduler — rank_for_schedule (sched.c), for when it IS scheduling
gcc-2.7.2's ready-list tie-break order (sched.c, byte-read R17): (1) PRIORITY = dependency-chain height to
end-of-bb (longest chain first); (2) CLASS vs last_scheduled_insn — prefer class 3 (independent / latency-1)
over class 1 (data-dependent on the last insn) — i.e. gcc fills an address-gen→load gap with an INDEPENDENT insn
(this EXPLAINS the "copy fills the slot before the lw" schedules the targets show); (3) LUID = original
SOURCE ORDER (the stable final tie-break). LEVER for the genuine equal-priority case: reorder the SOURCE
statements (the LUID tie-break — same family as §10's operand/statement-order idioms). When priorities differ or
coalescing intervenes, source-reorder alone won't flip it → use the pins above.
The genuine schedule WALLS (do NOT re-grind — stub)
- §10 cross-jump / delay-slot merge (func_8014FD54, close=2): two
return 0paths — the target keeps them SEPARATE (one fills the innerbeqzdelay slot withmove $v0,0, one is a standalone zero block); our cc1 cross-jump-MERGES them → the innerbeqzgets a NOP delay slot. The §5a:::"memory"barrier breaks the merge but overshoots +1 (41 vs 40). No C-source form reaches the merged-with-delay-slot-fill schedule. (§10 Residual-B.) - store-vs-load placement (func_8014F2E0/func_80150528, §20-confirmed): the store schedules between two arg-loads, mutually exclusive with base-preservation. Stub.
h_exact OVER-COUNTS ×134 — verify shareability before crediting a class's "reach-134" (R14)
func_80128ED8's crack is byte-identical in ov_SC01_077 but does NOT propagate ×134 — dedup_propagate --addr 0x80128ED8 (alone, no stragglers) still [drop]s it at ov_SC01_000: a cross-overlay byte-gate reject. So it
banks ×1, despite sig_image h_exact reporting members=134. Why: h_exact is RELOCATION-MASKED (the
%hi/%lo of unresolved syms are 0 in the object), so it matches across overlays that the shared-C macro then
can't reproduce byte-identically (overlay-local data/decl differences — the §24/§20 wall). Consequence for the
idiom-loop model: --assess's per-class "reach-134" count (from the backlog's reach field = h_exact) is
OPTIMISTIC; a cracked fn's real leverage can be ×1. Before committing a token-heavy wave to a class, probe
×134-shareability on the cracked exemplar (dedup_propagate --addr <fn>), not just the h_exact count. The
"schedule" class is therefore NOT a confirmed ×134 vein — its closest reach-134 exemplars are one ×1-coalescing
crack + one §10 wall.
The gate two-stage — sig_unify is a FALLBACK, not unconditional (gate_stage.py, §19 folded in)
gate_stage ran canon→cast→sig_unify in ONE pass. sig_unify REGRESSED the func_80128ED8 crack: it rewrote the
byte-correct def s32 f(s32,s32*) → a banked caller's canonical void* f(void*,void*) → gate reject (the raw
draft banked fine via harvest_verify). Fix (the §19 "canon-first" design, now IN gate_stage): stage 1 =
canon+cast → byte-gate (already-correct drafts, incl. hand-pinned cracks, bank here); stage 2 = sig_unify
ONLY the stage-1 failures → re-gate (def-side near-misses recover) — never regressing a stage-1 winner.
harvest_verify reads the CURRENT src as baseline, so verified fns ACCUMULATE across the two gate calls (a
stage-1 winner is no longer a stub for stage 2). This is mandatory now that hand-pinned self-contained cracks
flow through the same gate as recovery drafts.
§26 — The cheap close=0 recovery lever is EXHAUSTED; idiom_loop --assess was DOUBLY inflated (Phase 21, cont.7)
cont.6 left two "do this FIRST each cycle" cheap levers: the _a close=0 recovery (validated on one fn,
func_8012F568 ×134) and idiom_loop --assess's "51 close=0 reach-134 fns → ×134 for ~0 tokens." cont.7 ran the
_a lever to completion and probed the assess. Both were over-promises; the cheap recovery fuel is dry.
The _a close=0 recovery banks 0/20 — same def-side wall as MAIN (0/40)
Re-gated the 20 still-stubbed _a/o0 close=0 reach-134 fns through the cont.6 fixed pipeline (gate_stage --src-file, canon-first two-stage; sig_unify --src-file fired on all 20). banked 0. match_one calls them all
"MATCH" (close=0) but the whole-binary gate rejects every one — the §16/§20 DEF-side multi-way loose-typing
wall: the draft's byte-correct def needs a C type that conflicts with a banked caller's canonical decl, and no
single C sig fits both (sig_unify's arity-adopt only fixes the simple-arity case — cont.6's func_8012F568 was
that lone case; the rest are genuine multi-type conflicts). Caller-side fix is blocked (INCLUDE_ASM declares no
symbol, §20). Conclusion: close=0 + already-recovery-gated = a WALL, not fuel — do NOT re-run recovery on it.
--assess was inflated TWO ways (now fixed) — verify reach AND closeness before crediting a lever
The assess named "51 close=0 reach-134 → ×134 for ~0 tokens." Two byte-proven inflations (R14/§25 family):
reach= maskedh_exactdistinct-overlay count → OVER-counts the real ×134 (§25: func_80128ED8's--check-onlyplan saysmembers=134but real propagation is ×1). The "reach-134" label is a CEILING.load_backlogdidn't drop banked-since-logged fns (the ledger keeps stale statusnearfor a fn matched in a later session — e.g. func_8016EDEC/EE40 banked in cont.6's Option-C still counted). And the close=0 "lever" counted fns already recovery-gated-and-failed (the §16/§20 wall) as if fresh. Fix (tools/idiom_loop.py, cont.7 — R16 flywheel, so the next session doesn't re-burn the lever):
load_backlognow intersects the ledger with the liveINCLUDE_ASMstub set (_open_stubs(), mirrorsbacklog._matched_now) → drops every banked-since-logged fn (the same drop-now-matched P9 honestybacklog.renderapplies). This alone removed the bogus "unknown 14" class + ~10/class of stale-matched inflation.- The DETERMINISTIC-RECOVERY line now splits FRESH (never recovery-gated
recover-*source = genuine ~0-token fuel) vs WALLED (already recovery-failed = the def-side wall, "do NOT re-run"). Post-fix: 0 fresh, 46 walled. The cheap lever is genuinely empty. RULE: before trusting an assess class/lever, the live numbers are: real reach =dedup_propagate --addr --check-onlyis still h_exact (a ceiling) → the only truth is the gate; real closeness = re-measured through the gate's canon/cast/sig_unify transforms (the stored ledger closeness is optimistic — stale records read 0 where the live residual is 30–41). h_exact over-counts the numerator, stale-ledger under-counts the denominator.
Where this leaves the reach-134 tail (cont.6 option-3, now CONFIRMED byte-backed)
The cached reach-134 cheap fuel is dry: close=0 recovery = 0 fresh (46 walls); the codegen classes that look
tractable are h_exact-inflated (schedule "39 reach-134" but the cracked-exemplar ×134 fraction is ~50% on n=2 —
func_8012F568 ×134 / func_80128ED8 ×1 — at median 15 ins off = expensive per-fn pin work, poor ROI; do NOT wave
it on the inflated count, §20 "don't wave a wall"). The remaining levers are all token-heavy fresh-session
work: (a) the GIANTS (8 reach-134 >150 ins — the byte-weight lever, ~3% auto-yield so mostly hand-finish/backlog
fuel, Phase-16); (b) per-fn pin-cracking the genuine codegen near-misses (schedule-coalescing §25 / regalloc-order).
REFUTED — the "resolved-reach probe" (cont.7b, R14 self-correction): I proposed comparing per-overlay
linked .text to get a real ×134 count. It doesn't work, because sig_image's h_exact is ALREADY
SHA1(raw image bytes) — UNMASKED (sig_image.py:170-171; h_norm is the masked tier). So reach is the
ACCURATE shipped-byte reach; a resolved-byte probe would just reproduce it. The §25 "h_exact over-counts" wording
conflated two different hashes — the IMAGE h_exact (accurate) vs the match_one/draft OBJECT masking. The real
gap is reach (shipped bytes identical) ≥ realizable-×134 (shared-C macro reproduces all N): func_80128ED8's
shipped bytes ARE identical ×134, but its matched C can't propagate (a callee/decl/loose-typing wall in the
shared-C representation, §16/§20/§24). That gap is measurable ONLY by dedup_propagate's byte-gate on an
ALREADY-MATCHED fn — there is no cheap static probe. Don't build the resolved-reach tool.
The close-N tail can be PERMUTER-achieved, not source-close (cont.7b, R14): the backlog closeness is the
BEST achieved (often by the grinder's randomized regalloc/scheduling permutation), NOT the saved draft's
source-compile distance. func_801775E0 logs close=1 but its saved best_draft compiles 31-off from source
(prologue save-SCHEDULING, branch-sense, block-order all differ — the §20 regalloc/schedule wall); the close=1 was
a permutation the source doesn't capture. RULE: before assuming a cheap source crack on a close-1..4 backlog fn,
match_one its saved draft to get the SOURCE closeness — if it's far-off, it's a permuter-class wall (grinder
territory), not a pin target. The genuinely pin-crackable ones are source-close + a single coalescing/CSE residual
(§25 func_80128ED8); the regalloc/save-schedule/cross-jump ones are the confirmed walls (§20/§25).
§27 — Giant matching recipe (Phase 21 cont.7b — validated on func_80176D94, 152 ins)
Giants (reach-134 >150 ins) are the byte-weight lever. Validated approach (func_80176D94 → structurally matched, calls/constants/GPU-packet all byte-correct, residual = pure regalloc):
- Start from the cached Ghidra-C (
.run/ghidra_c/<fn>.c) — giants are often CLEAN (straight-line + many calls, no deep control flow), so the body structure comes nearly free. (cont.2's "stale+incomplete drafts" were the OLD m2c drafts vs the grown header; the fresh Ghidra-C + canonical context is the fix.) - Arg-arity is the #1 giant blocker (the manifest's
MCOMPILE_arg-aritybucket). Giants call many helpers; m2c/Ghidra miscount args. Declare each callee to match the ACTUAL call site — count the$a0–$a3(+ stack) set before eachjal, NOT the canonical sig. e.g.func_80177784canonical is 4-arg but called 3-arg here ($a3untouched) → declarefunc_80177784(void*, s32, s32). (The shared-header canonical conflict is the gate's job —cast_call_sites; it may bank ×1 if irreducible, §24.) - Sibling templates. Giants cluster in families (GPU-packet builders, coord transforms). Find an
already-matched sibling
DEFINE_func_*inengine_core.hwith the same idiom and mirror its PROVEN C form — e.g. the GPU-coord strength-reduce((s32)(D * 10355) << 1) >> 16(sibling func_80176FF4) and the GPU linked-list pointer((u32)addr & 0xFFFFFF) | 0x3000000. - §17 register pins for the regalloc-SHIFT. Giants use 5–7 callee regs; gcc's mapping often shifts whole-hog
(param→$s0 where target uses $s2, etc.). Pin the long-lived vars to their target regs
(
register T v __asm__("$NN")). CAVEAT (cont.7b, byte-proven): do NOT pin a var whose register the target REUSES for a later spill — pinning reserves the reg for that var's whole scope → blocks the reuse → gcc grabs a FRESH callee reg (+1 reg, +2 prologue ins). (func_80176D94: pinning uVar5→$s0 blocked the accumulator from reusing dead $s0.) - The giant residual class — accumulator-spill coalescing. A value chained through
$v0→$a0across calls, then needing to survive a LATER call, must spill to a callee reg. The target reuses a now-DEAD callee reg; gcc from natural C grabs a FRESH one (+1 reg). Hard to force from C (it's gcc's coalescing graph) → permuter fuel (the grinder randomizes allocation and may find the reuse). func_80176D94's saved draft (.run/backlog_drafts/) is exactly this — structurally done, 1–2 callee regs from byte-perfect. Net: giants reach STRUCTURALLY-MATCHED fast (steps 1-3); the last mile is regalloc-coalescing (step 5) — so giants are ISOLATED-AGENT + permuter work (perbreadth-isolated-agents-not-serial), not main-loop serial grind.
WAVE RESULT (cont.7d — the honest yield, R14/P9): a 6-giant worker_wave banked 0. The agents reached the SHAPE (the §27 recipe works — bodies/calls/constants/control-flow right) but the whole-binary byte-gate rejected all 6: 1 was close=0 match_one-MATCH yet gate-REJECTED (func_8014F74C — no callee/data conflict, so match_one OVER-predicted: the masked standalone match hid a real residual, §25), 1 wouldn't compile whole-binary (func_80144090, agent-claimed "154/154" but a decl the gate couldn't reconcile), and 4 were genuinely far (close 53–164 — the agents' "structural match" self-claims were match_one-optimistic). This confirms cont.2 / Phase-20 / Phase-16 (~3% wall): giants do NOT auto-bank — even with the §27 recipe + isolated agents, the whole-binary gate (masked-residual + plumbing + large regalloc) blocks them. Giants are HAND-FINISH / backlog fuel (the wave's deliverable is 6 RANKED near-miss drafts for human sessions, NOT banks). Do not scale giant auto-waves expecting %; the automated reach-134 harvest is COMPLETE at this fleet level (every automated lever — cheap recovery, permuter, giant auto-wave — banks ~0). Forward % = hand-finishing (Drew) or closing the phase.
Frame-pressure-locked residuals are PERMUTER-ONLY (cont.7d, byte-proven on func_8014EA4C, close=6). When a
giant's matched body needs a frame-forcing HACK to hit the right stack size — e.g. a DEAD u8 buf[16]; memcpy(buf+16, src, 8) (an out-of-bounds copy whose only job is to force frame 0x60) — its remaining schedule/regalloc residuals
become PRESSURE-LOCKED: any C edit that would steer them (materialize a temp to reorder a store/load; a ternary or
s32 retype of an abs; a register pin) shifts register pressure → gcc DROPS the dead buffer → frame shrinks
(0x60→0x58) → whole-function offset cascade (close 6 → 23–123). Two hand attempts both cascaded exactly as the
drafter predicted. The fix is NOT C-steering — it's the decomp-permuter (the grinder): it randomizes
regalloc/scheduling via semantics-preserving C perturbations the byte-gate scores, exploring the frame-PRESERVING
space the hand-edits can't. Re-log such giants with their true closeness + raw draft (source=giant-raw) so the
grinder (close≤30) picks them up; do NOT hand-grind them.
Deep frame-RE does NOT crack a scheduler-walled giant (cont.7d, byte-proven on func_8014EA4C). Drew chose
"deep re-RE the real frame so regalloc resolves." The RE finding: the frame is a dead aggregate copy — 8 bytes
of a param unaligned-copied (lwl/lwr→swl/swr, char-aligned dest) to a stack local at sp+0x20 that's NEVER read
(gcc-2.7.2 keeps it — no DSE for aggregates). The drafter's u8 buf[16]; memcpy(buf+16, a2, 8) (copy PAST the
array) is a PRECISE reproduction of the exact gcc stack layout — local[0x38] or any "robust" remodel produces a
DIFFERENT frame (0x88, copy at 0x30). So the frame is already correct at close=6; the residuals are NOT
frame-caused — they're loop-body scheduler (a global store vs a call-arg load order) + regalloc (abs in $v1 vs
in-place $v0), the irreducible §20/§25 wall. Lesson: when a giant's residual is loop-body schedule/regalloc,
deep frame-RE is a dead end — only the permuter explores that space. Net for Phase 21: 5 levers
(cheap-recovery, permuter, giant-wave, hand-finish, deep-RE) are byte-proven exhausted at fleet 63.17% for the
reach-134 tail; the residual is the gcc-2.7.2 scheduler/regalloc wall, addressable only by the (low-yield) permuter
or by accepting it as the matching ceiling at this fleet level.
§28 — Banking a "close=0 gate-rejected" giant: the canonical-extern recovery (Phase 22 T1, byte-proven on func_8015126C ×134)
Phase-21 (§25/§27 cont.7d) logged a class of giants as "close=0 (match_one MATCH) but gate-REJECTED" and treated them ALL as masked-residual/permuter fuel. That was incomplete (R14). A close=0-gate-rejected giant is one of FOUR distinct walls — and one is deterministically bankable BY HAND:
- PURE-EXTERN PLUMBING (bankable — the NEW lever). The draft's self-contained file-scope externs conflict with engine_core.h's canonical decls (the giant calls already-matched engine fns; the draft guessed their sigs, e.g.
extern void func_8015173C(void*)vs canonicalvoid func_8015173C(s32*)). match_one's isolated compile (own externs) MATCHes; the whole-binary TU fails to compile (conflicting types for func_X).sig_unify/cast_call_sitesdo NOT canonicalize these (the gap that stranded them). Fix =tools/recover_giant.py: for each callee with aDEFINE_func_X()in engine_core.h, rewrite the draft's extern to that macro's exact def-sig; then move ALL externs (callee +D_data) block-scope (inside the body, after{).find_site/compiles_standalone/dedup_propagatelift the body + its internal externs as one unit — file-scope externs are excluded from the lifted body → false "not self-contained" skip. Forward-refs (higher-addr callees) MUST stay declared. Thenharvest_verify→dedup_propagate×134. - MASKED RESIDUAL (permuter fuel). Compiles whole-binary but bytes differ — the relocation-mask hid a real codegen diff (func_8014F74C, §25). The recovery won't help → grinder.
- STRUCT-WALLED (type reconciliation). The draft uses a local named
struct S8/B8(the array-of-struct %lo idiom, §18) that collides with the TU's other defs (conflicting types for S8) — func_80156B74/func_8014F74C/func_80163C2C. Needs the type lifted toengine_types.h(named once) or rewritten anonymous/raw-cast. Not near-free. - REGRESSED DRAFT. The saved best_draft was clobbered by a later worse attempt (func_80178004: worklist close=0 but saved draft is DIFF 91). Re-derive.
Triage: recover_giant.py → match_one (DIFF → #4) → whole-binary build. byte-identical → #1 banked · conflicting types for func_X → was-#1, recovery fixes · conflicting types for <Type> → #3 · compiles but bytes differ → #2. Honest yield: of the 5 close=0 giants, only func_8015126C was #1 (pure-extern); the rest are #2/#3/#4 — the canonical-extern lever is real but the close=0-giant group is NOT uniformly near-free.
The coalescing pin (extends §25 — func_8015126C's last mile). The (s16)p[0x79] != 1000 compare temp wanted $a0 (coalesced with the soon-to-be-angle arg); gcc gave $v1. An eager named temp forces $a0 but HOISTS the load (+95 ins). Fix — pin AND keep the load lazy inside the &&:
register s32 cmp __asm__("$4"); /* $a0 */
if (cond1 && ((cmp = *(s16*)(p + 0x79)) != 1000)) { ... }
The in-&& assignment stays inside the short-circuit (not hoisted) yet lands in the pinned reg. Reusable for any coalesced-compare-temp residual.
Op gotchas (Phase 22 T1): run dedup_propagate in the BACKGROUND (134 builds > the 2-min foreground cap; a SIGTERM/interrupt leaves a non-atomic partial state — macro+instantiations applied, registry unwritten). And git checkout src/ does NOT revert config/dedup.us.yaml, so on a redo reset BOTH (git checkout src/ config/dedup.us.yaml) or registered_addrs() stays dirty and re-skips the function as "already shared."
§28a — decomp.wiki GCC patterns worth trying on BFM giants (decomp.wiki/compilers/GCC, raw at decompals/decompedia; PS1-applicable subset)
- Negative struct offsets in loops —
for (i=…; …; i++, p++)makes gcc advance the pointer + use negative member offsets instead of offset-folding. Directly targets func_801412A8's "OFFSET-FOLDS the 4× prim stores instead of ADVANCING $t6" residual. - Branch-invariant code duplication — when gcc hoists a shared tail (a call) out of two branches and swaps regs, duplicating that tail inside both branches fixes the regalloc. An alternative to §17 pins for the call-crossing swap class.
- Load coalescing — adjacent struct fields compared together (
if (t->a || t->b)) fold into onelw(+lui/ori/and mask when <4 bytes). Recognition aid for engine code. - div-by-constant magic table (0x66666667→/10, 0x55555556→/3, …) + s16/s8 div-by-2 sign-extension forms; gcc-2.7.2.x
slti …,0⇒(x & (1<<31)) != 0. - N/A to PS1 (don't chase):
bnel/likely branches (MIPS II+; R3000 has none),.lit4float-literal NOPs (PS2), C++boolload/store (BFM is C).
§28c — The close=0 recovery is NOT fully exhausted (§26 corrected, R14); + the dedup_propagate registry-skip recovery (Phase 22 T2)
§26 said "the cheap close=0 recovery lever is EXHAUSTED." That was over-broad (R14). Re-running recover_giant.py (canonical-extern + block-scope) + the whole-binary gate over the still-live close=0 reach-134 set (match_one MATCH, gate-rejected) banks a real ~15-20% tail the prior waves left — Phase 22 T2 banked 7 this way (func_80156ECC, func_80147E44, func_8015ADB0, func_801661CC, func_80166054, func_8012CFA8, func_8012A62C) out of ~43 candidates. The rest (~33) ARE the genuine DEF-side loose-typing / masked-residual wall (§20/§26 holds for THEM). Two enablers this session: (a) the §28b type-lift put more shared types in engine_types.h (so compiles_standalone passes for more bodies); (b) dedup_propagate --auto-from then propagates not just the new banks but pre-existing inline-matched-but-never-propagated functions for free (7 bonus this session — always run --auto-from after a harvest_verify batch to sweep them). Recipe: survey live close=0 reach-134 (fuel_manifest.targets ∩ backlog closeness==0) → recover_giant each (strip local struct typedefs first; lift shared ones per §28b) → batch harvest_verify --chunk N per region (main / _a, distinct --src/--asm-subdir) → dedup_propagate --auto-from → make check-all (the sole arbiter).
The dedup_propagate registry-skip + its recovery (byte-proven, recurring). A full dedup_propagate run (long byte-gate loop = 134 builds) sometimes ends with the source propagated (DEFINE macro + ×134 instantiations) but config/dedup.us.yaml partially/un-written (the long loop appears to get killed before/within the register step; the run still reports exit 0). Symptoms: check-all 136/136 (bytes correct) but dedup-check/progress.py under-count (group absent). Recovery (do NOT revert — the bytes are right): the tool can't re-register an already-propagated fn (--auto-from needs inline-def; --addr needs a non-macro site), so register directly via its own helpers — for each addr: h = load_sig('ov_SC01_077')[addr]['h_exact'] (stable: bytes unchanged), members = [ov for ov in onboarded_overlays() if load_sig(ov).get(addr,{}).get('h_exact')==h], then append_groups([dict(id=f"E_{sym(addr)}", tier='h_exact', hash=h, source='src/shared/engine_core.h', addr=addr, members=[{'binary':ov} for ov in members])]) (idempotent by id — safe to re-run). Verify with dedup_integrate.py --check. Prevention TODO: make dedup_propagate write the registry BEFORE the byte-gate loop (or add a --register-only mode for already-propagated addrs).
§28b — The struct-walled close=0 giant (§28 case #3) IS bankable: the engine_types.h type-lift (Phase 22 T2, byte-proven on func_80156B74 ×134)
§28 case #3 ("STRUCT-WALLED — needs the type lifted… Not near-free") was too pessimistic — it IS near-free once you do the lift, and the lift unblocks the whole class (every giant sharing those types, including already-banked-but-LOCAL ones via the §19 propagation cap). Recipe (byte-proven on func_80156B74, 214 ins, ×134, fleet 63.22→63.25%):
- Identify the draft's named struct/typedef types (e.g.
typedef struct {s16 a,b,c,d;} S8;,B8,Blk16,Buf32). These are why the draft was struct-walled: a close=0 draft that re-typedefs a name already in its TU = C89 duplicate-typedef →conflicting types; and a body containing atypedef/named-struct can't be lifted (dedup_propagateskips it, line ~366). - Lift the typedefs to
src/shared/engine_types.h(defined ONCE; reaches all 136 binaries viaengine_core.h's include). Gotchas: a typedef NAME (S8, ordinary-identifier namespace) legally coexists with an unrelated struct TAG (struct S8, tag namespace) — gcc-2.7.2 accepts both; check for a layout-identical existing type first (e.g.B8≡ the pre-existingBlk8) and keep the draft's name to avoid churning already-matched siblings. Remove the now-duplicate file-scope typedefs fromov_SC01_077.c(the per-function prelude block) or the lifted def collides. - Verify the lift is BYTE-NEUTRAL before touching the giant (R22): rebuild the binaries whose already-matched fns use those types (the prelude's owner fn, e.g. func_80156848) + 1 overlay + resident → all must stay byte-identical. Typedefs emit no code; only a name collision can bite.
- Strip the typedefs (+ any
#include) from the draft →tools/recover_giant.py(it canonicalizes the engine_core.h-callee externs + block-scopes ALL externs, so the body is self-contained for ×134 with its types coming from the shared header). recover_giant strips only SCALAR typedefs — remove the struct ones yourself first. - match_one caveat: match_one compiles standalone WITHOUT engine_types.h, so the recovered (typedef-free) draft won't compile there — that's a harness limitation, not a defect. Confirm the MATCH on the ORIGINAL self-contained draft (with its own typedefs); gate the recovered draft with the whole-binary
harvest_verify(which includes engine_types.h via engine_core.h — the real arbiter, G3/P9). Thendedup_propagate --addr 0x… --source-overlay ov_SC01_077(BACKGROUND, §28). - Op gotcha (Phase 22 T2, byte-proven): do NOT run a second
make-invoking job (make check-all,make report) CONCURRENTLY withdedup_propagate— parallel make corrupted a partial.o(file format not recognizedon ov_SC04_016, caught by check-all + cleared by a clean rebuild). Serialize all make jobs. NOTE (corrected — §28c): the propagation's registry-skip (source propagated butconfig/dedup.us.yamlgroup unwritten) is a SEPARATE recurring issue independent of concurrency (it recurred with NO concurrent make on func_80163C2C) — see §28c for the byte-proven recovery; re-runningdedup_propagatedoes NOT re-register an already-propagated fn.
§28d — The "macro-extern-injection" lever: freeing reach-134 inline matches dedup_propagate skips as "not self-contained" (Phase 23, tools/inject_capped_externs.py)
The Phase-20 backlog flagged a set of reach-134 functions matched INLINE in ov_SC01_077 but never propagated (dedup_propagate --auto-from reports "N not self-contained (local types)") — a pure propagation cap, not a codegen wall (their bytes are already byte-correct in 077). Phase 23 surveyed them: 29 such reach-134 fns in 077 (main + _a). Root cause (byte-diagnosed by running each lifted body through compiles_standalone = common.h + engine_types.h): find_site lifts only the function DEF (+ contiguous preceding externs), but 077.c declares the callees/data at FILE scope, so the lifted macro body has undeclared func_X/D_X in every other overlay. The classes (run the diagnosis — they are NOT uniform, R14):
- callee/data extern-undeclared (the clean majority) — inject the EXACT file-scope
extern …;the overlay already declares for each referenced symbol, BLOCK-scope (just inside{). Byte-neutral (block vs file scope = same codegen; the gate proves 077 staysd19c9580…).compiles_standalonethen passes →dedup_propagate --auto-fromlifts it ×reach.tools/inject_capped_externs.pydoes exactly this, fail-safe (only rewrites a fn if the injected body thencompiles_standalone; never churns one that still won't propagate). Phase-23 result: 8 freed fromov_SC01_077.cmain (each reach-134), 077 byte-identical, propagated ×134. - split-file (
_a.c/_o0.c) — same fix, run the tool with--src-file src/ov_SC01_077/ov_SC01_077_a.c(10 of the 29 live there). (_o0.cdefs are overlay-LOCAL, §18/§20 — never auto-propagate.) - missing-extern — the referenced callee has NO file-scope extern in 077.c (077 builds via gcc's implicit declaration); inject a no-proto
extern <ret> func_X();(the call-site cast gives the shape) — a tool extension, lower yield. - type-walled — the body references a struct/typedef not in
engine_types.h(Buf,Vec3,Loc, a PsyQMATRIX); needs the §28bbuild_engine_types.pytype-lift first (and the type must actually be inline-defined in 077.c — a PsyQ type needs its header, not a lift). Pipeline:inject_capped_externs --apply→make build BINARY=ov_SC01_077(gated19c9580…, else revert) →dedup_propagate --auto-from ov_SC01_077 --min-reach 2(BACKGROUND, serialize make, §28). The whole-binary byte-gate is the sole arbiter (G3/P9): a wrong injection can't pass. The lever compounds (R16): every future LLM/agent inline bank of a shared fn that references file-scope symbols is freed the same way — run the injector before propagating.
§29 — Reasoning-model (GLM5.2) DEF-side reconciliation idioms + the wall's hard limit (Phase 23 T10.7, tools/glm_reconcile.py)
The T10.7 OpenRouter A/B found GLM5.2 is ~10× better than v3 at hard-band CODEGEN (10/18 vs 1/18 match_one on 16–22-ins ov_SC01_077 struct-core fns) — but the DEF-side loose-typing wall (§20, Phase 16/20) caps banking at 4/18 regardless of drafter, because the conflict is overlay-forward-decl-vs-the-fn's-true-signature, independent of who writes the body. Aiming GLM's REASONING directly at the wall (glm_reconcile.py: body + the conflicting TU decls + this toolkit → a consistent byte-identical decl set; reasoning captured to .run/glm_reason/ for R16) banked only 1/7 stranded; mechanical fix_arity_callers --any-proto 0/7. Verdict: the wall is INTRINSIC — a frontier reasoning model with the full toolkit cracks ~1/7. GLM's reasoning is nonetheless expert-grade and the durable value; the byte-neutral reconciliation idioms it articulated (validated by the gate):
- Match a pointer PARAM's type to the TU's existing forward-decl (
void*def-param →s32*when the TU declaresfunc_X(s32*)): both 32-bit, body casts work identically, byte-neutral — resolvesconflicting typeswithout relaxing anything (bankedfunc_80175184). - Call-site cast for a value mismatch: passing an
s32*to afunc_Y(s32)→func_Y((s32)arg0)— same 32-bit value in$a0, gcc emits NO conversion, byte-neutral. - Cast a callee that is a DEFINITION (not a forward-decl, so un-relaxable):
((R(*)(A))func_Z)(args)— the §17a-1 cast idiom, which GLM derived independently. - Match a DATA extern's type to the TU's (
intvsu32at the same width → identical loads/stores; pick the declared one). - The HARD limit (why 6/7 fail): a def with a narrow-scalar param by value (
u16/s16/u8/s8/char/short/float) can't be no-proto-relaxed (K&R default-promotion changes the ABI) AND often can't match the TU's incompatible prototype — this is the irreducible narrow-param wall (§17-stop); GLM correctly diagnoses it (u16params emitsh; widening tos32would emitsw) but cannot dissolve it. Strategic read: GLM's role is (a) a $0.03–0.08/fn direct drafter for the def-conflict-FREE hard band (~22%, which v3 can't touch), and (b) an idiom TEACHER (capturereasoning, distill here + into corpus) — NOT a wall-breaker. The real lever past the wall is community labor (the public flip, Fable5 review §4.3), not a bigger model. ← §30 CORRECTS THIS for a model that reads the gcc SOURCE.
§30 — Fable5Max cracks §20/§10 "unsteerable" from the gcc SOURCE: store-vs-load is a /s aliasing flag, the def-side wall has a macro escape, + the birthing-boost (Phase 23, byte-proven on giant func_8014EE14 248 ins ×134)
A single Fable5Max agent (an Agent with model: fable, given the target asm + tools/ghidra_c/ + this cookbook + the match_one→gate_stage loop) matched a 248-ins reach-134 GIANT on the §20/§10 store-vs-load wall — the class 22 phases of Opus/GLM called "CONFIRMED unsteerable" — by reading the actual gcc-2.7.2 source (tools/reference/gcc-papermario) with -da RTL dumps. It banked ×134 (leaf MATCH (248 ins); whole-binary banked:1; check-all 136/136; dedup-check 1780/0). The §29 "not a bigger model" verdict is corrected: a frontier model that goes to the compiler INTERNALS is a wall-breaker for the codegen classes. Three byte-proven idioms:
- STORE-vs-LOAD IS A DETERMINISTIC ALIASING FLAG, NOT A SCHEDULER TIE-BREAK (revises §10/§20 "unsteerable"). gcc-2.7.2
expr.csetsMEM_IN_STRUCT_P(/s) only when a load's address is a member/aggregate ref or was "computed by addition" (PLUS_EXPR). The front end foldsp[0]→*p, so zero-offset / bare-deref loads never get/s→ they carry a hard true-dependence on an aliasing fixed-symbol store (D_xxx = 0) and get stuck below it; offset/member loads have/sand hoist freely. So the store-vs-load "coin-flip" is a binary flag you SET from C:- Grant
/s(make the load hoist over the store): write it as a struct-member ref →((struct { s32 field; } *)p)->field. Use an ANONYMOUS struct in the cast —dedup_propagate(line ~366) rejects inline named structs, so anonymous keeps the/sflag AND stays propagatable ×134. - Deny
/s(keep the load below the store, e.g. a separate reload the target shows): keep it a bare*p/p[0]. (CSE's fixed-scalar-store invalidation only kills non-/sentries — that's what forces the target's separate reload.) - The tell in any diff: a zero-offset pointer load stuck on one side of a fixed-symbol store while offset loads float. Re-test candidates:
func_8014F2E0,func_80150528,func_8014EA4C(close=6), and every §10/§20 "store-vs-load unsteerable" backlog verdict.
- Grant
- THE DEF-SIDE RETURN-TYPE WALL HAS A MACRO ESCAPE (extends §29). When a matching def must return
s32(avoidreturn DCEs a computed local → frame shrinks → no match) but the only conflicting caller-decl is a sharedDEFINE_func_*macro that DISCARDS the return: widen that macro'sextern void func_X(...)→extern s32 func_X(...). It's byte-neutral for the caller (the return is discarded — verifiedcheck-all 136/136fleet-wide). §29 said "no escape" because a bareINCLUDE_ASMstub declares no C symbol — but a macro DOES declare the symbol via itsexternline, so there is one. (The narrow-scalar-param-by-value wall from §29 still stands — this dissolves only the return-type conflict.) - THE "BIRTHING-BOOST" PROLOGUE-ORDER LEVER.
sched.c:adjust_priority(pre-reload only) boosts to max priority any insn whose dest reg is set exactly once in the fn (birthing_insn_p:REG_N_SETS==1); sched1 schedules each bb backward, so a boosted insn is picked early = placed late. Single-set param copies (s0=a0) then sink below multi-set const inits (s6=0/s5=8, reassigned in a switch → never boosted). Fix: one zero-byte NON-volatile re-tie__asm__("" : "=r"(x) : "0"(x))on the param, placed in a LATER basic block (after the switch). It counts as a 2nd SET (boost dead), emits nothing, adds no bb0 edges → all inits tie and the LUID/source-order tie-break restores params-first. Generalizes to any wrong prologue/init ORDER between single-set and multi-set defs.
Meta-lesson (feeds effort-map / R17): the "idiom well is dry / wall is intrinsic" verdict (T10.8/T10.9, §29) was model-relative — true for GLM and the local 7B, false for a frontier model that reads the gcc-2.7.2 source. The wall-breaker recipe: Agent(model:fable) + tools/reference/gcc-papermario (RTL -da dumps) + this toolkit + the match_one/gate_stage loop, on ONE giant at a time. Cost ≈ 375k agent-tokens / giant across the leaf-crack + whole-binary integration (2 rounds). NB: match_one (isolated) masks in-TU declaration conflicts — always finish on the whole-binary gate_stage (the §20 stale-.o trap can fake a pass; force a clean compile).
§30a — §30 generalizes to the FULL near-miss backlog (via STANDARD Opus agents, not just Fable5) + 2 more steer levers + the mechanical-integration throughput unlock (Phase 23 (a))
A toolkit pass re-tested 3 backlog "store-vs-load / CONFIRMED unsteerable" verdicts with §30, run by standard Opus agents (~80–130k tokens each) APPLYING the documented idiom (not Fable5 discovering it). Result: 2 banked ×134 (func_8014F2E0 66 ins, func_80150528 53 ins; fleet 64.74→64.82%), 1 partial (func_8014EA4C 6→3 — its store-vs-load part dissolved by §30, but an orthogonal abs-coalescing/frame-fragility wall survives = genuine permuter/Fable5 territory). The "unsteerable" backlog is largely MIS-VERDICTED — these near-misses ARE matchable, and standard agents applying a GROWING toolkit crack them cheaply. Three byte-proven additions:
- §30
/sREFINEMENT — a cast-wrapped PLUS does NOT get/s.expr.c:5535expr.c:4577(guard 4570-4576, insidecase INDIRECT_REF:@4540, inexpand_expr@4026 — citation corrected 2026-07-28; 5535 was a gcc-2.8.1 line and points atMIN_EXPR/MAX_EXPRoptab code in our 2.7.2) grants/swhen theINDIRECT_REFoperand is a top-level PLUS_EXPR;*(T*)(p + k)puts a NOP_EXPR (the cast) on top of the PLUS → no/s. Precision (same re-derivation): the guard is a 4-way OR, not a single test —/sis ALSO granted when the operand is aSAVE_EXPRwrapping a PLUS, when the dereferenced type is itself an aggregate (AGGREGATE_TYPE_P (TREE_TYPE (exp))), or when the pointer is anADDR_EXPRof an aggregate. So the cast-defeats-/srule holds for a scalar-typed deref through a plain pointer, which is the case this idiom is about — do not read it as an absolute. Only a bare-typed PLUS (q[k],qa typedT*) or a COMPONENT_REF (expr.c:5891expr.c:4888,case COMPONENT_REF:@4748 — genuinely unconditional: theif/elseabove it only chooses how the address is formed, both arms fall through to the same unguarded set; NB the otherMEM_IN_STRUCT_P (op0) = 1at 4873 IS conditional (BLKmode bitfield path, returns early at 4876) and is not this one) gets it. The universal grant is the anonymous-struct member-ref((struct{s32 f;}*)p)->f(COMPONENT_REF path), NOT*(s32*)(p+off). (byte-proven func_8014F2E0, func_8014EA4C). - IV-COMBINE: keep gcc's
combine_givsat N induction vars with a SINGLE base pointer. Far-field accesses off a separateq = base + offpointer make cc1 strength-reduce a spurious extra IV. Collapse to ONE base pointer with all fields as plain byte-offsets → cc1 re-combines to the target's IV count. NB: CONFLICTS with the §30 anon-struct read (which introduces a separate pointer → re-triggers the split) — read the diff and pick the lever the target's IV structure demands. (byte-proven func_80150528). - INLINE LOOP-LIMIT → preheader-hoist. An invariant loop limit written as an inline expression
D_x + Kin BOTH the entry guard and the loop condition (NOT a cachedendlocal) makes gcc's loop optimizer hoist it into the preheader — producing the preheader copy insn AND the target's callee-saved regalloc. A cachedu8 *endlocal pins it in one reg = wrong. (byte-proven func_80150528).
INTEGRATION IS MECHANICAL — the throughput unlock. All 3 giants banked this session (func_8014EE14, func_8014F2E0, func_80150528) hit the SAME whole-binary near-1 after a perfect leaf match_one: a DEFINE_func_* caller macro declares the fn extern void but the matching def needs s32 (a void return DCEs a live local → frame shrinks → no match). Fix = §30 #2: widen that caller macro's extern void→s32 — byte-neutral (caller discards the return; check-all 136/136 confirms). This is TOOLABLE — a gate_stage recovery pass that, on a leaf-MATCH-but-near-1 fn, tries widening a discarding-caller-macro's extern to the def's return type would let every agent's leaf-MATCH auto-bank. Campaign implication: leaf-matching the near-miss backlog is now cheap (Opus agents + the toolkit); the bottleneck is the mechanical integration step → build the tool, then the ~95 "schedule"/80 "regalloc"/31 "hoist" backlog verdicts (many mis-labeled) become an agent-wave harvest, with Fable5 reserved for the genuinely-novel residuals (frame-fragility/coalescing walls).
§31 — THE gcc-2.7.2 CODEGEN MAP: pass → residual → C-lever catalog (Phase 23; 4 Fable5 agents read the compiler source)
Instead of reverse-engineering the compiler function-by-function (Fable5 ≈ 375–475k tokens/giant), 4 Fable5 agents read the gcc-2.7.2 passes directly and produced source-cited, byte-proven, rerunnable catalogs. The full detail is in docs/gcc-2.7.2-map/{sched,regalloc,loop,cse_expr}.md — consult those for the exact lever + exemplar + file:line per class. This §31 is the INDEX + triage; apply it, drop to the source only for a class not covered here (then add it, R16).
⚠️ SOURCE-VERSION CORRECTION (propagate everywhere): tools/reference/gcc-papermario is gcc 2.8.1, NOT 2.7.2 — a behavioral difference (2.8.1 &&0-disables biv-elimination paths that are ENABLED in the real 2.7.2 cc1). The vanilla gcc-2.7.2 source is now at tools/reference/gcc-2.7.2/ (SETUP §5.6) — cite it. Every byte-proven lever we have still stands (all validated via match_one against the actual pinned 2.7.2 cc1, the ground truth); only the source citations in §17/§30 were on 2.8.1.
TRIAGE TABLE — route a residual by class → {STEERABLE (lever in the catalog) | INTRINSIC → permuter}:
| pass-group (catalog) | STEERABLE (byte-proven levers) | INTRINSIC → permuter |
|---|---|---|
scheduling sched.md |
S1 LUID=source-order (transcribe target order); S2 birthing-boost — both directions (fresh single-set local to create a sink; §30#3 re-tie to kill it); S4/5/6 load-gap-filler / mem-unit spacing / hazard-front-move (a 4th rank rule — §25 corrected); S7 prologue saves; D1 delay-slot content; D2 eligibility; D3 eager-steal | S3 load/mul chain-priority sink; S11 the LUID⊗alloc coupling knife-edge (func_801571C4) — ⚠ DOWNGRADED (Phase-24 T5b): try S12+S13 first (reused-s32-temp fence · body-local param copies · asm-copy · dead-read fence — sched.md §6); func_8014E048 (35-off "intrinsic") MATCHED+banked this way |
regalloc/reload regalloc.md |
RC-1 spill-slot shape (= DECLARATION order, not first-assignment); RC-2 wrong-value-spilled (allocno density — def/use placement); RC-3 $s-order; RC-4 coalescing un-tie; RC-5 pin side-effects (4 channels); RC-7 remat-vs-spill; RC-8 reload artifacts |
RC-6 pressure-lock (func_801770E0); RC-9 cross-block copy-fold when frame-fragile |
loop.c loop.md |
L1 IV-count/anchor (single-base-ptr); L2 index-biv elimination; L3 loop reversal; L4 hoisting/preheader (inline-limit); L7 invalid-loop; L8 increment-pos | L5 final-value compensation; L6 giv-add order |
cse/expr cse_expr.md |
cross-call ADDRESS-caching = hoist-vs-remat — STEERABLE (nested-block ptr + post-call volatile output re-set); cross-call VALUE-CSE (phantom $s, §30); /s aliasing — full 4-arm model + store-side flush lever; stack layout |
cse.c 1000-insn table flush (giants); QImode never gets /s |
WALLS BROKEN this study (classes long "CONFIRMED unsteerable", now byte-proven steerable): §10/§20 hoist-vs-remat (the biggest — blocks the 400-ins func_80132784); store-vs-load (/s flag, §30); dbr delay-slot (D1 + S2 fresh-local — func_801770E0 53→49, correcting its own agent's "unsteerable" verdict); the birthing-boost sink both directions. Incidental banks from the study: func_80149374 ×134 (+ func_801493D0 leaf-matched, whole-binary-deferred). Genuine remaining walls (route to permuter, don't hand-grind): S3 chain-priority sink, RC-6 pressure-lock (true form: every edit explodes 20+ insns), the cse mega-flush. S11 was downgraded in Phase-24 T5b — func_8014E048 (the canonical "S11 intrinsic" seed, 28-off even after the directed permuter) fell to the S12 reused-s32-temp fence + S13 head-skip escape + RC-10 preference steering (sched.md §6 / regalloc.md §F); audit pins + try those before any S11 verdict.
DIAGNOSTIC TELLS (pick the class in seconds): $t0 in a spill slot = reload artifact (spilled pseudo, not a source MEM); "every small edit moves 20+ insns" = RC-6 pressure-lock → permuter; a load stuck below a D_x=0 store while offset loads float = /s-flag (§30); a phantom 7th $s-reg held across the fn = cross-call CSE; a prologue init in the wrong order = birthing-boost (S2). The 16-row full tells table is in each catalog.
HOW THE CHEAP TIER USES THIS: given a near-miss diff → read the tell → look up the class's lever in the catalog → apply → gate. This is the artifact that lets Opus agents and the local model apply compiler-internal levers without reading 80k lines of source — the permanent-knowledge payoff.
§32 — The region-a CAMERA-GIANT idiom set: struct-base hoisting + 4 sibling levers (Phase 24 T7, Fable5-cracked on func_80129CF8 191 ins, match_one MATCH; transferable to the 6 sibling giants)
The region-a (ov_SC01_077_a.c) giants Ghidra flattens into per-global lui/%lo — but the target holds global base ARRAYS in callee-saved regs across the whole function. Fable5 read the gcc-2.7.2 source and cracked func_80129CF8 in 3 edits (202→143→10→0), no pins, no permuter. The idiom set (each transferable — .run/t7/func_80129CF8.c is the worked example, .run/t7/GIANTS_SURVEY.md the target list):
- Hoisted
$sNbase = an explicit POINTER LOCAL, assigned AFTER the first call, in the target'sluiorder (p2 = D_800AF630; p1 = D_80126DB8;right after the firstjal). gcc-2.7.2 has no cross-bb CSE, so a base living in a callee-saved reg across calls/branches can ONLY come from a source local — you cannot get it from a bare global access. Noregister __asm__pins needed: allocation order = density priorityfloor_log2(n_refs)*n_refs/live_length(global.c:594 allocno_compare) then first-fit regno (mips.h has no REG_ALLOC_ORDER) → most-referenced ptr→$s0, next→$s1, next→$s2. The init placement survives because sched1's ascending-LUID tie-break (sched.c:2414 rank_for_schedule) keeps zero-dep sets in order. A single-set/single-use base local is SAFE from the RC-7 init-sink (its hi/lo SET_SRC is a LO_SUM → failsrtx_equal_patlocal-alloc.c:1169-1172). - Branch polarity is READ OFF THE TARGET OPCODE (not Ghidra's
if): targetbeqz $v0,.Lcopywith the other arm as fall-through ⇒ writeif (sel != 0) {fallthrough-arm} else {copy}. On func_80129CF8 this single inversion fixed 133 of 143 mismatches. lw/lw/nop/addu/swper-element ladders = the S12 reused-s32-temp fence —a = p1[i]; b = cam[j]; cam[k] = a+b;reusing ONE(a,b)pair across all elements. But leave the LAST element in Ghidra's fresh-temp shape (t = p1[last] + cam[..]; cam[..] = ..; cam[..] = t;) — its unfenced load is what the scheduler hoists into the previous element's load-delay slot, and a following call-arg&cam[..]fills the gap before it, both automatically.- A grouped 3-loads/3-stores copy fed by
la $reg,SRC(split lui/addiu) = a 32-byte STRUCT ASSIGNMENT, never scalar copies:*(RView*)(p2 + 6) = *(RView*)D_800AE688;(RView = the GsRVIEW2-shaped 32-byte struct, now inengine_types.h).expand_block_move(mips.c:2361) emits onemovstrsi_internalfor ≤2*MAX_MOVE_BYTES(32) with 4 scratches;output_block_moveburns the last scratch onla a1,SRC→ 3 data regs → the 3+3 grouping. The dest must go through the pointer local (p2+6, folded to0x18($s2)); a bare global dest is CONSTANT_P and degrades to 2 regs. Scalara=src[i]; dst[i]=a;copies schedule as lw/lw/sw/sw pairs — WRONG shape. - Frame bigger than args(16)+saves by a round chunk = a DEAD LOCAL AGGREGATE: add an unused
RView view;(32 B) to reserve the missing 0x20 — gcc-2.7.2 assigns stack slots to local structs/arrays at expand time regardless of use, and -O2 never deletes them. Suspect this whenever a sibling's frame is 0x20/0x28 over-accountable. - This class needs NO pins, NO asm fences, NO permuter — the interp-loop software-pipelining (a2/sll/lh-next/sra/sw + a3-in-jal-slot) is deterministic sched2+dbr output once the bases sit in $s0/$s1/$s2.
BANKING (Phase-24 T7b — RESOLVED, see §33): a freshly-matched giant MATCHes standalone (match_one) but its loose data decls (struct BigCopy / s32 / s32[] / u8[] / s8-vs-u8) collide in-TU with engine_core.h when you try to bank it. The fix is a decl-reconcile — declare each symbol its canonical type and cast byte-neutrally at the access site (cam = (s32*)D_80126948;, p1 = (s32*)&D_80126DB8; for a struct BigCopy base, D_801151D4 = (s32)cam;, *(u8*)&D_801150D6 to force lbu under s8) — done by HAND for func_80129CF8, now automated by tools/reconcile_decls.py (§33). R14 correction: the T7 note that "banking ×134 hits the wall" was a MISDIAGNOSIS — once the ×1 bank is reconciled, dedup_propagate --recover propagates it to all 134 overlays byte-identical for free (proven: func_80129CF8 ×134, clean fleet 136/136). The wall was only ever the ×1 reconcile of the loose draft, not the propagation.
§33 — Automating the giant decl-reconcile: tools/reconcile_decls.py (the DATA analog of §20's cast_call_sites) + a fleet-majority type oracle (Phase 24 T7b, byte-proven on func_80129CF8)
The gap. A freshly-matched giant/wave draft byte-matches STANDALONE with its own guessed decls, but to bank ×1 (and then propagate ×134 via a shared DEFINE_func_* macro that EMBEDS its externs) its callee/data externs must be FLEET-CANONICAL, else conflicting types in-TU. cast_call_sites (§20) does this for callee FUNCTIONS; reconcile_decls does it for DATA symbols D_XXXX — the class the giants hit (they hoist global base ARRAYS, §32). It was the last manual step in the giant pipeline.
The oracle (fleet-majority type picker) — new; none existed (gen_harvest_targets read only engine_core.h + ONE overlay, first-seen-wins, never saw a fleet disagreement). reconcile_decls.canonical_data_map(): an engine_core.h macro decl is AUTHORITATIVE (the shared/propagated set every overlay co-instantiates — conform to it and the macro can't conflict per-overlay); else the plurality across ALL overlays + resident.c (tie → lexicographically-first spelling, deterministic). Verified 8/8 on func_80129CF8's symbols (--print-canon D_XXXX inspects one).
Byte-neutral cast taxonomy (the DRAFT's decl type = the intended access; cast every use to reproduce it under the CANONICAL storage decl → gcc folds the compile-time cast → identical opcode; the whole-binary byte-gate is the sole arbiter, fail-closed):
| draft decl | canonical | decl → | access → |
|---|---|---|---|
array Ed[] |
array Ec[] (Ed≠Ec) |
Ec[] |
D_x → (Ed*)D_x; D_x[i] → ((Ed*)D_x)[i] |
array Ed[] |
struct / scalar | canon | D_x → (Ed*)&D_x (address-of the object) |
scalar Td |
scalar Tc (signed/width) |
Tc |
D_x → *(Td*)&D_x (forces the Td opcode, e.g. lbu under s8) |
ptr P* D_x |
scalar Tc |
Tc |
D_x → (*(P **)&D_x) (cast-lvalue: store/read a pointer in the scalar slot) |
Implementation: ONE re.sub per symbol with a replacement fn (captures the optional leading & / following [), single-pass so a symbol in several forms on one line can't double-wrap (the first draft double-wrapped the ptr-store — the two-sub read+write templates re-matched; collapsed to one pass). |
Pipeline placement: canon_resident_calls → cast_call_sites → reconcile_decls → sig_unify → harvest_verify --chunk 1 (wired into gate_stage; idempotent / no-op on drafts without a data conflict → can't regress the wave). Standalone giant use: tools/reconcile_decls.py --overlay <ov> --src-file src/<ov>/<ov>_a.c --in <drafts> --out <drafts>-rc.
Byte-proof (T7b, no Fable5 needed): the full loose func_80129CF8 (intended decls s32 D_80126948[] … , un-cast accesses) → reconcile_decls (5 symbols reconciled) → swapped into the DEFINE_ macro → make build BINARY=ov_SC01_077 = BYTE-IDENTICAL d19c9580.
R14 — the tool is an AUTOMATION, not an "unlock": func_80129CF8 banks ×134 FREE via existing dedup_propagate --recover once reconciled (the "×134 wall" was a stale-asm misdiagnosis, §32). reconcile_decls' value is removing the manual ×1-reconcile step for every future giant / loose draft (the 6 sibling giants + the wave tail), not unblocking propagation.
§34 — The func_80138ED0 giant crack: gcc-2.7.2's 3-qty sort bug + the zero-byte asm allocation toolkit + the giv-init fence (Phase 24 T5; Opus→close=21, Fable5→MATCH ×134)
The 2nd region-a giant (159 ins, bit-unpack/tilemap; 2 giant-local data bases, callees func_8013914C/func_800599B8). Opus applying §32 reached close=21 (all semantics/control-flow/constants exact); Fable5 reading the vanilla gcc-2.7.2 source cracked the pure regalloc/schedule residual — every class C-reachable, no permuter. Each lever byte-verified via match_one + gdb-on-cc1. The banking then took the standard pipeline (cast_call_sites reconciled func_8013914C (u8*,u16*)→canonical (s32,s32)+call-site cast; reconcile_decls a NO-OP — the 2 data bases are giant-local, no fleet conflict) → ×1 → dedup_propagate --recover → ×134 byte-identical (pins/asm body propagates fine; func_800599B8's lone (s32,s32) decl is in ov_SC01_077.c, a different TU from the _a.c bank, so no per-member conflict).
THE HEADLINE — gcc-2.7.2's 3-qty local-alloc SORT BUG (local-alloc.c:1441-1463/:1494-1516). For a block with ≤3 local register quantities, the unrolled comparison switch compares fixed qty numbers (qty_compare(0,1),(1,2),(0,1)) but exchanges order slots — when pri(q1) is highest the third compare re-fires and undoes the first swap, so 3-qty blocks allocate in qty-CREATION order, not density order (≥4 qtys go through qsort, correct). Symptom: a low-density local grabs a reg it shouldn't. Fix: a zero-instruction DECOY qty (asm("":"=r"(decoy):"r"(x)); asm("" :: "r"(decoy));) bumps the block to 4 qtys → the qsort path → correct density-order first-fit (find_reg, global.c:904; regs_used_so_far pre-seeded with the call-used regs, global.c:352-355). A real compiler bug, now a reusable lever.
THE ZERO-BYTE ASM TOOLKIT (allocation/schedule dials that emit NOTHING — the byte-gate certifies the induced codegen):
- input-only dummy
asm("" :: "r"(v))— floats tov's def; a ref-count / density dial (raise a pseudo's priority / extend a live range one way). - multi-input dummy
asm("" :: "r"(a),"r"(b))— anchors at the LATEST def; a lifetime-extender / joint-release (releases a,b together in sched1's backward pass →rank_for_schedule's class rulesched.c:2385, cost-1 dep beats cost-2 load, orders the emit). - def+use pair
asm("":"=r"(d):"r"(s)); asm("" :: "r"(d))— mints a decoy qty (the 3-qty-bug fix). - giv-init fence
asm("":"=r"(ba):"0"(ba)); dst = ba;— forcesemit_iv_add_mult's giv-init MOVE (loop.c:5556 if (reg != result) emit_move_insn) =addu dst,ba,$zero; the general fix for the "gcc coalesced the giv init, dropping one instruction → full count mismatch" class on any giant with a counter-derived pointer. (Found by the Opus pass; kept.)
Statement-position / type levers (no asm): a leading pb = param_3; rides sched.c:3191-3215's "don't delay getting parameters" pin so combine folds the parm-save into it (prologue order); an explicit u32 pv = uVar1; before p = base; replaces loop.c's move_movables hoist-at-loop_start (loop.c:1652/1810) with source order; a HImode % 3 (a u16 var) defeats gcc's x%3==0 → beq (x/3)*3,x fold.
gdb-on-cc1 (the method that settled it): the shipped tools/bin/gcc-2.7.2-psx/cc1 is i386-static WITH symbols — breakpoint find_free_reg/post_mark_life (.run/t7/fable/gdbtrace.gdb) to dump the real qty order + register grants when hand-modeling stalls. Cite the vanilla tools/reference/gcc-2.7.2/ tree (now complete — global.c/local-alloc.c/reload1.c/toplev.c/function.c/flow.c/… from the FSF tarball; sched.c/loop.c/mips.c verified byte-identical to vanilla). Pass order (why upstream fixes reach the prologue): sched1 → local_alloc → global_alloc/reload → prologue threading (toplev.c:3103) → sched2 (:3117) → jump2 → dbr; prologue saves are sched2-scheduled, everything upstream tunes sched2's LUID tie-breaks.
Gotcha: parallel match_one runs on the SAME function share .run/match/<fn> — pass a unique --work dir or the scores are garbage.
§35 — The region-a sibling-giant harvest: difficulty ≠ $s-reg count (it's global-array hoisting) + the banking recipe + new loop idioms (Phase 24 T7, 5 parallel Opus-Max agents)
Cracking the 5 remaining region-a giants via parallel Opus agents applying §32/§34 surfaced a ranking law + a reusable banking recipe.
THE RANKING LAW (retires the "8-$s-reg = hardest" heuristic): a giant's difficulty is set by whether it hoists GLOBAL base ARRAYS into callee-saved regs, NOT by $s-reg count. gcc-2.7.2 has no cross-bb CSE, so a hoisted global base can only come from a source-local pointer in the exact lui order (§32#1) — the genuinely hard part, needing Fable5 for the last-mile regalloc. Giants whose $s pressure is param / local / output-buffer derived (even all 8 $s0–$s7 live) one-shot with Opus alone (density-order first-fit). Byte-proven: func_8012D098 (189, all-8-$s) + func_8012EC04 (178, sibling+GTE-tail) MATCHED one-shot; the 3 residual monsters stall only on tiny (2–15) compiler-internal walls, not the $s allocation. Triage a giant by its %hi data bases, not its $s count.
THE BANKING RECIPE (matched giant draft → ×134; per-giant integration is mechanical, §30a):
fix_arity_callers --apply --any-proto --from-file <fn> --drafts <dir>— no-proto the caller extern conflicting with the matched def-sig (the DEF-side wall: a shared macro in engine_core.h, or a sibling in the overlay, forward-declares the giant with a different sig →conflicting types). Byte-neutral (the call's arg bits are unchanged).sig_unify— canonicalize the giant's own CALLEE externs (no-protovoid f()→ the fleet's full proto; else args promote differently standalone vs whole-binary).- Strip draft-local typedefs already in
engine_types.h(a redundanttypedef …Blk16;=conflicting typesin the shared TU) and anonymize named local typedefs (typedef {…} Buf; Buf b;→struct {…} b;, else it collides with a sibling'sBuf). harvest_verify --chunk 1→ ×1, thendedup_propagate --addr <A> --recover→ ×134. Do NOT runcast_call_siteson these — it mis-casts a no-protovoid f()tovoid(*)(void)(0 params) and breaks the call (fix candidate: treat()as compatible-with-any).
New loop idioms (byte-verified; §31/loop.md fodder): dest-off-base giv — write every primitive store *(u16*)((s32)p + k) off ONE biv so they combine into a single dest-addr giv (kills giv-split); index-form source param_3[j+k] with j+=2 (not a walked pointer) reduces to one address biv; sltiu-outer/slti-inner without CSE — a redundant SIGNED range-guard that must stay slt/slti needs a separate signed-int copy (int s = u;), since only CSE-reuse canonicalizes signed→unsigned; div2 range-extension — a zero-byte asm("":: "r"(x)) after the last flag-test extends a pinned reg's live range so a trailing andi lands in the target reg.
The 3 deferred residuals (Fable5 batch — close=2/10/15; precise root-cause in .run/t7/<fn>.c headers + docs/backlog.md): func_801392FC (2: combine folds (s16)load → atomic lh before sched2, so the pipelineable split-load never exists; permuter can't reach) · func_8013A530 (10 → MATCHED ×1, §36 below — the "RC-6 reload-pressure" verdict was wrong: it was the $2/$3 pins themselves (reload-retry poison + sched1 load-hoist collision); cracked pin-free via the $0-add opaque copy + condition-operand flip + else-arm density dummy, regalloc.md §G) · func_8013AF20 (15: loop-invariant constant-hoist ORDER — the two AND-masks materialize opposite, coupled to the AND-operand order; permuter plateaued at 15). All single-class, high byte-weight (×134), ideal Fable5 targets.
§36 — Fable5 giant-crack levers (Phase 24 T7 Fable5 batch; accumulates as each lands — full RTL dumps in .run/t7/fable/)
func_801392FC (loop count-load, close=2 → MATCH ×134):
- CROSS-BB COMBINE LAW. combine never spans basic blocks (
flow.c:2087—LOG_LINKS(y)created only whenBLOCK_NUM(y)==blocknum). So a value crossing a loop BACKEDGE — au16 cntreloaded at the loop tail after a call,(s16)cnt+1at the top — is a VARIABLE, not a foldable(sign_extend(mem))→lh. The "split-and-schedulable" count sign-extend the target shows is cross-BB dataflow, not a fold-defeat;reorg.c fill_slots_from_threadsteals the top-BBsllinto the loop-backbnezdelay slot + redirects the label tosra→ the duplicatedsll(preheader fall-in + delay-slot copy). Don't hunt for an un-fold; make the value cross the backedge. - VOLATILE-FRAME-PARITY. Make the loop-carried count-load
volatile. A non-volatilecntgets cse-commoned with the same-address condition read → combine's keep-load fold (newi2pat;combine.c:2089 elim_i2) keeps a dead ashift-temp'sREG_DEADalive → a(use reg)planted at the nearest label (combine.c:10835-10847) → a stale allocno grabs an extra 8-byte reload slot (frame 0x90 vs target 0x80).volatilemems are never cse-hashed (clean condition fold) yetsched.c:811 read_dependence(both mems volatile) still lets thelhhoist above the volatilelhu.volatilematches BOTH frame size and schedule — suspect it when a loop-carried count is 8 bytes frame-over. - Secondary (byte-verified): route
buf+0xFbyte-store sums throughs32temps (defeats the C-frontend QImode-plus operand-swapaddu v1,v1,v0vs the target's SImodeaddu v0,v0,v1); statement position drives sched2's BACKWARD list scheduler (INSN_LUID tie-break,rank_for_schedule) — puti++/acc+=AFTER the call to fill thejaldelay; aregister __asm__("$5")re-arm before an arm's 2nd call limits post-reload cross-jump merge depth (jump2 sits between sched2 and dbr).
func_8013A530 (the LARGEST giant, 204 ins, clamp double register-split, close=10 → MATCH ×1; dumps .run/t7/fable/a530/):
- THE $0-ADD OPAQUE COPY (new tool — the un-reversible, un-cse-able
move).register int zr __asm__("$0"); iVar7 = fc + zr;emits the byte-identicaladdu $rd,$rs,$zerobut as RTL(plus reg $0), NOT(set reg reg): cse'smake_regs_eqvnever links the two (no canon poisoning in either direction —canon_regcse.c:2545 also never rewrites the hard-reg side), and combine cannot absorb the source's load into the dest (no extend+plus pattern). A plainint iVar7 = fc;(fc pinned) gets REVERSED by combine — load lands in the pseudo, the pin becomes the copy dest, 1 insn shorter. Use it whenever the target keeps load+copy as two live registers with compares reading the LOAD and arms reading the COPY. $0 is fixed → zero RC-5 side channels. - PIN → RELOAD-RETRY POISON (RC-5 ch.2 extends to retry_global_alloc). The old
iVar7 $v1/t $v0pins put $2/$3 inbad_spill_regs(reload1.c:3900-15regs_explicitly_used); the div-magic LO/MD "Need" spill forced CASE1's 2nd-product allocno throughretry_global_alloc, which skips bad_spill_regs →mflo $t2. Unpinned:.greg "Register 177 now in 3"→mflo $v1. A pin can move a register FUNCTION-WIDE through the retry path even where the pinned var is dead. - CONDITION-OPERAND ORDER = LOAD PLACEMENT (sched1 backward + mem-unit hazard).
(int)mem < t-extvst-ext > (int)memproduce the SAME canonical slt but mirrored expansion uids; sched1's backward scheduler (boosted-group ties break toward higher uid;blocking insn N for 1 cycleswhen the sh occupies the mem unit) hoists the mem-first spelling'slhto the block TOP — birthing the reload INTO the pin/pseudo's live window (the RC-6 "reload-pressure" was really this). The>spelling keeps the lh below the addiu that kills iVar7 → reload (local, first pick) and iVar7 (global) sit in $v1 disjointly. - DENSITY DUMMY PLACEMENT vs maspsx (#APP blocks the ASPSX slot-hop). cc1 emits
[lh;lh;addu;slt;beqz]; maspsx/ASPSX-2.56 HOPS the addu over the slt into the branch delay slot (and re-inserts the load-delay nop) — but NOT across#APP/#NO_APP. A zero-byte dummy between the addu and slt kills the hop (that was the last 1-insn diff). Park density dummies inside an ARM as a 2-inputasm("" :: "r"(v),"r"(t))anchored at t's def — +1 ref lifted iVar7'sallocno_comparepriority (3/10→8/11) past the fe-load's 3/5 → iVar7 allocates first → $v1, fe → $a0. - KEEPALIVE KILLS THE DYING-HARD-REG SUGGESTION. The else-compare's slt-result temp grabbed $a1 via
qty_phys_sugg(fc/$a1 dying in that slt; suggested qtys allocate first).__asm__("" :: "r"(fc));after the clamp keeps $a1 alive there → no suggestion → plain first-fit $v0 (target). - The 2nd split is NATURAL (promoted-HI store-copy).
if ((cmp)) *(s16*)(p+0xc) = *(s16*)(p+0xe);— the condition's(int)memexpands as HI-load+sll/sra, the body's re-load cse-folds onto the HI pseudo, combine merges the extend into onelhand re-emits the HI pseudo as a subreg copy →lh $v1; addu $a0,$v1; slt ..$v1..; sh $a0for free. Never pin for this shape.
func_8013AF20 (185 ins, 3 addPrim GPU-builder loops, close=15 "const-order⊗AND-order coupling" → MATCH, permuter had plateaued 40k iters; dumps .run/t7/fable/dumps_C/):
- BITFIELD STORE = THE MASK-ORDER DECOUPLER (the libgpu addPrim idiom). A target preheader that materializes
0x00ffffff(lui+ori) BEFORE0xff000000(lui) while the body still computes*dest & 0xff000000FIRST cannot come from user-mask C (*p = *p & 0xff000000 | *ot & 0xffffffmaterializes in expression order, and every operand reorder flips the AND/OR shape with it — the byte-verified 19/182-diff coupling). It comes from the SDK'ssetaddr24-bit BITFIELD store:store_fixed_bit_field(expmed.c:556) expands value∧mask_rtx(mode,0,bitsize,0)=0x00ffffff FIRST (must_and:667 → :679-681), THEN dest∧mask_rtx(mode,bitpos,bitsize,1)=0xff000000 (:694-696), THENior(destmasked,value)(:706 — dest chain stays op0). scan_loop then finds the mask movables in that insn order → move_movables emits the preheader consts […, 0x00ffffff, 0xff000000] with the body bytes unchanged. TranscribeaddPrim(ot,p)literally:((P_TAG*)p)->addr = ((P_TAG*)ot)->addr; ((P_TAG*)ot)->addr = (u32)p;with the realP_TAG {u32 addr:24; u32 len:8; u8 r0,g0,b0,code;}(a bitfield STORE with user-mask RHS also byte-matches; the read is optional). 15→8. Suspect this lever on ANY OT/linked-list 24-bit-pointer-field target. - 2-INSTRUCTION CONSTANTS DODGE THE EQUIV LL-DOUBLING → they WIN the low scratch regs (gdb-proven). sched1 splits every insn pre-reload (sched.c:4830
try_split) → mips.md:3208large_intdefine_split turnsli 0xffffffinto lui+ori = TWO SETS →reg_n_sets==2failsupdate_equiv_regs' single-set gate (local-alloc.c:1021) → the mask ESCAPES thereg_live_length *= 2const-penalty (local-alloc.c:1064) that hits every 1-instruction const (addiu-able 3/0x40/0x3d, lui-only 0xff000000). Priorities: mask fl2(7)*7/35 = 4000 ≫ 0x3d fl2(7)*7/72 = 1944 → the mask allocates FIRST (allocno_compare) → first-fit $a2 (loops 1/2) / $a1 (loop 3). gdb ground truth: n_sets {61:1, 0xffffff:2, 0xff000000:1}; LL pre→post update_equiv_regs 36/35/33 → 72/35/66. A preheader const-register contest that looks "impossible by density" is usually this. - A do{}while(0) barrier is a LOOP-DEPTH REF INFLATOR (flow.c), not just a LUID shifter. flow weights every mention by loop_depth (flow.c:2067/2315/2501/2711
reg_n_refs += loop_depth; here: preheader set=1, body mention=2, +1 more per extra NOTE_INSN_LOOP nest). The Opus draft's barrier wrapped the three0x3dbyte-stores → refs 7→10 → pri 4166 > 4000 → 0x3d stole $a2 from the mask (the final 8 diffs). Deleting it (obsolete once the bitfield form reshaped the body) restored the refs tie → mask-first allocation → MATCH. AUDIT inherited barriers whenever a preheader-const register identity is off by one.
§37 — The T7 §G giant endgame: 6/8 cracked, meta-laws + transferable levers (Phase 24, 2026-07-07; FULL byte-verified detail + gcc-2.7.2 line cites in docs/gcc-2.7.2-map/t7g-giant-harvest.md)
Cracked 6 of 8 reach-134 §G giants — func_801571C4 (permuter), func_8014EA4C/func_801372B0/func_801770E0/func_80176D94/func_80148094 (Fable5) — plus 2 genuine walls: func_80178004 (biv-init emit-order, close=7) and func_801412A8 (allocation placement-knot, close=29), both stay INCLUDE_ASM. 4 propagated ×134 (fleet 65.75→65.91%, clean 136/136); 372B0/770E0 banked ×1 (blocked ×134 by the §A pin/asm + local-type self-containment gaps).
META-LAWS (audit BEFORE deep work):
- SIBLING-ISOMORPHISM (mandatory step-0): GPU-packet/addPrim builders come in FAMILIES. Diff your target
.sMNEMONIC sequence vs already-MATCHED siblings (grep -oP '\*/\s+\K\S+'); an immediates-only diff ⇒ PORT the banked sibling verbatim + swap constants = one-shot (76D94 fell from 770E0 in ~15 min). - "MUTUAL-EXCLUSION / RC-6 unsteerable" IS A TELL, NOT A VERDICT: "X must be early AND late, coupled through one priority number / every small edit moves 20+ insns" = a MISSING dep edge or an allocno TIE — real only in the broken graph. 770E0 (the named RC-6 exemplar) fell fully source-reachable.
- RTL DUMPS ARE NOT STRIPPED (corrects §34): the shipped cc1 with
-dr -dj -dc -dl -dgemits full.rtl/.jump/.combine/.lreg/.greg— readable ground truth before gdb (.lregrefs/length,.gregalloc-order/conflicts/preferences,(use (reg:SIin.combine= phantom stack slots).
TRANSFERABLE LEVERS (byte-verified):
- The
/s-DEP LATTICE (load+store dual): forceMEM_IN_STRUCT_Pvia a struct-member-at-offset-0 access —((struct{u16 h;}*)&D_global)->h(load, 770E0) /((struct{u32 w;}*)p)->w=…(store, 76D94) — to restore the missing store↔load dep edge (sched.c:820drop clause needs one side /s+varying, the other non-/s+FIXED-address). SCOPE: inapplicable when all mem ops are register-addressed (412A8/48094). - Allocno-priority ref-boost (48094):
__asm__("" :: "r"(v));at the TOP of a block where v is already live-through → +1 flow-ref, 0 live-range, 0 bytes; crosses thefloor_log2(refs)step inallocno_compare(global.c:588) → v wins the reg over a short block temp. - Coalescable-copy insn_count bump (78004): whole reg-file shifted by one hoisted loop const ⇒
move_movables' threshold at its>=boundary — a coalescable copy{u32 m=v; store=m|…;}bumps insn_count +1 → hoist refused; combine coalesces m away = 0 bytes. BANKABLE (vs a TU-wide global-register-var). - Const-register PIN cascades store order (770E0): store order is downstream of a const's register via sched2 anti-webs → pin the const (
register u32 c __asm__("$6")=…), stores re-place free. - S2-kill re-tie (372B0):
__asm__("":"=r"(v):"0"(v))after last use → reg_n_sets==2 → no birthing boost → source order (single-set pins, INCL. hard-reg, DO boost — corrects §34). + S2 fire-tick via consumer store order. - qty_compare-TIE audit (412A8): dozens of "schedule" diffs often trace to ONE equal-priority allocno tie (
find_free_reg, gdb) broken by qty/block-scan order — a window-temp reuse flips it. + multi-death block-var law (local-alloc.c:472: reg_n_deaths≠1 → global allocno → chaos). - CSE-dodge without a barrier:
(u16)x(zero_extend) head vsx&0xffff(AND) tail hash differently → no cross-call CSE → no extra callee-save. Prefer overvolatile-asm re-ties (a FULL sched barrier per sched.c ASM_OPERANDS).
BANKING PATTERNS (main + _a split): caller-extern reconcile (void→canonical return, 571C4); asm-label alias for sibling-decl signedness/proto conflicts (extern u16 X __asm__("D_x"), 372B0) — beats reconcile's *(u16*)&D_x cast whose address-of PERTURBS regalloc; canonical call-cast + anonymize the shared /s struct for isomorphic-sibling ports (76D94). ×134 blockers (§A gaps → tool fixes): register-asm pins + overlay-local named types fail dedup_propagate.compiles_standalone → bank ×1 (needs a pin/asm self-containment shim + a uniquely-renamed type-lift).
TOOLING (flywheel): fixed a silent permuter bug — tools/p16_permute.py header comments broke cpp → decomp-permuter no-op'd (0s) on EVERY commented draft (strip_c_comments); a SECOND silent no-op of the same class (Phase 26 session 6): a draft whose GTE ops are #defines CONTAINING __asm__ (the PsyQ inline_c.h convention — i.e. most renderer code) got its macro DEFINITIONS chewed up by hide_asm (which is built for __asm__ statements / register pins and scans to the nearest ;{}), swallowing the function itself → pycparser Function <fn> not found in base.c → permuter no-op (0s). Fix: cpp_expand_macros() pre-expands with cpp -P so each GTE op becomes an inline __asm__ statement hide_asm can carry — applied ONLY when a #define ... __asm__ is present, so macro-free drafts are byte-untouched. LESSON: the permuter reporting no match (0s) is a TOOLING failure signature, never a real search result — always confirm workers actually ran. p16_permute also gained --asm-subdir (it was hardcoded to ov_SC01_077's main object, so no core in another overlay/split object could be permuted at all); NEW tools/permuter_ils.py (warm-restart iterated-local-search — descends where a cold run plateaus: 48094 72→29). Escalation: model to close=2 by hand, THEN directed-permuter the residual (372B0's last lever fell at permuter iter 291; permuter is low-ROI at close=8).
§38 — The WHALE func_80144B9C (770 ins): the -O0 struct-assign memcpy idiom + the -O0 reach-134 ×134 rollout (Phase 24 T7 §G, cheap Opus — no Fable5, no calls.c)
The single biggest byte-weight lever in the fleet (770 ins × reach-134 ≈ +1.6%). It sat at close=2 for a whole session, tagged "needs gcc-2.7.2 calls.c + Fable5" — but a cheap Opus one-liner cracked it. Three durable lessons:
(1) THE CRACK — a STRUCT ASSIGNMENT, not an explicit memcpy() call, for the -O0 block-move. The whale is an -O0 function (prologue 21F0A003; match_one at -O2 reads only 458/770 — compile it -O0). Its 2-insn residual was a memcpy of a 0x24-byte struct. The tell: the target marshals the memcpy args through temp pseudos — lw $v0=src ; lui/addiu $v1=&dst ; addu $a0,$v1,$zero ; addu $a1,$v0,$zero — which an explicit memcpy(&dst, src, 0x24) call does NOT emit (it loads $a0/$a1 directly). That precompute is the signature of gcc's emit_block_move → emit_library_call(memcpy): the original C was a struct assignment dst = *src_ptr; where sizeof(struct)==0x24. gcc-2.7.2 -O0 expands a > MOVE_RATIO-word struct copy to a memcpy library call whose args go through copy_to_mode_reg (pseudos) then addu into the arg regs = the exact 2 extra moves. So: an -O0 memcpy whose target precomputes dst/src into pseudos + addus them into $a0/$a1 (vs a direct load) ⇒ write a struct-assign, not an explicit memcpy(). (The prior session tried every call form — casts, K&R, builtin ±-fno-builtin — but never the struct-assign; no calls.c was needed.)
(2) THE -O0 REACH-134 ×134 ROLLOUT — per-overlay -O0 split + a shared HEADER (not a DEFINE_ macro). An -O0 function that is reach-134 (byte-identical in all 134 overlays because it refs only SHARED globals, not per-overlay data — unlike the Phase-20 overlay-local -O0 cluster) still propagates ×134, but NOT via a DEFINE_func_X() in an -O2 TU: it only matches at -O0 and gcc-2.7.2 has no per-fn -O0 pragma. Mechanism (tools/rollout_whale_o0.py, tools/split_whale.py): each overlay's single .c is splat-emitted in vram order, so a line-based split at the whale's INCLUDE_ASM line carves it with zero item-parsing (before → keeps the <ov> name + nonmatchings/<ov> asm paths; whale → its own -O0 object <ov>_o0b; after → <ov>_after with asm paths rewritten). The whale's C lives ONCE in a shared header src/shared/func_80144B9C.h (NOT a DEFINE_ macro — its 7 local typedefs make a 200-line \-continued macro fragile; a plain header is clean, and the typedefs stay TU-local because <ov>_o0b.c includes only common.h + this header, never engine_core.h). One Makefile wildcard rule $(WHALE_O0B_OBJS): CC1FLAGS := -O0 compiles every <ov>_o0b.o at -O0. Registered as an h_exact dedup group whose source is the header — this works because dedup_integrate.group_members keys only on (binary, vram); a header-share is as valid as a macro-share. Reusable for any -O0 reach-134 giant (verify h_exact reach FIRST, R14 — an -O0 fn is only ×134 if it refs shared globals; the Phase-20 -O0 cluster was overlay-local ×1).
(3) THE memcpy SYMBOL — an __asm__ label, not a shared rename, and never a two-symbol alias. The struct-assign emits jal memcpy (gcc hardcodes the libfunc name). 0x8005C324 IS memcpy (libc2/MEMCPY.o) but the overlays auto-name it func_8005C324. Resolve it in the overlay-only symbol file symbols.resident.txt (memcpy = 0x8005C324) — NOT symbols.us.txt, because main DEFINES memcpy via MEMCPY.o and a shared symbol there would multiple-define in main's link. For an EXPLICIT same-address call elsewhere (the engine_core.h block-copy macro's variable-size memcpys, which can't be struct-assigns), keep the non-builtin C name func_8005C324 but give its extern an __asm__("memcpy") label: the literal identifier memcpy triggers gcc's built-in-memcpy codegen → byte mismatch, whereas the asm-label emits the same jal memcpy under a non-builtin identifier (only a benign "conflicting types for built-in memcpy" warning). splat REJECTS two symbols at one address (func_8005C324 + memcpy in one symbol file → error reading … at extract), so the single-symbol + asm-label is the fix, never an alias pair.
§39 — The ×1→×134 giant-endgame: propagate a matched -O2 giant via the NATIVE DEFINE-macro path (Phase 24 T7 §G close, 2026-07-08)
After the whale, two matched-but-×1 reach-134 giants remained — func_801770E0 (152) and func_801372B0 (207). The whale-session handoff proposed a hand-rolled src/shared/<fn>.h shared header per giant (the §38 whale mechanism). That was an -O0 necessity, not the -O2 path — both banked via the ordinary DEFINE_func_X() (engine_core.h) macro + dedup_propagate --recover. Five durable lessons:
(1) R14 — verify each "×1" claim against the bytes BEFORE acting. The handoff named THREE ×1 giants; func_8014E048 was ALREADY ×134 (a 134-member group + DEFINE_func_8014E048() in every overlay, done an earlier task — the handoff conflated its earlier ×1 state). Confirm the current member count (awk the group's binaries: list) + a representative other overlay's site (INCLUDE_ASM stub vs DEFINE_) before "finishing" any function. A stale handoff line is a claim, not ground truth.
(2) The DEFINE-macro path HANDLES pins/asm/anon-structs — the whale's shared header is -O0-ONLY. func_8014E048 (register-__asm__ pins + volatile-asm barriers), func_801770E0 (pins + anon-struct /s casts), and func_801372B0 (pins + asm-label externs + zero-byte barriers) ALL propagate ×134 as plain DEFINE_func_X() macros — compiles_standalone (real cc1, -O2) accepts every one (the handoff's "compiles_standalone REJECTS pin/asm bodies" was WRONG; the real blockers are (3)–(5)). Reach for a whale-style separate-object shared header ONLY when the fn is -O0 inside an -O2 TU (gcc-2.7.2 has no per-fn -O0 pragma → it needs its own object). An -O2 giant = the native macro path, full stop.
(3) THE overlay_files POST-WHALE-SPLIT GAP (the reusable fix). The whale rollout (§38) split every overlay's <ov>.c into <ov>.c/<ov>_o0b.c/<ov>_after.c, but dedup_propagate.overlay_files only scanned _a/_o0 — so every function in the post-whale _after region was invisible to dedup_propagate, for BOTH source-def-find (func_801770E0 → "no source overlay has it matched") AND stub-replacement in the 133 members. Fix = add _o0b/_after to the suffix list. Rule: any new per-file split suffix must be added to overlay_files or its functions silently can't propagate (a green per-overlay gate never fires because the site is never found — verify via the fleet dedup-check member count, not just the build).
(4) find_site's backward extern scan STOPS at a non-extern line — keep the extern block contiguous. func_801770E0's 5 externs had a /* comment */ on its own line between them; find_site collected only the externs below the comment and dropped the 4 above → compiles_standalone failed on undeclared callees. Relocate any standalone comment ABOVE the extern block (byte-neutral). (A trailing extern …; /* … */ comment IS tolerated; a whole comment LINE mid-block is not.)
(5) LOCAL-TYPEDEF giant → UNIQUE-rename + lift to engine_types.h, NEVER the bare name. func_801372B0 declared local SVEC/GLINE typedefs (above the fn, not in the extracted body) → compiles_standalone (common.h + engine_types.h only) failed on the undefined types. Critically, a DIFFERENT SVEC (u16 fields — a different fn) lives in _after.c, so lifting the bare name would double-define with a conflicting layout fleet-wide. Fix: rename to Svec_801372B0/Gline_801372B0 (all occurrences confirmed scoped to the fn's region first), lift the renamed types to engine_types.h (the plan-builder explicitly allows bodies USING engine_types.h types — only inline typedef/named-struct{ DEFS are rejected), byte-verify the source overlay unchanged (d19c9580), then --recover. The rename is byte-neutral (pointer-passing + struct-member codegen is layout-driven, not name-driven).
Result: func_801770E0 + func_801372B0 ×134, clean fleet 136/136, dedup 1811→1813/0, fleet 65.95→66.02% (byte-weighted gain larger — 152+207-ins giants). The giant endgame's matched set is now fully ×134; the only remaining reach-134 ×1 fns are small (3 propagatable stragglers 0x80174650/0x8012A018/0x80165CA0 + 17 local-type-blocked) = the T8 tail (Drew's focus directive: giants only, no small sweeps).
§40 — Structural families: the MECHANICAL symbol-remap (crack one exemplar → remap the rest, ~0 tokens) (Phase 25 T3/T7, 2026-07-08)
The reframe: regroup the UNMATCHED frontier by STRUCTURE (h_norm) not bytes (h_exact). A multi-member
h_norm family = the SAME engine fn recurring at the SAME address across overlays, byte-shattered only because each
member references PER-OVERLAY symbols (its level's data/code addresses). tools/family_manifest.py regroups + ranks
them (Phase-25: 2,764 families / 11.1 MB; levers by ov_SC01_077 membership: draftable / matched-free / absent).
h_norm families are TEMPLATES, not free dedup (byte-proven, R14): dedup_propagate --tier h_norm on a matched
exemplar banks 0/133 siblings — one C body can't name 134 overlays' different symbols (D_80187xxx in ov077 vs
D_8017Fxxx in ov000). h_norm masks the reloc fields, so h_norm-identical ⟹ diffs are RELOC-ONLY, but the reloc
TARGETS are per-overlay → not shareable by a single body.
TRAP — extract_unit mistook a DECLARATION for a DEFINITION (Phase 26 session 8, R14). Its guard was
not ln.rstrip().endswith(";"), but m2c writes declarations with a trailing comment —
M2C_UNK func_80178D40(s32, s32); /* extern */ — so the raw line ends in */ and sailed through. The forward
brace-scan then ran past the decl and swallowed the NEXT function's body, handing remap_hseq a garbage unit.
Measured: 15 of 35 substantial-family exemplars were phantom "matches" (all still INCLUDE_ASM stubs,
including func_80178D40 and the carried-queue func_801670E4), and 3 more anchored on the Phase-17
canonical-sig layer's extern … /* match-first, arity N */ decls and templated garbage — so those families were
silently unbankable. The whole-binary byte-gate rejected every one, so no wrong match was ever banked
(G3/P9 held) — but the engine burned a build per sibling on them, and any extract_unit-based readiness analysis
was wrong. Fix: strip trailing comments before the ; test. The general lesson (the phase's FOURTH silent-skip
bug, after find_site's braces, overlay_files' splits, and reconcile_decls' fn-ptr regex): a tool that
silently no-ops on input it cannot parse is indistinguishable from a tool that had nothing to do. Prefer
fail-loud on unparsed input, and regression-gate any change to a "proven" text scanner by snapshotting its output
over the whole corpus before/after — that is what caught this.
The lever — mechanical per-overlay symbol remap (tools/family_remap.py): two h_norm-identical members have
identical instruction streams except in the masked reloc fields. So disassemble both overlay images at ADDR
(extracted/retail/<SC>.CD.dir/FILE_<nnn>.dir/0.4.dec, vram 0x80128158), positionally pair the resolved reloc
targets (jal target; lui/lo combined addr via hi-register tracking — VERIFIED 22/22 vs splat .s), and substitute
the exemplar C's per-overlay symbol NAMES (D_<ADDR>/func_<ADDR>, UPPERCASE hex) with the sibling's. Shared
EXE/resident symbols (0x8002xxxx) map to themselves. Result = the sibling's C, generated for ~0 agent tokens.
The fleet sweep (tools/family_sweep.py): for each matched ov077 fn with unmatched same-address h_norm-siblings,
remap → gate into each sibling. Two-phase (stage all remaps grouped by (overlay,split) → gate each group ONCE) so
it's ~a few hundred builds, not 156×134. GOTCHAS: (1) gate remapped drafts with plain harvest_verify, NOT
gate_stage's canon/cast/sig_unify transforms — they perturb an already-correct remap → 0-bank. (2) match_one
pre-classify first: type-using families (LOCAL types like MatEntry, defined in ov077.c not engine_types.h)
CC1-FAIL in isolation → defer them, else harvest_verify bisection explodes (~30k extra builds); recover with
build_engine_types.py --source ov_SC01_077 --strip (lift local types → engine_types.h, byte-neutral) then re-sweep.
(3) func_ names are UPPERCASE-hex in src + .s filenames.
Result (matched-free harvest): +16,512 member-matches in ONE deterministic ~0-agent-token pass, R22 clean-fleet
136/136, fleet 66.02% → 70.82%. The 11.1 MB structural-family frontier is CHEAPLY recoverable: crack ONE
exemplar per family (the only real work — agent/wave), then family_sweep fills the ~133 members free. Transferable
to ANY overlay/bank-based decomp where a fn recurs per-region with per-region symbols (→ cross-project idea).
§40a — The DECL-RECONCILE pass (type-lift so remapped drafts compile in the sibling TU) (Phase 25 T7.2, 2026-07-08)
When the matched exemplar's body references a type defined INLINE in the source overlay's .c, the mechanical remap
(§40) reproduces that reference — but the type isn't in the SIBLING overlay's TU, so the sibling draft won't compile
(the sweep's match_one pre-classify tags it type/decl). Fix = lift the type into the shared header every overlay
includes (src/shared/engine_types.h via engine_core.h), then re-sweep. +1,729 members this way (fleet
70.82→71.32%, clean-fleet 136/136). Three load-bearing gotchas:
-
The pre-classify is a FALSE-NEGATIVE for lifted types.
family_sweep'smatch_onepre-classify compiles the draft in ISOLATION (mipsel-cpp -Iinclude+ a prependedcommon.h) — it does not seesrc/shared/engine_types.h(that's pulled only by the real overlay TU, via../shared/engine_core.h). So a type-lifted family still pre-classifies astype/decleven though it compiles + byte-matches in the REAL TU. Verify against the real gate, not the proxy (R14): a singleharvest_verifyof the remap into one sibling = byte-identical. Usefamily_sweep --no-preclassifyto route every remappable exemplar straight to the real-TU byte-gate (still the sole arbiter — a wrong draft is compile-failed/reverted with bounded bisection cost, since the lift makes most compile). -
Overlay SPLIT files (
_a/_after/_o0) are SEPARATE .o TUs — a blind "lift all splits" is UNSAFE. Each split has its OWN local type namespace, so two splits can define the same-named type with a DIFFERENT layout and never clash (different TUs). Lifting such a type fleet-wideconflicting types for <T>in the other split's TU. Also, a split may locallytypedef … MATRIX/VECTOR(PsyQ SDK names) — lifting those fleet-wide SHADOWS the real SDK types. So:build_engine_types --file src/<ov>/<ov>_after.c --exclude <colliding names> --striplifts one split's types, leaving conflicting/shadowing names TU-local. Detect collisions FIRST (the tool's ownfind_defs/find_typedefsacross all splits + the existing header; a same-name-DIFFERENT-body pair is the landmine). ov077's only cross-split collision wasBuf(_a≠_after);_a'sMATRIX/VECTORwere the PsyQ shadows → both deferred. Safe mechanical ceiling = base types + one non-shadowing split minus its colliders. The rest (cross-TU renames, PsyQ-layout verification, -O0 clusters) is genuine per-type reconciliation, NOT mechanical → backlog. -
Every lift must be byte-NEUTRAL. Type defs emit no code, so
--strip(move def → header) leaves the source binary identical — but VERIFY: rebuild the WHOLE source overlay (all splits) and confirm its locked SHA (ov077d19c9580) before sweeping. A broken strip / mis-ordered header surfaces as a compile failure or SHA drift at this cheap ~30s gate, before any expensive sweep. Then the full R22 clean-fleet 136/136 confirms no fleet-wide header collision. Tools:build_engine_types.py --file/--exclude,family_sweep.py --no-preclassify.
§40b — The reloc-tracker blind spot: the indexed-global idiom that hid the "reach-1 tail" (Phase 26 Task 1, 2026-07-11, byte-verified V0/V1)
The discovery (why the "36k unique tail" was largely a measurement artifact). Both norm_stream
(sig_image.py) and reloc_targets (family_remap.py) tracked lui-hi/lo pairs but popped the pending hi on
ANY R-type write (pend.pop(rd)). gcc-2.7.2's indexed-global access D[i] compiles to
lui $at,%hi(D); addu $at,$at,$idx; lw $v1,%lo(D)($at) — the addu PRESERVES the hi anchor (the index shifts
the runtime value, not the symbol). So every function that indexes a per-overlay global array left its %lo fields
raw in h_norm → the function normalized DIFFERENTLY per overlay → it looked fleet-unique (h_norm reach-1)
when it is actually a per-location family, AND family_remap's symbol map dropped those indexed D_ symbols →
the sibling draft kept the exemplar's array name → byte-gate fail (an earlier "unremappable" wall).
The fix (≤15 LOC, reloc_targets R-type branch only — NOT norm_stream). On add/addu (funct 0x20/0x21),
propagate the pending hi to rd when a source reg holds one, else pop:
elif op == 0: # R-type
funct = w & 0x3F; rd = (w >> 11) & 0x1F
if funct in (0x20, 0x21): # add/addu: address arithmetic preserves the hi anchor
rs, rt = (w>>21)&0x1F, (w>>16)&0x1F
if rs in pend: pend[rd] = pend[rs]
elif rt in pend: pend[rd] = pend[rt]
else: pend.pop(rd, None)
else: pend.pop(rd, None)
The %lo resolution is unchanged (pend[rs] + signext(lo) = the symbol; the index is a runtime reg). Leave
sig_image.norm_stream / h_norm UNTOUCHED — the fleet metrics, the proven h_norm sweep, and the manifest all
depend on its stable (blind) hashing; the h_seq family key (mnemonic skeleton) is unaffected by the tracker, so
families still cluster correctly, and the fix only makes the SYMBOL PAIRING correct so the remapped body byte-matches.
Verified (build-free, V0/V1 2026-07-11): func_801407F4 resolves 15/15 relocs vs splat .s (pre-fix: 10),
recovering the indexed arrays D_80187B88/90/B0; func_80141100 (no idiom) stays 22/22 identical (zero regression);
across 160 real h_norm sibling pairs the new remap output is byte-identical to the committed pre-fix output (96 SAME,
0 lost), differing only where it strictly recovers indexed relocs. Reproduce: .run/v0_reloc.py, .run/v1_regression.py.
Companion fix — single-pass simultaneous substitution. The old remap applied renames sequentially
(for src,dst: re.sub), which corrupts a chained/permuted map (D_A→D_B then D_B→D_C, or an immediate value
permutation 0x10→0xA & 0x4→0x10). Never tripped on h_norm data (disjoint exemplar/sibling address spaces) but the
h_seq imm engine (T2a imm_map_tier1/remap_hseq) needs it: build ONE \b(alt|…)\b regex over the full table (symbols ∪ self-rename ∪
immediates), replace via a dict lookup on the match — each source token is matched once against the ORIGINAL text.
This is also where the T2b cross-address self-rename (func_<FROM>→func_<TO>, definition + recursion) and the
T2a immediate imm_map merge into one pass. remap(addr, from_ov, to_ov, to_addr=None, imm_map=None) — backward
compatible (to_addr defaults to addr; the pre-26 same-address callers are byte-unchanged).
§40c — The h_seq per-sibling reconcile: templating a reconcile-class crack ×134 (Phase 26 Task 8, 2026-07-12, byte-proven)
A cracked exemplar whose body needs canon_sig_reconcile to bank (type-using / Ghidra-sig — the §41 def-side
wall) cannot template plainly: the reconciled ov077 body is TU-SPECIFIC (its canonical-sig casts +
Name_<addr> collision-renames fit ov077, not the sibling TUs). Plain remap_hseq of the reconciled body
re-hits the wall in every sibling → 0/134. Proven (Task-8 validation slice): the 23 triage isolation-cracks
gate 0/23 raw, reconcile 4/15 into ov077, but the 4 then template 0/4 plainly.
The fix — per-sibling re-reconcile from the RAW draft (the h_seq port of §41c's h_norm M2 path):
family_sweep --hseq --reconcile-raw <RAWDIR> → for each sibling, family_remap.remap_hseq_body h_seq-remaps
the RAW crack draft (symbol §40b + immediate T2a + cross-address self-rename T2b) then canon_sig_reconcile
re-reconciles against THAT sibling's own TU. The whole-binary byte-gate is the sole arbiter. Byte-proven:
the 4 triage cracks templated 463/0 ×~133 this way (0 failures). This is the pipeline every reconcile-class
crack (the type-using triage cracks AND the Fable5 cores) flows through to reach ×134. PURE cracks (clean
bodies, no reconcile) still template via plain --hseq (Task 5: 399 banked). Key: reconcile PER SIBLING from
the RAW draft, never remap the ov077-reconciled body.
§31-triage — R17 applies to CODEGEN residuals, never to a compile ERROR (Phase 26 session 7, Drew asked)
Before reaching for tools/reference/gcc-2.7.2/ or the docs/gcc-2.7.2-map/, classify the failure:
| Symptom | Cause | Tool |
|---|---|---|
| The build SUCCEEDS but the bytes differ | a codegen decision (regalloc / sched / cross-jump / CSE / loop) | R17 — read the pass, or gdb-on-cc1 (§45-B) |
The build FAILS to compile (conflicting types, undeclared, parse error) |
plain C89 semantics — gcc is CORRECT | our tooling. Reading gcc source tells you nothing |
Byte-example: the func_8015AE2C ×133 sweep blocker is conflicting types for D_801812A4 — two incompatible
file-scope decls of one identifier in one TU. That is not a gcc quirk; reconcile_decls was picking a
fleet-majority canonical type instead of the type the TU can actually SEE (its §8b carried decl layer).
A loop.c/global.c read would have been pure waste. Contrast func_8017BEBC (close=2, an allocno-priority
tie in global.c): compiles fine, wrong bytes → exactly an R17/§45-B target.
Rule of thumb: "wrong BYTES" → read the compiler. "won't COMPILE" → read our Python.
§41 — The DEF-SIDE canonical-sig wall: mechanically banking a drafted giant past conflicting types (Phase 25 T5b batch-2, 2026-07-09; tools/canon_sig_reconcile.py, byte-proven on func_8013B274)
Addendum (P31 S64 t5o-t5r, func_8017BEBC @ ov_SC03_007, 246 ins) — A FINDABILITY KEY, not a new law. §41 step 3
("Cast each type-changed param AT ITS USES — NEVER via an intermediate local") also owns a purely REGALLOC
symptom, and this section's declaration-wall title hides it: a drafter hunting a register residual greps past §41
entirely. The tell: a closeness plateau that will NOT move under clamp / loop-shape / integer-width levers,
with the instruction COUNT already exact, on a function whose C declares T *out = (T *)paramN; and stores through
out[i]. The named alias is a fresh pseudo — gcc gives it its own register, which frees the incoming arg register
for a competing tail temp and shifts the whole prologue schedule. Casting in place at each store
(((T *)paramN)[i] = …) adds no pseudo and is byte-free. Byte evidence: n7 (s16 *out = (s16 *)a1; out[i] = …)
= near, closeness 17, nins 246; n9 (same body, alias deleted, ((s16 *)a1)[i] = …) = MATCH, closeness 0, nins 246;
n11 re-confirms. ⚠ The decl-order control is INERT (§67): n10 swapped the two pointer locals' declaration order,
kept the alias, and stayed at 17 — so an unchanged closeness under reordering does NOT exonerate the alias. This is
the FOURTH confirmation of §41 step 3 / §67's two-pseudo law, and the transcript printed only aggregate
status closeness nins lines, so it sharpens findability, not the mechanism.
The wall (dominant for GIANTS — ~universal, vs ~35% clean-bank for small fns): a drafter writes an
isolation-MATCH giant body (match_one c=0) with Ghidra-derived TYPED params — void func(u32 *a0, s16 *a2).
Placed in the real overlay TU it fails the whole-binary gate on conflicting types for func_X (a declaration
conflict, NOT a byte diff). The gate's sig_unify/cast_call_sites can't fix it and it banks 0/16. Two sources:
- The TU's callers reference the fn through the CANONICAL signature declared in
src/shared/engine_core.h(extern void func_8013B274(s32 a0, s32 a1, void *a2);) — but that decl lives inside aDEFINE_func_*macro, sosig_unify(which rewrites file-scope externs) never sees it. The draft's typed sig conflicts with it. - Absent an engine_core.h decl, a caller above the definition gives gcc-2.7.2 an implicit K&R
int func_X(); the draft'svoid/typed-param def conflicts with that. (This is why the overlay's "Phase-17 canonical-sig layer" at each_a.ctop usess32 func(s32,…)— it's K&R-int-compatible AND byte-neutral.)
The crack — reconcile the DEF to the canonical sig, BYTE-NEUTRALLY (3 steps, all proven on func_8013B274 → banked
d19c9580 byte-identical):
- Strip the draft's redefinitions of ambient symbols. A
typedef … P_TAG;identical toengine_types.h's is a redefinition error in gcc-2.7.2/C89 (not "compatible" like C11). Strip identical-def typedefs; stripexterndecls (func / dataD_*/memcpy) the TU or engine headers already declare — the draft's Ghidra-typed re-declaration is a conflict source. (memcpyalways: a mismatched prototype tripsconflicting types for built-in memcpy; the TU macros / builtin provide it.) - Rewrite the def signature to the canonical (
engine_core.hdecl if present, else the implicit-int-compatibles32 func(s32,…)at the draft's arity; arity-grow adds unused params so an N-arg implicit caller still matches). - Cast each type-changed param AT ITS USES — NEVER via an intermediate local. THE load-bearing insight:
u32 *a0 = (u32*)arg0;at the top introduces a fresh pseudo → gcc allocates it a different reg → regalloc shifts → byte diff (measured: cast-locals gave70ff4748, wrong). Casting the param in place —((s16*)a2)[i],((s16*)param + 1)(preserves stride!),*(T*)p— adds no pseudo, is free, and preserves the isolation-match codegen.tools/canon_sig_reconcile.pyblanket-wraps every use of a changed param in((origtype)name)(correct for index/deref/arith/member/already-cast alike). Give it--tu <split.c>so it treats that TU's already-declared symbols as ambient (strips their redundant draft externs too).
Result: 5/16 batch-2 giants banked purely mechanically (func_8013B274 80130D48 80167DBC 8016DC20 8018514C);
3 then family_sweep'd ×134. This is the phase's #1 def-side lever, now partly automated — reusable across the
whole giant tier AND the batch-1 backlog of "match_one-MATCH but gate-rejected" near-misses (the dominant gate-failure).
The residual walls (the other 11 — genuine per-fn T7, NOT this mechanical pass; backlogged with cause):
(a) non-identical ambient types — draft's SVEC/ApplyMatrixSV differ in layout from engine_types.h's (can't
strip: not identical; can't keep: conflicts) → needs a rename or a real layout reconcile. (b) data symbols declared
inside DEFINE_ macros (D_80078EB0) — macro-local, not file-scope, so stripping the draft's extern leaves the body
referencing an undeclared symbol, and keeping it conflicts → reconcile_decls.py/§33 byte-neutral-access-cast territory.
(c) primitive-typedef redefs the reconcile missed; (d) genuine byte-diff (reconcile compiles but codegen differs —
back to permuter/hand). Sweep fragility: a reconciled body carries ov_SC01_077-specific canonical sigs/casts, so
family_sweep to sibling overlays (with their OWN engine_core.h decls) byte-matches only some siblings (func_8016DC20
= 133 siblings failed → exemplar-only). A robust sweep of reconciled giants must re-reconcile per sibling TU (T7 follow-up).
§41a — v3.1: the def-side wall was ~71% TOOL-shaped — the five measured defects + the laws that dissolve them (Phase 25 T6, Fable5, 2026-07-09)
The T6 curriculum session R14-re-verified ALL 95 draftable-exemplar stubs (every draft in every
.run/drafts-t5* dir, best-of, match_one): 62/95 are genuine isolation-MATCH — the frontier's
"match" statuses were honest, and all 11 batch-2 "walls" + the 3 _o0 giants have byte-correct bodies.
A 6-iteration probe program (reconcile → splice into the REAL TU → full cpp|cc1|maspsx|as →
relocation-masked byte-compare of the fn inside the TU object, .run/t6_reconcile_probe*.json) then
decomposed the §41 wall into five mechanical defects of the v1 reconcile itself — fixed in
tools/canon_sig_reconcile.py v3.1, taking the mechanically-bankable set 10 → 44 of 62 (4,254 ins,
13 giants ≥145; func_8013DD68 gate-validated byte-identical through make build):
- Scalar-typedef dups (
typedef … u8;) must be stripped (C89 redef error) — same set match_one strips. Was 11 fns of "redef:u8/u16/s16/s8". - Canonical truth = the PREPROCESSED TU's file scope (cpp + brace-depth-0 scan), not a token-scan:
token-scanning counted macro/block-scope names as ambient (over-strip →
undeclared) and missed the TU's own decl of the fn (self-conflicting types). NB: aDEFINE_macro's extern lands at FILE scope when the macro is instantiated at file scope — and gcc-2.7.2 REMEMBERS block-scope extern types TU-wide, so both kinds bind later defs. - Never strip a draft extern — BLOCK-SCOPE-MOVE it (types verbatim) when no decl is visible above
the splice point. The draft's extern types are LOAD-BEARING (%lo-folding, access width, alignment:
an ambient-type rewrite byte-drifted 18/62 — e.g.
((s16**)&u8_sym)derefs at alignment 1 → lwl/lwr). Block-scope decls are private and legal even when a DIFFERENT file-scope decl exists below (recover_giant's idiom, generalized). Visible-above + identical → drop; visible-above + different → ambient + cast-at-use (fn callees per §17a-1; data via access-casts — alignment caveat above routes the narrower-object cases to §33/reconcile_decls TU-retype instead). - Colliding typedefs are RENAMED (
Vec3 → Vec3_<addr>, attribute-tolerant), never layout-reconciled: type names emit no code, so the def side NEVER has a real layout problem (SVEC/Vec3/Prim/S8/ApplyMatrixSV walls all fell to the rename — 2 giants banked). - Blanket use-site substitutions must skip decl lines ("decl lines are never cast" —
cast_call_sites' rule; violating it emits
extern void ((void(*)(…))f)(…)parse errors). Also: find the def on a COMMENT-MASKED copy (drafts' @stuck headers quote the sig and mis-anchor the rewrite).
The residue is three real classes (T6 curriculum tiers M3/M4/F): (a) arity conflicts with a
visible typed prototype (a banked caller's macro declares void f(void*) arity-1, the byte-true def
needs 3 params) — no draft transform can fix; the cure is the fix_arity_callers-class no-proto rewrite
of the engine_core.h macro extern (extern s32 f();, byte-neutral for the loose callers), R22-gated —
6 fns. (b) stale TU decl types from earlier banked drafts (u8 vs s16* on D_801870B0 etc.) →
reconcile_decls/§33 fleet-majority retype, then the drafts bank verbatim — 8 fns. (c) genuine per-fn
residue — 4 fns (incl. func_80166994's real mixed-arity loose-typing entanglement).
The ×134 sweep law (Q5, 6/6 proven): sweep the RAW draft via family_remap.symbol_map, then
re-run canon_sig_reconcile against EACH SIBLING's own TU, then gate. §41's "sweep fragility" was
exactly the missing per-sibling re-reconcile (the ov077-reconciled text carries ov077-specific
decisions). Sibling stubs live in the SAME split-file name fleet-wide (_after etc. — the whale-rollout
structure); read the asm subdir off the sibling's INCLUDE_ASM line.
Refuted: the batch-3 "-O0 in-context byte-diff needs an -O0-specific reconcile" — all 3 _o0
giants (329/198/154 ins) probe BANKABLE at -O0 under v3.1 unchanged; the old diagnosis was v1's
declaration perturbation, not a -O0 return-type law.
Probe-method notes (reusable): the in-TU masked byte-compare (insns_from_object(tu_o, fn) vs
insns_from_s(splat_s)) is a fast, link-free gate proxy — but it is jal-symbol-blind (mask eats
the target field), so harvest_verify stays the arbiter (G3/P9). And never hand-type a SHA: a
mistyped --good-sha made a byte-perfect gate run report MISMATCH — read it from config/check.*.sha.
§41b — T7 execution: the object-only probe OVER-counts BANKABLE by two link/rodata classes (Phase 25 T7-M1, 2026-07-10)
Executing the §41a curriculum banked 37 of the 40 non-jumptable M1 exemplars byte-identical through the
whole-binary gate (tools/t7_bank.py: reconcile-at-bank-time + harvest_verify, chunk-bet with per-round
re-reconcile for cross-fn ambient mutation). The 7-fn gap between the T6 probe's "44 BANKABLE" and reality is
two integration classes the T6 in-TU masked object-compare could not see — a sharper statement of "probe ≠
gate" (R14): the probe compiles to an OBJECT and masks jal/%hi/%lo, so it is blind to both rodata and link.
-
Switch jump tables in rodata (4 fns: the 3
_o0giants +func_8012ACE0). Their.textis byte-perfect — the unmasked diff is 100%j .L…/lui/addiu %hi/%lo(jtbl_…), all masked-EQ, zero real.textdiffs — but the switch emits a jump table in rodata (jtbl_801D836C…) that the object-only compare never looked at, and the whole-binary SHA diverges there. This REFUTES the T6 curriculum's "Q3 -O0 reconcile REFUTED" claim (the probe said the_o0giants bank; the gate says no) — the batch-3 "in-context byte-diff" finding STANDS. Route: the jump-table-in-rodata workflow (cookbook §8, the LZSS/§5a precedent — carve/match thejtbl_*rodata), F-band, NOT mechanical M1. -
Last-referencer link-wall (3 fns:
func_8016D688/D_801D9C20,func_8016D1D8/D_801D9C20+D_801D9C60,func_80165240/D_8018977C). The fn is the ONLY asm referencer of a scratch data symbol; splat auto-generates that symbol intoundefined_syms_auto.txtfrom the disassembly, so C-ifying the last referencer drops the symbol →ld: undefined reference to D_801D9C20. Compiles clean, fails at LINK (the object-only probe never links). Fix (M-linkwall tier): declare the symbol so ld resolves it — a manual undefined-syms entry or a splat data-symbol carve at that address (the bytes already live in the overlay image). Deferred pending the splat symbol-provisioning mechanics (don't guess an address into the byte-locked build). Reusable class: ANY bank that removes the last asm reference to an overlay-local data/scratch symbol.
Method upgrade for future curricula: an in-TU object probe is a necessary filter but NOT the gate — it
misses rodata (jump tables, float/string pools) and all link-time resolution. Size a "mechanical" tier from
the WHOLE-BINARY gate on a sample, or expect a ~15% object-probe over-count and treat the surplus as the two
classes above. tools/t7_bank.py (reconcile-per-round + chunk-bisection) is the reusable M1 driver.
§41c — T7-M2: the ×134 def-side-wall sweep via per-sibling RE-reconcile (Phase 25, 2026-07-10; 4,389 banks, ~0 agent tokens)
§40's mechanical family_remap (symbol-remap a matched exemplar → sibling) banks 0 for the def-side-wall
giants: the ov077-reconciled body carries ov077-specific block-scope-vs-ambient decisions, and each sibling's
DIFFERENT decompile state (different fns banked above the splice) needs those decisions RE-COMPUTED. The
Q5-proven fix, now tools/family_sweep.py --reconcile <rawdir>: per (exemplar, sibling), symbol-remap the
RAW draft (source→sibling via family_remap.symbol_map) then re-run canon_sig_reconcile v3.2 against
THAT sibling's TU, then the plain whole-binary byte-gate. engine_core.h is SHARED so the canonical sig is
identical fleet-wide; only the per-overlay symbol names + the sibling's visible-above set change.
Result: 4,389 of 4,655 member-remaps banked (94%) across 133 overlays for the 35 M1 exemplars — fleet
72.29% → 73.58% (+1.29%), R22 clean-fleet 136/136, dedup-check 1813/0. The 266 misses are per-sibling
loose-typing walls (the sibling banked a conflicting-type neighbor) → backlog. Cost: local cpp+build only,
zero agent tokens. Cost note: the per-member re-reconcile runs cpp on the sibling TU prefix for
visible_above — ~2 s/member (≈45 min staging for 4,655), then the group gate. _AMBIENT_CACHE/_VISIBLE_CACHE
are cleared per sibling-TU inside the sweep (the sibling source is static during phase-1 staging, so the caches
stay valid across exemplars for one TU). This is the endgame's economic engine for the def-side-wall giants:
crack + reconcile ONE exemplar, sweep it ×134 mechanically.
§41b addendum — M4 "reconcile_decls" tier is ALSO a probe over-count: 0/8 mechanical (Phase 25 T7-M4, 2026-07-10)
The T6 curriculum's third "mechanical" sub-tier (M4: 8 object-probe BYTEDRIFT fns projected to bank via a
reconcile_decls §33 TU-retype) is REFUTED by the whole-binary gate: 0/8 bank (reconcile_decls → canon_sig_reconcile → gate).
Root cause, same R14 pattern as §41b's jumptable/linkwall: the T6 object-probe's "BYTEDRIFT" does NOT imply a
data-type conflict. 4 of the 8 have reconcile_decls "touched 0" — no data decl even differs from the
fleet-canonical — so their in-TU drift is pure codegen (scheduling ORDER: e.g. func_8013E83C reads
D_80115118 before the prologue in the target; volatile-loss: func_801418F8's D_8011511A read-back;
callee interactions). The other 4 have real data-decl differences but reconcile_decls' byte-neutral cast
still perturbs the schedule. These 8 are F-band (byte-correct in ISOLATION — match_one c=0 — but drift
8–69 in the real TU) → permuter-ILS / §31, not a mechanical tier. reconcile_decls remains valid ONLY for a
genuine data-TYPE conflict where the cast is schedule-invariant (its §33 giant proofs); it is not a driftfix.
Net honest tally of the T6 "mechanical" projection (58 fns / ~2.5 MB): truly mechanical = M1 37 + M3-clean 2 = 39 exemplars (banked + swept ×134 = ~4,694 fleet fns, fleet 72.29→73.66%). The other 19 were probe over-counts → F-band/specialist: 8 M4 (codegen drift) + 4 jumptable (rodata) + 3 linkwall (undefined-sym) + 4 M3-residue (arity/loose-typing). Lesson (reinforces §41b): size a "mechanical" tier from the WHOLE-BINARY gate on a full sample, never from an object-only probe — it can't see rodata, link, OR in-TU codegen perturbation. Expect ~⅓ of an object-probe "BYTEDRIFT/COMPILE-FAIL" bucket to be genuine per-fn work.
§41d — void→s32 is NOT always byte-neutral: gate the RAW draft FIRST (Phase 26 session 6, byte-proven)
R14 correction to the Phase-17 canonical convention. The canonical-sig form ("s32 return — void→s32
is byte-neutral, §3a-1") is false for a void body with no return statement: promoting the return type
makes gcc-2.7.2 emit one extra instruction. Byte-proven on func_80182268 (31-ins jr, ov_SC01_077):
| draft | result |
|---|---|
void func_80182268(void *a0) (raw) |
MATCH, 31 ins |
s32 func_80182268(void *a0) (return type alone) |
DIFF, 32 ins |
s32 func_80182268(s32 a0) (what canon_sig_reconcile emits) |
DIFF, 32 ins |
The extra word is invisible in a leaf diff but lethal whole-binary: it pushed the isolated object's .text
4 bytes long, shifting every data symbol +4 → ~271,000 differing bytes and a 5-byte-longer image. match_one
said MATCH; only the whole-binary gate caught it (G3/P9 again).
The rule (generalizing §19's sig_unify lesson): every recovery pass is a FALLBACK, never unconditional.
canon_sig_reconcile exists to break the §41 def-side wall — it must not run on a draft that already compiles.
jtbl_family_bank now gates raw → (on failure) reconciled, and canon_sig_reconcile only promotes the
return type when a canonical extern actually demands it. Corollary: a function with no canonical decl
anywhere (grep engine_core.h + the overlay .c) should be banked exactly as drafted.
§42 — The F-band ≤28 regalloc crack wave: register-pin/DENSITY levers beat the permuter (Phase 25 T7 F-band, 2026-07-10; Ultracode 9-worker wave, 4/9 banked byte-identical, 266 swept ×134)
The F-near ≤28 band (14 fns / ~1,393 ins, one ov_SC01_077 h_norm exemplar each) is regalloc-order-DOMINATED
(9 of 14 = saved-register $sN allocation/ordering swaps). The permuter is structurally blind to this class:
it mutates C source, and pycparser rejects register __asm__ (§5a/§17), so a pure $sN-allocation swap has no
source-mutation reachable. Empirically proven this wave: permuter-ILS (regalloc-directed _REGALLOC weights,
8×120 s warm-restart) plateaued at base on EVERY regalloc fn (func_80134C20 stuck@3; schedule-class func_8017EF50
stuck@4, func_80168828 8→5) — 0 closed. A 9-worker Ultracode wave applying MANUAL §17/§31 levers cracked
7/9 to byte-0 in isolation, of which 4 banked byte-identical through the whole-binary gate.
The winning levers (all zero-runtime-code, semantics-preserving; full RTL in subagents/workflows/wf_0329d3c2-75c/):
- The §31 DENSITY lever (the workhorse for
$sNraces). To win a razor-thin saved-reg allocation, ADD a zero-byte dead-read__asm__ __volatile__("" :: "r"(v));on the pseudo you want gcc to prefer — it bumpsv's ref-count so the local-alloc density heuristic gives it the contested$sN. Calibrate the COUNT exactly (func_80134C20: ONE dead-read of the master reclaims$s5; TWO over-boost it into$s4→ 13-off). Proven: func_80134C20 (230, MATCH), func_801365B8 (155, 11→2). - The opaque asm-COPY for a param live-range split.
__asm__("addu %0,%1,$zero" : "=r"(copy) : "r"(orig));(or the §17 in-place re-tie__asm__("" : "=r"(p) : "0"((T)p));) forces gcc to keeporigin its incoming arg reg for early reads whilecopycarries the later reg — reproducing the target's single-pseudo live-range split. Proven: func_8017B614 (RC-9 hoist-vs-remat, MATCH). CAVEAT: reorg.c forbids__asm__in a delay slot, so an asm-copy that must fall in one lands a slot early (func_801365B8's irreducible 2-off). - Frame-pad induction:
s32 pad[2]; (void)&pad;— address-taken-then-discarded local defeats -O2 DCE, reserves 8 unused var_size bytes to match a target frame (0x20 vs 0x18), shifting every save offset;(void)&pademits zero code. Proven: func_80141A60 (MATCH). CAVEAT: frame-pad is ov077-specific — its 133 h_norm siblings ALL byte-drift on remap (each sibling's natural frame differs) → frame-pad families are EXEMPLAR-ONLY, NOT ×134-sweepable. - Array-initializer LUID shift:
s32 a[2] = {x, y};vs twoa[0]=x; a[1]=y;reorders the const-materialization LUIDs → sched2 emits the callee-save stores before the const chain (matches a target prologue-weave, S7). Proven: func_80180F10 (MATCH). - u16 zero-extend for a high-bit halfword store constant:* storing 0x8000+ through
unsigned short *zero-extends →ori $r,$zero,0xFFF8(opcode 0x34) vsshort *'s sign-extendaddiu/li -8(0x24). func_80141A60.
DIRECT register T v __asm__("$21") pins OFTEN BACKFIRE on giants — they wreck the prologue save-birthing order
and clobber the dead pinned regs (func_80134C20: direct pins = 97-off vs density = MATCH; func_80180F10: pin = 37-off
vs array-init = MATCH). Reach for the DENSITY lever first; use hard pins only when the residual is a clean,
uncontested-reg home (the dont-conclude-unsteerable memory still holds: try SOMETHING before declaring a wall,
but density > pins on the giants).
Attrition — isolation-MATCH ≠ real-TU bank (reinforces §41b): 7 iso-MATCH → 4 banked, 3 real-TU byte-drift
(compile OK, byte-differs). func_8017B614's drift = the T1 memcpy-builtin→call class (the sibling TU's
extern memcpy disables the builtin, so the worker's inlined lwl/lwr block-move lowers to a CALL) → re-crack with
field-by-field or explicit memcpy(x,y,8). func_801365B8 = a GENUINE irreducible cse-representative conflict → G4/INCLUDE_ASM candidate.
Tooling gotchas (each cost a false-fail cycle): (a) harvest_verify.py for a NON-resident binary MUST pass
--out build/<bin>/<bin> — its build() removes+sha1s --out (default build/resident/resident), so an overlay run
without it reports "final SHA None"/fail for EVERY draft even when byte-identical. (b) canon_sig_reconcile can't
extract a def whose body has a fn-pointer cast ((s32(*)(...))func) — such a draft banks RAW (no reconcile) if its
sig is already canonical (func_80180F10). (c) R22 clean-fleet: make clean nukes the WHOLE splat tree (asm/); make extract re-splits only the DEFAULT binary — you must make extract BINARY=$b for ALL 136, else 135 fail "can't open .s"
(a build-infra false-fail, not a byte mismatch).
Wave economics: 9 xHigh workers ≈ 1.66 M subagent tokens → 4 banked + 266 swept ×134 = ~270 fleet fns. The ≤28 regalloc band is genuine frontier — budget ~40-50% bank-rate per wave, NOT the mechanical tiers' ~94%.
§42a addendum — wave 2 (residuals + 29-100 band): iso-MATCH ≠ real-TU bank, the memcpy→struct-assign fix, +5 levers (2026-07-10b)
THE #1 LESSON — a crack worker must verify against the RECONCILED REAL TU, not isolation. Wave 2's 14 workers
produced 9 iso-MATCHes but only 4 banked — 5 iso-MATCHes DRIFTED in the real overlay TU (func_80136824/
80164930/8014DD8C/8016C188/80168828). The ONE iso-drift fn that banked (func_8017B614) did so because its worker
embedded the def into a scratch copy of the real split .c, compiled the WHOLE TU (builtins ON = the real
condition), and objdump-compared to the isolation MATCH — catching the drift cause and fixing it. isolation
match_one uses -Iinclude+prepended common.h; the real TU adds engine_core.h types, a memcpy decl, and the
reconciled sig — any of which shifts codegen. Wave-3+ crack prompt MUST require: after iso-MATCH, splice into a
scratch copy of src/ov_SC01_077/<split>.c, cc1 the TU, and confirm the target fn's bytes are identical modulo
link relocation — THEN report MATCH. (Cheap: one extra TU compile per worker; converts ~50% real-TU attrition to near-0.)
The memcpy-builtin→CALL fix (extends the T1 class, byte-proven func_8017B614): a small fixed-size mem-copy written
as memcpy(x,y,8) inlines to lwl/lwr/swl/swr in ISOLATION but lowers to a jal memcpy CALL in any TU that declares
extern memcpy (a sibling triggers conflicting types for built-in function memcpy, disabling the builtin TU-wide) →
byte-drift. FIX: typedef struct { u8 b[8]; } Blk8; *(Blk8*)dst = *(Blk8*)src; — struct-assign routes through
emit_block_move (identical lwl/lwr/swl/swr bytes) but references NO memcpy SYMBOL, so it is immune to the
builtin-disable. Mirrors the codebase's own family idiom (matched sibling func_8017B368 uses (*(SV4*)&D_x)=loc;).
Verify with cc1 -fno-builtin: struct-assign still emits lwl/lwr; the memcpy draft emits jal memcpy.
Five lever refinements (wave-2 journal wf_dbadb86a-6b7):
register intNOTregister shortfor a pin whose value is already sign-extended (anlhresult) —register int g __asm__("$6"); g = *(short*)p;pins to $a2 with nosll/srapenalty;register shortre-adds the extend (func_8017EF50).- Never density-dead-read a pseudo that is LIVE ACROSS A BLOCK — the
__asm__("":: "r"(v))adds a real instruction (count+1) and backfires; instead RESTRUCTURE the pseudo away (compute fresh at each use) (func_80136824). - When density fails, use STATEMENT-BLOCK reordering for
$v0/$v1birth order — group the var you want in $v0 so it is first-born + dense; density dead-reads that must keep a var live past its consumingsllproduce the wrong schedule (func_80164930). - Birthing-boost coupling: a single-set const-load (
li $v1,0x40) sinks to just before its EARLIEST-scheduled consumer, not to its C statement position — to move the load, reorder the CONSUMER store-block, not the assignment (func_80168828). - for-init LUID ordering controls the delay slot —
for (i=0, lim=0x19, p=P; i<N; i++)makesi=0win the beqz delay slot and emitslimbefore the pointerlui/addiu; a plain pre-loopint lim=…;captures the delay slot instead (func_80164930).
Wave-2 economics: 14 workers ≈ 2.47 M tok → 4 banked + 399 swept = ~403 fleet fns. Bank-rate 4/9 iso-MATCH — LOWER than wave 1 (real-TU attrition), fixable by the real-TU-verify rule above. The 5 nears (func_80134A74 71→16, func_80133AB0 →28 aligned, func_8012FCC4 beqz/jal delay-swap, func_80185BA4 65, func_801670E4 70 "irreducible") are permuter-ILS fuel / G4 candidates.
§42b addendum — wave 3 (Max, 2026-07-10c): THE STALE-OBJECT GATE TRAP + the read-global &-cast drift + fix
THE #1 METHODOLOGY BUG (invalidated wave-2's "iso-drift" labels; fix ALL gates). A per-function real-TU
check that does make build BINARY=<ov> >/dev/null 2>&1 and then runs asm-differ -o <fn> without checking
the build exit code and without removing the split .o first will diff a STALE object whenever the build
FAILS — reporting a phantom score 0 / "MATCH" for a draft that never compiled. Measured this wave: three
wave-2 "iso-drift" fns (func_8016C188, func_80168828, func_80136824) read as score-0 on the first pass, then
NOCOMPILE on a forced-clean pass (rm build/src/<ov>/<split>.o + exit-code check). Root cause of the false
score: the stale .o from a prior good build survives the failed compile, and asm-differ -o happily diffs it.
This is almost certainly why wave 2 mis-classified 5 fns as "iso-MATCH → drift" — several likely never
compiled in the real TU at all. MANDATORY gate shape (now in .run/crack3/diff.sh): git checkout <split> →
splice → rm build/src/<ov>/<split>.o → make build BINARY=<ov> and assert exit 0 → sha1sum the built
binary vs config/check.<ov>.sha (the real whole-binary arbiter) → only THEN asm-differ -o for the diff view.
Never trust a piped make build you didn't exit-check. (Compounds with the §42a --out gotcha — both produce
false PASS/FAIL on overlays.)
The *(T*)&D_sym read-global drift (a canon_sig_reconcile defect) + the fix — byte-proven on func_80164930.
canon_sig_reconcile rewrites an ambient-conflicting global access as *(u16*)&D_sym (cast-at-use, to dodge a
type conflict). For a write-only global this is byte-neutral (lui at,%hi; sh v,%lo(at) — direct addressing).
For a read (esp. read-modify-write) global it DRIFTS: &D_sym forces gcc to materialize the FULL address
into a held register (lui a0,%hi; addiu a0,a0,%lo; lhu v0,0(a0)) instead of the target's direct
lui v0,%hi; lhu v0,%lo(D_sym)(v0) — and it reuses that held reg for the store, shifting the whole schedule.
The wall: the target read needs lhu (u16) but the ambient TU decl is s16; a block-scoped
extern unsigned short D_sym inside the fn is a hard conflicting types ERROR in gcc-2.7.2 (cc1 exit 33,
NOT a warning — signed/unsigned short mismatch). The fix: flip the file-scope decl to the exact type
(extern s16 D_8018971C; → extern u16 D_8018971C;) — byte-neutral when the only other referencer is store-only
(func_801647A4 stores = 0x80 → sh either way) — and reference the global directly (no *(T*)&). Result:
whole-overlay d19c9580 BYTE-IDENTICAL, func_801647A4 unaffected. General rule for drafters/reconcile: a
read global that needs a specific load width (lhu/lh) must be a direct-typed lvalue at file scope, never
*(T*)&sym; align the whole TU on one type rather than casting at use. Sweep caveat: the file-scope-decl
flip is per-TU, so family_sweep --reconcile must also flip each sibling's decl (or the sibling's caller must be
an unmatched stub with no conflicting decl) — else siblings NOCOMPILE like the frame-pad class (§42 lever 3).
Wave-3 consequence: the wave-2 uc2_gate_* drafts are not reliable seeds — several NOCOMPILE (unreconciled
callee externs conflicting with the TU canonical-sig layer, e.g. conflicting types for func_80015954) and the
"iso-MATCH" labels were stale-object phantoms. Wave-3 targets must be re-reconciled + rigorously rebuilt per fn
(the .run/crack3/ harness), not gated from the wave-2 artifacts. Confirmed banks this wave: func_80164930
(the read-global fix above).
§42c addendum — wave 3 (Max orchestrator + CORRECTED Ultracode fan-out, 2026-07-10c): the real-TU-faithful parallel harness (rtu_match) + 7/9 crack, ZERO iso-drift
THE TOOL that makes a reliable crack fan-out possible — tools/rtu_match.py (real-TU-faithful, parallel-safe).
Wave-2 workers self-checked in ISOLATION (match_one), blind to in-TU decl/global-type/memcpy-builtin drift, so their
iso-MATCHes drifted at the whole-binary gate (~50% attrition). FIX: compile the WHOLE split .c with the candidate
spliced and INCLUDE_ASM neutralized (-DINCLUDE_ASM(a,b)= + -Isrc/<source> for the relative ../shared include)
→ masked-diff the fn. No asm/, no shared overlay build → many workers run in PARALLEL in per-fn temp dirs. Because
gcc-2.7.2 -O2 compiles each global fn independently, the neutralized whole-TU compile reproduces the exact ambient
context, so a rtu_match MATCH HOLDS at the whole-binary gate. Measured: 7 real-TU MATCHes → 7/7 banked
byte-identical (individually + combined d19c9580), ZERO drift (vs wave-2's ~50%). Corrected fan-out = 9 xHigh
workers ~1.27 M tok → 7 MATCH + 2 DIFF(→permuter). This is the reusable engine for the phase tail: reconcile-first
- rtu_match-gated + the levers below. Supports
//@EDIT old||newfile-scope pre-edits.
DURABLE LEVERS from the 7 cracks (all rtu_match-byte-gated):
- Callee-ARITY unblocks a delay-slot "steal" (func_8012FCC4 — the "irreducible" that wasn't). A spurious extra
register arg on a callee that is LIVE ACROSS the call blocks gcc reorg
fill_slots_from_threadfrom sharing a downstream constant into a branch delay slot (reads as an irreducible ~3-off beqz/jal delay swap). Before conceding a delay-slot residual as irreducible, RE-DERIVE THE CALLEE ARITY FROM THE ASM: drop the bogus arg → the target schedule falls out of stock reorg, no barrier/pin/mutation. - Pointer-holding global via
*(T**)&sym→lui;lw %lo(load ptr)+lh off(ptr)(deref) (func_80136824). A file-scopeextern u8 D_xthat actually HOLDS a pointer: read as(*(s16**)&D_x)[i]. Byte-neutral vs the u8 decl. - Array-decay CSE (func_80136824, the load-bearing extra): reading
extern s32 D_x[](ARRAY) as*(s16**)&D_xorD_x[0]makes gcc CSE the decayed BASE addr into a held reg (lui;addiu;lw 0(reg)reused) vs the target's per-use directlui;lw %lo(sym). FIX://@EDIT extern s32 D_x[];||extern s16 *D_x;(flip to a SCALAR POINTER). (Scalar u8 symbols fold %lo fine; only the array decays.) - §17 zero-reg-copy
x + zrfor a delay-slot-SAFE live-range copy (func_80134A74):register u32 zr __asm__("$0"); y = x + zr;copies a pseudo with NO__asm__op, so it CAN land in a branch delay slot (an__asm__volatile copy cannot, and disrupts delay-fill → +1 ins). Use to hoist a masked value into a bnez delay slot / before a range-check. - void→s32 flip for a discarded-return callee decl (func_8014DD8C): when a fn truly returns a value (
addiu $v0,1) but a sharedDEFINE_func_*macro in engine_core.h declares itextern voidand the caller DISCARDS the return, flip that macro-internal externvoid→s32(byte-neutral fleet-wide; stops the void-decl DCE'ing the return). R22-confirm fleet neutrality. Precedent: §20 func_8014EE14. - register-arg capture into a NORMAL pseudo for a callee-saved param (func_80168828, SWEEP-SAFE, no //@EDIT):
to force incoming
$a0into a callee-saved reg (targetaddu $s1,$a0,$zero): declare the fn(void), thenregister s32 a0v __asm__("$4"); s32 param_1 = a0v;. The copy into a normal pseudo (live across calls) gets a callee-saved home. A directregister ... __asm__("$4")leaves it in call-clobbered $a0 (wrong frame → 100-off). - Free-floating load temp for a scheduler hoist (func_8016C188): extracting an arg-load into its own statement
(
s32 t34 = *(s32*)(s1+0x34);) lets the scheduler hoist it early to fill a load-delay slot (vs pinned late by the call) — closed 63 mismatches at once.
block-extern-vs-definition is an ERROR, not a warning (func_80133AB0/8014DD8C): in gcc-2.7.2 a block-scope
extern whose sig conflicts with the function's own DEFINITION hard-errors (cc1 exit 33). A TU that forward-decls the
fn with a wrong/loose sig must be reconciled (match the def's sig; //@EDIT the caller decl when it discards the
return or the arg is already the right width in-register). The dominant "reconcile-first" wall for the F-band exemplars.
The 2 DIFFs (permuter tier), seeds in .run/crack3/wave3/: func_801670E4 (70→48; block birth-order levers landed,
"assign p/i late" shape from sibling func_8016A290) and func_80185BA4 (structurally 177/177, pure scheduler +
caller-saved temp-numbering residual, no responsive C lever) — decomp-permuter fuel.
§42d addendum — wave 4 (rtu_match fan-out over the mapped frontier, 2026-07-10c): 24/26 MATCH, +5 durable levers
META-YIELD (validates the frontier-map "reconcile-first" bucket): a 26-worker rtu_match fan-out over the
tractable-band draftable exemplars (the frontier map, docs/phase25-frontier-map.md) landed 24/26 MATCH
(20 banked byte-identical, 2 permuter, 4 needing per-fn link/drift fixes). Confirmed: for the F-band exemplars,
reconcile-first is often the WHOLE fix — several (func_80131B14) were byte-correct in the body and only their
TU-canonical decl layer conflicted; strip/align the decls → MATCH with no schedule/regalloc grind. The engine =
reconcile-first + rtu_match-gated + the §42/§42c/§42d levers.
NEW / generalized durable levers:
- Return-type flip goes BOTH ways (generalizes §42c #5). If a fn genuinely RETURNS a value but a discarding
caller's decl says
void, flip the declvoid→s32/short(func_8014FE60, func_8016CF04) — the void decl DCE's the return computation. INVERSELY (func_8016DF5C): if a fn is effectively VOID (barereturn;) but the draft declares its32, flips32→void— an s32 return keeps$v0LIVE at the epilogue, blocking reorg's eager fall-through delay-slot steal (a single-instruction cascade). Read the asm: does$v0carry a value out? - Address-recompute-vs-CACHE — the unifying read-global rule (subsumes §42b read-global + §42c array-decay CSE).
Taking
&D_sym(via*(T*)&symor a cached local ptr) makes gcc materialize the symbol address into ONE reg (lui;addiu) and CSE it across all uses → FEWERluithan a target that recomputes%hi/%loper reference (direct global access). When the target shows a freshlui $scratch,%hi; op %lo(sym)at EACH use, declare the global directly at the right type/scope (extern volatile unsigned short D_x;etc.) and reference it plainly — never&sym. When the target instead HOLDS the address in a reg across uses, cache it (T* p = ...;). Same root cause behind func_80164930, func_80136824, func_801418F8, func_80136334. - The full-inline-asm TRAMPOLINE idiom (func_8014FBC0, the 22×1996 family). The scratchpad-stack-switch
trampolines (func_8014F468/F6F4/FA04/FCFC/…) are hand-asm: the callee symbol AND the global live INSIDE the
__asm__string (%hi/%loescaped as%%), so ZERO C externs are declared → nothing to reconcile. maspsx 2.56 auto-fills thejaldelay slot with a nop (do NOT write an explicit post-jal nop). family_remap must substitute the callee/global symbols INSIDE the inline-asm string, not as C extern lines (the x134 sweep of an inline-asm family needs this — else siblings drop). - memcpy→struct-assign, re-confirmed at scale (func_8017B238, §42a): the TU's file-scope
extern memcpydisables the builtin → 8-byte moves lower to CALLs; model on the matched sibling's align-1typedef struct{u8 b[8];}struct-assign (routes emit_block_move, zero memcpy ref). Pair with theregister u8* __asm__("$16")+ in-place re-tie pin to hold the src pointer across the moves. - phantom-frame induction (func_80136334, §42-refined): a value live across BOTH arms of a branch makes gcc
reserve a spill slot the no-frame twin lacks — induce the frame with
s32 frame_pad[2]; (void)&frame_pad;.
Wave-4 economics: 26 workers ~2.36 M tok → 24 MATCH → 20 banked + swept ×134. Bank-rate 20/24 at the
whole-binary gate (4 hit rtu-blind link-walls / drift — rtu_match is .text-only, §41b/§42b caveat; those need the
whole-binary/link gate). The 2 permuter DIFFs: func_8012E364 (c=4), func_801549F8 (c=3, jtbl delay-slot).
§42e — propagating a CRACK ×134: the def-finder bug + the byte-drift residual (the "remap-fail" misdiagnosis)
Cracked F-band exemplars don't all propagate ×134 through family_sweep --reconcile — waves 3/4 dropped ~1,200
siblings. Diagnosis (a two-layer story; both matter for future sweeps):
- THE def-finder BUG (
canon_sig_reconcile, fixed) — mislabeled "remap-fail".family_sweep'sreconcile_remapreturns None on ANY failure and the caller counts it as "remap-fail", butfamily_remapitself SUCCEEDS (verify withtools/family_remap.py --addr … --from … --to …— it pairs the symbols fine). The real None came fromcanon_sig_reconcile.reconcileraising "no definition of func_X found in draft": its def-finder regex required a leading\n(\n(<type> fn(...)){), but a raw draft whose//@EDITheader lines were stripped has the fn definition on line 1 → no match. FIX:\n→(?:^|\n)(also match a def at draft start). This alone fully recovered func_8014FE60 (133/133 siblings) once paired with its shared-header return-type flip. - THE byte-drift residual (the genuine
--edit-remapwork). Families cracked with a file-scope//@EDIT(the array-decay pointer flip §42c#3, the no-proto flip) or a shared-header return-type flip (§42d#1) reconcile per sibling but BYTE-DRIFT, because those edits live OUTSIDE the function body thatfamily_sweepremaps: the pointer/ no-proto//@EDITtargets per-overlay decls (must be symbol-remapped + applied per sibling), and the return-type flip targets the ONE shared engine_core.h macro (apply once, globally — like func_8016CF04/8014FE60).family_sweepcarries neither. So a--edit-remap= {per-sibling: remap the exemplar's//@EDITsymbols and apply to the sibling split; once: apply any shared-header flip globally} recovers this class. func_8016DF5C/80136334/8013D9B0/80156044 are the backlog exemplars.
Forward rule (frontier-map leverage realism): a crack's ×134 is only free if its body is self-contained (no
//@EDIT, no shared-header flip). Before counting a cracked family's ×134, note whether it carries out-of-body edits;
if so it's exemplar+--edit-remap, not exemplar×134-free. LESSON (R14): trace a tool's real exception, not its
summary label — "remap-fail" was a swallowed reconcile-throw two layers down.
BUILT + measured (Phase-25 task B, 2026-07-10): family_sweep --edit-remap MANIFEST (JSON: per family, edits
= split-scope //@EDIT old||new in EXEMPLAR symbols, symbol-remapped per sibling via family_remap.symbol_map;
ec_edits = once-global engine_core.h flips, byte-neutral). Per sibling it applies the remapped edits to the split
- stages the
family_remapbody + gates the (overlay,split) group via plainharvest_verify. Orphaned edits from a failed sibling are byte-neutral (R22-checked). Manifest at.run/edit_remap_manifest.json.
THE CC1-CRASH WALL (the decisive R14 finding — only 2 of the 6 backlog families recovered): the whole-binary byte-gate is the sole arbiter, and it revealed that out-of-body-edit families split into two classes:
- array-decay pointer-flip (
extern s32 D_x[];→extern s16 *D_x;, a per-overlay symbol) — recovers cleanly ×134.func_80136824+func_80136334→ 266/266 siblings banked byte-identical, 0 failed (2×133). Light register pressure;family_remapbody + the remapped split-edit is sufficient (no reconcile, no extern injection). - register-pin-heavy (GTE 20-pin bodies
func_8013D9B0/func_8016DF5C; an exoticregister int zr __asm__("$0")zero-register pinfunc_80133AB0; the inline-asm trampolinefunc_80156044) — the original verdict here was "cc1-2.7.2 SIGABRTs compiling the sibling TU… ov077-TU-context-specific… NOT mechanically ×134-recoverable, stay exemplar-only (×1)." ⚠️ REFUTED — Phase-27 (Fable5 characterization,.run/giants/pin_crash_sigabrt.md). See the corrected verdict below; the pin-×1 ceiling was a STAGING-TOOL artefact, not a compiler wall, and it is fixed.
§42e-CORRECTION — the "pin-crash wall" is the extract_unit macro-drop, not the pins (Phase-27 T5 + SIGABRT
characterization, 2026-07-15). The SIGABRT is real and now exactly located — gcc-2.7.2/sched.c:2725,
create_reg_dead_note(): if (dead_notes == 0) abort();, a sched1 REG_DEAD-note conservation bug (flow places the
pinned reg's death on the fall-through path; sched1's clobber-aware per-block recount demands a death note for a
use-after-call in the CALL's block, whose harvested note-pool is empty → abort; backtrace abort ← create_reg_dead_note ← attach_deaths ← attach_deaths_insn ← schedule_block). But it was TRIGGERED by
family_remap.extract_unit dropping the body's file-scope #define dependencies, not by any TU context:
- Of the 4 "crash-walled" families only
func_8013D9B0ever genuinely SIGABRTed — and only because the droppedgte_*macros became implicit-declaration CALLS, putting its caller-saved pins into the fatal shape. The other three were exit-33 plumbing (a dropped multi-line typedeffunc_80133AB0; a dropped single-line typedeffunc_8016DF5C; the one-line-wrapper false-positivefunc_80156044) misfiled as crashes because the era one-big-split gate shared a TU compile with d9b0 and reported its Error-134 for all of them (the R14 lesson, recursed: one exit code folded three distinct failures into a phantom "universal SIGABRT"). - Properly staged, all four compile CLEAN in sibling TUs:
func_80133AB0133/133 (today AND at the era commit),func_8013D9B0133/133 (today, fleet-swept), df5c + x6044 spot-proven. T5's_carry_macrosfixes (a) the#definedrop; (b) multi-line typedefs route through theengine_types.hlift; (c) the one-line-wrapper false-positive is already fixed by the current comment-strip guard; (d) per-sibling decl flips are--edit-remap. - The fatal-pin predicate (checkable at DRAFT time, probe-matrix-proven): FATAL = a
register T x __asm__("$N")pin where$Nis caller-saved ($2–$15, $24, $25), the value is used after a CALL_INSN, and the post-call use has a branch-dependent use-then-conditionally-set shape. SAFE = callee-saved pins ($16–$23, $30) in any shape; caller-saved pins whose live range never crosses a call; use-only or single-level-conditional shapes;$0pins. (12-line minimal repro + probe matrix inpin_crash_sigabrt.md;-fno-schedule-insns/-O1 suppresses it — a safe "is this the dead-notes bug?" probe, useless for matching.) So ov077 banked these pins precisely because, in its TU (macros present), no pin crossed a real call. - DIAGNOSTIC (R14, corrected): exit 134 + the
create_reg_dead_notebacktrace = this bug, always; exit 33 = ordinary decl/typedef plumbing. Distinguish them (T4 surfaces cc1 stderr; don't fold both into "cc1-crash"). - Takeaway: the pin-×1 ceiling does NOT exist — P31's pin-propagation route is OPEN. Route pin-heavy families
back to the mechanical
family_sweepharvest (macros now carried); byte-identity per sibling is the byte-gate's question, but cc1-crash is no longer a barrier. (Array-decay pointer-flip families were never affected and still recover cleanly ×134.)
§43 — The K&R s16-param definition DISSOLVES the "narrow-param wall" for by-value register args (Phase 25 task A, Fable5 crack of the 369-ins giant func_80166994 ×134, 2026-07-11)
§17/§29 called a def with narrow-scalar by-value params an irreducible wall: it can't be no-proto-relaxed
(K&R default-promotion "changes the ABI") and often can't match the TU's s32 canon-sig prototype → stub it.
A Fable5 giant crack byte-proves that verdict is too broad for s16 (and s32) by-value params.
The refinement — use a K&R definition:
s32 func_X(param_1, param_2, param_3, param_4)
s32 param_1; s16 param_2; s16 param_3; s16 param_4; /* K&R: params declared narrow */
{ ... }
On MIPS all four args arrive in $a0–$a3 as 32-bit words. K&R promotes the s16 params to int for the
PROTOTYPE — ABI-identical to the canon-sig s32(s32,s32,s32,s32) (so no conflicting types, no //@EDIT
for the param types) — while the BODY still treats them as s16, producing the target's lazy per-use
in-place narrow/extend: sll aN,aN,16 ; sra aN,aN,16 on the arg register itself, with the raw values
stashed to callee-saved pseudos first (s3←a1 …) and re-extended per use after calls. The (s16)param_of_s32
cast form CANNOT reproduce this — it extends into fresh v0/v1 temps instead.
Triage tell (read it off the diff): target does sll aN,aN,16 in place on an arg reg + copies the raw
aN elsewhere first ⇒ true s16 param ⇒ K&R form. Extends into v0/v1 temps ⇒ it's a cast-of-s32, keep s32.
The return-type flip pair (void-return value-drop): if the def returns s32 but the ambient decls say
void, gcc-2.7.2 discards return expr; in a void fn (pedwarn) → you lose the target's exit
materializations (addiu v0,zero,1 / addu v0,zero,zero). Fix = flip void→s32 at BOTH:
- (a) the split canon-sig decl (
//@EDIT void func_X(...);||s32 func_X(...);) — a self-fn decl, EXEMPLAR- SPECIFIC (the canon-sig layer put it in ov077; siblings usually have 0 of these → make the split-edit OPTIONAL infamily_sweep --edit-remap: apply where present, never skip — the byte-gate is the arbiter), and - (b) the engine_core.h
DEFINE_func_*externs (ec_edit, once-global, byte-neutral because every caller discards the result — the func_80156044 trampoline precedent, now for a real returning fn).
Zero-footprint body ⇒ ×134-clean: put ALL typedefs + externs block-scope inside the function (a
conflicting file-scope typed extern is a hard cc1 error, exit 33, not a warning). Access a global as
&((Struct *)D_xxx)[i] over an ambient-compatible extern u8 D_xxx[];. No register __asm__ pins → it
propagates ×134 via family_sweep --edit-remap with no cc1-crash (contrast the pin-heavy §42e families
that SIGABRT in sibling TUs — structural cracks are the ×134-safe ones).
Scope (byte-tested = s16 only): proven for s16 by-value register params. u16/u8/s8/float
by-value, and any narrow param accessed via memory (sh/sw width differences), remain §29 walls until
byte-tested. So §29's blanket "narrow-param wall" narrows to "narrow params that aren't s16/s32-by-value in
an arg register."
Flywheel (R16): this idiom is now cheap-Opus-applicable — no Fable5 needed — for any giant whose diff
shows the in-place-sll triage tell. Check each remaining giant for the s16-param class before spending the
Fable5 tier. (Also caught: the prior wave's @stuck: none — MATCH note on func_80166994 was stale/false
— match_one re-ran DIFF 366/369; verify a "MATCH" claim against the bytes, R14, never trust a stale note.)
§44 — The Phase-25 cheap-Opus giant batch: 5 structural levers + the §43 extension (2026-07-11, 6 crackers over the frontier giants)
A 6-agent cheap-Opus batch (each applying §43 + §31 + the giant recipe, escalate-if-new-class) over the 6
frontier giants (209–399 ins, all reach-134): 3 banked ×134 (func_80166994 §43; func_80135480;
func_80163EC8), 4 pin-free/light-pin Fable5 seeds (the intrinsic wall), and 5 reusable levers. Meta-
lesson: cheap-Opus-first was right — 3 giants + 5 levers + clean seeds for far less than 6× Fable5 — and
§43 does NOT universally transfer: only 1 of 6 was a K&R-s16 case; each giant is its own class.
Lever 1 — §43 EXTENSION (widen the triage). The §43 tell "in-place sll aN,aN,16 on an arg reg" is too
narrow. If the target holds arg0 in two callee regs (a non-coalesced duplicate, e.g. move s6,a0; move s7,s6 — one for a sign-test, one for a mask-test), the K&R s16 arg0; def reproduces that duplication with
zero pins even when the sign-extend lands on the callee stash (sll $sN,16), not on $aN. (s16)cast
collapses it to one reg. Rule: try the K&R s16 form whenever the target shows a duplicate-arg0 pattern,
not only the in-place-$aN tell. (func_80133CD4.)
Lever 2 — pointer-var decl (avoid the &sym CSE-hoist). A held global pointer the target reloads per use:
declare it extern u16 *D_xxx and access it directly (D_xxx[i]), NOT via (*(u16 **)&D_xxx)[i]. The
&D_xxx form CSE-hoists the address into a callee reg (one lui;addiu, reused); the direct pointer-var form
emits a fresh lui %hi; lw %lo per use — matching the target's reload pattern. (func_80133CD4,
func_80135480.)
Lever 3 — block-scoped-pointer-split (local-alloc a reused output pointer). A single pointer reused to
write multiple output-store groups across separate return tails becomes a global allocno pinned to one
register, so it can't match a target that uses a different reg per tail. Split each store-group into its
OWN block-scoped set-once / used-N / dies-once pointer → each becomes a local-alloc pseudo that picks
the per-window lowest-free scratch, reproducing the target's per-tail allocation AND un-sticking coupled
delay-slot fills elsewhere in the schedule. Pin-free. (func_80135480, 258 ins; §31 RC-4 extension.)
Lever 4 — cross-jump the duplicated tail (steer a "permuter-only" dbr class). For a shared reset/exit tail
whose target shows call-arg-hoist into a branch delay slot + per-predecessor const-rematerialization: write
the tail duplicated inline in BOTH predecessors, NOT as one shared goto block. gcc-2.7.2 jump.c
cross-jumps the two copies, reproducing the exact dbr schedule (the call arg hoisted into the bnez delay
slot serving both paths; the mask re-materialized per-predecessor in the j/beq delay slots, sharing a reg
with the neighbouring lh). This cracks a residual §31 files under D1/D2 as permuter-only — it is
steerable. (func_80163EC8, 234 ins; one benign $v0 pin.)
Lever 5 — the "intrinsic wall" (what cheap-Opus canNOT do → Fable5/permuter). The §37 allocno-tie /
RC-6 pressure-lock / scheduling-position class: a pin-free structural seed floats at close 30–67 but the
residual is a whole-function register permutation or a schedule-position tie-break that no C-lever reaches
at the Opus tier — a caller-vs-callee allocno heuristic choice (func_80133CD4 s0v→$v0-vs-$s0). The cheap
tier's job here is to produce a pin-free, structurally-complete seed (correct body + count, zero file-scope
footprint) and hand off honestly (no forced/pinned false match). Escalation: Fable5 with
tools/reference/gcc-2.7.2/ and the §34 gdb-on-cc1 find_reg/post_mark_life method (it reads the
allocator's actual decision), or the pin-free seed → decomp-permuter.
⚠️ Phase-27 reclassification (regalloc-map §H,
.run/giants/*.fable.md): the three functions this lever once cited as intrinsic —func_8014D820"RC-6 pressure-lock",func_8016CBC0"coalescing knife-edge",func_801670E4"i=0/p co-location" — were each oracle-refuted:func_8016CBC0's callee-saved swap CRACKED byte-zero (afloor_log2density gap, and gcc-2.7.2 has no coalescing so "knife-edge" was never the class),func_8014D820's block-0 cracked pin-free 261→110 (reused-load-temp serialization; the sched.c:3199 pin was a red herring), andfunc_801670E4's dominant residual is RC-6 register allocation, not S3 scheduling (proven by the reg_renumber-swap oracle). The pattern (continuing map §F/§G): an "RC-6 intrinsic" verdict is usually map-incompleteness — audit for a density/lifetime/merge lever before declaring it. And the old "NEVER ship the pinned variant — it SIGABRTs sibling TUs (§42e)" is corrected (§42e-CORRECTION): the SIGABRT was a staging macro-drop, now fixed; a pin whose live range does not cross a call is safe to propagate. Prefer pin-free still (fewer failure modes), but the pinned-×1 ceiling is not real.
§45 — The flagship func_80133CD4 crack (399 ins ×134): the merged-variable permutation-breaker + the 1-death local-alloc gate (Phase 25 task A giant escalation, Fable5 gdb-on-cc1, 2026-07-11)
The 399-ins flagship — a "whole-function register permutation" that walled the directed permuter (masked-172) and had been tagged intrinsic for ~22 phases — fell PIN-FREE (×134-clean) to a Fable5 gdb-on-cc1 crack (whole-binary byte-gate BYTE-IDENTICAL d19c9580, banked ×134). Four reusable, byte-proven levers (worked example .run/giants/func_80133CD4.fable.c; dumps + gdb oracle in .run/giants/fable_cd4/):
Lever A — MERGED ACCUMULATOR VARIABLES break a "whole-function permutation" (the headline: 378→147 mismatches). When the target holds ONE $sN across disjoint value-regions (e.g. $s0 = {call-3 result → denominator → loop-accumulator}), gcc-2.7.2 global-alloc has no coalescing (K8), so one hard reg spanning disjoint regions can only come from one reused source variable. Merge the disjoint C variables into one → the allocno becomes call-crossing (K4, global.c:917) with a high merged ref-count → top density (K2, global.c:594 allocno_compare) → it allocates FIRST → plain regno first-fit (K3) reproduces the ENTIRE callee-saved permutation (the arg0→$s7/$fp end is §43's K&R double-copy). AUDIT for reused-variable chains BEFORE calling a whole-function permutation "unsteerable" — it is the original C reusing one variable per accumulator chain, not a compiler mystery. Retires the "N-callee vs N−1-callee permutation" giant-wall class.
Lever B — the 1-death local-alloc gate + the in-out-asm fix (67→13; found by a gdb ORACLE). A shared read-temp serialized through one register (target: lh; lh into the same reg separated by a byte-visible nop) is a 2-SET variable, which local-alloc REJECTS: reg_n_deaths != 1 (local-alloc.c:472) forces it to a GLOBAL allocno, allocated after every block-local qty → it loses the low-scratch first-fit and the whole caller-saved block permutes. No pure-C spelling yields 2-sets/1-death (flow emits REG_DEAD per region flow.c:2533; combine's 2-insn merges undo, its split path needs i1 = 3-insn combos only combine.c:1737; cse dissolves every 1-set spelling — all byte-tested). The escape (flow.c:2511): no REG_DEAD when a reg is SET in the same insn it last USES — expressible ONLY as an in-out asm __asm__("lh %0, off(%2)" : "=r"(h) : "0"(h), "r"(p) : "memory") (the "0"(h) input-tie makes read-2's lh use+set h in one insn) → 1 death → LOCAL qty → wins $v0 by qty-birth tie-break → the rest cascade by first-fit. PIN-FREE / ×134-safe (generic constraints, real opcode, no hard-reg names — NOT a register __asm__("$N") pin → no §42e sibling-TU SIGABRT). The "memory" clobber doubles as a delay-slot fence.
- THE METHOD — the gdb ORACLE (§34 flywheel). When a hypothesis reduces to ONE compiler-internal quantity, patch it mid-compile and diff the output (
break *local_alloc; set reg_n_deaths[h]=1). One run turns "plausible root cause" into "proven," licensing the (expensive) hunt for the C form that induces it.-dS/-dRdump sched1/sched2 with per-insn dependence lists on reload-born insns — read those before hand-modeling. In the shipped i386 cc1,qty_first_reglives at0x82c5404(theinfo addresssymbol is stale for this binary).
Lever C — offset-0 /s store asymmetry (last 5 diffs). p[0] = x expands non-/s (mem (reg)) while p[k≥1] are mem/s → a fixed-address (reload-born) load keeps its true-dep ONLY against the offset-0 store (sched.c:820 drop-clause needs /s+varying on one side, non-/s+fixed on the other). ((struct { s32 w; } *)p)->w = x; /s-ifies the offset-0 store → dep dropped → the load floats to the earlier delay gap. Store-side twin of §37's load-side /s lever.
Lever D — goto-shared-return isolates the exit li (tail). A common return 1 reached by goto ret1: gets its OWN basic block → stops sched1 hoisting the exit li v0,1 into a last-element load-delay slot cross-BB (freeing $v0 for a trailing temp); dbr still steals the li into the branch delay slot. Use when a return-constant materializes one instruction too early.
Transfer caveat (the §44 meta-lesson holds): each giant is its own class — Levers A/B are regalloc-permutation tools; apply them to a walled giant only when its residual IS a merged-variable or 2-set-temp permutation (read the .greg/.lreg tell first). The Phase-25 flywheel applies A–D via cheap-Opus to the sibling walled giants (func_8014D820 RC-6, func_801670E4, func_8016CBC0), escalating to Fable5 only for a genuinely new class.
§46 — The func_80178D40 crack (890 ins ×134, the heaviest core in the game): four LOOP-STRUCTURE levers cheap-Opus found by reading loop.c/jump.c/cse.c (Phase 26 session 8, 2026-07-13)
The heaviest jr core (890 ins, reach 134 = 477 KB) sat at close=39 with every case byte-exact but one. All 39 residuals lived in a single 44-instruction case body. No pins, no permuter — every residual was structural, and the permuter could not have reached any of them. Cheap-Opus + the §31 map closed it to MATCH 890/890. These four levers are new and general; the classes recur in every loop-bearing overlay function.
L1 — A loop's break must NOT land on the loop's own fall-through label (the PEEL lever).
When a break target coincides with the loop's natural fall-through exit, the RTL leaves NOTE_INSN_LOOP_BEG
followed by an unconditional jump — which fires duplicate_loop_exit_test (jump.c:2131, called from
jump.c:599). gcc rotates the loop and peels iteration 1; if the induction variable is provably 0 the peeled
i++ const-folds (li $a2,1) and drags a whole lui/addiu/lw address re-materialization block with it.
Fix: write goto <label>; instead of break; — the same destination, a different construct. It stays a plain
do-while and the peel vanishes. Tell: an extra address-materialization block and a li reg,1 at the loop head.
L2 — To make a reg-reg COPY survive, split its def and uses across extended basic blocks.
A source-level fp = q; always dies: cse's canon_reg rewrites later uses back to q (qty_first_reg keeps
the older register) and flow deletes the dead set. Every "just write the copy in C" variant collapses. But cse
resets its hash table at a label with >1 predecessor (a loop top), so a copy defined in a guard block and
used only inside the loop cannot be propagated away:
if (arg1->a.w != 0) { fp = arg1->a.w; ... do { ... fp ... } while (...); } /* addu $v1,$v0,$zero survives */
Test the memory, assign inside the branch. (Hoisting the same load into the loop preheader instead puts the
copy AFTER the lui/addiu — right copy, wrong place.)
L3 — A store merged into a shared tail must be written INSIDE the branch that reaches it.
if (i != 10) { D_801DAB2C = 3; break; } lets jump2 tail-merge the lui/sh into the shared tail label and lets
reorg steal the li 3 into the bne delay slot. Storing unconditionally before the if blocks the merge and
costs 3 inline instructions. (Same family as §8's cross-jump-merge-of-direct-stores.)
L4 — The un-coalesced loop copy = a NON-REPLACEABLE giv, and it needs all THREE parts.
This copy cannot come from C (see L2); it must come out of loop.c as a reduced giv whose move survives:
(a) an index giv — p = &D_801DA764[i]; (not a hand-rolled pointer walk);
(b) the giv is used outside the loop (e.g. the "found" body after the loop stores through p) → record_giv
(loop.c:4437) marks it non-replaceable → emit_insn_after (move dest, new_reg) at loop.c:3945;
(c) the biv increment is LAST in the body — loop.c inserts the reduced giv's addiu immediately before
the biv increment, so i++ at the bottom puts addiu $a0,$v1,4 into the loop-back delay slot. With
i++ at the top the giv-add lands at the top, nothing fills the delay slot, and reorg steals the loop-top
move into it — duplicating it, +1 instruction (891 vs 890).
L5 — Two structurally identical loops must differ in a REGISTER, or cross_jump merges their tails.
Loop3 reusing loop1's pointer pseudo put both "found" bodies in $a0, made them textually identical, and jump2
cross-jumped them into one block (−2 ins). Giving loop3 its own pseudo frees it into $v1, so
sw $zero,0($v1) ≠ sw $zero,0($a0) and the blocks stay distinct. Corollary of §8's cross-jump lever, inverted:
when you need two tails to STAY separate, separate their registers.
Meta (confirms the §44 law + Phase-23's finding). Every one of these was found by reading the actual
gcc-2.7.2 passes (jump.c, loop.c, cse.c in tools/reference/gcc-2.7.2/) and none by search: the residual
class was "the compiler produced the wrong BYTES", so R17 applies and the map/source is the lever. And the
tier held — cheap-Opus applying the documented map cracked the game's heaviest core; Fable5 was not needed.
§47 — The live-length SLIDER: splitting a global.c allocno-priority TIE with one zero-byte asm (Phase 26 session 8, Fable5 Max, byte-proven on func_8017BEBC 952 ins ×113)
The close=2 endgame class: allocation order and emission order are COUPLED (both follow creation/LUID
order), but the target needs them to DIFFER. func_8017BEBC's two hoisted invariant addresses (&g.sz1,
&g.sz2) tie in allocno_compare priority; the tie-break is creation order, so natural operand order gives
correct emission + swapped allocation (close=10), and permuting the asm operand list gives correct allocation +
transposed preheader emission (close=2). The permuter provably cannot reach it (not statement-permutable; 25 min,
no close). The fix decouples them: make the priority difference REAL so the tie-break never fires.
The method (no gdb needed — the dumps are the oracle):
- Compile with the pinned cc1 +
-dl -dg(a file input, sot.i.lreg/t.i.gregappear). Find the two preheaderaddius in the.gregRTL by theirconst_int(the sp offsets), take their insn UIDs, find the same UIDs in the.lregRTL to get the PSEUDO numbers, then read each pseudo's line:Register 228 used 13 times across 783 insns/Register 230 … across 782 insns. - Compute
pri = (int)((double)(floor_log2(n_refs) · n_refs) / live_length · 10000 · size)(global.c:594). Here: int(390000/783) = 498 = int(390000/782) — an exact int-truncation tie. - Find the boundary: the tie splits when the pair straddles an integer of
numerator/L. Here +1 on both (L = 784/783) gives 497 vs 498 — split. (−1 would also split; you can only ADD insns.) - The slider:
__asm__ volatile ("");placed BETWEEN TWO EXISTING GTE volatile asms inside the common live range. Adjacent to an existing volatile asm it adds NO new cse/sched barrier (one is already there) — it is purely +1 static insn at global-alloc time, emitting only#APP/#NO_APP(zero bytes).
Why the split can only go the right way: the later-created pseudo is defined one insn later in the
preheader and dies at the same last use → it ALWAYS has the shorter live range → pri(later) ≥ pri(earlier),
with equality only on a quantization plateau. Sliding the window off the plateau therefore always hands the
later-created pseudo the earlier allocation — which is exactly the "allocation ≠ creation" the target needs.
(If the target needed the OTHER direction, it would be unreachable by this dial — creation-order permutation
covers that case instead, §45-A.)
- Placement rule: next to an existing volatile asm (GTE-heavy functions are full of them). A bare
asm("")elsewhere is a cse table-flush + sched barrier + a maspsx#APPhop-killer (§42) — the classic perturbation trap. Between two volatile asms all three are already blocked. - The slider adds +1 live-length to EVERY pseudo spanning the insertion point — any OTHER exact-tie pair
straddling a boundary could flip.
match_oneverdicts the collateral instantly (here: none; MATCH first try). - ×N template-safe: the slider is body-local, pin-free, and travels with the template.
- Banked through the §8 whole-binary gate (jr function — match_one alone is NOT the arbiter, §8a): lazy
isolation → carve (9-piece interleave) → splice → BYTE-IDENTICAL. One TU-visible decl reconcile was
needed on the way (
D_800B9A02— declare the TU'sshort, force the unsigned access at use(*(u16 *)&D_800B9A02), the §8d sub-class (b) hand-move).
§48 — The 12-core jr crack wave: the ALLOCNO-PRICING dials and the EBB rule (Phase 26 session 8, Ultracode, 9/12 MATCH first pass)
Twelve heaviest unmatched jr cores, one agent each, §31/§46/§47 in the prompt: 9 byte-exact MATCH,
3 near (close=2/2/21), 0 dead ends. Every crack came from READING the pass (loop.c, jump.c,
cse.c, global.c, local-alloc.c, mips.c in tools/reference/gcc-2.7.2/), none from search.
The levers cluster into three families — and the first family is the important one, because it turns
register allocation into something you can steer from C without changing a byte.
A. ALLOCNO-PRICING DIALS — move a value into the register you want, byte-neutrally
All three exploit global.c:594 pri = (int)((double)(floor_log2(n_refs) * n_refs) / live_length * 10000 * size).
Higher density allocates first (and first-fit takes the lowest free reg). So any C edit that changes a
pseudo's refs or live-length while leaving the emitted insns identical is a free register dial.
- §48-A1 — SINK THE INIT INTO THE IF/ELSE ARMS (the biggest of the three;
func_8015A3C8).Byte-identical: jump2/cross_jump runs AFTER regalloc and merges the two identicalif (c) { min=A; grav=B; } → if (c) { min=A; grav=B; hi=0; } else { min=C; grav=D; } else { min=C; grav=D; hi=0; } hi = 0; /* nothing at the join */move rD,zerotails back into the single insn at the join. But at ALLOCATION time the pseudo was re-priced: measured 5 refs/318 live → 6 refs/162 live, priority 314 → 740 — enough to jump two other allocnos and take$s0. An init at a merge point is live across every path into the merge; the same init duplicated into the arms is not. Rule: to RAISE a local's priority, sink its initializer into the arms of a preceding if/else. To LOWER it, hoist the init to the join. (Companion to §47's live-length slider: that one shifts a length by ±1 to split a tie; this one collapses a length outright.) - §48-A2 — the LOCAL-ALLOC
$s0OCCUPANT (func_8015A3C8). A temp that is (a) referenced in ONE basic block and (b) crosses a call gets a callee-saved reg from local-alloc, before global-alloc runs. It lands in$s0and entersregs_used_so_far, which forces the highest-priority global allocno (thearg0copy — always first) off$s0onto$s1. If the target has arg0 in$s1, look for a call-crossing block-local temp and give it its own variable. m2c will happily merge it with a same-register variable; that merge destroys the occupant and rotates every$sreg. - §48-A3 — BLOCK-SCOPED PER-CASE TEMPS ARE A local-alloc TIE GATE (§44-L3, now source-cited;
func_8017A4AC). A function-scope scratch shared by 9 switch arms is a MULTI-BLOCK pseudo, andlocal-alloc.c:1765refuses to tie it — solhu/sll/sragets three different hard regs. Declare the temp INSIDE the case and local-alloc ties operand 0 to the dying input, collapsing the chain into one register. In a jr-switch dispatcher, NEVER share a scratch across arms.
A4 — SINK THE CONSUMER CALL INTO THE ARMS (the inverse of A1; func_8016AB6C, byte-proven)
A1 sinks an init to SHORTEN a live range. This sinks the consumer to DELETE the allocno outright.
if (c) { s3 = f(A1,P1)+3; p = P1; } → if (c) { s3 = f(A1,P1)+3; g(obj,P1); }
else { s3 = f(A2,P2)+9; p = P2; } else { s3 = f(A2,P2)+9; g(obj,P2); }
g(obj, p); /* `p` no longer exists */
The mechanism (why the join-copy is poison). A value defined in both arms and consumed only by a call in
the join becomes a cross-block global allocno whose copy-preferences include the ARG register. And
find_reg's copy-preference override scans for (i = 0; i < FIRST_PSEUDO_REGISTER; i++) — plain ascending
regno, NOT reg_alloc_order — so $5 (=$a1) deterministically beats $16 (=$s0). The only escape is
allocno_calls_crossed > 0, which makes find_reg set used1 = call_used_reg_set (global.c:906) and strip
the caller-saved prefs — but a pseudo defined after one call and dead before the next crosses ZERO calls,
so the arg reg wins and your value lands in $a1 instead of the target's $s0.
Duplicating the call into both arms deletes the pseudo: the pointer demotes to a block-local that crosses
a call, so local-alloc parks it in a callee-saved reg — and that also PRESERVES the §48-A2 $s0 occupant
that pushes arg0 onto $s1. The duplicated [addu $a0][addu $a1][jal] tail is identical in both arms after
reload, so jump2's cross_jump re-merges it into one join block — zero extra bytes — and dbr fills the jal's
delay slot with the second move.
Rule: if a value is defined in both arms and the target keeps it CALLEE-SAVED, duplicate its consumer call into the arms. cross_jump refunds the bytes. (Also note
set_preference,global.c:1535, appliesreg_renumber[]— so a locally-allocated pseudo appears in the pref set as its hard reg. That is why$16was even a contender.)
B. THE EBB RULE — the general form of §46-L2
cse resets its hash table at a label with >1 predecessor. So anything you need to survive cse must have its def and its uses in different extended basic blocks. Three instances, one rule:
- a reg-reg copy (§46-L2):
fp = q;always dies — unless defined in a guard block and used in the loop. - a held global address (
func_8015B950):la $sN,&Gin a loop preheader. gcc will NEVER emit this from plain global refs (a SYMBOL_REF is already a legal MIPS address), ands16 *p = &G;is constant-folded straight back by cse'sfind_best_addr. Fix: definep = &Gat the top of the loop body and use it only inside a case body reached through the jump table — cse starts a fresh table at the jtbl target label, cannot seep == &Gthere, thelasurvives, andloop.c move_movableshoists it to the preheader where global-alloc gives it a callee-saved reg. - a pointer-to-global held across calls (
func_8017A4ACL2):struct X *w = &D_SYM;at function top, used deep in a switch arm → survives, spans calls, gets$s0.
Corollary (
func_8013F350L2): a pointer-to-global survives only if every use is at offset 0. Withp[k], k≠0, cse'sfold_rtxfolds the SYMBOL_REF intoCONST(sym+k)— a legal address — and thelaloses its last user and is rematerialized away. For offset uses you need a struct (below).
C. TYPE- AND SHAPE-DRIVEN CODEGEN (the C type literally selects the addressing mode)
- §48-C1 — STRUCT vs SCALAR GLOBAL (
func_8013F350L1 — verified with 14 micro-probes). A scalar-typed global always folds to the direct macro (lui %hi; lhu %lo). A struct-typed global accessed by field always materializes a base (la $b,SYM+off($b)) once there are 2+ struct MEMs in the block. So: target showsla+ nonzero offsets → declare a struct. Target shows plainlui/%lo→ declare a scalar. A scalar and a struct at the same address may coexist; declare whichever each site needs. - §48-C2 —
lwl/lwr/swl/swrblock copy == a plain struct assign of a 2-BYTE-ALIGNED struct (func_80131340L1;mips.c:output_block_move, vanilla line 2580). Thelw/swarm is taken ONLY whenbytes>=4 && align>=4; align 1 AND align 2 both fall through to the unaligned pair. Sotypedef struct { u16 x,y,z,w; } V; a = b;emits lwl/lwr+swl/swr even between two 4-aligned stack slots. m2c's(unaligned s32)on an 8-byte object means "declare a 4×u16 struct and assign it" — not "hand-roll a byte copy". No memcpy, no packed attribute. - §48-C3 — DEAD-SIBLING-SCALAR TRAP (
func_8017A4ACL1 — cost 108 instructions). An arm that fills a param block and passes its address MUST use a real array. Declareu16 sp18, sp1A, sp1Cand take only&sp18, and gcc sees the siblings as never-address-taken → their stores are DEAD → flow deletes the stores and the loads feeding them. Diagnostic signature: the index chain repeated N times but only ONE load. - §48-C4 — CONST BEFORE LOAD (
func_8015A3C8L-B): to getluiinto a branch delay slot, give the compare constant a source temp one statement EARLIER, so its def precedes the operand load (lo = -0x94000; t = *p; if (t < lo)).gen_int_relationalforce_reg's a large compare constant AT the compare — i.e. after the load — and sched2 ties on LUID. Dead end: writingif (-0x94000 > t)does NOT work;compare_from_rtxcanonicalizes a CONST_INT op0 back to op1.
D. THE CROSS-JUMP RATCHET (the sharpest new trap — func_80131340 L-C)
Two cases needing opposite branch senses on the same test cannot be written as a mirrored if/else.
It looks right and even emits the right blez — but after cross_jump collapses both bodies to jumps,
jump.c's "invert a cond-jump that jumps over an uncond-jump" fires, flipping blez→bgtz; the two
cases are now byte-identical, so the NEXT cross_jump round swallows the compare entirely (−4 ins).
cross_jump + jump.c-invert together are a RATCHET toward collapse. Break it with explicit gotos into
labels living inside the other case's body: the branch targets become FAR, the invert-over-jump has no
adjacent label to fire on, and only the intended tails merge. (Same family as §46-L5: when two sites must
stay distinct, make them structurally distinct — separate registers, or separate branch targets.)
E. Meta
- 9/12 first-pass MATCH with cheap agents. The map is doing the work: every core was cracked by an ordinary Opus agent applying documented idioms + reading the pass. The Phase-23 tier doctrine holds — Fable5 DISCOVERS a class; everyone else APPLIES it.
- The
jr§8a check is mandatory and it caught nothing this time — because it was in the prompt. Every agent verified its.rodatatable against the target jtbl and reported the evidence. Bake the trap into the prompt, not into the post-mortem. - The 3 near-misses are all pure allocation/emission-order residuals (close=2, 2, 21) — §47-slider class.
§49 — The LUID DIAL: a zero-byte SCHEDULING dial (the sched.c analogue of §47) — func_8017A4AC (536 ins ×134), Phase 26 session 8
§47 splits a global.c allocno-priority tie by shifting a live-length. This splits a sched.c
rank_for_schedule tie by shifting an insn's position in the expand stream. Same philosophy: when a tie is
broken by an accident of ordering, change the ordering — without changing a single emitted instruction.
The residual. Two adjacent instructions transposed, registers already identical — a pure emission-order
residual. Not §47: global.c was innocent.
The mechanism (two passes, and the proximate cause is not the root cause).
- sched2 (proximate). gcc-2.7.2 schedules BACKWARD (
.sched2printsT-1= the last insn). At the tie point both candidates measuredpriority = 2and the same class vslast_scheduled_insn, sorank_for_schedulefell through to its final tiebreak —return INSN_LUID (tmp) - INSN_LUID (tmp2);— i.e. the tie is decided purely by position in the.gregstream. - sched1 (root).
adjust_priority→birthing_insn_p(bb_live_regs[dest] && reg_n_sets[dest]==1) hands every register-DEFINING insnLAUNCH_PRIORITY = 0x7f000001(max_priority, sched.c:2574). That boost lets the load chain seize the early backward cycles and sinks the un-boosted insn past its rivals — so the.gregLUID order comes out inverted and sched2's tiebreak then picks the wrong one. (A dead-end store —(set (mem) …), never "birthing", priority 2 — is starved and always floats to the front of its block.)
THE DIAL — materialize a call argument's sign-extension into an explicit s32 temp, placed AFTER the
intervening statement.
case 19: {
s16 a, b; s32 ea, eb;
a = ring[i]; i = (i+1) & 0x1FF; ea = a; /* NOT next to the load — see below */
b = ring[i]; i = (i+1) & 0x1FF; eb = b;
f(ea, eb); /* prototype widened to (s32, s32) */
}
It moves the sll/sra 16 pair EARLIER in the expand stream (lowering its INSN_LUID) while emitting exactly
the same instructions. Two placement rules are load-bearing:
- The prototype must be
(s32, s32)so the call itself adds no conversion. ea = a;must sit AFTER the store, not next to the load. Adjacent to the load,combinefuseslhu+sll+srainto a singlelhand you LOSE 3 instructions. The intervening store blocks that fusion, so thesll/srapair survives — identical bytes, earlier LUID.
The zero-byte dial family is now three (all emit nothing; all steer a tie):
| dial | pass | what it shifts | § |
|---|---|---|---|
live-length slider (asm("") between two volatile asms) |
global.c allocno priority |
live_length ±1 | §47 |
| sink-the-init / sink-the-consumer-call into the arms | global.c / local-alloc |
refs + live-range, or deletes the allocno | §48-A1/A4 |
| LUID dial (materialize a temp, placement-controlled) | sched.c rank_for_schedule |
INSN_LUID (expand-stream position) | §49 |
Method note (this is how it was measured, and it is reusable): -dS -dR on cc1 emits the .sched/.sched2
traces — the ready lists, the computed priorities, and the chosen order. When a residual is "two instructions
swapped, same registers", dump the schedule and read the tie: if the priorities are equal, you are on a LUID
tiebreak and the fix is a placement change, not a register change. Harness: .run/a4ac/dump2.sh.
§50 — Refinements that BOUND §47/§48 (from the func_80135EB0 wall, 21→6; Phase 26 session 8)
The one wave core that did NOT close still paid for itself: it produced the exact encoding of the §47 priority formula, a hard limit on the "cross_jump refunds the bytes" claim in §48-A1/A4, and a maspsx gotcha that explains a layout choice in the original we had never understood.
§50-A — THE PRIORITY ENCODING (use this; do not re-derive it).
pri = floor_log2(refs) * refs * size / (death − birth), where birth/death are 2 * insn_number — and
death is 2*M, not 2*M+1 (discriminated experimentally by the pre/post behaviour of a probe pseudo).
Ties break by ascending qty number = BIRTH ORDER. Worked: hoisting one statement above another pushed a
pseudo's birth one insn later, shrinking its qty range 11→10 → pri 0.4545 → 0.5, exactly tying a rival —
and because its qty is numbered first, it won the tie and took $v1. A tie you can compute is a tie you can
break: shift a birth, or shift a death.
§50-B — ⚠ THE CROSS-JUMP REFUND HAS A FLOOR (this BOUNDS §48-A1 and §48-A4).
§48-A1/A4 say "duplicate the code into both arms; cross_jump re-merges the identical tails after regalloc, so
it costs zero bytes." That is only true when the tails are ≥ 2 instructions, or when one path FALLS THROUGH
into the merged block. jump.c:1993 calls find_cross_jump(..., minimum=2) and does not count the jumps
themselves — so two js with a 1-instruction common tail will NOT merge. Only the minimum=1 path (a
jump compared against the code before its own target label) merges a single instruction.
Before using A1/A4, check the tail length. A 1-insn tail reached by two jumps costs you a real instruction — the refund does not arrive. (This is what turned a correct-registers attempt into 290 ins.)
§50-C — s16 PARAM + x | 1 MANUFACTURES A POISON TEMP; s32 DOES NOT.
With s16 arg1, v = arg1 | 1; expands to ior→T; sll; sra (REG_EQUAL sign_extend) — the ior can never
write v, so a temp is born, and its hard reg leaks into the allocator as a plain preference (see §50-D).
With s32 arg1 it is ONE insn writing v: combine's split reuses i2dest (combine.c:1818-1836, gated on
!reg_referenced_p(i2dest, newpat)) and no temp exists at all. Byte-neutral when the target's prologue does
no truncation (addu $s3,$a1,$zero). Widening a parameter can delete an allocno.
§50-D — COPY PREFERENCES BEAT PLAIN PREFERENCES, AND THEY NEED A BLOCK BOUNDARY.
set_preference (global.c:1535) runs a pseudo through reg_renumber[], so a block-local temp leaks its
hard reg as a plain preference — and find_reg scans plain prefs in ascending regno, so $v0(2) beats
$a1(5) deterministically. To override it you need a copy preference (find_reg checks those FIRST,
global.c:1000-1030), which requires the arg setup to be a bare (set (reg $aN) (reg P)) — and that means the
copy must live in a different basic block from P's last def, because combine's LOG_LINKS never cross
blocks. Route a second call through the same call site (a goto into a label at the shared jal) to give
that block ≥2 predecessors; reload then deletes the now-no-op copy — zero bytes.
§50-E — maspsx/gas MERGES lui $at FOR TWO STORES TO THE SAME 64 KB PAGE.
Reordering global stores to lengthen a live range loses an instruction: when two stores to the same 64K page
become adjacent, the assembler merges their lui $at. This explains a layout we had never understood — the
original interleaves D_801152AA / D_80126720 / D_801152A8 / D_80126724 / D_801152AC / D_80126722 precisely to
keep same-page stores apart. Never "tidy up" the store order of a matched function.
§50-F — the documented wall (honest defer). The residual 6 are a local-alloc.c:1568 (qty_compare_1 /
block_alloc) priority race needing the opposite winner from §50-A's: Q_x2 = 5 refs/range 18 → 0.5556 beats
Q_unkE = 2 refs/range 4 → 0.5. Flipping it needs either x2's range ≥ 11 (pri ≤ 0.4545) or unkE's range = 1 —
and both are blocked by sched.c, which always fills the load-use stall between lh and its consumer, and
places byte-free (reload-deleted) copies only in such stalls. The one productive angle left: a lever that
injects a reload-deleted no-op reg copy inside [lhu 4($a2) … sh %lo(D_801152AC)]. That is the whole delta.
§51 — TOOLING INTEGRITY: the silent skip, and how to hunt it
Phase 26-A (the inserted tooling-integrity audit). 68 findings across ~36 tools. This section is the METHOD and the LAWS; the findings themselves are in
docs/tooling-audit.md, the strategic why indocs/decision-log.md. Read this before writing any tool that scans the corpus.
§51a — The bug class
A scanner extracts N items from a corpus. The true count is M > N. Nobody ever compared N to M.
That is the whole class. It is not a typo, it is a structural blind spot, and it produced every one of the seven bugs found in Phase-26 session 8 and the twenty-eight found in the audit. Its signature:
- the tool reports success;
- the number it reports is smaller than reality and self-consistent;
- nothing downstream can tell, because a target that is never nominated produces silence, not an error.
Measured consequences in this project: 91.6% of all remaining work was invisible to target selection (a 3-file allowlist against a 14-file tree); the byte-gate could reach 4.9% of the canonical overlay; 62% of the endgame plan's byte-weight was already-matched phantom targets; and one 10% hole in a callee-signature oracle made nine byte-exact functions look like an intrinsic compiler wall.
§51b — Why the byte-gate cannot save you
The whole-binary byte-gate is a perfect CORRECTNESS oracle and a NULL COVERAGE oracle. It has never
once accepted a wrong match. It is also blind by construction to work never attempted: it has been
green since Phase 5, when 0% was decompiled, because INCLUDE_ASM pastes the ORIGINAL assembly.
A green byte-gate is compatible with ANY decomp percentage.
So the instrument the project trusts absolutely cannot see this class at all. Do not reach for it here.
§51c — THE METHOD (do not audit by reading the regex)
Reading regexes is the failure mode that wrote these bugs. For every scanner:
- Build a deliberately OVER-APPROXIMATING candidate detector for what it is supposed to find.
- Run both the detector and the real scanner over the real corpus (production data, not a toy).
gap = candidates − parsed.- Classify EVERY item in the gap — real silent skip, or justified exclusion. "I sampled a few" is not acceptable; if the gap is large, classify by shape and count each shape.
- Measure the blast radius against the corpus. Not "this could affect X" — go count how many functions/members/banks are actually affected today. Distinguish LIVE from LATENT (armed but not firing). Both are real; conflating them is not.
- Pair each finding with an adversarial skeptic told to REFUTE it. In this audit the skeptics killed 4 of 32 findings outright and corrected magnitudes in both directions.
§51d — THE LAWS
LAW 1 — Derive, don't re-derive (R33). Where a proven invariant answers the question, derive the answer from it rather than re-parsing the source.
- The invariant here: the fleet builds byte-identical, and
INCLUDE_ASMpastes the ORIGINAL assembly, therefore a function NOT wrapped inINCLUDE_ASMis byte-exact. progress.pyis the proof, in one file:weighted_metrics()derived from the invariant and was correct;classify()re-parsed C and inherited a bug. Same question, two tools, and the one that refused to re-derive was the one that was right.- The best outcome of an audit is a DELETED SCANNER, not a fixed regex. 28 findings collapsed to one
defect — a hand-maintained model of the corpus layout sitting on top of a filesystem that already
answers the question — and the fix was one derived oracle (
tools/corpus.py) and ~10 deleted scanners. A dict literal is strictly worse than the filesystem AND it fails OPEN (silently yields a plausible wrong answer) instead of closed. - Corollary: a derived fact cannot rot; a hand-maintained copy of it is a liability that grows with
every structural change.
.run/fuel_manifest.jsonrecorded 130 live stubs on 2026-07-08; the same code returned 30 six days later, because a TU split moved ~100 stubs out from under a dict literal.
LAW 2 — Assert your COVERAGE, not merely your correctness (R32). A tool that scans the corpus must compare what it found against an over-approximating candidate set, and fail on the gap.
⚠️ The sharpest lesson of the audit, and a correction to the first draft of R32:
build_engine_typeswas never silent. It printed[overlap] … handle manuallyevery single time, for four phases, while hard-exiting on 81% of its own corpus — the type-heavy tail's only sanctioned unblocker, unable to run on the corpus that tail lives in. It went unfixed because the message reads like a rare edge case rather than a four-fifths coverage failure, so nobody ever counted it. A loud failure that nobody counts is exactly as invisible as a silent one. "Fail loud" is not the rule. "Assert your coverage" is the rule.
LAW 3 — When an oracle is structurally blind to a class of error, add a SECOND ORACLE THAT CAN DISAGREE WITH IT (R34). Not a better assertion inside the first one.
config/symbols.us.txtdeclared a main-EXE RAM symbol at an address that is live code in every overlay. splat cut 97 real functions in half and invented 96 phantoms — 193 slices unmatchable by construction (one phantom's.sliterally beginslw $ra,0x10($sp)/addiu $sp,$sp,0x18/jr $ra: splat cut a function immediately before its epilogue and called the epilogue a function).- The byte-gate stayed green throughout and always would have, because the
.shalves are pasted back verbatim in original order. One phantom even got banked as a real match. - What exposed it:
sig_imagecomputes function boundaries from the ORIGINAL bytes without splat, and disagreed.make audit-corpusis now that second oracle, standing. - We had both oracles all along and never made them argue. Redundancy is only worth what you spend comparing it.
- ⚠️ Scope a cross-oracle check to the domain where the second oracle is genuinely independent. Run naively over all 136 binaries the same check reports 914 slices; the truth is 193. main/resident are signed by the Ghidra dumper, whose boundaries are shorter by design — so the comparison measures Ghidra's limits, not splat's errors. A check applied outside its valid domain does not become more thorough; it becomes noise.
LAW 4 — A rule that needs a human to remember it is not a gate. Make it structural.
.o ← .sis not a dependencymakecan see: assembly arrives viaINCLUDE_ASM, expanded to a.includeconsumed by maspsx/as after cpp, while-MMDtracks headers only. Re-extract, build incrementally, and make links a stale object.- This is not merely slow.
INCLUDE_ASMpastes the ORIGINAL bytes, so a stale object still yields the original image: SHA1 goes GREEN while the split you just changed is never exercised. A brokenconfig/change can be "verified" by an incremental build. - R22/H3 already legislate this ("clean rebuild"; "
make cleanafter anyconfig/change"). They are right, and they were broken anyway — by me, mid-audit. Soextractnow deletes the objects that include what it just rewrote. Structural, not advisory.
§51e — The false-wall pipeline (why this is not just hygiene)
A silent skip does not stay quiet. It compounds into a false wall:
wave_targetshands a drafter an asm path that does not exist (78 of 87 targets).- The drafter drafts against nothing and fails.
- The failure is booked into the backlog as a matching failure.
reserved_walls()reads the backlog and permanently blacklists a function that was never attempted.
Same shape with a lying closeness oracle: masked_diff left R_MIPS_PC16 unmasked, so 155 functions
scored a phantom non-zero against an unresolved placeholder that can never compare equal. An agent
grinds forever at a wall that is not there, and the result is filed as an intrinsic compiler residual.
Before you write up a wall as intrinsic, prove your instruments could have seen the alternative. How many of the walls "byte-proven" across 26 phases were lookup misses wearing a wall's clothes?
§51f — Checklist for any new corpus-scanning tool
- Does the filesystem already answer this? Then glob it — never keep a second copy (LAW 1).
- Does a proven invariant already answer this? Then derive it — never re-parse (LAW 1).
- Over-approximating candidate set +
assert found == candidates, failing with the unparsed items (LAW 2). - Does it print a count nobody checks? Then it is not asserted — it is decoration (LAW 2).
- Symbol regexes: any C identifier, not
func_[0-9A-Fa-f]{8}— curated names exist, and curated naming increases as RE quality improves, so afunc_-only oracle rots by design. - File lists: glob, never a suffix allowlist — the next split kind re-opens the hole.
- Definition detectors: handle K&R (
f(a)/int a;/{), multi-line signatures, and single-line bodies. K&R is this project's house style for exactly the biggest, highest-reach functions. And find the signature's closing paren with a real paren-walk —line.count('(')andsplit(')')[-1]both land on the wrong paren for a one-line body containing a call.
§51g — When the thing you are scanning has a GRAMMAR, parse the grammar (tools/cdecl.py)
The
cdeclbuild (Phase 26-A). Fifteen tools carried their own regex model of "what is a C declaration". They disagreed — two tools in ONE pipeline disagreed about whetherextern s32 D_a, D_b;is a declaration at all — and all fifteen shared one character class:extern\s+([A-Za-z_][\w\s\*]*?\bD_[0-9A-Fa-f]+\s*(?:\[\s*\])?)\s*;which cannot hold(,,, or a non-empty[N]. So three whole shapes were invisible to every one of them: fn-ptr / jump-table arrays (extern void (*D_8018E858[])(void);), sized arrays (extern s32 D_80127530[4];— one unparsed[4]blockedfunc_801387B8in 134 TUs), and multi-declarators (where the whole line is dropped, not just declarator 2..N).
THE LESSON. Do not enumerate shapes — parse the grammar. The audit's own prescription was a shape-aware alternation per tool. That is N more chances to diverge, and it only ever covers the shapes someone remembered. C's declarator grammar is small, closed, and total: it describes fn-ptr arrays, 2-D arrays, multi-declarators, fn-ptr parameters and K&R identifier-lists without being told they exist. A ~250-line recursive-descent parser is less code than the fifteen regexes it deletes, and it is total by construction rather than by anyone's memory.
Measured, whole corpus: 2,952,246 depth-0 statements → 2,731,521 declarators, 0 parser defects; 50,405 distinct declarations round-tripped through the real cross-gcc, 0 rejected.
LAW 4 — The candidate set can be DERIVED too (R33 applied to R32). Every other tool here hand-maintains an over-approximating candidate regex in order to measure its own coverage. It does not need one: at file scope, C admits nothing but declarations. So the candidate set is every depth-0 statement, taken from the grammar itself — the strongest assertion available, and one that cannot rot, because there is no second model to drift.
LAW 5 — Let the compiler adjudicate your own coverage gap. When 40 statements would not parse, the
temptation is to decide for yourself which "don't count" — which is grading your own homework, the exact
habit that produced the fifteen bugs. Instead, hand each one to gcc: a statement gcc also rejects is
not C, so rejecting it is correct and the INPUT is corrupt; a statement gcc accepts and your
parser does not is your defect. The exclusion set becomes a verdict from the C front end rather than an
opinion. (Outcome here: 33 residual, all 33 adjudicated NOT-C by gcc, all in dead .run/drafts*
scratch, none in src/. And an honest R14 near-miss: they were written by a recovery tool that
prepended extern to an if statement — but the current oracle emits 0 garbage over 300 signatures,
so the bug was already fixed in Phase 19. Mechanism confirmed, consequence nil: verify the blast radius,
not just the defect.)
LAW 6 — Column 0 is not file scope. m2c emits goto labels (done:, block_13:) at column 0 inside
function bodies. Every tool that equates "starts at column 0" with "is at file scope"
(reconcile_tu.tu_visible, scope_data_externs, jr_isolate_all) rests on a heuristic the corpus
violates. Track brace depth; it is ten lines.
LAW 7 — A raw text scan cannot see a TU's declarations, and cpp can. src/shared/engine_core.h is
23,546 backslash-continued lines inside 1,801 #define macro bodies. A declaration in a macro body
declares nothing — it becomes a declaration only where the macro is invoked, above the invocation
point (the §8c law). So a raw scan is wrong in both directions: skip the #defines and you miss all
1,801; read them and you invent ambient decls the TU never had. cpp answers it exactly, in 54 ms on
the largest TU (~20 s for the whole 678-TU fleet, cacheable) — which is why
canon_sig_reconcile._file_scope_statements was the one scanner the audit measured CLEAN, and why any
tool carrying its own model of macro expansion (reconcile_tu._macro_externs(), .rstrip('\\') and all)
is re-deriving what the build already guarantees. Corollary: reconcile_decls.DATA_DECL_LINE_RE is
line-anchored, and every decl in engine_core.h ends in a \ — so its "authoritative tier" over the
shared header finds zero declarations. It has always been empty.
LAW 8 — A block-scope extern is not a file-scope canonical. gen_harvest_targets and sig_unify
both count externs declared inside a function body (6 of them in engine_core.h) as authoritative
file-scope declarations. A block-scope decl is private to its function and expires at its }; promoting
one to ambient truth is precisely the confusion behind the §8d conflicting types for D_801812A4 wall.
A depth-aware parser excludes them for free.
And the dividend: a real parser lets you assert things that were previously unaskable. With cdecl
in hand, "every canonical signature the callee oracle emits must PARSE as a C declaration" becomes a
one-line check. Before it, nothing in the repo could tell a signature from garbage — which is how
extern if ((func_80029178(0x119) & 0xFF) != 0); got written into a draft and then read, downstream, as
a compiler wall.
Gate: make audit-cdecl (coverage + gcc). Standing, because a loud failure nobody counts is exactly
as invisible as a silent one (LAW 2).
LAW 9 — THE ADJUDICATOR MUST BE THE COMPILER THAT COMPILES YOUR CODE. Not the C standard, and not
whatever gcc is on the PATH. Building cdecl.compatible() ("will this declaration coexist with that
one?" — the question every recovery pass actually asks) against modern mipsel-linux-gnu-gcc and
against the real gcc-2.7.2 cc1 gives three different answers, and only one of them is the truth:
| declarations in one TU | C standard | modern gcc (C11) | gcc-2.7.2 cc1 |
|---|---|---|---|
typedef int X; twice |
error | accepts | ERROR — redefinition of 'X' |
extern u16 X; + extern volatile u16 X; |
error | error | ACCEPTS |
void X(s16); then void X(); |
error | error | ACCEPTS |
void X(); then void X(s16); |
error | error | ERROR |
Validate against modern gcc and you encode rules cc1 rejects and miss rules cc1 accepts — a recovery pass
that "approves" declarations the real front end refuses is exactly the failure this whole audit is about.
cdecl --compat therefore adjudicates with tools/bin/gcc-2.7.2-psx/cc1, and now agrees with it on
1,485/1,485 live corpus pairs. Two rules in that table were refuted by the oracle after I had
already written them from the standard.
🏆 AND THE LAST ROW IS A WALL COMING DOWN. The no-prototype rule is ORDER-DEPENDENT: only
()-then-narrow-prototype fails; prototype-then-()compiles fine. Phase 15 wrote this class up as "the 159 arity/narrow-param conflicts — no clean deterministic fix" and closed it. The stated cause does not hold. Whether the resulting codegen matches is a separate question the byte-gate answers — but the door was never locked. It took four three-line probes and 90 seconds to find out. Probe the compiler for FACTS; read its source only for LEVERS; byte-validate both (we readgcc-papermariofor five phases believing it was 2.7.2 — it was 2.8.1).
LAW 10 — DERIVE WHICH TU, TOO — not just what is in it. cast_call_sites / sig_unify /
reconcile_decls take --src-file, an optional, hand-passed flag naming the TU to canonicalize
against; unset, it defaults to src/<ov>/<ov>.c. No caller knows about the Phase-26 _jr_<ADDR> carves.
Measured on ov_SC01_077: 263 open stubs across 12 TUs — only 13 in the main .c. 95.1% of drafts were
being reconciled against a translation unit that would never compile them, while harvest_verify
(fixed in A3) correctly spliced them into the right one. corpus.stubs() already knows the answer — the
INCLUDE_ASM line is self-describing. Ask, don't assume. Fixing it took the callee-conflict repair
from 8 → 58 of 196 drafts (7× reach).
AND THE HONEST OTHER HALF (P9/R14): those 58 produced ZERO new banks. The historical draft tail fails on codegen, not plumbing —
func_801387B8, which the audit blamed on a single unparsed[4], is really 67/100 instructions off with a$s0/$s1swap. What the fix did buy is real but narrower: 52 drafts moved from "won't compile" to "compiles, N instructions off." That is not a bank — it is the difference between an invisible failure that reads as a compiler wall and a scored near-miss the permuter and the §47/§48 dials can act on. Which is the audit's thesis exactly. Do not sell it as more than it is; three times in one session a confirmed mechanism produced a null consequence.
LAW 11 — A FIX IS NOT LANDED UNTIL ITS CALLER STOPS OVERRIDING IT. The single worst defect in the audit was not in a scanner. It was one default argument in the caller of a scanner we had already fixed:
# tools/gate_stage.py:315
summary = run_gate(a.drafts, binary=b, src=a.src or f"src/{b}/{b}.c", ...) # <- ALWAYS the main .c
src restricts the byte-gate to ONE translation unit. A3 had just taught harvest_verify to derive
each draft's home TU when --src is omitted, lifting the byte-gate's reach from 4.9% to 100% — and
gate_stage never omits it. So the primary banking path (every wave, the grinder, the orchestrator,
bulk_harvest) stayed structurally incapable of banking 250 of 263 stubs, after the fix, because of
its own caller's default.
AND HERE IS WHY IT SURVIVED 26 PHASES.
harvest_verifycannot splice a draft whose stub is not in the TU it was pointed at, so the draft simply never verifies — and is then logged asnear/failed, i.e. as a matching problem. The wave reports a low close-rate and the function goes to the backlog as a residual. A tool that CANNOT bank a function is indistinguishable, in every log this project keeps, from a function that CANNOT BE banked. Proof, same draft, same gate, same second:gate_stagerejectedfunc_80129C40;harvest_verifyrun directly (no--src) verified it byte-identical and banked it.
And a counting bug that hid the hiding: when match_one says MATCH but the whole-binary gate rejects,
gate_stage logs status="near" and never increments the counter. A run of 63 such drafts printed
banked 0, near 0, failed 0 — three zeros that do not sum to 63 — for phases. Nobody added them up.
(LAW 2 again, and note the shape: the number was not wrong, it was ABSENT.)
Checklist item, promoted to the top: after fixing a scanner, grep every call site and ask does any
caller pass a default that re-disables this? An audit that stops at the callee is half an audit.
§52 — The WALKER-FAMILY skeleton: 6 regalloc-order levers + a deeply-characterized intrinsic wall (func_80178004, 165 ins ×134; Phase 26, Fable5, 2026-07-15)
func_80178004 is the exemplar of the regalloc-order residual class (12 reach-134 siblings share it: the
9 close=0 + 3 close-21+ cores). A ~93-min / 477k-token Fable5 pass drove a PIN-FREE draft to
structure-exact (163 vs 165 ins); residual = pure register identity — byte-verified (match_one 126/165
masked; the 126 is dominated by one $s0↔$s2 swap rippling ~90 lines). It did not byte-match: the wall
reduces to three compiler-internal integers and is (probably) intrinsic to vanilla gcc-2.7.2.
Correction (R14): the historic "pinned MATCH" was a myth — the pinned seed was NEVER a match (best
historic permuter score 5, pinned); this is the deepest state the function has reached. Artifacts + gdb
oracles under .run/fable_80178004/ (repro runorc.sh); Fable5-derived, headline byte-verified.
The 6 levers (the "walker-family skeleton" — apply to the 12 siblings; levers 1-5 retire ~half the residual)
- Mutated-parameter pointer. Declare the walking pointer AS the mutated parameter (
u32 *p=arg0, thenp += …). Anyp = (u32*)a0COPY leaves the initial value live in the param pseudo → cse1 rebases every store block onto constant offsets and DELETES the pointer-walk. The single biggest lever; transfers to every walker-style function. - No derived-base variable. Never declare the second pointer — write
p[1..3]directly and let loop.c mint a combined DEST_ADDR giv of the biv (anchor = last-recorded giv → the -8/-4/0 offsets; preheader init reads the biv reg). A source-levelqbiv makes loop.c reduce the (q-4,q-8) pair into an EXTRA pointer (loop.c:3824 worthwhile test). - In-loop constant remat. A constant the target recomputes in-loop must be a user variable assigned in
BOTH if/else arms (
n_times_set==2fails scan_loop's movable gate, loop.c:698-712 → stays in-loop). A bare literal is hoisted. - Split the OR across two statements.
t = …|0x4000; p[3] = t|0x6d160000;beats tree-level constant reassociation → in-loopori+ a hoisted lui-only temp. - Break biv-recognition. Interpose one statement between
n = i+1andi = n(basic_induction_var follows a copy only to the immediately-previous insn, loop.c:4862) → the target's literal counter survives instead of a<<16giv. - Diagnose allocation walls with gdb-on-cc1 FIRST.
peek2.gdb(allocno_order/find_reg) + a 3-integer oracle (reg_n_deaths/reg_n_refs) tells you in minutes whether a register identity is even reachable — before grinding C.
Why the wall is (probably) intrinsic
The walker is a block-local 1-death qty → local-alloc runs first and hands it the first free callee-saved
$s0 (local-alloc.c:472/2103; nothing can pre-occupy s0/s1 for a call-crossing qty). Force it global
(deaths≥2) and its priority floor_log2(44)*44/103 ≈ 21000 dominates → allocated first → pass-1 first-fit
$s0 again (global.c:924, callee prefs stripped for call-crossers). The target needs pri < 2400 ⇒ effective
refs ≤ ~10-12, but REG_N_REFS counts RTL mentions, fixed at 44 by the bytes, and every legal construct
only pushes it UP (each ruled out with file:line). One untested lever: instrument qty_n_refs (local-alloc
SUMS at tying, local-alloc.c:1869) vs flow's per-reg REG_N_REFS with peek2.gdb on tied-copy chains — if a
tie shape yields a low-ref global view of an s2-window value, the wall falls; else it's the ×1-pinned +
whole-binary-gate route.
Flywheel note (R16): even walled, this pass paid off — a cheap-Opus wave applying levers 1-5 should crack the siblings that are NOT at the intrinsic wall. Fable5 DISCOVERS the skeleton; cheap-Opus APPLIES it.
§52a — The regalloc sibling wave: new levers + two new wall classes (cheap-Opus applying §52, 2026-07-15)
Ran §52 as a 6-agent Opus wave over the regalloc-order reach-134 cluster: 2/6 banked ×134 (func_80171FFC,
func_801775E0 → 268 instances, R22 136/136), the other 4 = precisely-characterized walls. Banks and walls
each yielded a byte-verified, reusable lever:
New banking levers (each verified by a whole-binary bank):
- Pass the callee its real arguments —
f((s32)a0,(s32)a1), NOT the void-cast no-arg trick((T(*)(void))f)(). When params are saved to callee-saved regs before the first call, the void-cast form STRIPS the param pseudos' arg refs → shuffles the$s0/$s1/$s2order (RC-10 density). Passing the args naturally BOTH suppresses arg-setup moves (params already in place) AND preserves the density order. (func_80171FFC.) - Recompute a store-base in BOTH if/else arms (the §52-lever-3 analogue for a base pointer): keeps the
recomputed base live across the branch-merge so subsequent
p[k]address off it, not gcc address-CSE onto a still-live keeper base. (func_801775E0.) - Copy-chain direction: use the incoming param DIRECTLY as the persistent keeper (
keep = param+off) with a working copyw = param; gcc savesparam→$sK, chains$sJ=$sK. An explicit keeper var reverses it. (func_801775E0.) pp-declaration position drives the prologue schedule (RC-1/K1): declare a param-copy pseudo AFTER the one you want saved first — sched1 emits in pseudo-creation order. (func_801775E0.)- A
const/RTX_UNCHANGING_Ppre-call load frees a sched2 prologue save-order tie (RC-3):f(*(const s32*)(a0+off))— sched.c:true_dependence drops the load↔store deps sosw $s0/sw $rastop readying simultaneously → the descending-regno save tie staggers right. BOUNDARY: it can OVER-free (the load then hoists above the saves), so it fixes save-order but not always the whole function. (func_80131A34.)
Two NEW intrinsic-wall classes (byte-characterized, P9 — distinct from §52's flagship $s0 local-alloc wall):
- Caller-saved priority-first-fit wall. A long-lived block-local value (
$v0/$v1/$a0scratch) stored many times has LOWqty_comparepriority (floor_log2(refs)·refs·size/(death−birth)) → loses the low-reg first-fit to its SHORT-lived competitor. §31-B in-out-asm and §47 live-length split TIES only, never a priority GAP, and the competitor can't be lengthened without deleting an instruction the target keeps. Needs pins → ×1-only. (func_80169228, bounded to 3 register identities.) - Non-coalescing delay-slot copy wall. A value computed in
$vX, tested, then copied to$vYto survive a clobber in the branch delay slot needs two non-coalescing equal-valued pseudos. Every pure-Cy=xis destroyed by cse.ccanon_reg/make_regs_eqvhead-promotion or global-alloc coalescing; the only preserving forms are#APPasm (blocks reorg's delay-slot fill) or a$0-add (SIGABRTs cc1 in sibling TUs, §42e). Intrinsic pin-free/sweep-safe wall. (func_80177AD4, 1 instruction.)
Wave economics: ~⅓ of a fully-walled cluster cracks pin-free by applying the idiom; the rest wall on a small set of distinct, now-named mechanisms. Cracks bank ×134; walls become permuter seeds or documented dead-ends.
§52b — Sibling wave 2: more de-pin levers, a third wall class, and the match_one→whole-binary gap at scale (2026-07-15)
A second 6-agent Opus wave (armed with §52a) over the close=0 regalloc cluster: 3/6 banked ×134
(func_801379FC, func_801497A8, func_801495C4), 2 whole-binary-near, 1 new wall. Additional VERIFIED levers
(each proven by a whole-binary ×1 bank):
- Per-loop pseudos for a register role-swap — when the target uses pointer=$s0/index=$s2 in one loop and the
swapped roles in another, a single shared C var can't (one hard reg each). Declare SEPARATE per-loop locals
(
s32 p; s32 idx;inside each block); the swap falls out of K2 density (the ref-heavier value wins the low callee-saved per block). (func_801379FC.) - The RC-7 "second-set" dial —
u8 *s = SYM; __asm__("" : "=r"(s) : "0"(s));makesreg_n_sets(s)==2, failingupdate_equiv_regs' single-set gate → NOREG_EQUIV→ the value is NOT rematerialized at its use → it must hold a callee-saved reg across calls (matching a target that keeps a base in$s1). Zero bytes, sweep-safe (generic"=r"/"0", no$N— distinct from the §42e$0-add). (func_801497A8.) - Value-barriers dissolve the CSE-stack-address-common wall — when correct stack-slot order (RC-1 decl order)
forces a
&buf(sp+off) to be CSE-commoned across two calls into a call-crossing pseudo that steals a callee-saved reg, wrap each&bufuse in__asm__("" : "=r"(m) : "0"(&buf))so cse can't fold them → each rematerialized fresh at its call. STEERABLE (banked), not intrinsic. (func_801495C4; itsfunc_8014964Ctemplate used a register pin + 5 barriers — the pin was superfluous.)
A third intrinsic wall class (byte-characterized, P9): the symbol-address-base "wins-low-needs-high" wall
(dual of §52a's caller-saved-priority wall). A 2-instruction symbol-address base feeding its own N loads is the
densest block-local pseudo → first-fits the LOW reg ($v0), but the target needs it HIGH ($a1); making it
low-priority is structurally impossible (a base can't out-rank the loads it feeds), and the movstri form that
would place it high triggers the §52-flagship (plus $fp const) local-alloc theft (update_equiv_regs can't
rematerialize a non-CONSTANT_P source). Only a register pin resolves both → ×1-only. (func_8012B4B8, close=15.)
Process finding — the match_one→whole-binary gap at wave scale: ~half of the agents' match_one close=0 drafts do NOT bank whole-binary (isolated reloc-masked compile overstates; A10). The whole-binary gate is the sole arbiter — a match_one MATCH is a CANDIDATE, not a bank; budget the gate cycles.
§53 — SWEEP A FAMILY WITH THE TOOL ITS EXEMPLAR NEEDED: the jr/switch carve, and how omitting it manufactured the "families don't template" doctrine (Phase 28 T1, 2026-07-15)
The law (one line): a family sweep must reproduce every build step the exemplar's own bank required. Omit one and the gate rejects every sibling — a result that reads exactly like an intrinsic wall, at any scale.
The case
0x8017BEBC (952 ins, the largest unmatched core in the game; 115 unmatched members across 21 addresses,
addr_tag: scattered, PURE 109 / IMM 6) was the roadmap's B2 — "possibly the largest cheap win left."
Phase-27 T5 swept it with family_sweep, banked 0 of 8, recorded "all genuine byte-DIFF", and
generalized it to "h_seq/h_norm structural families do not mechanically template (≈0%)" — which
rewrote the endgame's arithmetic to "(cores cracked) × (reach), NOT (families) × 120"
(PhaseEnd_Phase27 Roadmap delta; docs/calibration.md called it "the decisive P28/P29 input").
Re-run through the carve path: 8 of 8 BANKED (4 same-address + 4 cross-address via to_addr),
make clean + extract-all + check-all → 140/140 byte-identical.
Why 0/8 was structural, and predictable from two words
0x8017BEBC is a jr/switch core. §47 banked its exemplar as "lazy isolation → carve (9-piece
interleave) → splice → BYTE-IDENTICAL" and explicitly noted the fix is "×N template-safe."
family_sweep.hseq_sweep stages C and gates — it has no carve step. So gcc's generated jump table is
never placed at the sibling's address, and the residual is exactly:
classify_member(ov_SC01_000 -> ov_SC01_001) = PURE, ndiff=2 # positions 343, 345 — TWO WORDS
idx 343: 3c01801a vs 3c01801f lui $at,%hi(jtbl_801EC44C)
idx 345: 8c224374 vs 8c22c44c lw $v0,%lo(jtbl_801EC44C)($at)
config/overlays.mk:112 ov_SC01_000_JTBL_INTERLEAVE := ...,ov_SC01_000_jr_8017BEBC.o,tail12.data.o,...
config/overlays.mk:134 ov_SC01_001_JTBL_INTERLEAVE := ... <- no jr_8017BEBC entry. The table is unplaced.
A PURE, ndiff=2 family is the closest thing to templatable that exists. It failed on a build-config gap.
The rule
- A family with
has_mid_jr: trueMUST be swept withtools/jtbl_family_bank.py, neverfamily_sweep. It carves per sibling (jtbl_carve→make extract(+ld_interleavesandwich) → remap → whole-binary gate, revert-on-fail).family_sweepis correct only for non-jr families. --rawthe standalone crack, not the banked TU.extract_uniton a banked jr body hands the sweep the TU's file-scope decl layer (here twoD_800B9A02externs) = §41c pollution. The preserved crack (.run/phase26-cracks/<fn>.c) carries the fix and its reconcile at block scope, so it travels.- Trust the file, not its header.
func_8017BEBC.mdstill says "close=2 of 952" — the state BEFORE §47's slider closed it. The.cwas updated; the.mdwas not. Templating from a body you believe is a near-miss, or reading a stale header as current, produces zeros you will misread as a wall. symbol_mapdoes NOT need ajtbl_prefix. Tempting and wrong: a compiler-generated switch table is never named in C, so there is no token to substitute (grep jtbl src/ov_SC01_000/…_jr_8017BEBC.c→ nothing). The fix is placement (carve + interleave), not substitution. (This was a real mid-plan error: the diagnosis "symbol_map cannot generate jtbl_" is TRUE and the fix derived from it is FALSE.)
The meta-lesson (R35, and why this one is expensive)
The 0/8 was cited as the decisive input for two phases of planning. Three compounding failures made it:
- Wrong tool for the class — swept a jr family with a carve-less sweeper.
- n=1, least-representative —
has_mid_jris 3 of 163 matched-exemplar families; the rarest class was generalized to the whole frontier. - The corroborating evidence was pre-fix — the three Phase-26 exhaustion probes (tiny-IMM 0/241,
PURE 0/134, pinned 0/133) all predate
_carry_macros(P27 T5,commit:0637). P27's own decision-log calls its re-probe "a fourth phantom exhaustion proof" — naming the mechanism that would have faked the first three, and never re-running them.
Before a 0% retires a lever, ask: did I run the steps the exemplar's own bank required? is this family representative of the class I'm generalizing to? was the corroborating evidence taken through the same broken tool? A 0% from a broken tool and a 0% from a working one are the same number and opposite facts.
§55 — Core-crack wave levers + the GATE-ORCHESTRATION law (Phase 29 T3, 13-agent ultracode wave, 2026-07-17)
A 13-agent worker_wave over 6 fresh cores (260–371 ins) + 7 B3 near-misses (100–141) returned 7 MATCH /
6 near; 5 banked whole-binary. The durable yield is the levers + the orchestration law.
§55a — New byte-proven levers (each from a banked or near draft)
- §49-variant — suppress sched1's
birthing_insn_pLAUNCH_PRIORITY boost byreg_n_sets1→2 (func_801325B8, 113 ins, BANKED, reproduced twice). Route the load through a temp assigned in BOTH halves of a branch: the pseudo now hasreg_n_sets==2, so it is no longer "birthing", the priority boost disappears, and two transposed loads un-invert at zero byte cost. Companion to §30's birthing-boost. birthing_insn_pgoverns sched1 PLACEMENT, steerable both ways (func_80177940, 101 ins, close=5): an in-place single-variable update makes a chain non-birthing → sched1 stops sinking it (18→10); a fresh pseudo forc<<16makes it birthing (10→5). Also:__asm__("" : "=r"(v) : "0"(v))is a zero-code CSE fence that blocks the cse2 fold of a duplicated chain.cc1 -dLprints loop.cmove_movablesdecisions (moved / not-desirable, per movable) — the threshold (−3/movable) is steerable by statement order. A dead second set (col = 0;) makesn_times_set != 1, so a constant is not a movable and stays in-loop. (func_80177940.)- Switch TREE vs jump table —
CASE_VALUES_THRESHOLDis 5 (func_801387B8, 100 ins, BANKED): 4 cases{1,7,10,23}< 5 ⇒ gcc emits a branch tree (forward-beq-to-body + lone medianslti), NOT a jtbl — so a guard likeif (cmd != 0)must stay OUTSIDE the switch or a 5th case forces a jump table.u8 cmdbuys a signedsltion the split AND an unsignedsltiuoncmd >= 0x20for free; case bodies emit in SOURCE order. Ghidra's if-chain for a switch is a decompiler artifact (floors at 49-off) — recognize the dispatch tree. - Block-scope beats
*(T*)&symfor a conflicting extern (func_8014ADE0, BANKED): when aDEFINE_func_*macro declaresextern s16 D_Xand you needs32, a block-scopeextern s32 D_Xis a warning (not an error) — but it is order-dependent: block-scope must precede the file-scope decl (reverse = hard error, cc1 exit 33).*(s32*)&D_XFAILS (~100-ins shift — the §18&symmaterialize + CSE trap).
§55b — THE GATE-ORCHESTRATION LAW (3 traps, ~3.5h lost; all recovered, 0 data lost)
gate_stage's propagate step is FLEET-WIDE (dedup_propagate --auto-from), NOT scoped to the drafts you gated. Runninggate_stageonce per src-file group therefore re-runs the whole-fleet scan N times; each exceeds the 3600s timeout and dies mid-mutation → partial propagate damage (measured: 90/140 overlays broken, 887 files, engine_core.h +561). LAW:--no-propagateon every per-group gate, then ONE targeteddedup_propagate --addr <banked addrs>at the end.- COMMIT the cheap verified banks BEFORE the expensive propagate. The byte-gate is minutes; the propagate is ~2h and mutates 300+ files. Gating and propagating in one motion means every propagate failure takes the (already-verified) banks down with it. Commit, then propagate as a standalone revertable step.
- A reverted
src/needs a RE-EXTRACT (the R22 corollary, again):git checkout -- srcrestores theINCLUDE_ASMstubs, butasm/still reflects the BANKED state (splat emits no.sfor a matched fn) ⇒corpus.CorpusError: N stub(s) have NO .s on disk. Recovery =git checkout -- src docs .run+make extract BINARY=<ov>. (R34's second oracle caught this loudly — working as designed.) gate_stage's default.run/harvest_verified.txtACCUMULATES across runs and its CLI exposes no--verified-out⇒ after a revert it reports a phantombanked: Nfor functions still stubbed insrc(stale residue). Trust the SOURCE (grep INCLUDE_ASM), never the report — R32/R35 class, and still armed: either expose--verified-outon the CLI or unlink the default before each gate.
§55c — Sizing the propagate: a TARGETED propagate is ~4 min/core, and "it's slow" was a BROKEN-TREE ARTIFACT
--check-only prints the real plan first — always use it (it also aborts free when a group is undroppable).
The real cost, measured on a HEALTHY tree: --addr <1 core> = 233s for 138 members → 3 cores ≈ 12 min.
[ OK ] 138 overlays byte-identical after propagation.
The trap (and a live R14/R35 self-correction worth remembering): the same 3-core targeted propagate had
timed out at 3000s earlier in this task, and I wrote "needs ~2h+" into the commit + this cookbook as a
measurement. It was nothing of the kind — it was propagating into a tree still carrying the partial damage
of a previous killed --auto-from (90/140 overlays broken), so every member-gate was failing/retrying. On a
clean tree the identical command is ~20× faster. A timing taken on a broken tree is not a measurement of
the tool — it is a measurement of the breakage. (Same shape as the §53 carve-law and the §54 def-sig
findings: the number was real, the attribution was wrong.) Recover the tree FIRST (revert + re-extract),
THEN measure.
- Only
--auto-fromis genuinely fleet-slow (it scans every matched fn) — that is the one to avoid, not the targeted--addrpath. h_exactshare is all-or-nothing: one straggler overlay (ov_SC03_093) drops the whole group unless--recover(per-overlay exclude) is passed.- Local-type bodies are skipped ("not self-contained") until
build_engine_typeslifts their types (§19/§20 propagation cap) — 2 of this wave's 5 banked cores were blocked this way.
§54 — --fix-def-sig: the member's CANONICAL DECLARATION is a build step too (tiny-IMM mega-pools, +4,801, Phase 29 T6, 2026-07-16)
The §53 law — sweep a family with the tool its exemplar needed — has a fourth instance beyond the jr-carve
(§53) and the -O0 flag (Task 1): the member's shared-header declaration. The two tiny-IMM mega-pools
(0x80131eec, 0x80130d0c; ~15-ins jump-table dispatchers D_x[*(u16*)(a0+2)](), ~2600 members each) banked
1/4966 (0.0%) via plain family_sweep --hseq AND 0/2470 via --reconcile (canon_sig_reconcile).
Root cause (byte-proven, after 3 masked-metric mis-reads — see decision-log 2026-07-16): family_remap
copies the EXEMPLAR's def signature onto each member. When a member is forward-declared in
src/shared/engine_core.h with a caller-derived signature (a shared engine fn calls it:
extern void func_8015FAAC(s32 *a0);) that differs from the exemplar's (void *a0), the member TU throws
conflicting types for func_X (cc1 exit 33) and never compiles. The exemplar itself has no such header decl
(that asymmetry is why the family templates in ov_SC01_077 but not its members). This is INVISIBLE to
diff_regions/match_one/masked_diff (they compile the draft STANDALONE, so no header decl to conflict
with → they report O2:MATCH(0)), and to --reconcile (it reconciles against the sibling TU's local decls,
not the included header).
The fix — byte-neutral, gate-arbitrated. family_sweep --fix-def-sig (header_sig_map parses the 1005
extern … func_X(…) decls in engine_core.h/engine_types.h; reconcile_def_sig rewrites the member draft's
DEF sig to that canonical). A pointer-type param diff (s32* vs void*, (s32)a0 identical) doesn't change
codegen; the whole-binary gate rejects anything that does (G3/P9). Result: pool 1 94%, pool 2 99% =
4,801 banked. Consider making --fix-def-sig default-on for the h_seq path. Meta: a masked/standalone MATCH
is a candidate, never a diagnosis — reproduce the real member TU and read the real cc1 error (R35).
§56 — Banking a hand-drafted GIANT into its exemplar TU: self-contained draft vs live-TU decls, 4 reconciliation tactics (func_8013FAF8, 312 ins ×138, Phase 29 T4, 2026-07-17)
A hand-crafted giant draft is self-contained (its own typedefs + externs) so match_one can compile it
standalone. Splicing it into its live exemplar TU surfaces decl conflicts the standalone compile is blind to
(same blind spot as §54, but a GIANT touches dozens of symbols). harvest_verify strips the typedefs the TU
already provides (per-TU strip-set) but NOT the externs — those you reconcile by hand. Every fix below is
byte-neutral and re-verified two ways: match_one must stay green AND the whole-binary gate must stay
byte-identical (G3/P9). The loop: harvest_verify --binary <exemplar> --drafts <isolated-dir> → it prints ONE
PLUMBING: <cc1 line> per fail. That classifier line is often a red herring — it grabs the FIRST
conflicting types in stderr, which may be a pre-existing benign warning (conflicting types for built-in function memcpy, present in the 140/140 baseline). Get the FULL cc1 stderr (splice manually, make build BINARY=<ov> 2>&1 | grep error) and fix the REAL errors bottom-up.
The four conflict classes and their byte-neutral fixes (all proven on func_8013FAF8: s16/s16 def, 3 data + 2 fn conflicts, ~5 gate iterations):
-
Def-sig conflict (draft
void func_X(s16,s16)vs the fleet-canonicalextern void func_X(s32,s32), 404 decls). NARROW the extern fleet-wide s32→s16 — NOT--fix-def-sig.--fix-def-sigrewrites the DRAFT to the canon; here the s32 def diverges from the target at insn 22 (thefor(i=arg0;i<arg1)compare promotes differently). Direction test (R35): the correct direction is whichever keepsmatch_onegreen — test the canon-typed variant explicitly before assuming--fix-def-sig. Byte-neutral iff EVERY fleet caller passes cast/small-const args — verify (grep -rh 'func_X('): here all were(s16)-cast or0/5/7/8. -
Data-symbol conflict, file-scope decl BEFORE the splice point (block-scope is §55a-BLOCKED — a block-scope extern after the file-scope one is cc1 exit 33). Use the TU's established §18 cast-at-use-site convention:
*(s16*)&D_80115128(forceslh),*(u16*)&D_800B9A02(forceslhu),((s32*)&D_80187AC0)[i]— the cast fixes the load width independent of the decl's signedness, so you keep the TU's existing decl untouched and there is no conflict at all. This is also propagation-safe: unlike block-scope, it carries no macro-slot-ordering dependency across the 137 member TUs. Caveat (§18):&symcan CSE-hoist across many uses in a tight loop — verify it didn't (func_8013FAF8: 5 uses of&D_800B9A02, no hoist; giants with few uses/symbol are safe, re-test if a symbol is hammered in an inner loop). -
Data-symbol conflict, file-scope decl AFTER the splice — block-scope extern is legal here (§55a), but prefer the tactic-2 cast form anyway for propagation safety (the member TUs won't share the ordering).
-
Function-extern conflict (draft
extern s32 func_Y(s32,…)vs the TU's own DEFfunc_Y(s16,…)/int func_Z(int)): reconcile the DRAFT's decl to the TU's type + a byte-neutral call-site cast, e.g.ot = (s32*)func_80141100((int)ot). Pointer↔int and s16↔s32-of-an-already-sign-extended-value are free.
Then R22 clean-fleet (mandatory): the tactic-1 narrowing edits engine_core.h (404 decls, 266 files) —
harvest_verify gates only the exemplar binary, so the fleet-wide byte-neutrality of the narrowing is unproven
until make clean && extract-all && check-all = 140/140. func_8013FAF8: 140/140, +312 ins ×1 (propagate ×137
is the separate §55b step). The README's "pure def-sig plumbing" undersold it — a giant is a multi-symbol
reconciliation; budget ~5 gate iterations, not one edit.
§56b — PROPAGATING an h_seq giant: the exemplar's externs MUST be fleet-canonical, not the draft's types
An h_seq family (masked-identical body, per-overlay symbol names/reloc targets) propagates via
family_sweep --hseq --only <addr> --source <exemplar> --allow-pins — it copies the exemplar's matched C
into each member and remaps the per-overlay data symbols (the undefined-D_xxxxxxxx-reference the raw
exemplar throws in a member is exactly what the remap resolves; a manual harvest_verify of the raw exemplar
into a member is NOT a valid family test — only family_sweep remaps). But it copies the exemplar's extern
block VERBATIM (only the data symbols are remapped, not the callee-extern types). So if your hand-crafted
exemplar declared a callee with a type that diverges from the fleet-canonical (the member consensus /
engine_core.h), every member TU throws conflicting types for func_Y and the whole sweep banks 0/137.
The fix (byte-neutral, unblocks the entire family in one edit): before sweeping, audit every callee extern
in the banked exemplar against the fleet-canonical —
grep -rh 'extern.*\bfunc_Y\b' src/ov_*/ | sort | uniq -c | sort -rn | head -1 — and rewrite the exemplar's
decl to the high-count consensus form. Return-type and pointer↔int param diffs are byte-neutral (gcc-2.7.2
warns, doesn't error; the call's arg values are unchanged, and a discarded/(cast)-assigned return emits
identically). func_8013FAF8 had 4 divergent externs (func_8005A600 void→s32, func_80024054
s32→void*, func_80137D08 s32*→int, func_8013AB54 s32*→s32); aligning them in the committed exemplar
.c (family_sweep reads the tree, not your .run/ draft) took the sweep from 0/137 → 137/137 banked,
+312 ins ×137 ≈ +42.7k ins, R22 clean-fleet 140/140. Meta: a hand-authored exemplar carries the drafter's
type guesses; the fleet already voted on the canonical — make the exemplar agree before it becomes the template.
(Contrast the h_exact path — dedup_propagate --recover — which auto-reconciles conflicting caller externs;
family_sweep does not, so you pre-align the callee externs by hand.)
§57 — The SELF-decl normalize: the sibling's OWN caller declares the templated fn divergently (the third §17a-1 direction; tools/normalize_self_decls.py, func_801670E4 ×137, Phase 29, 2026-07-18)
§17a-1 (fold a compile-time fn-ptr cast of a known symbol back to a bare jal, byte-neutrally) has three
directions, one per "who declares the conflicting signature":
| tool | the conflicting decl is of… | lives in |
|---|---|---|
cast_call_sites (§20) |
a callee the draft calls | the DRAFT's own forward-decls |
reconcile_tu (§8d/§33) |
a data symbol D_xxxx |
the DRAFT's own externs |
normalize_self_decls (this) |
the templated fn F ITSELF | the SIBLING TU's other already-banked functions |
The blocker (byte-proven, func_801670E4 ×137). family_sweep --hseq templates F's DEF into each sibling TU.
But that sibling's OWN already-matched caller of F may carry a block-scope decl of F left when that overlay's
caller was matched — in a different C form than the exemplar's caller used. ov_SC01_004, ov_SC01_004_jr_8015AE2C.c:
the stub of F (→ where the def lands) and func_80167540's local extern void func_801670E4(struct Entity_80167540 *, s32, s32, s32); sit in one TU; splice F's def (s32 func_801670E4(s32,s32,s32,s32)) and cc1 exit-33s with
conflicting types for func_801670E4 — twice (once vs the def, once vs the canonical decl the DEFINE_func_* macro
injects at file scope; the second is error-recovery cascade from the same one decl). The EXEMPLAR never hit this:
ov_SC01_077's copy of that same caller used the fn-ptr CAST form instead of a decl, so the conflict is per-sibling
and invisible in the exemplar (like §54's header-decl asymmetry and §56b's exemplar-externs, a third "the two overlays
matched the same code in different C forms" trap). All 133 still-stubbed siblings carried the identical block-scope decl.
The fix (byte-neutral, gate-arbitrated). For each decl of F in the sibling TU incompatible with F's def
(cdecl.compatible, the cc1-validated oracle — a no-prototype void f() never conflicts in either TU order, §51g,
so skip it): DROP the decl (F's def is now the declaration cc1 sees) and CAST every call of F in that decl's
scope to the decl's ORIGINAL sig — func_F(a) → ((void(*)(struct Entity_80167540 *,s32,s32,s32))func_F)(a). This
is the byte-faithful move (preserves the caller's exact ABI, the form the exemplar's caller already used); the
sig_unify "rewrite the decl to canonical, keep the bare call" alternative reconverts the args to the canonical
param widths and drifts codegen (§20). Manually proven byte-identical on ov_SC01_004 (remove the stale output
first — a failed compile leaves a passing STALE binary that false-passes SHA, §42b) before the tool existed.
Wired. tools/normalize_self_decls.py (fix(tu_text, fn, ref_decl) — scope via cdecl._masked brace-depth,
block scope → the enclosing top-level fn body, file scope → to EOF; idempotent no-op when no divergent decl exists).
family_sweep --hseq --normalize-self-decls: a new per-sibling stage after reconcile_def_sig that edits the
sibling TU FILE (F's callers live in F's own TU = harvest_verify's baseline, like edit_remap_sweep), with a
snapshot + final-SHA-MISMATCH revert backstop (a MISMATCH ⇒ a transform bug, since harvest_verify always
reverts a wrong DRAFT — so a wrong draft leaves the binary byte-identical, only a non-neutral TU edit can MISMATCH).
Result: func_801670E4 133/133 banked, 0 failed (family 4→137/137), 0 backstop fires. Do NOT add --fix-def-sig
for this family: the raw draft def s32 func_801670E4(s32 arg0,…) is already type-compatible with canonical
(param NAMES are irrelevant to a C prototype); --fix-def-sig renames the params to a0..a3 while the body still
says arg0..arg3 → arg0 undeclared (the "rare name mismatch" its own docstring warns of). Diagnose the sweep's
ACTUAL blocker (splice one member, read cc1) before stacking plumbing flags (R35).
Not every type-lifted family needs this: func_8016CBC0 (also 137-member, also blocked) has NO divergent
self-decl (survey the members: grep 'func_X(' the-member-TUs) — its blocker is a local-typedef lift (§ type-lift,
like func_8012956C), a different lever. Route by the real cc1 error, not by "it's a stuck 137-family."
§57a — Two NSD corrections + the SURGICAL-ONLY law + the honest broad-sweep yield (Phase 29, 2026-07-18)
Three findings from applying §57 at scale, all byte-grounded:
(1) REWRITE the divergent decl to canonical — do NOT drop it (the def-after-caller trap). The first §57
build DROPPED the divergent decl. That is byte-neutral only when F's def sits ABOVE the caller in the TU (so the
def itself provides the caller's forward visibility — func_801670E4, def-before-caller). func_8013D53C is
def-AFTER-caller: a FILE-scope void f(void) forward decl, F's def spliced BELOW it — drop the decl and the
caller gets func_8013D53C undeclared. The fix is exactly what cast_call_sites does: rewrite the decl to
the def's canonical sig (matches the def → no conflict, AND keeps forward visibility), then cast the calls to the
decl's ORIGINAL sig (byte-exact). Works both ways (a canonical forward decl above/below the def is compatible).
(2) (void) is NOT no-proto — do not skip it. The first build skipped params in ('', 'void'). But void f(void) is a 0-param PROTOTYPE that genuinely conflicts with a >0-param def (cdecl.compatible(void f(void*), void f(void)) == False), while a true no-proto void f() is compatible in either order (§51g). Skip ONLY the
literal empty () (also the §32 no-proto-mis-cast guard); let cdecl.compatible judge (void). This was why the
BUILD SPEC filed D53C's (void)/(T) arity class as "not this pass" — it was a tool gap, now closed.
(3) NSD is SURGICAL-ONLY; --fix-def-sig is broad-safe — the blast-radius asymmetry. --normalize-self-decls
edits the TU file (F's callers live in F's own TU), so a non-neutral edit poisons the ENTIRE (overlay,split)
group — and unlike a bad DRAFT (which harvest_verify bisects away per-member), a bad TU edit can't be isolated;
the whole group's build MISMATCHes and the backstop reverts all of it. A broad --band substantial --normalize-self-decls (esp. combined with --fix-def-sig, whose canonical-rewrite changes NSD's reference sig)
banked 7 with ~752 groups backstop-reverted. --fix-def-sig edits DRAFTS (bisect-safe per member), so it IS
broad-safe. Law: apply NSD per-family (--only, on a surveyed self-decl blocker); apply --fix-def-sig broadly.
The backstop makes broad NSD SAFE (0 false banks) but useless.
(4) The honest broad-sweep yield (R14/R35): staging ≠ banking. The 60 substantial matched-ov077 families with
2,163 stubbed members STAGE 2,169 drafts, but a broad --fix-def-sig sweep banks only 137 (one def-sig family +
stragglers) — 2,032 fail. The substantial-family frontier is NOT broadly mechanical; each family carries its own
blocker (self-decl / type-lift / def-sig+caller / genuine codegen). Gate-probe a SAMPLE before scaling a yield
estimate off the STAGED count — the phase's own invariant, which I violated by projecting ~1,500 from 2,169 staged.
func_8013D53C is the archetype: NSD + a Cmd_8013D53C type-lift clear its plumbing (it now COMPILES), and the
sweep banks 14/137 — the h_seq members whose bodies happen to match the exemplar — while 123 carry a genuine
per-member codegen DIFF (the hard cse.c-wall crack does not fully template). So it is a PARTLY-mechanical family:
the plumbing levers harvest the easy fraction ×0 tokens, the residual 123 are permuter/Fable fuel. NB the NSD
edits are byte-neutral even where the member does NOT bank (the caller's canonical decl + cast matches with F still
a stub), so they persist on all 137 TUs — revert the ones that didn't bank (still carry the stub) so you commit
banks, not churn on matched code. The classifier's PLUMBING count is a §56 memcpy red-herring; read full cc1 stderr.
§58 — match_one MATCH ≠ BANK: the four blind spots + the crack-wave reconcile-before-bank law (Phase 29 crack-wave, 2026-07-18)
A crack-wave drafter iterates a C body against tools/match_one.py (standalone compile of ONE function
with the DRAFT's own externs + relocation masking). That is the right per-function oracle, but it is
STRUCTURALLY BLIND to everything that only surfaces when the def is spliced into its real TU + the whole
binary is linked. An 11-core wave produced 9 match_one MATCHes; all 9 gate-failed the whole-binary bank
with ZERO codegen problems — every failure was one of four integration classes match_one cannot see:
(a) Ghidra symbol names. Drafters paste DAT_801d9c20 / PTR_DAT_80186ad0 (Ghidra) instead of the
splat D_801D9C20. match_one links the draft's own extern, so it never notices; the whole-binary
link fails undefined reference. FIX: rename DAT_<hex>/PTR_DAT_<hex> → D_<UPPERHEX> (verify the
splat label exists: grep -rn D_<HEX> asm/<ov>/data/).
(b) Def-sig conflict vs the fleet. The draft's def sig (from m2c/Ghidra) diverges from engine_core.h
or the TU's own caller-decls. The draft sig is byte-TRUTH (it MATCHed); the header/caller decl is
often a stale stub-era guess (void func_80164E40(void) where the truth is s32 — canon_sig_reconcile
forcing the draft to void produced a DIFF, i.e. dropped the return computation). Conform the DECLS to
the draft, byte-neutrally: narrow the engine_core.h extern void→s32 (callers that ignore the return
are unaffected — R22 clean-fleet confirms neutrality), or tools/normalize_self_decls.py --tu --fn --canon "<draft sig>" when the TU's own caller declares F divergently.
(c) Callee-decl conflict. The draft declares a callee with a sig conflicting with the TU's canonical →
tools/cast_call_sites.py (fn-ptr-cast the calls, drop the divergent extern).
(d) Opt-level. A function in an -O2 segment can actually be -O0 (frame pointer, per-case stack
reloads). match_one masks this if the agent tried --o0; the whole-binary -O2 build then DIFFs. FIX:
relocate the def into the overlay's _o0/_o0b object (whose .text covers that addr). A "DIFF" verdict
is often THIS, not a codegen miss (func_8013C0F8 was mis-filed as a real -O0 DIFF; it was §8 jtbl).
LAW: a crack-wave's match_one MATCHes are CANDIDATES; budget a reconcile pass before banking. Best is a
pre-bank auto-reconcile (rename → cast_call_sites → reconcile_tu → normalize_self_decls / narrow-extern), then
the whole-binary gate. And feed it upstream: tell the drafters to use splat D_ names + the engine_core.h
canonical callee sigs, which removes (a) and (c) at the source. harvest_verify's per-draft failure LABEL is
a first-diagnostic red-herring (it reported a shared built-in memcpy @4017 for 7 unrelated drafts) — always
read the REAL error by splicing ONE draft and reading full cc1/ld stderr (and beware the §42b stale-image
false-pass: on a build FAIL the old image lingers, so confirm rc==0 before trusting a sha). 6/9 banked this way.
§59 — Three h_seq sweep-residual classes match_one/the-exemplar-bank don't reveal (Phase 29 crack-wave close, 2026-07-18)
After a core banks ×1 in ov_SC01_077 and swaps to matched-ov077, family_sweep --hseq --only can still bank
0/137 for reasons invisible at the exemplar (they're per-SIBLING, or per-overlay data facts). Three seen in one batch:
- (1) The exemplar's LOCAL struct type is dropped by the remap (func_80165240, 0→137/137). The exemplar TU
defines
struct W4 { u32 w; } __attribute__((packed,aligned(1)))inline;remap_hseqtemplates the DEF but not that local type, so each sibling's*(struct W4*)dst = *(struct W4*)srclowers to a DIFFERENT-sized memcpy → byte-DIFF (compiles clean — it's a DIFF, not a compile error, so match_one/standalone never sees it). FIX: prepend the local struct decl to each member draft (the tiny-inline-type analog of the §57/func_8016CBC0 engine_types.h lift — but too small/local to lift; carry it in the template). Symptom is "compiles, 0/137 DIFF." - (2) h_seq data is per-overlay RELOCATED — there is NO fleet-fixed data address (func_8016D1D8/D688,
0→274/274). The exemplar body reads
D_801D9C20(a tail work-buffer); the WRONG assumption is that 0x801D9C20 is the same in every overlay. It is NOT — each overlay's buffer sits at a different base (ov_000 → 0x801A4B78).remap_hseq'ssymbol_mapkeys the byte-OFFSET labels (D_801D9C21…) not the base symbol the C uses, so the base is left unresolved →undefined reference. FIX, scripted per sibling:base = symbol_map["D_<off>"] − off; declareD_<base> = 0x<base>; // type:u8inconfig/symbols.<ov>.txt(in-overlay → re-extract emits the linker def, byte-neutral); remap the exemplar's base symbol →D_<base>. (A few SC07 stragglers also needed carried externs likeApplyMatrixSVdropped — their TU declares them with a conflicting sig, §20/§58c.) - (3) jtbl carve isolation/table-drift walls (2 cores blocked, genuine tooling).
jtbl_carverefuses a NON-CONTIGUOUS same-subseg table (another matched fn's carve already occupies the subseg); isolating requiresjr_isolate/split_src_region, which cannot partition the Phase-17 canonical-sig-layer TU (trim: cannot resolve address of item). And a merged two-table span (§8e-2) tripsjtbl_rodata_pads: more rodata .align than pad specswhen the -O0 code object holds more jump tables than the span's derived pad spec ("one contiguous .rodata run per object"). Both are documented walls — bank the contiguous/last-table cases, report the rest.
Meta: a family_sweep --hseq "0/137 banked" is a per-sibling INTEGRATION signal, not a codegen verdict — read one
sibling's real gate result (COMPILE-fail vs byte-DIFF) before concluding. Extends §58 (match_one blind spots) to the
sweep stage.
§60 — Classify the residual, don't rank it: the deterministic residual→class classifier and what it measured about the backlog (Phase 29 Task-13A, 2026-07-21)
The instrument. tools/residual_class.py decides a near-miss's class FROM THE BYTES. It decodes each
mismatching MIPS word into (operation-skeleton, register-fields, immediate) and runs a decision tree:
drift first (a single inserted instruction desynchronises the tail and inflates closeness by the tail's
length — a 1-instruction structural delta wearing a 200-mismatch costume), then a consistent-injective
register map (⇒ REGALLOC-PERM, the §31 S11/RC-3 class), same-multiset-different-order (⇒
SCHEDULE-REORDER), nop-vs-instruction (DELAY-SLOT), then operation-family splits (WIDTH lw↔lh =
the §18/§43 idioms, BRANCH-POLARITY, STRENGTH, ADDRESSING), then immediate-only (IMM-OFFSET
constant delta = a frame/struct-layout shift, IMM-VALUE). Every path ends in a NAMED class; an opcode the
decoder does not cover is UNKNOWN and COUNTED (R32). Each class routes to a bucket — permuter /
structural / integration / redraft — which says WHICH TOOL the failure wants.
tools/autopsy.py collect materialises the corpus by recompiling every open backlog draft through the
EXISTING match_one path (R33 — never a second copy of the pipeline), deriving the two silent-artefact
inputs rather than guessing them: the asm subdir (from the stub's self-describing INCLUDE_ASM line) and
the -O0 flag (corpus.is_o0, parsed from the Makefile's own -O0 rules with a coverage assertion). 1,752
drafts in ~21 s at -j12.
Cross-check (R34). The classifier's closeness is computed by a different route than
masked_diff.structured_diff's; collect asserts equality on every row and refuses the corpus on any
disagreement. 1,673/1,673 agreed, 0 classifier errors — so the MIPS decoder covered every opcode in the
real corpus.
What it measured — the whole open backlog, byte-grounded
| bucket | fns | reach-wtd | meaning |
|---|---|---|---|
redraft |
699 | 2,162 | the stored draft is not this function (a 15-ins body vs a 132-ins target) |
structural |
578 | 7,761 | local mutation cannot introduce it — wants a C idiom, not CPU |
integration |
306 | 2,303 | byte-correct standalone; blocked on plumbing (§58/§59) |
permuter |
75 | 740 | a search-closer can actually reach it |
unknown |
2 | 2 | the LLM tier's residue |
THE FINDING: of the 972 records the grinder's own filter admits, 75 (7.7%) are permuter-shaped. 547 are
structural and 348 are junk drafts. The daemon has been spending ~92% of its CPU where the permuter provably
cannot win — which is the byte-grounded explanation of "7 banks all-time, all in Phase 21, 0 since"
(Phase-22 audit). It was never a missing transform; it was targeting. grinder.candidates() now filters
on the measured bucket (1,303 → 78 candidates) and takes its directed permuter_weights profile from the
measured class instead of the logged label — 91% of records carry NO label, so classify() returned None and
the search ran on gcc defaults. Degrades to the old undirected behaviour if the corpus is absent, and says
which mode it is in (--no-targeting A/Bs it).
Two corollaries worth remembering
- A large
closenessand a hard function are different things. 699 records rank as "near-misses" with closeness up to 278 purely because a stub-sized draft was scored against a large target. Ranked by closeness they look like a wall of nearly-done work; they are un-attempted work misfiled as near-misses — fresh crack fuel, not a backlog of hard functions. Hence the separateredraftbucket: the routing is opposite (re-draft vs seed-tweak). match_oneMATCH still ≠ bank (§58), measured. A 12-draft gate probe of theintegrationbucket (reach-134, ov_SC01_077) banked 1 of 12; the other 11 failed PLUMBING. So the 306 is a pool of integration candidates whose conversion depends on the reconcile ladder — it prices Task 14, it is not 306 free banks. (Note the harvest_verify failure LABEL is the §58 red-herring: 10 of the 11 reported the sameconflicting types for built-in functionline from an unrelated TU position.)
The parallel-probe race this surfaced
masked_diff._common_typedefs() wrote, read and deleted ONE shared path src/.masked_diff_probe.c. Under N
concurrent match_one/permuter processes, whoever unlinked first made another's open/parse fail, and that
process died with a traceback instead of a verdict: 14 of 1,752 drafts lost in a single 12-way run (0.8%)
— and every parallel wave has paid it invisibly, because a drafter that crashes on its self-check merely
looks like a drafter that failed. Now per-PID. Same defect class, one level down, as the Phase-28
match_one --work shared scratch whose docstring promised the isolation its default contradicted.
§60a — What the first DIRECTED grinder run exposed (Phase 29 Task-13B, 2026-07-21)
Turning the targeting on and running the grinder bounded (--once --batch 8 --permute-secs 90) produced a win
on the FIRST candidate — func_80181F78, close=1, classified DELAY-SLOT/schedule, banked in ~6 min — and
then immediately surfaced three latent defects that had been unreachable because the daemon had not banked
anything since Phase 21. All three are the same shape: a step whose REPORT and whose WORK had quietly
diverged.
gate_stage's commit path crashed onsrc=None.srcis deliberately never defaulted (the Phase 26-A audit: a default would silently PIN the gate to the main.c), but the commit didgit add src …unconditionally. So every caller that omitssrc— grinder, orchestrator, idiom_hunt — crashes the moment it banks. Fix:git add -u src/(every modified tracked file under src/), which also retires thesrc/ov_*/*.cfilename glob that once omitted 4 R22-verified banks from a commit because a family's members do not all live in the same-named split. COMPLEMENTARY HOLE (2026-07-22):git add -umisses the NEW files an ISOLATION creates. A jtbl sweep cuts a fresh region file per sibling (src/<ov>/<ov>_jr_<ADDR>.c), which is UNTRACKED — so-ucommits the modified TU and drops the file holding the banked body, i.e. a tree that cannot clean-rebuild. For any carve/isolation bank usegit add -A src/ config/.jtbl_family_bankalready refuses to sweep on an uncommittedconfig/+src/(its per-sibling revert restores from HEAD), and that guard is what caught this — a fail-loud precondition doing exactly its job.- The
_xformladder dirs accumulate.<drafts>-cn/-cast/-rc/-uniare reused across runs and the transform tools only write the drafts they are handed, so every stale draft from every previous run survives and is re-submitted to the byte-gate. Measured: the grinder submitted 1 draft, the gate processed 34 and banked 2. Nothing wrong entered the tree (G3/P9: the gate banks only byte-identical output) — but a run banked a function it was never asked to try, and would have committed it under a message naming a different one. Fix: clear the out dir per run. Note the symmetry with R32: a scanner that silently NARROWS its input hides work; a stage that silently WIDENS it fabricates provenance. - The grinder fired the fleet-wide propagate from inside the gate.
gate_stage(propagate=True)runsdedup_propagate --auto-from— the §55b path that timed out at 3600 s and left 90/140 overlays broken, and which, being inside the gate, takes the banks down with it when it fails. The unattended caller must never fire it: bank withpropagate=False, commit the cheap verified banks, then run ONE targeteddedup_propagate --addras its own batch.
The transferable point: a tool that has been failing for a long time accretes latent bugs on its success
path, because nothing exercises it. Before trusting an unattended fix-and-run, budget for the first success
to fail — and check the tree state, not the exit code (here the durable Task-12 winner save in
.run/permuter-winners/ is what made the crash a non-event).
§60b — The plateau autopsy's verdict: a partial drift is a WRONG DRAFT, not a missing transform (Phase 29 Task-13B close, 2026-07-21)
hindsight-study §7 predicts that a search-closer's plateaus decompose into missing-transform (extend the mutation set — "the highest-value bucket and the whole point"), seed-structural, and genuine-wall. Run against real plateaus, this class produced ZERO missing-transforms. The autopsy is worth recording because the answer was legible in the bytes and needed no LLM at all.
The measurement. A 20-target probe of the length profile: tail drifts (a single shift point explains
the whole tail) converted 1/6; partial drifts (length differs AND other positions differ) converted
0/12. Reading three partial plateaus directly:
func_8017F0C0,func_801806C8— target containssltiu $v0,$v0,0x1. That is gcc's codegen for!x/x == 0. The drafts wrote(u32)(D_x ^ 1), which emitsxori. No local mutation rewritesxoriintosltiu: it is a different operation, chosen by the front end from a different C expression.func_8017FF90— the draft stores toarg0 + 8; the target stores to a global (lui $at,%hi(D_…)/sw $zero,%lo(D_…)($at)). Not the same function at all.
Two permanent fixes, both NARROWING what the offline tool is allowed to attempt:
_drift_routenow admits a drift to the permuter only when|Δ|<=2ANDexplains == "tail". Length-profile pool 339 → 34; permuter bucket 389 → 84. Apartialdrift is seed-structural by construction — the count is wrong and other positions are wrong, which is not one local edit.SIZE-MISMATCHgained a PROPORTIONAL test (|Δ| >= 0.5*nt) alongside the absolute one.max(2, 0.15*nt)is far too permissive on a tiny target: a 2-instruction draft against a 4-instruction target is|Δ|=2and read as a near-miss when it is a wholesale mismatch.
The transferable lesson. The §7 taxonomy tacitly assumes the plateaus are near. Ours mostly were not —
they were bad drafts wearing a small closeness. So the highest-value autopsy outcome was not a new
transform but a tighter admission rule: the way to raise a search-closer's yield is at least as often to
stop feeding it unreachable work as to widen its mutation set. Same knife as Task-13A's targeting fix, one
cut finer.
Cookbook idiom for drafters (recurring): sltiu rd, rs, 1 ⇒ the C is !x / x == 0, NOT x ^ 1.
The XOR form emits xori and can never match.
§61 — Task 14: the gate ladder's missing stage is the ARITY pre-pass, and it is TU-side not draft-side (Phase 29, 2026-07-21)
gate_stage's recovery ladder was four DRAFT rewrites (canon_resident_calls → cast_call_sites →
reconcile_tu → gate → sig_unify → gate). The dominant residual blocker is not in the draft at all.
Diagnosis (not assumption). The 12-draft integration probe banked 1/12 and reported the SAME failure
label for 10 of the 11 failures: warning: conflicting types for built-in function 'memcpy'. That label is
the §58 red-herring — it is a WARNING, from an unrelated TU position, and it is not the failure. Splicing
three of the highest-reach failures individually and reading real cc1 stderr gave the actual cause:
src/…_jr_8016AB6C.c:3839: conflicting types for `func_8016EFC8' <- 3 of 3
src/…_jr_8012ACE0.c:879: redefinition of `struct V8' <- a SECOND class
The first is the loose-typing arity conflict: an already-banked shared caller macro in
src/shared/engine_core.h declares the function with FEWER parameters than its byte-true definition takes
(extern s32 func_8016EFC8(s32); and it calls with one arg, while the def takes two — the original calls
K&R-style with fewer args than the callee reads). A C89 prototype makes that a hard error.
The fix already existed and was simply not wired in. tools/fix_arity_callers.py --any-proto rewrites
those caller decls to the no-prototype K&R form (byte-neutral: an empty/short call emits identical code, and
a no-proto decl is compatible with a definition whose params are default-promotion-safe). Byte-probe:
func_8016EFC8 (reach-138) went gate-REJECTED → BANKED byte-identical after 7 caller decls were rewritten.
Wiring notes that cost real time:
- It is a TU-side pre-pass, not an
_xform: it edits shared state (engine_core.h+ the overlay's own inline caller decls), so it is scoped to the drafts in play and reverted for every function the gate then rejects — a bank that SUCCEEDED must keep its loosened decl or the tree stops building. --funcsis REQUIRED;--draftsis only the narrow-param filter. Wiring it with--draftsalone made the stage exitno funcs given— and becausesh()does not raise on a non-zero exit, the surrounding try/except never saw it. The stage silently did nothing and the gate reported 0/6 as if diagnosed. Hence the explicitreturncodecheck now in the ladder: a pre-pass that quietly no-ops is indistinguishable from one that found nothing to do, which is the exact failure this ladder exists to remove (R32). Author's note: this was committed roughly an hour after writing §60b about silent skips.
Measured: on the 7 highest-reach-weighted ov_SC01_077 integration candidates, the enriched ladder banks
2 (func_8016EFC8, func_80164418) against a 1/12 baseline for the old ladder.
THE INCIDENT THIS STAGE CAUSED, and the constraint it establishes. Pairing --apply --any-proto
with --revert for the drafts that did NOT bank broke 138 of 140 binaries. --revert rewrites
() -> (void), which inverts a PLAIN apply but not --any-proto (which relaxes ANY prototype), so the
round-trip turned an unbanked function's real decl extern void func_801708B0(void *a0); into (void) —
a DIFFERENT signature — in engine_core.h and in 6 places in the overlay's own sources.
A SINGLE-BINARY GATE CANNOT VALIDATE A FLEET-WIDE EDIT. harvest_verify --binary ov_SC01_077 reported
byte-identical and was RIGHT — about that one binary. The other 137 were broken and structurally invisible
to it, because the edit lands in a header all 138 overlays include. Only the standing R22 clean-fleet sweep
saw it. This is the same shape as §55b's propagation law, one level down, and it yields a hard constraint:
Any ladder stage that mutates SHARED state (
engine_core.h,engine_types.h, another binary's TU) must be undone by SNAPSHOT RESTORE, never by an inverse transform, and must be validated fleet-wide (R22) rather than by the per-binary gate that authorised it.
gate_stage now snapshots every file the pre-pass touches and undoes by restore + re-apply-for-the-banked-
set-only: exact by construction, and incapable of inventing a signature. The planned type-lift stage edits
engine_types.h — also shared — so it inherits this constraint by default.
The residual class, named for the next stage: redefinition of 'struct <T>' — the draft defines a local
struct the TU already defines. That is the type-lift / local-typedef-uniquify class (§19/§57a/§59), NOT the
arity class, and three of the five remaining failures carry a (void) header decl that the arity pass alone
does not clear. Wire that next, and validate it the same way: splice one, read real stderr, probe, then wire.
§61a — The Task-5 wave: 11/12 MATCH, 0 banked — three DISTINCT integration walls, each now named (Phase 29, 2026-07-21)
A 12-agent Ultracode wave over freshly-prefetched ov_SC06_018 exemplars returned 11 MATCH / 1 near
(~2M agent tokens), including all three giants (710 / 673 / 478 ins). The whole-binary gate banked ZERO.
This is §58's law at its sharpest — and splicing each class individually gave three different blockers,
none of which the ladder currently clears:
-
jtbl NON-CONTIGUOUS CARVE — 10 of 12 drafts. (Corrected: I first filed this as "§8e-2 table-count drift". That is the SYMPTOM the filter reports; it is not the wall, and the fix is NOT a jtbl_carve code change.)
jtbl_rodata_pads: more rodata .align directives than pad specs (2) — table-count drift vs the carve. The draft introduces a switch/jump table into a TU whose carve has a FIXED pad spec, sojtbl_rodata_padsfires. But re-runningjtbl_carve --func <fn>to re-derive the spec REFUSES with the real reason: "subseg would host NON-CONTIGUOUS .rodata carves (0xaa810 and 0xaa920) — a single object can't leave a gap for the unmatched jtbl between them." The newly-banked function's table is separated from the TU's existing carve by an UNMATCHED function's table, and one object cannot straddle that gap.THE RECIPE (byte-proven on
func_80135A4C, 181 ins / 138 members):tools/jr_isolate_all.py <ov> --only <fn> # give the fn its OWN code subseg make extract BINARY=<ov> && make build # isolation is BYTE-NEUTRAL by construction — verify <splice the draft> tools/jtbl_carve.py <ov> --func <fn> # now the table carves contiguously in its own object make extract BINARY=<ov> && make build # -> BYTE-IDENTICALThe tool names its own remedy in the refusal message, and
jtbl_family_bankalready auto-isolates on this class (Phase-29 Task-8) — butgate_stage/harvest_verifydo NOT, which is why a wave that banks through the ordinary gate reports a flat 0 and looks like a compiler wall. A config change needsmake extract, not justmake build(the R22 corollary) — both steps above. The structural finding: fresh crack fuel in a well-matched overlay CONCENTRATES in jtbl-carved TUs (10 of 12 here), because the non-carved TUs were harvested first. So §8e-2 is not a rare straggler — it is the gate on the next tranche of substantial cracking. -
§57 self-decl / prototype conflict — the 2 plain-TU drafts.
argument 'arg2' doesn't match prototype(def at :595 vs the TU's own decl at :447).tools/normalize_self_decls.pyexists for exactly this and is wired intofamily_sweepbut NOT intogate_stage— the same gap the arity pre-pass had. -
Local-type redefinition (
redefinition of 'struct V8') — seen in the Task-14 diagnosis set; wants the type-lift.
So gate_stage's ladder needs three stages, not one, and today only the arity pre-pass landed. Ranked by
what they unblock HERE: jtbl-drift (10/12) > self-decl (2/12) > type-lift.
Method note that made this cheap: the gate's own per-draft label is useless for this (§58's memcpy
red-herring), and make build … | grep -i error MISSED the real failure twice — once because the true error
was a jtbl_rodata_pads line containing no "error" token, once because the build failed at a later stage
than the warnings I was reading. Check rc, and read the tail unfiltered. A filtered build log is a
selection tool, and every selection tool in this project has eventually lied (R32/R35).
Preserved: all 12 drafts at .run/giants/t5wave_* (R20) — they are genuine cracks with per-function
lever notes, recoverable the moment the three ladder stages exist. Do NOT re-draft them.
§61b — The jtbl gate stage: built, and the ORDERING law it exposed (Phase 29 Task-14 stage 4, 2026-07-21)
gate_stage now carries a jtbl stage (_jtbl_prepare): for every draft whose function references a
jtbl_, carve its table into a contiguous object, auto-isolating (jr_isolate_all --only <fn>) on the
§8b walls — the logic lifted from jtbl_family_bank rather than re-implemented (R33). It is wired, it
runs, and it does not yet bank, for a reason worth writing down:
THE CARVE MUST FOLLOW THE SPLICE. The non-contiguity that requires isolation is only detectable once the function's body is in the object. While it is still
INCLUDE_ASM,jtbl_carvereports SUCCESS and produces a spec that does not hold once the body lands.
Byte-witnessed both ways on func_80135A4C: carving the spliced function → NON-CONTIGUOUS … 0xaa810 and 0xaa920; carving the unspliced one → prepared 1/1, no isolation, and the draft then
gates as a byte-DIFF. The MANUAL order banks it byte-identical:
jr_isolate_all --only <fn> ; make extract ; <splice> ; jtbl_carve --func <fn> ; make extract ; build
gate_stage runs the stage before _gate1, but harvest_verify owns the splice — so the fix is a
per-draft prep INSIDE the splice loop (harvest_verify), not a batch pre-pass in gate_stage. That is the
next increment; the stage's carve/isolate/undo machinery is correct and reusable as-is.
RESOLVED (2026-07-21, same session): the prep belongs in harvest_verify, and it BANKS there.
harvest_verify._jtbl_prep() splices each table-bearing draft TEMPORARILY, asks jtbl_carve,
isolates on the §8b walls, un-splices, re-extracts, and re-derives the stub map + baseline (isolation
MOVES a stub's TU, so both are keyed on stale paths otherwise). Byte-proven: func_80135A4C goes
[jtbl] carved 1/1 -> + chunk(1) -> BYTE-IDENTICAL, fully automated.
BUT THE BATCH STILL FAILS, AND THE REASON IS A NEW, PRECISE TOOLING GAP:
Banking a jtbl core makes its own carve UNOWNED to
jr_inventory, which then refuses every subsequent isolation in that overlay —committed .rodata carve ownership is not 1:1 (R32/R33) — a stranded/duplicated carve (§8b func_801734BC class): [('UNOWNED', '0x801d288c')].
Byte-proven both ways: on the COMMITTED tree jr_isolate_all --only func_80135260 --dry-run succeeds;
with func_80135A4C banked it fails the ownership assertion. The assertion is right — a banked
function's stub .s is pruned, so the owner lookup finds nobody — but its conclusion is wrong: the
carve IS owned, by C rather than by a stub. So today jtbl cores bank ONE PER OVERLAY.
Measured on a 10-draft batch: 6 table-bearing, 1 carved, 4 isolate-FAILED on this assertion, 1
stale-asm carve failure.
NEXT INCREMENT (precise): teach jr_inventory's ownership check to attribute a carve to a
BANKED (C) function — i.e. resolve owners from corpus.matched ∪ stubs, not stubs alone (R33: the
same derive-don't-reparse move that fixed the corpus oracle). That unblocks batch jtbl banking and
the 9 preserved cracks.
Two sub-findings, both paid for:
- A wholesale
git checkout -- config/…undo is WRONG in a batch gate.jfb.revertis right forjtbl_family_bank's one-function-at-a-time flow, but here it discarded a PREVIOUSLY-banked-but- uncommitted carve in the same overlay, leaving that bank's source with no subseg →undefined reference to func_80136C90at link. An inverse/wholesale undo cannot know what it did not do. Now a SNAPSHOT-RESTORE ofconfig/splat.<ov>.yaml+config/overlays.mk, plus removal of only the region files THIS run created (§61's constraint, applied where I had first ignored my own rule). - Being in a
_jr_*TU ≠ having a table. Only 4 of 8 wave drafts in jtbl-carved TUs actually reference ajtbl_; the stage correctly prepares only those. The other 4 fail for other classes.
§61c — The jtbl bank is INCREMENTALLY valid and CLEAN-INVALID (Phase 29, 2026-07-21) — the blocking finding
func_80135A4C banks through the automated jtbl path every time: [jtbl] carved → + chunk(1) →
verified 1 / failed 0 BYTE-IDENTICAL. And it fails a clean rebuild, twice, identically:
incremental (harvest_verify's own gate) : BYTE-IDENTICAL
make clean && extract-all && check-all : 139 passed, 1 failed ([FAIL] ov_SC06_018)
So the carve+isolation path yields a state that is not reproducible from committed config + source — the incremental tree carries something the clean pipeline does not reconstruct (extraction order, or asm that only exists mid-flow). This is the §42b stale-incremental false pass in its most expensive form: the gate that authorises the bank cannot see the defect, because the gate IS the incremental build.
Until that reproducibility gap is closed, NO jtbl core can be banked — not by hand, not by the
ladder. The correct next step is to diagnose the divergence itself (diff the incremental vs clean
build/ov_SC06_018/** object set and the generated .ld/asm for the carved subseg), NOT to bank more.
⛔ §61c IS REFUTED — the blocker does not exist (2026-07-22, R35/R14)
The diagnosis above was run, and it never got as far as diffing objects, because the failure does not reproduce. On a tree carrying ONLY this bank, applied through the single-function automated path (
harvest_verify --chunk 1,[jtbl] carved func_80135A4C→+ chunk(1)→ BYTE-IDENTICAL):per-binary clean (rm asm+build for the ov; extract; build) : BYTE-IDENTICAL cbbc4f44… make clean && extract-all && check-all (run 1) : 140 passed, 0 failed of 140 make clean && extract-all && check-all (run 2, independent) : 140 passed, 0 failed of 140So the carve+isolation path IS reproducible from committed config + source. There is no extraction-order effect and no mid-flow asm: the state the incremental gate blesses is the state a clean pipeline reconstructs.
What the 139/140 actually was. The failing R22 runs were taken on the tree left by the batch
_jtbl_prep— the same run that ended6 table-bearing → 1 carved, 4 isolate-FAILED, 1 stale-asm carve fail. That tree carried the residue of five failed preps (stranded carves and half-applied isolations); the per-function snapshot-restore that removes exactly that residue landed after those runs, in the same commit that named the blocker (commit:0803). The measurement was real; its attribution was to the wrong cause. The failing tree is not recoverable, so this is stated as the best-supported explanation, not a byte-proof — but the claim that matters (the path is clean-invalid) is byte-refuted twice, and that is the claim that was blocking the work.The transferable lesson is R35 pointed at ourselves: a fault observed on a tree that is known to be polluted must be re-observed on a clean one before it is written down as a property of the mechanism. "Twice, identically" felt like replication; it was two reads of the same contaminated state, which is one observation. A replication has to re-create the state, not re-run the check.
Faults 1 and 2 below are unaffected — they are real, they are what polluted the tree, and their fixes are what makes the single-function path reproducible. Constraint that stands: jtbl drafts are processed one per
harvest_verifyinvocation until the undo is region-aware.
Two design faults found on the way, both real and both fixed in harvest_verify:
- A stranded carve poisons the overlay.
_jtbl_prepcarved a draft the gate then REJECTED; the carve stayed with no owner (the fn is stillINCLUDE_ASM), andjr_inventory's 1:1 ownership assertion then refused EVERY later isolation in that overlay ([('UNOWNED','0x801d288c')]= func_801299C8's table). The assertion was RIGHT and caught it — R32/R33 working exactly as designed. Fix: per-function carve with snapshot-restore on gate rejection. - Per-function undo is unsound in a BATCH. Isolation REPARTITIONS shared source, so restoring one
draft's snapshot deletes region files that now host OTHER pending drafts — their stubs vanish
(
KeyErrorin render). jtbl drafts must therefore be processed one perharvest_verifyinvocation, or the undo must be region-aware.
Measured, so the next session does not re-derive it: of 11 preserved wave cracks, exactly ONE
(func_80135A4C) reaches byte-identical through the carve path; the other four table-bearing ones
fail one-at-a-time too, on the PLUMBING classes (§57 self-decl et al), not on the carve.
§61d — The undo was eating the tree: two tools, one defect, invisible to the byte-gate (Phase 29, 2026-07-22)
The §61c blocker turned out not to exist (see the REFUTED block above), and chasing why it had ever been observed found the mechanism — in two tools, both times invisible to the gate that caused it.
jr_isolate_allrepartitions a code object by writing region 0 back over the ORIGINALsrc/<ov>/<nm>.c, TRUNCATED to just that region, and emitting the remainder as new_jr_<lo>.cfiles. An undo that restores onlyconfig/and deletes the new region files therefore leaves the original TU permanently truncated — its stubs are gone, and nothing regenerates them (splat does not rewrite a committed overlay.c).
Both harvest_verify._jtbl_restore and gate_stage._jtbl_prepare snapshotted only the two config files.
Measured, twice, on live re-probe runs: live stubs 419 → 414 → 406 → 395, five orphan region files, and
undefined reference to func_80192F64 at link.
Why it survived so long: it is invisible to the byte-gate. The incremental build keeps linking the stale
objects (§42b), so make build stays GREEN while a clean rebuild fails — the gate that authorises the bank
is the same incremental build that hides the damage. This is the §42b blind spot doing real, cumulative
damage, and it is what manufactured the "139/140, twice" reading that became the §61c blocker.
Fixes. harvest_verify: snapshot every src/<binary>/*.c, restore on rejection, and delete exactly the
files the attempt created — derived from the snapshot's file set, not re-guessed from the _jr_* name shape
(R33). gate_stage._jtbl_prepare: DELETED, not patched (R33 — the best outcome is a deleted stage). It
was wrong on two independent axes: §61b had already byte-proved the carve must FOLLOW the splice, and
harvest_verify now does the correct per-draft prep one layer down. Two implementations of one capability,
the outer one both ineffective and destructive.
A label that is constant carries no information (and is worse than none). classify_fail searched the
whole build stderr, so warning: conflicting types for built-in function 'memcpy' — benign, from an
unrelated TU position, present on essentially every overlay build — won the match on 8 of 8 failures
spanning four genuinely different causes. That is the §58 red-herring, and the cookbook had been recording
"the gate label is useless here, splice individually and read real cc1 stderr" as a manual workaround for a
one-line bug. Fix: classify on NON-warning lines; fall through to CC1-FAIL:<last error line>. The same
failure instantly became ov_SC06_018.c:447: prototype declaration.
What the honest re-probe then measured (11 preserved t5wave cracks, one invocation each, clean tree):
func_8018F694(478 ins) BANKED — a giant previously inside "the gate banked ZERO".- The other 10: 4 data-decl · 3 callee-decl · 3 self-decl conflicts. ZERO jtbl-drift, ZERO local-type redefinition, ZERO codegen DIFF. So §61a's "§8e-2 jtbl drift blocks 10 of 12" does not survive the carve-follows-splice prep — the carve succeeds; what is left is ordinary decl plumbing.
- Through
gate_stage's ladder: 0/10 bank, but 9/10 now COMPILE and land as whole-binary byte-DIFF.match_oneclose=0 on several andrtu_matchMATCH-in-real-TU forfunc_80135888, whilefunc_801299C8's transformed draft does not compile in its real TU at all — the residual is MIXED, and at least one is an IMAGE-level effect rather than the draft or its TU context (prime suspect: jtbl/rodata carve placement). Deliberately not generalized from one data point.
The pricing this yields: the existing ladder converts 0 of 10 of these residuals. Task 14 stages 2-3
are therefore NOT "wire in normalize_self_decls + the type-lift and collect ten banks" — a measurement, not
a projection, which is the error §57a already caught once this phase.
The rule, stated generally: any stage that mutates shared state must undo by SNAPSHOT-RESTORE over the complete set of files it can touch — config AND source — and a stage whose undo scope is narrower than its write scope will silently destroy work that no byte-gate can see. §61's law, one level deeper.
§62 — The jtbl RECONCILE must also follow the carve: the post-carve draft reconcile (harvest_verify._jtbl_reconcile, Phase 29 SESSION-11, 2026-07-22)
§61b's law was "THE CARVE MUST FOLLOW THE SPLICE." This is its sibling: the decl RECONCILE must follow the carve too, and run against the CARVED TU.
The wall. A loose-typed jtbl function (calls/references shared symbols declared incompatibly across the
engine) drafts to a clean match_one MATCH, but the whole-binary gate fails to COMPILE — conflicting types for D_XXXX / func_XXXX — not a byte miss. jr_isolate_all accumulates each earlier region's file-scope
decls as the new split region's §8b carried-decl layer (the canonical externs for the data/callees the
body touches). The draft declares those SAME symbols its own way for the byte-match, and where the two
disagree, cc1 rejects the TU. A batch of these read as "carves, 0 banks" and looked like a codegen wall.
Why the existing reconcile ladder misses it (the ordering bug). cast_call_sites (§17a-1 callee cast) +
reconcile_tu (§8d data, conform the draft to what THIS TU declares) already fix exactly this class and take
--src-file. But gate_stage runs them pre-carve and against the default src/<ov>/<ov>.c — never the
jtbl fn's real TU, because for a jtbl fn that TU is the split file, which does not exist until harvest_verify
carves (gate_stage.py:271 deleted its batch jtbl stage for this reason: "harvest_verify owns the splice, so
the prep belongs there").
The fix. harvest_verify._jtbl_reconcile(fn): after _jtbl_prep_one establishes the carve, run
cast_call_sites then reconcile_tu with --src-file <the carved TU> and update drafts[fn]['c'] in place,
before the gate re-splices. A draft-only rewrite (no shared-state edit → no §61 undo needed); the whole-binary
gate stays the sole arbiter (G3/P9). Guarded by _jsnap is not None, so it runs ONLY when a carve happened.
Validated: func_80135260 (callee func_80134A74) and func_80191C50 (data D_801152A8) both went
conflicting types → a genuine codegen DIFF — the reconcile dissolved the plumbing.
The honest finding it exposed (R14/R31). Dissolving the plumbing does NOT bank these families — it reveals
what the plumbing hid. All four ov_SC06_018 jtbl drafts had a DEEPER issue: func_80135260/func_80191C50 a
real codegen residual (a %hi-sharing regalloc the agents' match_one MATCH over-claimed — reloc-masked
match_one cannot see it); func_8012AAAC a def-side-arity conflict AND it is in engine_core.h (fleet-shared)
AND it still DIFFs after the arity fix (a def-side register-threading wall); func_80135EB0 an [jtbl] isolate FAILED. So the reach-138 jtbl families are genuine near-misses/walls, not plumbing-only wins — the projected
"+0.58pp from 3 jtbl families" is refuted. The fix's value is (a) it BANKS any jtbl family that is
plumbing-only-blocked with a true MATCH, and (b) it makes the jtbl gate HONEST — it attributes the blocker
(plumbing vs codegen) instead of reporting every loose-typed jtbl fn as an unbankable wall.
Two traps re-confirmed while building this (both §61-class). (1) Gate jtbl functions ONE AT A TIME — a
single [jtbl] isolate FAILED in a multi-fn --chunk 1 batch corrupts the tree for the whole batch (final SHA None); the per-function restore does not contain a mid-batch isolation failure's damage. (2) The
fleet-shared-state restore trap: fix_arity_callers --funcs <fn in engine_core.h> edits src/shared/ engine_core.h fleet-wide; a git checkout HEAD -- src/<ov>/ restore MISSES it (wrong directory), and the
per-overlay build stays byte-identical so nothing flags the leak. Restore src/shared/ too, and R22
clean-fleet after any fix_arity probe (this bit me exactly as §61 warns — caught by a full git status).
§63 — The fresh-138 DEF-SIDE blocker: fix the HEADER decl, not the draft (fix_header_decl.py, Phase 29 SESSION-13, 2026-07-23)
The blocker. A genuinely fresh reach-138 family (live in ~138 overlays, matched nowhere, no
DEFINE_func_* macro) with a byte-perfect universal draft still fails the gate as
conflicting types for func_X (cc1 exit 33). Cause: a shared caller macro in src/shared/engine_core.h
forward-declares it with a SIMPLIFIED, caller-derived signature — extern void func_X(s32 a0, void *a1, void *a2); — that conflicts with the byte-true def int func_X(s32, u16*, u16*) the moment the def is
spliced into an overlay TU that #includes the header. gate_stage's §61 arity pre-pass is
param-COUNT-only (misses return type + pointer element type); §54 reconcile_def_sig rewrites the DRAFT
to match the header (wrong direction — the header is the simplified one, matching it can DCE the return).
The fix — proven ×138 (func_8014CD80). Rewrite the HEADER decl to the byte-true def. It is byte-neutral:
(a) return void↔intN — the true fn always sets $v0; a caller that declared it void never read $v0
(unchanged), and no caller USES the return; (b) pointer element type void*↔T* — register-passed
regardless. One edit → harvest_verify banks ×1 BYTE-IDENTICAL → dedup_propagate --addr fills 138/138
byte-identical → R22 140/140. Fleet +0.1pp instr off ONE family.
tools/fix_header_decl.py automates it: --fn --draft [--check|--apply]. Parses the byte-true def sig,
canon-compares (typedef-aware: int≡s32, unsigned short*≡u16*) to skip ALREADY-OK decls, REFUSES any
rewrite that changes param COUNT / flips pointer↔scalar / changes scalar class (ABI change — left to a
human), and preserves the macro line's trailing \ continuation (dropping it corrupts the DEFINE macro
fleet-wide — a self-test caught this). Snapshots + prints the git checkout restore (§61: undo = restore,
never inverse; validate FLEET-WIDE by R22, never the per-binary gate).
Market (SESSION-13 scan): of the 75 fresh (≥100-live) families, 46 carry an engine_core.h caller
forward-decl, 38 SIMPLIFIED = this pattern → each a candidate ×138 (≈+1.5–2.8pp instr). RESIDUAL RISK: the
header is only the INTEGRATION half — each family still needs a byte-true UNIVERSAL body from a wave; a
non-universal body (overlay-local D_* refs) is a genuine per-member wall regardless (func_80165CA0,
0/135). Distinguish: h_exact=1 does NOT — the BODY's universality does. Pipeline: wave (build_wave_args.py --rank live --min-live 100) → fix_header_decl --apply → harvest_verify → dedup_propagate --addr → R22.
§64 — The §20 type-lift's three laws: fold the tagged typedef, check VISIBILITY, and strip only what is TEXTUALLY IDENTICAL (lift_types.py, Phase 29 SESSION-14, 2026-07-23)
The setup. The broad §20 lift (154 fleet-local types → engine_types.h, 2,958 files stripped) had been
carried three sessions as "needs collision-vetting + -O0 strip precision." Both framings were wrong, and the
one real collision class was misdiagnosed. Fixing the tool first (R35) changed all three answers.
Law 1 — a tagged typedef is ONE entity, not two. typedef struct Tag {...} Alias; is matched by BOTH
find_defs (as Tag) and find_typedefs (as Alias), with the struct span CONTAINED in the typedef span.
Treating them as independent does three wrong things at once: it lifts the inner span, which starts at
struct, so the header gets struct Tag {...} Alias; — a variable definition of Alias in every TU; it
then lifts the typedef too (Alias redeclared as different kind of symbol); and it strips both spans
highest-first, so the outer span's end offset is stale by the length of the inner one and the second
delete removes that many EXTRA characters past its intended end. MEASURED: 13 tag/alias pairs, 6,142
occurrences, and 0 of those tags is ever defined standalone — folding is always the correct read.
build_engine_types.resolve_type_defs() is now the ONE model both tools call (R33), and it guarantees
defs/tdefs spans are pairwise disjoint; assert_disjoint() enforces that at every mutation (R32).
The Phase-29 "case-variant name collision" (
actor4cvsActor4C) was this, not a naming problem. A single-def filter accidentally hid 12 of the 13 pairs and leaked exactly the one whose tag and alias differ in case, which made a case-insensitive exclude look like the fix. It is not: it would also wrongly drop the legitimate, non-collidingObj/objandVec/vecpairs. Key by (kind, name) — the C namespace.
Law 2 — never strip a definition out of a TU that cannot SEE the replacement. MEASURED: exactly 1 TU
of 3,226 (ov_SC01_077_o0.c, the -O0 split) deliberately omits engine_core.h — for the documented reason
that its -O0 functions cannot propagate through a header other overlays compile at -O2. Stripping its types
does not consolidate them, it DELETES them, and the symptom surfaces three steps downstream: type undeclared
→ parse error on the next declaration → "data definition has no type or storage class" → gcc falls back
to implicit int → that TENTATIVE definition collides at LINK as multiple definition of D_801DAA08 — a
link error naming a data symbol nobody touched. That is the whole of the "-O0 strip precision" mystery.
bet.type_visible(path) derives the visible-header set from the src/shared include graph (R33) and the
lift keeps such defs local, naming them.
Law 3 (the expensive one) — strip a local def ONLY if what becomes visible is TEXTUALLY IDENTICAL to it.
--candidates classifies per ENTITY and defers VARIANTs, but --types takes NAMES: struct Prim was
LIFTABLE while typedef Prim was a deferred VARIANT (header ≠ the 103 overlay copies), and passing the
shared name dragged the deferred entity back in. Result: 103 overlays silently repointed at a different
Prim layout. It compiled clean, the per-binary pre-filter passed, and R22 failed 37/140 — the 103
failures were EXACTLY the 103 Prim-stripped overlays (set equality, byte-verified). The guard now lives at
the mutation, not in the selector, so a selector bug cannot reach the source; divergent copies are reported
as the per-camp reconcile work they are.
Sequencing that paid for itself. make build BINARY=ov_SC01_077 reproduced the -O0 failure in 0.26 s
with a compile error naming the five affected types, where the fleet cycle had produced a link error naming
an unrelated symbol. But a pre-filter is only evidence about what it filtered: ov_SC01_077 passed the
Prim-broken run too. Pre-filter on a binary that FAILED, not one that passed (§61 one level down).
Result. 154 types lifted, 2,958 files stripped, R22 140/140 byte-identical, engine_types.h +510
lines. Deferred and named, not silently dropped: 8 VARIANT entities (MATRIX/Buf/Vec8/Prim/Handler/…) awaiting
the per-camp field-access reconcile, 14 carried tags, and 5 types kept local in the -O0 TU.
§64a — VARIANT types: UNIQUIFY the camps, do not reconcile them (uniquify_type.py, Phase 29 SESSION-14)
The trap. §64 defers any type with 2+ distinct definitions as a VARIANT, and the obvious next move reads as "pick the canonical layout and reconcile the other camps' field access." For most of these names that is actively WRONG. Measured on this fleet:
Vec8 = { s32 w[8] } 180 files (32 bytes)
Vec8 = { s16 unk0,unk2,unk4,unk6 } 139 files ( 8 bytes)
MATRIX = { s32 m[3][3]; s32 t[3] } 578 files | { short m[3][3]; long t[3] } 71 files
Buf = { s16 h[8] } 578 files | a 0x20+ struct 6 files | { DrawEnv env; … } 1
These are not one type with two spellings — they are different types that happen to share an
identifier in different TUs of the same overlay (each TU carried its own local guess, which is
exactly why they diverged). "Reconciling" them MERGES two layouts and silently repoints the minority
camp's TUs at the wrong struct — the same failure mode that broke 103 binaries on Prim (§64 Law 3).
The correct operation is the opposite: keep both layouts, give them distinct NAMES.
reconcile -> merges two layouts -> breaks the minority camp WRONG
uniquify -> preserves both layouts -> every camp becomes liftable RIGHT
Renaming a type is byte-neutral (a type name emits no code) and TU-local by construction: the
definition and all its uses live in the same file. Once each camp is uniquely named it has exactly ONE
definition fleet-wide, so lift_types.py lifts them all by its ordinary rules and the §20 propagation
cap lifts with them. tools/uniquify_type.py --type <T> [--apply] does it: deterministic camp ordering
(file-count desc, then normalized text, so re-runs assign the same suffixes), majority keeps the name,
camp n becomes <T>_c<n>, and it rewrites ONLY files that DEFINE that camp — a file that merely uses
the name is getting the type from elsewhere and must keep referring to it (\bT\b word boundaries also
keep Buf from matching Buf80153978).
Validated on Buf (the cheapest camp: 578 / 6 / 1 files). 11 identifiers rewritten across 7 files →
3 camps all LIFTABLE → lifted (585 local copies stripped) → R22 140/140 byte-identical → the blocked
core queue fell 13 → 11 (0x8012ea90, 0x801749c8 freed, both Buf-blocked). Recipe:
uniquify_type --apply → lift_types --candidates → --types <camps> --apply → pre-filter build →
R22 → dedup_propagate --addr. Remaining camps by cost: MATRIX (578/71/6), Vec8 (180/139 — no
clear minority, so expect to name BOTH camps), then Handler / Blk8 / V8 / Prim / Prim_8016E7C8.
Also fixed here (R32): dedup_propagate's skip line printed a COUNT and no names, and aggregated
three unrelated causes into n_local — a body skipped merely for containing a // comment or a line
continuation (macro-unsafe, a one-line fix) was reported identically to one genuinely using an
overlay-local type. The queue is now named and split by cause, so the work it represents is visible.
§63 UPDATE (Phase 29 SESSION-14) — fix_header_decl's "SAFE" verdict is FLEET-BLIND; it MUST be R22-validated
The §63 header-decl reconcile (void→s32 widening of a shared engine_core.h caller decl) banked 3 of
3 fresh cracks under the PER-BINARY gate (gate_stage --no-propagate on ov_SC07_006) — then R22
clean-fleet FAILED (139/140): ov_SC01_077 broke. Reverting the biggest-fanout decl (func_8012CC88, 12
refs) did NOT fix it — at least one of the 2-ref void→s32 widenings (func_8014D12C/func_8014CF04)
ALSO perturbs ov_SC01_077, where those functions are already matched and a caller's codegen shifts under
the widened (even no-proto ()) shared decl. fix_header_decl --check's [SAFE] verdict only inspects the
ONE caller's return-use; it is structurally blind to the other ~137 overlays the shared decl reaches.
RULE: a fix_header_decl (or any engine_core.h decl) edit is a §61 shared-state mutation — validate it
with a full R22 clean-fleet, NEVER the per-binary gate that authorised it. The per-binary gate is a
NECESSARY-not-sufficient filter here. The byte-gate caught it; the whole recovery pass was reverted, 0
broken landed. The 3 drafts are byte-correct in isolation — they need a per-overlay-local decl or an
alternate integration path (not a fleet-shared header widen), logged to the backlog.
§65 — The stranded-draft recovery: BLAST-RADIUS TIERS, and the per-overlay de-macroize that refutes §20's DEF-conflict wall (tools/demacroize.py + tools/blocker_probe.py, Phase 29 SESSION-16, 2026-07-24)
The population. A crack wave banks ~27% of its drafts; the rest are stranded. Measured over the
s14+s15 residue (36 drafts, ov_SC07_006): 24 are match_one MATCH, 11 are near, 1 ERR. So the
audit's "~92% byte-correct" is a WHOLE-WAVE figure; among the stranded residue it is 67%. A third
of the "we strand paid-for correct functions" premise was never correct — and the near ones are
exactly the drafts that compile in their real TU and DIFF. Measure the residue separately from the
wave before pricing a recovery pass off it (R14).
Blockers STACK, and cc1 reveals only the FIRST. Per-blocker counts over those 36: self_decl_hdr
21 · callee_decl 19 · data_decl 16 · self_decl_tu 5 · local_type 5 — on 36 functions. So a
per-function verdict read off one cc1 run is always an underestimate; route on a static oracle's
COMPLETE list and use cc1 to confirm the class, not to enumerate it. A function's recovery tier is the
MAX over its blockers, never the first one's.
§65a — The blast-radius taxonomy (makes §61's law structural instead of remembered)
| tier | write set | validator | why |
|---|---|---|---|
| T0 draft-only | the draft file | per-binary gate | nothing else changed |
| T1 binary-local | src/<binary>/** |
per-binary gate — SUFFICIENT | the write set cannot reach another binary |
| T2 fleet-shared | src/shared/**, config/** |
R22 clean-fleet, MANDATORY | one edit reaches ~140 binaries (§63 UPDATE) |
The §63 disaster was not "a per-binary gate is untrustworthy" — it was a T2 edit validated by a T1
validator. Stating the tier makes the right validator mechanical. Every stage should DECLARE its tier
and the driver should MEASURE the write set (git status --porcelain before/after) and assert
containment: a stage whose undo scope is narrower than its write scope destroys work no byte-gate can
see (§61d), and one that lies about its tier should abort, not be trusted. Validated empirically
here: a src/<binary>/**-confined batch of 14 banks passed R22 140/140 three times.
§65b — The escape: de-macroize the instantiation, don't touch the shared header
The blocker (the largest single class, 21 of 36). A DEFINE_func_* macro in engine_core.h
forward-declares the draft's own function with a caller-derived signature the byte-true definition
cannot satisfy (typically void where the real return is live). Both obvious fixes are byte-proven
wrong: rewriting the DRAFT to the header sig compiles but MISMATCHes (§20/§58b — the body needs its own
sig), and rewriting the SHARED decl is fleet-blind (fix_header_decl banked 3/3 per-binary then failed
R22 139/140, §63 UPDATE).
The key structural fact: the conflicting extern lives INSIDE the macro BODY, so it exists only
where the macro is INSTANTIATED — and for a given overlay that is a handful of sites in that overlay's
own TU. Replacing those instantiations with the macro's own expansion, correcting ONLY the conflicting
declaration to the draft's byte-true signature (never DROPPING it — §57a-1's def-after-caller
trap), dissolves the conflict as a T1 edit. Every other overlay is textually untouched, so no R22
risk is created by construction. This is the "per-overlay-local decl" §63's own note named as the
unexplored alternative.
⇒ §20's "the DEF-conflict class is byte-proven unrecoverable by text transform" is REFUTED for the per-overlay case. §20's reasoning was sound for the mechanism it considered (the shared macro's
externis the only declaration in the 137 STUB overlays, so it cannot be dropped, and it "can't be edited per-overlay — it's in the shared header"). The missed move is that you do not have to edit the header to change what ONE overlay sees: you expand the macro there. Measured: 13/14 of the clean candidates MATCH in their real TU; 14/14 of the whole-binary attempts banked.
Generalizes to callee conflicts. The same transform, applied to any decl in the body that the DRAFT
declares incompatibly (not just the draft's own function), reaches the callee-conflict variant — e.g. a
macro declaring RotTransPers differently than the draft does. The draft is byte-truth for every type
it was compiled against; the macro body is what must yield.
The price, stated before the work, not after. A de-macroized function can no longer be propagated
×138 from the shared macro (dedup_propagate would re-macroize it). Such a bank is ×1: it credits
the FULL distinct-code unit (progress.py marks an h_exact class matched if ANY instance is —
matched_cls.add(hx)) but only ~1/138 of the instruction-weighted headline. Measured: 14 banks →
distinct-code +14 unique fns, instr-weighted and fn-count ~flat. For the 0-stubs completion contract
that is real progress; for the decomp.dev display number it is not. Say which one you are buying.
§65c — rtu_match MATCH → bank held 13/13 on self-decl, but broke on the FIRST callee-decl case
rtu_match is far better than match_one here (it compiles the real TU, so it sees decl conflicts),
and its real-TU MATCH predicted the whole-binary bank 13/13 for the self-decl class. It then failed
on func_8012F49C — a callee-decl case. That is its documented blind spot doing exactly what it
says on the tin: it is relocation-masked, so a wrong call TARGET is invisible to it and fatal to the
real link. Trust rtu MATCH more when the correction is to the function's own declaration; trust it
less when the correction is to something it CALLS. The whole-binary gate remains the only arbiter
(G3/P9), and it cost one build to find this.
§65d — Existing-ladder baseline, measured (do this before building a recovery stage)
Running the existing draft-side ladder (cast_call_sites + reconcile_tu) over all 36 clears exactly
what it targets — callee_decl 19→3, data_decl 16→0 — and converts 1 of 36 from CC1-FAIL to
compiling, which then DIFFs. §61d verbatim: dissolving the plumbing reveals what the plumbing hid.
The conclusion is not "the ladder is broken" — it is that the ladder's classes were not this
population's blocker. A recovery pass that had been built on the assumption it was would have measured
~0 and been abandoned as "the wall". Baseline the existing tooling against the actual residue first; it
costs seconds and it re-prices the whole task.
§65e — Two oracles, and the disagreement is the finding (R34 in practice)
tools/blocker_probe.py runs a STATIC oracle (cdecl.parse + cdecl.compatible — cc1's own
acceptance question) beside the REAL cc1 (via rtu_match), and reports where they disagree. Two things
this bought immediately:
- Text equality is never the question. The deleted
.run/diag_plumbing.pycompared declaration TEXT, soextern u8 D_X;vsextern unsigned char D_X;read as a conflict althoughcommon.htypedefs make them the same type and cc1 accepts both silently. That artifact alone accounted for the pre-probe "~10 data-extern co-blockers" estimate. - The static oracle's own blind spot, named by the other oracle. A bare
struct Tag {...}redefinition is not aDeclaratorconflict, so the static side reported "no blocker" forfunc_80173A60while cc1 saidredefinition of 'struct S80126B38'. The CC1-ONLY column is where a single-oracle design would have silently mis-routed the fix. Also:cdecl.tu_scoperuns REAL cpp (tu_statements, the §8c law), so it already expands instantiated macros and is authoritative about whether a conflict EXISTS. A separate macro scan is still needed, but only for attribution — TU text vs shared-header macro body are the same tier and different edits.
§65f — The de-macroize lever's BOUNDARY, measured: byte-neutral 14 times of 15, and the 15th is why the gate exists
De-macroizing corrects a declaration that an already-matched function (the macro's own body) is compiled against. Usually that is byte-neutral — but not always, and the failure is not a compile error:
| outcome | n | signature in the gate |
|---|---|---|
| banked BYTE-IDENTICAL | 14 | verified N / failed 0, final SHA == locked |
| edit shifted the macro's own matched function | 1 | verified 0 / failed 1, final SHA is a REAL hash ≠ locked |
func_8014C4AC was rtu_match MATCH and still failed, because the baseline (draft reverted, the
de-macroize edits still in place) no longer built byte-identical. Read the final SHA, not just the
verified count: final SHA None = the build produced no image (a compile/link break, e.g. §65g);
final SHA <hash> ≠ locked = the TU edit itself moved bytes. Those are different faults with different
fixes, and the count alone does not distinguish them.
So the lever's precondition is not "the draft is byte-correct" — it is "the draft is byte-correct AND correcting the decl does not perturb the macro's own function." The second half is unknowable in advance and costs one build to test. That is exactly the §63 failure mode, relocated from the fleet (where it broke 139/140 invisibly) into a place the per-binary gate can see and reject for free.
§65g — Where the cheap levers STOP: the local-type and in-TU-self-decl classes did not yield
Measured, both reverted with nothing landed:
local_type(4 fns) — 0 banked.canon_sig_reconcile._uniquify_draft_typesrenames the draft's colliding type (byte-neutral: a type name emits no code), but that renames the type in the draft's ownextern <T> D_x;declarations too, so it trades aredefinition of struct Tfor aconflicting types for D_x. Re-ordering (uniquify BEFOREcast_call_sites/reconcile_tuinstead of after) fixes the ordering bug and lets one compile — which then DIFFs 48/53, because the data reconcile's cast-at-use changes real codegen when the body's semantics depend on its own struct layout. A struct-TYPED data extern is the casereconcile_tucannot cast.self_decl_tu(3 fns) — 0 banked.normalize_self_declsskips a literal()unconditionally (blind to a RETURN-type conflict, sofunc_801376E8normalizes 0), and its narrow-param rewrite on the other two broke the build outright (final SHA None). §57a already classes it SURGICAL-ONLY — it edits the TU file, so a bad edit poisons the whole group and cannot be bisected per-member.
⇒ The recovery pass's measured yield is 14 of 36 (39%), and that is where the EXISTING transforms stop. The remaining ~8 blocked functions are not a matter of running one more tool; each needs a transform that does not exist yet. Do not re-run these two classes expecting a different number — the negative is byte-recorded here precisely so the next session does not re-buy it.
§66 — Exercise a banking driver's SUCCESS path before pointing it at a wave: the free re-bank test (Phase 29 SESSION-17, 2026-07-24)
A driver can be "built, proven, and encoded" and still have never run its own success path. SESSION-16
ended exactly there: recover_integration.py's end-to-end run banked 0, so pass 2 (re-stage winners
→ propagate), --commit, the r22() helper and --report had never executed. A driver that mutates
src/ and can commit is the wrong place to discover that.
THE TEST, and it is free. Revert ONE already-banked function to its INCLUDE_ASM stub, then re-bank
it through the driver on its saved draft, with --commit --r22. Cost: one build. It exercises
pass1 → restore → pass2 → commit → R22 → report on a case whose answer is already known.
Two properties make it a strong test rather than a smoke test:
- A byte-checked baseline for free. The reverted (stub) state must rebuild byte-identical — the
stub pastes the original asm — so a faithful revert proves itself before the driver runs. Reverting a
banked def needs
make extractfirst: splat only emitsasm/nonmatchings/**/<fn>.sfor functions not defined in source, so the.sthe restored stub includes does not exist until you re-extract (the R22 corollary, in its source-reverted form:can't open …/func_X.s for reading). - An exact equivalence check on the output.
git diff <pre-revert-commit> -- src/must come back empty: the driver has to reproduce the known-good banked state character-for-character. Anything else is a real behavioural difference, not a judgement call. (Ours: one stray blank line, from the hand revert, not the driver.)
§66a — The widest write in a pipeline is the one most likely to be UNDECLARED
The §65a blast-radius taxonomy was applied to the stages the driver author wrote (arity = fleet,
demacroize = binary) and not to the pipeline's own inherited default: run_gate(propagate=True)
shells out to dedup_propagate --auto-from, which writes src/shared/engine_core.h and up to 138
overlay .c files. So --max-tier binary — the default, chosen to mean "no shared-state edits" —
still performed the widest write in the toolchain, and --no-propagate was a thing you had to
remember (the checkpoint carried it as a "STANDING HAZARD", which is the tell).
A measurement-based guard cannot save you here. assert_write_set diffs git status --porcelain,
and under --commit the writes are already committed by the time it could look — it sees a clean tree
and passes. Containment by measurement must run before anything commits; otherwise refuse up front,
the way stage tiers already are. Now: propagation requires --max-tier fleet AND --r22, and is
refused outright after a demacroize stage (whose banks are ×1 by construction, and which
--auto-from would re-macroize and undo). Both refusals negative-control-tested, exit 1.
Generalize: when you add a blast-radius taxonomy to an existing pipeline, enumerate what the pipeline already did — inherited defaults are exactly the writes nobody re-reads.
§66b — A metric parsed out of another tool's prose goes NULL silently when the label changes
gate_stage scraped the fleet % from progress.py --fleet with a regex for byte-identical :.
progress.py later replaced that single line with the three-metric block (FLEET fn-count byte-ident:
/ FLEET instr-weighted : / FLEET distinct-code(uniq):). The regex matched nothing from that day
on, fp became None, and 50 automated bank commits recorded fleet None% in their messages
before anyone added up the zeros (R32 — a silently-nulled number is a defect, not a no-op). Fixed to
read FLEET instr-weighted with the legacy label as fallback and a loud stderr warning when neither
matches, so the next rename cannot go quiet.
Same shape, one level up: a digest regenerated while an experiment's edits are still on disk gets
committed as if it described the committed tree. docs/progress.fleet.md at HEAD disagreed with HEAD's
own source by 45 in the dedup-shared column — it was generated during the §65g local_type trial and the
edits were reverted afterwards. Provable in one command: regenerate from committed source and compare
(two independent runs, in-gate and standalone, agreed). Regenerate digests on a clean tree, or the
digest describes a state that no longer exists.
§66c — Before a wave, verify the FUEL exists; an "already attempted" set built from the wrong directory lies in BOTH directions
build_wave_args.py ranks candidates by live-sibling count but has no notion of already attempted —
so its "FRESH (>=100 live)" line means "fresh by leverage", not "never drafted". Filtering it is on you,
and the filter is where the traps are. Both of these happened in one afternoon:
- Exclusion set too NARROW → false work. Built from the backlog alone, it missed
.run/giants/, so the top three "fresh" targets werefunc_80176734(close=76),func_80176218(close=271) andfunc_80140958(close=116) — ~2.6M agent tokens of preserved, characterized, permuter-only drafts a wave would have paid to re-derive. Preserved drafts do not all live in the backlog (.run/giants/,.run/wt_uni/,.run/drafts_wave4/, the per-wave.run/drafts-*dirs); 30 of 141 high-reach cached targets have a draft and no backlog record at all. - Exclusion set too WIDE → false exhaustion. Corrected to scan
.run/**/func_*.c, it swept in.run/ghidra_c/func_*.c— which is the Ghidra decompile INPUT, not an attempt — and reported 5,488 attempts, making 100% of every pool read "already tried". A 0 from that filter and a 0 from a correct one are the same number and opposite facts (R35, one level up from tools onto queries).
The query that actually answers "is there fuel?" — over all binaries, per candidate: (cached
Ghidra-C?) ∧ (still an INCLUDE_ASM stub in ≥N binaries — the TRUE lever, recomputed from
corpus.stubs, never a manifest's stale reach) ∧ (no backlog record) ∧ (no preserved draft
anywhere outside .run/ghidra_c/). Answer at this checkpoint: 141 cached high-reach targets → 111
gated, 30 draft-only, 0 never attempted; the only never-attempted cached fuel fleet-wide is 40
functions at live 1–4.
The structural point: wave fuel at high reach is created by a per-overlay Ghidra-C prefetch, not found — the cached pool is a consumable, and once a wave drains it the next wave's cost/benefit is computed against a pool that no longer exists. Re-measure the fuel before each wave, not the leverage.
§66d — The permuter⇄reader loop: alternate a random search with a byte-verified idiom, and let residual_class decide whose turn it is (Phase 29 SESSION-17)
func_80177940 (101 ins, reach-138) was preserved as close=5 with the note "equal-priority
birthing_insn_p ties broken by INSN_LUID, not steerable from source order — swept all 6 assign
orders/pin combos". Both halves of that note were right, and it still banked, because the two halves
belong to different tools:
| residual | who closes it | why the other cannot |
|---|---|---|
| 4-ins scheduler tie (INSN_LUID order) | the permuter (5 → 1, 900 s) | a hand sweep enumerates source orders; the tie is broken by internal LUIDs, which randomized decl/statement churn moves and a human cannot address |
andi $a2,$v0,0xf vs addu $a2,$v0,$zero |
the reader | the permuter's mutation set adds masks/casts; it does not invent "delete this mask AND hard-pin the destination" |
| the register/schedule fallout of that fix (6 left) | the permuter (6 → 0, 1800 s) | ditto the first row |
The handoff signal is free and already computed. tools/match_one.py prints residual_class's
bucket on every run: [permuter] → hand it back to the search; [structural] → read it. Here it went
structural(OPCODE-MIXED) → after the idiom fix ADDRESSING [permuter] profile=cse
→ MATCH. Feed that profile straight to p16_permute --klass (permuter_weights.classify accepts a
profile name verbatim).
⚠️ Correction, same session, before this rule could mislead anyone: [structural] is a HINT, not a
veto. func_80177940 was structural(OPCODE-MIXED) at close=5 and the permuter still took it
5 → 1 — at low closeness a randomized search reaches ties the class label says nothing about, since
the label describes only the dominant residual. Read [structural] as "a search alone probably will
not finish this", not "do not run the search". Cheap policy: at LOW closeness run the search anyway (it
costs CPU, not tokens); at HIGH closeness read first, because a search that must invent a semantic
change will burn hours to plateau.
Diagnose from a byte-verified SIBLING, never from first principles. The fix came from
func_801778A8 — same family, same nibble walk, already banked byte-identical — which writes
nib = uVar1; (a plain copy) between two hard-pinned variables after the identical (x << 16) >> 28
shift pair. And test the naive reading first: deleting the redundant & 0xf alone collapsed the
copy (100 vs 101 ins, 52 mismatched), because gcc then reused one register. That failure is what proved
the target needs a distinct pinned register — the mask was never the point. A residual of one
instruction can still be a two-part fix.
§66d-1 — What transfers between giants is the LOOP, not the PIN
Applied to func_8014D820 (304 ins, close=33), whose residual is the mirror image (target holds the
lhu results in $v1, keeps $a0 live to fill the load-delay slot), the same move — pin the
reusable temp to the register the target uses — went 303 vs 304 ins, 285 mismatched: $v1 is needed
elsewhere in that function. §44's "each giant is its own class" holds at the level of the specific pin.
Its permuter run improved 33 → 27 and plateaued (seed kept for an ILS warm restart). Budget the loop
per giant; do not budget a pin.
§66d-2 — Two operational sharp edges
p16_permute.setupwipes.run/permuter/<fn>/. Re-running a target destroys the previous run'soutput-<n>-*dirs, best score included. Copy the bestsource.cout before re-running — the score-1 candidate here survived only because it had been copied to a run-local path first.run_permuter's cleanup used a globalpkill -f permuter/run_masked.py, which matches every concurrent run — so two permuters on a multi-core box silently killed each other the moment the first timed out, with no error anywhere. Now scoped to the run's own scratch dir (it is in argv). This is what makes grinding several giants at once safe, and it is likely why the giant queue had only ever been run one at a time.
§66d-3 — Read the ILS per-cycle SERIES, not its final best: a repeated score and a still-falling one look identical in a summary line and mean opposite things
Two permuter_ils runs, same session, same machine, same 8×240 s budget:
| function | per-cycle best | verdict |
|---|---|---|
func_8014D820 |
25, 25, 25, 25, 25, 25, 25, 25 | DONE. The warm restart has nothing left to find; more CPU is waste. Escalate to a reader / Fable5, or accept the stub. |
func_80140958 |
80, 75, 72, 69, 64, 62, 60, 59 | BUDGET-LIMITED. Monotone, never repeating, still dropping on the last cycle → buy more cycles. It is CPU, not tokens. |
ILS done: best=NN is the same line in both cases, and choosing on it alone gets both decisions wrong
— you stop the one that was still descending and keep paying for the one that had finished. The series
is already printed per cycle; the rule is: a repeat means stop, a fall means continue. (A first-cycle
drop followed by repeats — func_8014D820's 27→25 then ×7 — is the "one easy waypoint then done" shape,
which still means stop.)
§67 — The arg-copy PLACEMENT lever: launder a parameter into a fresh pseudo AT the statement where the target's copy lands (Phase 29 SESSION-18, func_8014D820 25 → 16)
func_8014D820 (304 ins, reach-138) sat at the permuter floor (ILS 27→25, then 25 ×7) with the
SESSION-17 note "target births $s3←a1 FIRST, $s4←a0 twelfth; mine the reverse … §17 register-ORDER
class". The diagnosis was right and the class label was wrong: this is not a register-ORDER problem you
solve by pinning registers (four pin/barrier/staging variants were byte-measured inert or worse). It is
an instruction-PLACEMENT problem — where gcc materializes the entry copy addu $sN,$aX,$zero.
The cascade, and why it looks like three unrelated bugs. gcc-2.7.2 schedules the arg→pseudo entry copies as ordinary in-block insns. An unconstrained copy gets hoisted to the earliest ready slot, and whichever parameter's copy goes first frees its argument register, which then gets used as the early load temp. So one root cause presents as:
| symptom | actually |
|---|---|
sw $s4 / move $s4,$a0 first, target has sw $s3 / addu $s3,$a1 |
the copy raced to slot 2 |
my load temp is $a0, target's is $v1 |
consequence — a0 left $a0 early, so $a0 was free |
I am +1 ins with an unfilled load-delay nop |
the target fills that slot with the deferred copy |
a later scratch is $v1 where the target uses $a0 |
downstream of the same free-register difference |
Do not chase these separately. Fix the placement and they collapse together (here 25 → 16 in one move, with the whole prologue going exact and the instruction count landing on 304).
The lever. Declare a plain, unpinned local and launder the parameter into it at the statement where the target's copy sits, then rewrite every later use of the parameter to the new name:
s32 a0v; /* NOT a register pin -- see below */
...
__asm__ __volatile__("" : "=r"(a0v) : "0"(a0)); /* zero instructions; forces the copy HERE */
ent = *((Ent **) (a0v + 0x170));
"0"(a0) ties the input to output 0, so gcc emits exactly the copy it was going to emit anyway — the
asm only fixes when. It costs no instruction.
Placement is the tuning knob, and it is NOT linear — sweep it. Measured for func_8014D820, whose
target copy sits at idx 12 (between the a1[0] load and the dx subtract):
| launder placed before … | copy lands at | result |
|---|---|---|
dx = t - u; |
idx 10 (hoisted above both loads) | 305 ins, worse |
t = a2[2]; / u = a1[2]; / dz = t - u; |
idx 12 ✅ | 304 ins, 16 mismatched |
the if (…) goto fail; |
idx 16 (the other delay slot) | 304 ins, 18 mismatched |
Three adjacent statements all give the same correct slot, so the target is a plateau, not a knife-edge —
sweep 3–4 anchors and take the best. Adding an artificial input dependency ("r"(t)) to force a slot
did nothing: gcc still hoisted the copy above the load. Placement, not dependency, is the control.
Two hard rules, both byte-measured:
- Do not pin the laundered variable to a hard register. Pinning
a0vto$s4(__asm__("$20")) made gcc pre-stage through$t0— an extramove t0,a0at idx 1 and 305 ins. Unpinned is correct; gcc picks the same callee-saved register by itself. (Same failure shape as pinning a reused temp: pinning the load temptto$3globally gave 287 mismatched / 303 ins.) - Launder inside the block that owns the delay slot. Placing it after the
beqzthat ends block 1 forces a second materialization (the value must already survive the branch), so you pay an extra insn and gain nothing.
Prerequisite — collapse redundant pointer aliases first (the two-pseudo law). The draft carried
new_var2 = a1; and used both names. That splits one pointer into two pseudos: gcc served the first use
out of the incoming arg register and deferred the callee-saved copy, which is exactly the defect. One
sed collapsing new_var2 → a1 made a1[0] load from $s3 like the target and removed idx 11 from
the diff. Do this before reasoning about copy placement, or you are debugging two mechanisms at once.
Re-test your older pins after the fix — they may be crutches. A register s32 t __asm__("$3") pin
was load-bearing before the launder (it forced the $v1 load temp) and provably redundant after
(identical 16 either way), because once a0 stays live through the early loads the temp must be $v1.
Drop it: same bytes, simpler C, one less thing to explain to the next session.
This is a PATTERN, not a one-off — it fired twice on the same function. The t pin ($3) went
redundant after the launder, and later the u pin ($2) went redundant too (identical 9 either way),
leaving only the a2 $7 pin, which stays genuinely load-bearing (dropping it costs 9 → 29). A register pin is a crutch for a mis-scheduled value; once the
value is scheduled correctly the pin is dead weight — and a stale pin actively costs you, because it
reserves a hard register the allocator then cannot use where the target does. After any structural
fix, re-test every pin you inherited and drop the ones that are inert. (Corollary, byte-measured:
adding a new pin to chase a register choice is usually worse — pinning y0 to $v0 to force the
target's early pos.y store went 9 → 12.)
A refuted hypothesis, recorded so it is not re-bought. SESSION-17 left an "untried, cheap lever":
maybe birth order follows first-use order (§31 regalloc RC-1/RC-2/RC-3 — declaration/use order drives
allocno_compare density). It does not, for this class. func_8014D820's use order already matched
the target (a1 first used at body-line 28, a0 at body-line 41 — a1 before a0, exactly the target's
birth order) while the birth order was inverted. Declaration order is likewise inert (moving ent/p to
the end of the decl block: byte-identical result). What moves the copy is its schedulable position,
and the launder is how you set it.
Where this applies. Any draft that is +1 ins with an unfilled load-delay nop and shows mirrored
sw $sN / move $sN,$aX prologue pairs. That signature is the tell — count the instructions first: a
draft one over with a nop the target fills is a placement bug, not a regalloc wall.
§67a — Run the symbol-set guard BEFORE you pay for a gate (tools/symcheck.py, Phase 29 SESSION-18)
SESSION-17 ended func_801463A0 with an open TODO: "a cheap guard worth building: diff a draft's
referenced symbol set against the target .s's %hi/%lo/jal set before gating." Built, negative-control
proven, and it belongs in front of every gate cycle.
Why nothing we already had can do this. All three of our fast oracles are blind to it by construction, not by accident:
| oracle | blindness |
|---|---|
match_one / masked_diff |
compares relocation-masked words; object-vs-.s mode is symbol-agnostic, so a reloc against the wrong symbol matches at that position |
rtu_match |
compiles but never links — an extern no symbol table defines is invisible to it |
| the whole-binary byte-gate | correct, but it tells you rejected, not why, and costs a full cycle |
The measurement that proves the point (func_8014D820, one data extern renamed to an invented
_s alias):
| correct draft | draft with an invented symbol | |
|---|---|---|
match_one |
14 mismatched | 14 mismatched — identical |
symcheck |
SYMS-OK 12 symbols agree |
SYMS-DIFF + names MISSING and INVENTED, exit 1 |
Two directions, both reported: MISSING (target references it, draft does not — the invented-alias signature, since the alias silently stands in for the real symbol) and INVENTED (draft references something the original never touched).
Where it goes: immediately before harvest_verify, and immediately after any transform that
rewrites declarations (sig_unify, canon_resident_calls, cast_call_sites, canon_sig_reconcile) —
those rewrite extern names, which is precisely how an alias gets invented. It is a necessary
condition, not a match oracle: SYMS-OK means "no link-level defect of this class", nothing more.
Finish on the byte-gate (G3/P9).
§66d-4 — "ILS converged" means converged FOR THAT WEIGHT PROFILE, not a floor (amends §66d-3; Phase 29 SESSION-18)
§66d-3 gave the read-the-series rule: a repeat means stop, a fall means continue. That rule is right
about when to stop the current run and was over-applied to mean the function is at its floor.
SESSION-17 used it to record all four remaining giants as "at their measured permuter floor (ILS
converged)". func_8014D820 then fell from 16 to 10 under nothing but weight changes.
The measurement (func_8014D820, each pass warm-started from the previous pass's best waypoint):
| pass | profile | series | best |
|---|---|---|---|
| 1 | --klass REGALLOC |
14, then ×9 unchanged | 14 |
| 2 | --klass SCHEDULE |
12, then ×7 unchanged | 12 |
| 3 | --klass cse |
10, then ×9 unchanged | 10 |
Three profiles, three identical shapes: one drop in cycle 1, then dead flat. The flatness is real —
continuing that run is waste, exactly as §66d-3 says. But it is a statement about the mutation
distribution, not about the function: permuter_weights.classify biases decomp-permuter's pass
selection toward one class's levers (regalloc.md RC-, sched.md S-, the CSE address-fold levers), so a
converged run means this profile's neighbourhood is exhausted around this seed — and the seed has
just changed, because the pass rewrote it.
The rule, corrected:
- A flat series ⇒ stop this run. (§66d-3, unchanged.)
- Before calling a floor, re-run with a different
--klassfrom the new best waypoint. Only after all three profiles come back flat from the same seed have you measured a floor. - Cheap, unattended, ~0 tokens — so it is the first thing to try on any "converged" giant, ahead of reader time and far ahead of Fable5.
And check every pass's diff for operand-order regressions. A random search cannot tell that one of
its own edits made a local position worse while the total improved. The cse pass here swapped
if (p == ent) → if (ent == p), which cost idx 110 (beq $s2,$s1 vs the target's beq $s1,$s2);
reverting just that operand order, keeping every other gain, took 10 → 9 for one compile. Same for
x >= k ↔ k <= x. These are free points and they are invisible to the scorer's total.
⚠️ QUALIFY THIS: profile diversity is worth TRYING, but it is NOT reliable. Everything above is
measured on ONE function. The same session ran the identical experiment on func_80140958 — base 56,
prior profile regalloc, fresh --klass cse pass — and got best=56, zero improvement across all 8
cycles. So the honest rule is "before declaring a floor, spend one cheap unattended pass per unused
profile", not "each profile is worth ~2 points". Score so far: 3-for-3 on func_8014D820,
0-for-1 on func_80140958. The test is cheap enough (~0 tokens, unattended CPU) that it stays worth
running — but budget it as a lottery ticket, not as expected yield, and do not plan a session around it.
MEASURED: a repeated profile does NOT yield again. REGALLOC round-2, warm-started from the close=9 seed (a seed it had never seen — SCHEDULE, cse and a reader fix had rewritten it since), came back flat for all 10 cycles, 0 gain. So the lever is profile diversity, not seed novelty: each of the three profiles is worth about one drop, and re-spending one buys nothing. Budget accordingly — three passes per giant, then stop.
That also makes rule 2 above operational: a floor is measured when all three profiles come back flat
from the SAME seed. For func_8014D820 at close=9, REGALLOC is flat; SCHEDULE and cse from that seed
are the remaining evidence needed before calling 9 a floor.
§66d-5 — residual_class's "structural ⇒ permuter CPU is waste" is WRONG for schedule permutations (measured, Phase 29 SESSION-18)
tools/residual_class.py documents its buckets (lines 55–58) as:
structural — local mutation CANNOT introduce it (a different access width, a flipped branch, a multi-instruction shape change). Spending permuter CPU here is waste; it wants an idiom.
Byte-measured counterexample. func_8014D820 was classed OPCODE-MIXED [structural], and later
WIDTH [structural] sig=WIDTH/lhu!=sh, at every waypoint — and the permuter moved it 16 → 14 → 12
→ 10 across three weight profiles. Six points of gain inside a bucket whose documented guidance is
"don't run the permuter." Meanwhile the guidance the bucket does give ("read it") was exhausted:
eleven source-shape attempts on the same window, all inert or worse.
Why the classifier is fooled. It reasons position-by-position. A pure schedule permutation
changes what lands at every index in the affected window — so a block whose instructions are all
present but reordered presents as "different operations, no single family" (OPCODE-MIXED) or as a
width flip (WIDTH), because index i now holds a sh where the target holds an lhu. The signature
of a reorder is indistinguishable, per-position, from a genuine shape change. The classifier's
SHIFT-DRIFT rule catches this only when ONE shift point re-aligns the tail; a permutation within a
window with matching endpoints re-aligns nowhere.
The corrected routing rule:
[permuter]⇒ search, as before.[structural]⇒ read first — the idiom is the cheap win when there is one.- But
structuralis NOT a permuter veto. If reading fails 2–3 times AND the instruction COUNTS match AND the same multiset of operations appears in the window in a different order, treat it as a schedule permutation and run the three profiles (§66d-4). Instruction-count equality is the tell: a genuine shape change usually changes the count; a permutation never does.
Cost of the old reading: this is a project-wide mis-route, not a one-off — every schedule-permuted residual that ever landed in OPCODE-MIXED/WIDTH was steered away from the one tool that moves it. Worth a targeted re-check of backlog entries carrying those two class tags with equal instruction counts.
§68 — A comment-only line halted the extern scan, and the skip label blamed the type cap (Phase 29 SESSION-18)
dedup_propagate.find_site() extracts a matched body as "preceding contiguous externs .. closing
brace". Its backward walk skipped blank lines but not comment-only lines. So a body written
like this — which is ordinary house style — silently lost its first extern group:
extern s16 D_80126940; /* <- DROPPED: everything above the comment */
extern s16 D_80126942;
/* ------------------------------------------------ */ /* <- the walk stops HERE */
extern s32 func_80012ABC(s32, s32, s32); /* <- only these were carried */
s32 func_80174CB0(s32 param_1, s32 param_2) { … }
The truncated body then failed compiles_standalone() on the now-undeclared symbols — and the caller
filed every such failure under "overlay-local TYPE (the real cap)".
That label is why the class went unfixed for ~4 phases. The Phase-21 backlog already carried the right answer — "a macro-extern-injection (or canonical-callee-sig embed) frees them ×134 (~+0.3%)" — but every session after that read the skip line, saw "the real cap", and treated it as the known-hard overlay-local-type wall. A diagnostic that discards its cause is invisible work (R32); one that asserts the WRONG cause actively redirects everyone who reads it.
Two fixes, both small:
compiles_standalone()returns(ok, stderr); the caller classifies by actual cc1 output —missing file-scope extern (CARRY-FIXABLE): <names>vsoverlay-local TYPE (the real cap).- the backward walk treats comment-only lines like blanks, and the emitted body filters them out so
make_macronever meets a//.
Result: func_80174CB0 went from "not self-contained (local types)" to a 138-member plan.
⚠️ "CARRY-FIXABLE" is NOT one cause — measured. After fix (2), the 7 skipped functions in
ov_SC01_077 (0x8016A73C 0x80155800 0x80167540 0x801535F4 0x8016F0AC 0x80142B2C 0x8014FE60) are
still blocked, so at least one more sub-class exists: their missing symbols are not adjacent to the
def at all (e.g. D_801152A8 appears 26× in engine_core.h but only ever INSIDE DEFINE_func_*
macro bodies, never at file scope). Those need the genuine carry mechanism — collect the referenced
file-scope decls wherever they live (cdecl.scope() is the right oracle, R33) and emit them inside
the lifted macro. Do not assume fix (2) cleared the queue: re-run
--auto-from <ov> --check-only and read the per-class counts.
The general lesson: when a tool refuses, make it print what the compiler said, not what the
author guessed the compiler meant. Every wrong-label bug in this project (§53's carve, the 193
listCdBuffer slices, masked_diff's 150 closeness lies, this one) had the true cause available and
threw it away.
§69 — How to attack a behemoth: map it, don't draft it (Phase 29 SESSION-18, func_80183814, 5,122 ins)
First attempt ever on the game's largest unmatched function. It did not match, and the useful
output was never going to be a match — it was the map. Every claim below is byte-verified against the
target .s.
What it is. Not a straight-line giant: an actor state machine. ~48-ins preamble → switch (*(u16 *)(a0 + 0x34)) over 21 cases via jtbl_801F4CE4 (sltiu $v0,$v1,0x15) → 19-ins shared
tail. 359 jals to only 37 distinct callees (verified). A cutscene/boss-script driver.
THE FINDING — it is not 5,122 unique instructions, it is a few templates repeated:
| sub-shape | instances | notes |
|---|---|---|
| "spawn-effect" packet | 35 | ~60% of cases 11–20 (~1,400 ins). Body is DEFINE_func_80142454's s16 st[10] packet verbatim. Crack one → 34 free. |
| "wait/countdown" | 7 | if (a0->0x1C < 7) …; if (--a0->0x1C != 0) break; — already reproduced at 0 skeleton diffs (cases 2/5/7) |
| "6-slot HUD/text" | 12 | cases 0/1/3/4; and case 0 ≈ case 3, case 1 ≈ case 4 (identical call histograms, 363/365 and 280/281 ins) — near-free twins |
Coupling is light: only 3 cross-jump edges (cases 5, 10, 12 all j into case 14's merged tail), |
||
| so those four must be written together and everything else is independent. |
TWO LAWS FOR GIANTS, both measured here:
- Register pressure is GLOBAL, so a partial draft can never show a matching prefix. A truncated body (15 of 21 cases stubbed) was allocated 10 callee-saved regs instead of 8, and from case-0 idx 278 gcc CSE'd address constants into callee regs that the real function cannot afford and rematerialises. ⇒ Do not aim for a contiguous matching prefix on a giant. Write ALL cases coarsely first to restore the true pressure, then refine. A "first N instructions match" partition is structurally unavailable until the whole body exists.
match_one's global number is meaningless on a partial giant (here:mine=666 target=5122,SIZE-MISMATCH). Measure region-aligned instead — per case, registers masked, relocs masked. Tool:.run/giants/s18_regions_comparator.py(reusable for any giant). It gave PROLOGUE+PREAMBLE 0 diffs, cases 2/5/7 0 diffs, 139 real skeleton diffs over 638 decoded ins (78% skeleton-identical). Caveat that must travel with the metric: it masks register NUMBERS andj/jaltargets, so it proves sequence/opcodes/constants/offsets — structural correctness — and is never a closeness score. Finish on the whole-binary gate (G3/P9).
Two idioms cracked in passing:
D_8018E034[t + K]folds the+Kinto thelw(lw a1,%lo(base+4K)). The target keeps a runtimeaddiu, so the source needs a separate index variable and a separateidx = t + K;statement. Worth 246 → 83 skeleton diffs on case 0 by itself.- A
u16field read as(s16)*(u16 *)p(lhu+sll 16+sra 16), and/455via magic0x90090091with thehi+nadd-back form.
Verdict: tractable, but a ~2,000-line WRITE, not a hard puzzle. No scheduler wall, no unsteerable
regalloc, no exotic idiom — every construct decoded on the first or second try. The blockers are
volume plus the two couplings above. Recipe for the next attempt: all 21 cases coarsely → fix the
frame/8-reg allocation → crack ONE spawn-effect instance and paste it 35× → the twin pairs (0↔3, 1↔4)
→ the 7 wait instances (already solved, reuse verbatim) → cases 17/18/20 (560/484/525 ins) last.
Banking will need §27-step-2 / §28 plumbing recovery: func_80178970/func_80178D18/func_800599B8/
func_8017D8A4 are all called at arities that disagree with their canonical externs.
§70 — The giv-init base register: walk the PARAMETER, not a copy of it (Phase 29 SESSION-18, func_801777BC)
func_801777BC (59 ins, reach 138) came down to one instruction: mine emitted
addiu $t0,$t1,0xC, the target addiu $t0,$a0,0xC. A general-induction-variable based on the wrong
register. This is the REGALLOC-PERM/$tN>$a0 signature and no pin or permuter pass reaches it —
it is decided in loop.c before allocation.
Why the natural C can NEVER produce the target (read out of tools/reference/gcc-2.7.2/):
with the obvious form —
u32 *q = (u32 *)a0; /* … */ q += 5; /* … */ return q;
cse.c:make_regs_eqv makes q the canonical register of the equivalence class, because q
out-lives a0; and loop.c:update_reg_last_use then refuses to extend a0's last-use, because the
giv-init insn's UID is ≥ max_uid_for_loop. So every giv gets based on the copy, forever.
The fix — make the loop pointer the parameter itself:
a0 = (void *)((u32 *)a0 + 5); /* … */ return a0;
Now loop.c:record_initial finds the biv's initial value is the hard register (reg:SI 4),
valid_initial_value_p accepts it (precondition: no calls in the function), and
emit_iv_add_mult bases the giv on $a0 — which yields both addu $t1,$a0,$zero (index 0) and
addiu $t0,$a0,0xC (index 16) for free.
Look for it when: a pointer-walking loop returns the walked pointer, and the only residual is a
giv/base register differing between a $t/$s temp and an argument register.
Two supporting levers from the same function:
- Hoist
i = 0;OUTSIDE the guard. Left inside,reorgsteals thelui $t4constant into theblezdelay slot instead of the target'smove $11,$0. - Reuse one variable sequentially for two constants (
mask = 0xffffff; t = … & mask; mask = 0x3000000;) so both share$v1instead of pinning two registers simultaneously.
The meta-point, and it is a tier shift. Phase 23 established that reading the gcc source to break a class was the Fable5 tier — the expensive wall-breaker. This was done by an ordinary Opus 5 drafting agent, unprompted, as part of finishing one function (270k tokens). The §31 map plus a local copy of the real compiler source appears to have moved compiler-internals reasoning down a tier. Do not read this as "Fable5 is unnecessary" on one data point — but DO give routine drafting agents the gcc source path and expect them to use it.
§71 — Before mapping a giant, look for an already-matched SIBLING beside it (Phase 29 SESSION-18, func_8017D960, 3,338 ins)
Behemoth #2. Result: 3,334 of 3,338 instructions, 98.8% register-masked-identical, 88.3% byte-aligned, byte-exact prologue AND epilogue, exact 0x320 frame, the same 10 saved registers at the same offsets, and the identical ~110 stack slots — on a 3,338-instruction function, in one session. Not a match (G3), but an order of magnitude closer than behemoth #1.
THE LEVER, and it is embarrassingly cheap. func_8017D960 is the lit variant of
func_8017CA80 — the 952-instruction renderer immediately above it in the same source file
(ov_SC03_090_jr_8017CA80.c, the file is named after it), already matched. Diffing against that
sibling handed over ~90% of the C for free and produced a 3,334/3,338-instruction draft on the FIRST
compile. No mapping phase was needed to get there.
Do this first on every giant: check whether an already-matched function elsewhere is the same ROUTINE. It costs one
grepand can replace days of structural analysis.
HOW to run the check — fingerprint by CALLEE SET, not by h_seq or adjacency (corrected same
session). Two wrong versions were tried first:
- Adjacency found
func_8017CA80only because it happened to sit next door. Running it over the six remaining behemoths: 0 hits — their neighbours are all 2–128-ins helpers. h_seqfleet-wide also returned 0 hits, and is structurally incapable of finding these:h_seqis a mnemonic skeleton, so a 952-ins base renderer and its 1,511- and 3,338-ins variants never share one. Worse, it produces FALSE families —func_8017F510andfunc_8017F5B4share anh_seqyet call completely different functions (jal Xandjal Yboth normalise tojal).- The CALLEE SET works.
func_8017CA80(952, matched),func_8017D960(3,338) andfunc_8017F510(1,511) all call exactly{func_800491EC, func_80052E38, func_800547D8}and nothing else — three variants of one renderer, found in one grep:grep -oE 'jal[[:space:]]+[A-Za-z_]\w*' <fn>.s | awk '{print $2}' | sort -uA small, distinctive callee set is a far better routine-identity fingerprint than any hash we have, because it survives the size differences that variants are made of.
§69 IS PARTLY REFUTED — corrected here, do not follow its headline blindly.
| §69 claim | verdict on a non-dispatcher |
|---|---|
| Law 1 — write the whole body coarsely first; a partial draft gets the wrong callee-saved set, so no matching prefix exists | CONFIRMED and decisive. Doing this got the exact frame + saved-reg set on the first compile. |
Law 2 — match_one's global number is meaningless; measure region-aligned |
CONFIRMED and essential (its 1806 is noise; a 4-instruction length drift destroys positional comparison) — but the tool did not transfer. s18_regions_comparator.py is per-switch-case. |
| Headline — "the deliverable is the map, not a match" | REFUTED for this shape. §69 was derived from a 359-call dispatcher with no sibling. Here mapping was not the lever at all; the sibling was. |
Tooling supersession: .run/giants/b2_mask.py + b2_full.py are a shape-agnostic word-level
masked sequence aligner (structural number and byte number, no switch assumption). They replace
s18_regions_comparator.py — use them for any giant.
What it is (useful for the family): a 3-source volumetric-light mesh renderer. Same skeleton as
the matched sibling — 3-call prologue, Part[] outer loop (stride 0x14, 8-corner AABB rtpt/rtps
- screen-bbox reject),
Prim[]inner loop (stride 0xC,rtpt/nclip/stopz), OT insertion. The added work is three axis-aligned light boxes ({s32 enable; u16 cx,cy,cz; s32 range}, stride 0x1C): per vertex, a separable per-axis linear falloff over the outer 0x80 of the range, axes visited x, z, y — that ordering is load-bearing, summed and clamped to 0x80 into a grey gouraud colour. Lit →POLY_GT3/GT4; unlit →POLY_FT3/FT4withrgbc = tp[0] & 0xFF000000.
Two shape facts worth generalising: only 2 back edges in 3,338 instructions (it is two nested loops, not a maze); and the 8-byte stack stride that looks like an exotic aggregate is just gcc's spill-slot granularity — every non-array local is spilled. Do not invent a struct to explain it.
Residual (4 ins short, ~40 divergent): the only genuine structural error is gcc fold
reassociating the colour OR-chain; a rewrite fixes structure to 99.1% but cascades a live range and
drops byte-alignment to 76% (s18_func_8017D960_b2_rgbchain.c — an unresolved trade, do not re-buy
blindly). The rest is register naming. Family: func_8017CD9C (ov_SC03_102) and func_8017E778
(ov_SC03_091) are the same 3,338-instruction function with only the 3 light-descriptor symbols
changed — one crack templates ×3.
⚠️ A PROFILING ERROR OF MINE, recorded (R14/R35): I briefed this agent that the function had no
switch, from grepping sltiu jump-table bounds. Wrong — the switch is compiled as a comparison
tree (23 slti). A jump-table grep is not a switch detector. The agent caught it; a less careful
one would have inherited my false premise.
§72 — A register __asm__ pin is a PREFERENCE, not a reservation (Phase 29 SESSION-18, func_8017F510)
Behemoth #3, and the best behemoth result so far: 1,511 of 1,511 instructions (exact length),
frame 0x258 exact, byte-exact prologue AND epilogue, identical sp-slot set, 99.5%
register-masked structural / 93.3% byte-aligned, SYMS-OK, 97 divergent. Not a match (G3) — but ~93
of the 97 trace to a single register decision (below). §71's callee-set lever delivered: diffing the
two references gave ~90% of the C and a first compile of 1528/1511.
⚠️ THE CORRECTNESS FINDING — this qualifies §17
Pinning a local with register s32 amb __asm__("$30") produced a seductive 1,511 ins / 98.9% —
and was a MISCOMPILE. gcc-2.7.2 also allocated $s8 to an unrelated live value (hi_z): one
hard register holding two live values at once.
A local
register … __asm__("$N")declaration is a hint to the allocator, NOT a reservation. gcc-2.7.2 will still hand that hard register to another pseudo. Never ship a pin without inspecting the pinned register's defs in the output.
§17 (pins + a scheduling barrier) remains valid — it is how func_8012B8E4 and others were cracked —
but it now carries this obligation. Anything already BANKED is safe by construction (the
whole-binary byte-gate would have rejected a miscompile); the exposure is un-gated drafts.
ACTION: .run/giants/s18_func_8017D960_b2.c (behemoth #2, 3,334/3,338) carries FIVE pins
($25 $17 $19 $20 $21) and must be re-checked before anyone builds on it.
The honest fix was source-level and cheap
The +17-instruction drift was live-range stretching: reusing w/wz (which already carry
prim->w1/w2 and part->zz) for the vertex-word reads stretched two live ranges enough to spill
amb, costing 7 lw+nop pairs. Dedicated temps for the vertex loads (vw/vzw) →
1528→1511 ins, 88%→99.6% structural, no pin.
Generalisable: when a draft is long by a handful of
lw+noppairs, look for a REUSED local whose live range now spans a region it did not before. A fresh temp is cheaper than a pin and cannot miscompile.
Giv record order (the §70 family)
part->prim must be read BEFORE part->nprim. loop.c:combine_givs walks bl->giv in reverse
record order, so the last-recorded giv becomes the combined base: prim-then-nprim yields base
part+0xC (the target); the other order yields part+0x10.
What is left, and what is byte-recorded as SPENT
Residual 97 = 2 ins (box-build emission/allocation transposition — source order sets both, via
sched.c UID ties and global.c allocno ties; needs a third form) + 3×2 ins (unlit-tail OT-tag
scheduling) + ~93 ins of pure register naming off ONE seed: c3 (4th quad vertex colour) is
$a2 in the target and $a3 in the draft; tp is one pseudo across all four emit tails and takes
the other of the pair, renaming the whole block. Fix c3 → $a2 and ~93 should fall together.
Do not re-buy: decl-order permutations (gcc numbers pseudos by first use, not declaration) ·
block-scoping the emit temps (1509 ins) · splitting or inlining tp · reusing f0 as c3 · vertex
x/z/y orderings · decomp-permuter, 4,724 candidates at -j 10, base 97, ZERO improvement — a §3
hard tail outside the C-randomisation search space. The remaining move is to reason the c3
allocation out of global.c, not to spend more compute.
Tooling: .run/giants/b3_align.py (shape-agnostic structural+byte aligner) and b3_pos.py
(positional diff for equal-length drafts) supersede the b2_* set.
§73 — A def-side self-decl conflict has TWO axes: RETURN (fleet widen, T2) and PARAMS (casts at each use, T0). Diagnose which before paying for the expensive one (Phase 29 SESSION-19, func_8014F3E8 + func_8014D4C0)
§30 #2 established the return-type escape: when the byte-true def must return s32 but the fleet
canon declares extern void func_X(...) inside a DEFINE_func_* macro, widen the macro's extern
— byte-neutral wherever the caller discards the return. What the recipe did not say, because the
function it was derived from had a single scalar param, is that this is only one of two independent
axes of the same conflicting types for 'func_X' error.
The batch that separated them. Both functions were byte-correct, real-TU verified, and blocked on
the identical cc1 message. One fleet-wide sed -E 's/extern void (func_8014F3E8|func_8014D4C0)/extern s32 \1/g' over src/** (16 decls in engine_core.h + 5,079 across 3,459 overlay .c files) was
proven byte-neutral in isolation — then the gate banked func_8014F3E8 (32 ins) and still refused
func_8014D4C0 with the same error. The residue was the parameter half: canon
(s32, void *, void *) vs the draft's (s32, u16 *, u16 *).
| axis | symptom | fix | blast radius |
|---|---|---|---|
| return | def needs s32, canon says void; a void def DCEs the value-set (delay slot → nop, or a live local dies → frame shrinks) |
widen extern void→extern s32 in every spelling together (macro + each overlay's carried decl layer) |
T2 fleet-shared ⇒ R22 mandatory |
| params | def wants a narrower/typed pointer than the canon's void * (or vice-versa) |
keep the canonical param types in the signature and cast at each use — ((u16 *)a1)[1], *(u16 *)a1 |
T0 draft-only — no fleet edit at all |
The param fix is §17a-1 applied to the definition's own signature (the same move func_80174CB0
needed in SESSION-18, where the wall was the fn's own decl rather than a callee's). Casts are
compile-time; the emitted code is unchanged; nothing outside the draft is touched.
Practice:
- Read the cc1 error's two halves before acting.
conflicting typesdoes not tell you which axis; diff the canon decl against the byte-true signature term by term (return, then each param). - If only the params disagree, do NOT reach for the fleet widen. It costs an R22 cycle and still fails — exactly what happened here (the widen was independently justified by the other function).
- If both disagree, fix the params in the draft first — then the widen batch only has to carry the return axis, and the fleet edit stays as narrow as possible.
- Batch the return-axis widens (one R22 for N functions, never one per function, §63) and isolate the shared edit from the drafts: build 1–2 representative binaries after the widen ALONE before splicing anything. Here that step is what made the single failure instantly attributable to the param axis rather than to the widen — one cheap build replaced a diagnosis.
- Pick the isolation probe deliberately: an overlay that instantiates the return-casting macros
(
((s32 (*)(...))func_X)(...), §17a-1) is the only place a decl's return type could plausibly touch codegen.ov_SC01_000served that role forfunc_8014F3E8.
§74 — Auditing a pinned draft: the §72 hazard is CALLER-SAVED pins spanning a call, and only the disassembly can tell you (Phase 29 SESSION-19, func_8017D960 b2, 5 pins)
§72 established that a register __asm__("$N") pin is a preference, not a reservation — gcc will
happily put another value in $N while your variable is notionally live. The banked corpus is safe
from this by construction (the whole-binary byte-gate rejects any miscompile), but un-gated drafts
are not, and a behemoth draft can carry pins for days before it ever reaches a gate. This is the
cheap audit.
Two distinct failure modes, only one of which is real most of the time:
- CALLER-SAVED PIN SPANNING A CALL — the one that silently corrupts.
$25($t9) and the$t0–$t9range are call-clobbered. gcc-2.7.2 does not save/restore an explicit-register variable across a call, so if the pinned variable's live range crosses ajal, the value is destroyed with no diagnostic. Callee-saved pins ($16–$23=$s0–$s7) are immune — the prologue/epilogue save/restore covers them. - SCRATCH REUSE OF THE PINNED REGISTER — usually benign. gcc will use
$Nas a temporary for an unrelated value before the pinned variable's own value lands there. Observed here:lui s4,..; lw s4,0(s4); addiu s4,s4,-128—$20(pinnedr1lo) carried the raw global for two insns, andaddu t9,s4,zerocopied that value out to$25(pinnedr1) on the way. Self-consistent; nothing live was clobbered.
The audit (no recompile needed if a match_one object survives — .run/match/<fn>.<pid>/<fn>/t.o;
confirm it is the draft you think it is with cmp t.c <draft>):
mipsel-linux-gnu-objdump -d <t.o> > dis.txt
# (a) does any call exist after the first pin write?
grep -nE '\bjalr?\b' dis.txt # compare addresses against the pin-write addresses
# (b) how many times is each pinned reg WRITTEN? (first operand, excluding sw/branch/jal/mult-class)
Expect writes == (assignments in the C) + 1 epilogue lw for each callee-saved pin. Anything above
that is scratch reuse — read those sites before assuming they are benign.
Worked verdict (.run/giants/s18_func_8017D960_b2.c, pins $25 $17 $19 $20 $21): 3 jals total,
all at 0x2c–0x50, and the first pin write is at 0x58 — no call after the pins are
established, so the caller-saved $25 pin never spans one and mode (1) does not arise. Modes (2)
sightings on $20/$21/$17 are the benign scratch pattern above. Draft is safe to keep building
on. (The C corroborates: everything after the three prologue calls is a macro — gte_*, BOXTEST,
ATTEN, CLAMP80 — not a function call. Verify that with a token census, not by eye: a 636-line
behemoth hides a jal easily.)
Standing rule of thumb: prefer a callee-saved register for any pin whose variable outlives a
call; if the target's register really is caller-saved and the value really does span a jal, the pin
cannot express it — that is a genuine wall verdict, not a drafting slip.
§75 — A propagation cap is usually a MINORITY-SPELLING SOURCE OVERLAY, not a wall: census the carried extern before believing the exclusion message (Phase 29 SESSION-19, func_8014F3E8 ×4 → ×138)
dedup_propagate authors the shared macro from ONE source overlay and carries that overlay's
file-scope extern lines into the macro verbatim. Those externs are then instantiated in every
member TU. So whatever spelling the source overlay happens to use becomes the fleet's spelling —
and if the source is an outlier, the macro conflicts everywhere else and the group silently caps its
reach at the outlier's island. The exclusion message calls that byte-diverge / irreconcilable,
which is the wrong cause and reads like a wall.
The tell: the bytes cannot be diverging. Members are selected by h_exact — the SHA1 of raw
instruction bytes, relocs included. If a group has N members it is because all N are byte-identical.
So an exclusion is a compile conflict, never a byte one. Any message saying otherwise is lying
(cf. §68's mislabel — same tool, same defect family).
The census that names the real cause (30 seconds, read-only). Take the symbol the carried extern declares and count every spelling of it across the tree:
grep -rh '<sym>' src --include=*.c --include=*.h \
| grep -E 'extern|^\s*(void|s32|int|u32)\s+<sym>\s*\(' \
| sed -E 's/^\s+//; s/\s+/ /g; s/ \\$//' | sort | uniq -c | sort -rn
Worked case — func_8014F468, carried by the func_8014F3E8 body:
| spelling | count | |
|---|---|---|
extern s32 func_8014F468(void); |
1,710 | fleet canon |
s32 func_8014F468(void) (def) |
134 | fleet canon |
extern void func_8014F468(void); |
20 | the outlier |
void func_8014F468(void) (def) |
4 | the outlier — and all 4 are ov_SC07_{006,007,010,011} |
The source overlay I banked from was one of the four. The macro inherited extern void, and the 134
overlays that define the symbol s32 rejected it — propagation landed on exactly the 4-overlay
island. Nothing about the code was hard.
The fix is NORMALIZATION, not a reconciliation engine. Flip the minority spelling to the fleet
canon (here 24 occurrences: 4 definitions + 19 overlay externs + 1 line in the freshly-authored
macro), byte-gate the affected binaries, then extend the group. func_8014F468 is a pure inline-asm
$sp-switch trampoline, so the return type carries no C-level value flow — and 134 overlays already
proved s32 is byte-correct for the identical function. All 4 flipped binaries stayed
byte-identical.
Why a reconciliation engine is the WRONG shape here. One macro text is instantiated in 138 TUs;
it cannot carry a per-overlay extern. If the members genuinely disagree, no per-member rewrite of a
shared body can satisfy them all — you must make them agree first. (reconcile_tu.fix() already
does "TU wins + cast at use" for DATA decls and deliberately skips d.kind == 'func'; extending it
would not have helped, because the conflicting text lives in the shared header, not in a draft.)
Practice:
- Prefer a majority-spelling source overlay. Before propagating from overlay X, census X's decls of every symbol the body carries. A minority source caps the group by construction.
- Always pass
--recoveron a targeted--addrrun. Without it, the first culprit overlay triggers the historical all-or-nothing drop and the group banks ×0 instead of ×(N−1). This session lost a whole group to that flag before re-running. - After normalizing, the body is already a macro — so the follow-up is
dedup_extend(--binaries <the excluded set>), notdedup_propagate --addr, which can only author from an inline def and will report "no source overlay has it matched". - Re-check any group historically stuck at a small reach for the same cause —
func_80174CB0(×3 since SESSION-18) was carrying the identical class.
§75a — The exclusion classes, enumerated with named causes (Phase 29 SESSION-19, the 134-binary dedup_extend sweep)
After the §75 normalization, one dedup_extend --binaries <134> sweep banked 134/400 planned and
the two residual groups failed in every binary — with harvest_verify's classifier naming a
different cause for each. That enumeration is the useful artifact: "propagation-capped" is not one
class, it is at least three, and only the first is cheap.
| class | cc1/ld says | example | remedy | cost |
|---|---|---|---|---|
| A — minority spelling (§75) | conflicting types for <callee> where one spelling dominates the census |
func_8014F468: 1,710 s32 vs 4 void defs |
normalize the minority, byte-gate, dedup_extend |
cheap, byte-neutral |
| B — genuine arity/type split | same message, but the census shows two real populations | func_8012F14C: 1,944 (s32) vs 968 (s32,s32,s32) |
NOT a typo — two live call conventions. The §29 loose-typing wall. A K&R () in the macro may be compatible with both (compatible(): () first + prototype second is ACCEPTED when no param default-promotes, and s32 does not) — but it is order-dependent, so it only works if the macro's decl precedes the TU's. Measure before moving. |
unknown — probe first |
| C — missing carried extern | undefined reference to '<sym>' (a LINK error, not a type error) |
func_80165CA0: undefined reference to 'SHB' |
the SESSION-18 CARRY-FIXABLE class — the body references a file-scope decl the extraction did not carry | cheap once carried |
The discriminator is one grep, and it decides the whole remedy: census every spelling of the symbol cc1 named. A lopsided count (≥95/5) is class A — normalize. Two substantial populations is class B — do not normalize on a guess; an arity change is not byte-neutral by inspection (§29's narrow-param wall is exactly this). A link error is class C and has nothing to do with types.
Do not generalize from one member's error. The same blocked function reported different callees
in different overlays (func_80012ABC at ov_SC01_000, func_8012F14C at ov_SC01_001) — so one
sample names one blocker, not the blocker set. Collect the classifier's line across the whole sweep
before scoping the fix.
§75b — A body's preamble can carry #defines, not just externs; extraction lifts only the externs (Phase 29 SESSION-19, func_80165CA0 ×3 → fleet)
dedup_propagate's extract_unit walks BACKWARD from a definition collecting contiguous
extern …; lines into the macro. It does not collect #defines — so a body matched with a
file-scope macro in its preamble loses that macro when it is lifted into engine_core.h, and the
shared body then compiles only where the source overlay's #define happens to be in scope above
the splice point. This is the dedup_propagate counterpart of the family_remap._carry_macros
gap Phase 27 closed.
The signature of this class is a LINK error, not a type error. undefined reference to 'SHB' —
because an unexpanded SHB(x) is parsed as a call to an undeclared function (implicit int), which
compiles cleanly and dies at link. Anything reporting conflicting types is a different class
(§75/§75a). One grep tells them apart: is the name a #define or a symbol?
grep -rh '\b<name>\b' src --include=*.c --include=*.h | sort | uniq -c | sort -rn | head
If the top hit is #define <name>(x) …, it is this class.
Worked case. func_80165CA0 sat at ×3 for a phase. Its preamble in the source overlay is:
extern s32 D_8011D030;
extern s32 D_80126728;
#define SHB(x) __asm__ __volatile__("" : "=r"(x) : "0"(x)) /* <- NOT carried */
DEFINE_func_80165CA0()
The two externs were lifted; the #define between them was not. The other 132 overlays do define
SHB — about 300 lines further down (ov_SC01_001: stub @4462, #define @4781), i.e. below the
splice point. Pure ordering. And it explains the membership exactly: the 3 members are precisely
the 3 files carrying the __volatile__ spelling of SHB — that define is the function's own
preamble, still sitting above its instantiation.
Fix — the shared header OWNS the macro, under a distinct name:
#ifndef ENGINE_SHB
#define ENGINE_SHB(x) __asm__ __volatile__("" : "=r"(x) : "0"(x))
#endif
and rewrite the body's uses. Do not reuse the original name: the overlays define SHB
themselves in two different spellings (volatile in 3 files, non-volatile in 132), and a shared
#define SHB with a different replacement list is a hard redefinition error. A distinct name cannot
collide with either. Pick the spelling the currently-banked members actually compile with (here
volatile), not the one a stale body comment claims — then let the byte-gate confirm on those members
before extending.
Generalises: any preamble construct that is not an extern — #define, a file-scope typedef,
a static helper — is silently dropped by extraction and will cap the body's reach at whatever
subset happens to supply it. When a group's reach is stuck at a suspiciously small number, diff the
source overlay's preamble against what the macro actually carries.
§75c — Class-B's remedy is the FULL §17a-1 PAIR (decl and call-site cast); a decl-only fix moves the error and looks like a new wall (Phase 29 SESSION-19, func_8012F14C)
§75a's class B is a genuine arity split — the same callee declared with two different arities across
the fleet (func_8012F14C: 1,944 (s32) vs 968 (s32,s32,s32)). The obvious move is to make the
shared macro's carried decl compatible with both, using the no-prototype () form that
cdecl.compatible() measures as accepted in either order when no parameter default-promotes. That
is half a fix, and the half that does not work.
What actually happens. () really does dissolve the declaration conflict — the byte-gate's
classifier moved the failure from PLUMBING to CC1-FAIL, which is the tell that the first wall fell
and a second appeared. Reading real cc1 stderr (hand-splice the macro into one member, build that
object; do not trust a bare make … Error 33):
ov_SC01_001_jr_801734BC.c:2616: too many arguments to function `func_8012F14C'
C's composite-type rule: after void f(s32); then void f();, the composite is still void f(s32) — the earlier prototype wins. So a 3-argument call is a hard error no declaration spelling
can rescue.
The remedy is the pair §17a-1 always specified (and what cast_call_sites.py implements):
extern void func_8012F14C(); /* conflict-free in either order */
((void (*)(s32, s32, s32))func_8012F14C)((s32)&mtx, (s32)&vec, (s32)&out); /* call bypasses the prototype */
gcc-2.7.2 folds a cast of a known function symbol back to a direct jal, so the body's bytes are
unchanged (byte-gated on all 3 existing members: 7ca772be / b3b95547 / 9885af74).
Rule: for class B, change the decl and the call together. A decl-only change is not a smaller version of the fix — it relocates the diagnostic, and a session reading only the failure class will record a fresh wall where there is none.
§76 — The allocno CLASS (local vs global) is the dominant regalloc lever, and C reaches it ONLY through declaration scope and variable reuse (Phase 29 SESSION-19, behemoth #3 func_8017F510 1,511 ins, 97 → MATCH, pin-free)
Behemoth #3 sat at 97/1511 mismatched after a full pass at a lower reasoning tier that had already localized every mismatch, exhausted ~10 C-shape levers, and run 3,663 permuter candidates at base 97 with zero improvement. The residual was not in the C-randomisation search space — and the reason is the reusable lesson: four of the five decisions were local-vs-global allocno CLASS choices, which C expresses only through declaration scope and variable reuse. Statement order, expression shape, register pins and random search cannot reach them.
The mechanism, with citations
local-alloc.c:472admits a pseudo to LOCAL allocation only whenREG_BASIC_BLOCK >= 0 && REG_N_DEATHS == 1. A function-scope variable used in four emit arms has 4 deaths ⇒ it becomes a global allocno ⇒combine_regs(local-alloc.c:1825) can no longer tie its producer chain into it (global-alloc has no coalescing), so an in-placesra/sll/adduchain on that register is unreachable. Declaring the same variable inside each arm makes it four 1-death local pseudos and the chain ties.global.c:668-671re-marks every pseudo that local-alloc already placed (reg_renumber[i] >= 0) as a hard register for global-alloc's conflict scan. So a local pseudo's placement removes a register from the global pool, and that shows up as a hard-reg number in the;; N conflicts:tail of the.gregdump. In this function a globalotpin$a2pushed a per-tail constant into$a0, which made the target'sotp = $a0structurally impossible — not merely lower-priority. Read the;; N conflicts:hard-reg tail before reasoning about priorities.global.c:594 allocno_compareranks byfloor_log2(n_refs)*n_refs/live_length, with refs counted × loop depth. Reusing an existing variable as a second temp (§45-A / RC-14 MERGE) raises its ref count — herecb27 → 39 refs — and flips the grant ORDER of a 3-way colouring (cb→$a1, c3→$a2, tp→$a3instead oftp→$a2, cb→$a1, c3→$a3). Recomputing that formula off the.lregdump reproduces the;; N regs to allocate:order exactly, so you can PREDICT a flip instead of searching for one.
The attribution primitive (use this before calling anything a scheduling residual)
Compile the draft with -fno-schedule-insns and again with -fno-schedule-insns2. If a
transposed instruction pair keeps source order under both, the ordering was fixed at RTL
expansion, not by either scheduler — so the lever is source statement order, and no amount of
sched.c reasoning applies. That test converted this function's residual A from "needs a third form of
a scheduling tie-break" into a one-line source swap.
Two diagnosis traps this function proved
- A "scheduling" diff can be a register grant in disguise. Residual B (an OT tag materialised
before a two-insn constant) was never a sched.c choice: once the unlit colour temp was granted
$a1, the0xFFFFFFconstant — also wanting$a1— could not be materialised until that temp died, so sched2 slid it below the tag by itself. Fixing the allocation fixed the "schedule". - A "single seed cascade" model can be wrong even when the cascade is real. The prior dossier
attributed ~93 of the 97 to one seed (
c3must win$a2).c3turned out to have no lever of its own — it moves only as a side-effect ofcbout-rankingtp. The cascade was real; the seed was not the thing you can steer. Steer the ranked variable, not the symptom variable.
Practice
- When a residual survives a full permuter sweep at its measured base, stop searching and ask which allocno CLASS decisions the C is failing to express. Search cannot reach a class choice.
- Sweep declaration scope at three granularities — function / innermost-
if/ per-ARM — for every temp in the divergent block. Per-arm is a distinct granularity from per-case and is frequently the one that matters (here: per-case = neutral, per-arm = the crack). - Then sweep variable REUSE (merge two temps into one) to move
allocno_comparepriority. - Keep the measured result of every lever; behemoth #3's session produced a ~50-row do-not-re-buy
table (
.run/giants/s19_f510_report.md) that is worth more than the match itself.
§77 — Every extraction tool carries a NARROW hard-coded preamble set; anything outside it silently caps the body's reach. Diff the preamble before you gate. (Phase 29 SESSION-19 — three variants in one session, two different tools)
§75b found that dedup_propagate's extract_unit lifts extern lines but not file-scope
#defines, and predicted the generalisation: "any preamble construct that is not an extern —
#define, a file-scope typedef, a static helper — is silently dropped." That prediction was
confirmed three more times the same day, in a second tool, while templating behemoth #3's crack
onto its sibling with family_remap:
| # | construct dropped | tool | how it surfaced |
|---|---|---|---|
| 1 | file-scope #define (SHB) |
dedup_propagate |
undefined reference to 'SHB' — a LINK error (§75b) |
| 2 | multi-line typedef struct {…} T; |
family_remap |
'PolyGT4' undeclared + a cascade of parse error before ')' |
| 3 | file-scope extern block sitting above a #define block |
family_remap |
'D_801B79E8' undeclared |
| 4 | the exemplar's own #include lines |
family_remap |
'PolyFT4' undeclared (it lives in engine_types.h) |
| 5 | a static inline helper |
family_remap |
LENGTH-DRIFT/-56 — no compile error at all, just a short body (§82's inlined-helper case, func_8017C730 @ ov_SC03_013) |
Variant 5 is the nastiest, and this section PREDICTED it before measuring it (the closing line
below named static helpers). It is the only variant that produces no diagnostic whatsoever — the
draft compiles clean and is simply ~56 instructions short, which reads as a codegen residual rather
than a missing construct. If a mechanically-remapped sibling shows a NEGATIVE length drift and no
compile error, look for an uncarried static/inline helper before touching a single lever.
⚠️ COROLLARY (measured, and it cost a bank): carry the MINIMAL CLOSURE, not the whole file
Fixing variant 5 by carrying the exemplar's entire region file as preamble produced a clean
standalone match_one MATCH — and then failed the whole-binary gate on PLUMBING, because
2,993 lines of unrelated declarations collide wholesale in the target TU. Over-carrying does not
"include a bit extra"; it trades a match_one failure for an in-TU collision.
The correct carry is the minimal transitive closure of what the body actually references: here the
bandsetup helper + its 5 externs + the 18 gte_* macros of that region file (helper + externs alone
still left -34). Measured ladder on one function: -56 (nothing carried) → -34 (helper + externs)
→ MATCH-but-uncommittable (whole file) → the minimal set is the only bankable point.
CLOSED AND BANKED (Phase 29 SESSION-20). The final rung is no longer a prediction: the minimal
closure — 18 gte_* macros + the 5 externs + the static inline helper, 519 lines instead of
2,993 — gave match_one MATCH (1061 ins) and went through the §81 carve chain and the
whole-binary gate first try (harvest_verify verified 1 / failed 0, R22 clean-fleet 140/140).
Full ladder, all four rungs measured on func_8017C730 @ ov_SC03_013:
-56 → -34 → MATCH-but-uncommittable → MATCH + BANKED. Cost: minutes, no drafting, no agent.
The CANDIDATE gate and the REAL gate need DIFFERENT preambles — keep the difference out of the bank
match_one compiles the draft standalone (cpp -Iinclude, no engine_core.h), so a body using
shared types (PolyFT4, SVECTOR2, MATRIX2) hits 'PolyFT4' undeclared + a cascade of
parse error before ')' — which looks exactly like a broken draft and is not one. The real TU has
those types in scope for free: every region file opens #include "../shared/engine_core.h", which
includes the guarded engine_types.h.
So the types header belongs in a throwaway PROBE COPY, never in the draft you bank:
{ echo '#include "common.h"'; echo '#include "../src/shared/engine_types.h"'; \
cat .run/drafts-X/<fn>.c; } > $SCRATCH/probe.c # ../src/… resolves via -Iinclude
python3 tools/match_one.py <fn> --c $SCRATCH/probe.c --asm-subdir asm/<ov>/nonmatchings/<subseg>
Why this matters beyond convenience: the shortcut of pasting the include into the draft itself has
already leaked an absolute path (#include "/home/musashi/bfm-decomp/src/shared/engine_types.h")
into 21 git-tracked source files / 23 lines. Every one is a guarded no-op semantically (all 21
have engine_core.h at line 2, verified), so it is byte-neutral — but cpp still has to FIND that
literal path, so those 21 TUs cannot preprocess on any clone not at /home/musashi/bfm-decomp.
No byte-gate can ever see this (the path exists on the machine that made it) — R34's null-oracle
shape again, this time aimed at portability rather than coverage.
Also: the walk-back-to-previous-} heuristic breaks on an ISOLATED REGION FILE (a _jr_<addr>.c
produced by jr_isolate_all), where the construct immediately above the function is the helper you
need — the walk stops right after it and returns a 1-line preamble. Any preamble-carry tool needs a
reference-closure rule, not a positional one.
Why #2 and #3 happen, precisely. family_remap's backward preamble walk accepts a line only if it
starts with extern / // / /* / * / typedef. A multi-line typedef ends with } PolyGT4;,
which starts with none of those, so the walk halts there — and everything above it (including a
perfectly ordinary extern block) is lost. The tool documents the typedef half of this
("Multi-line typedefs aren't carried — those functions route through the engine_types.h lift") but
the consequence is broader than the note implies: one unrecognised line truncates the whole
preamble, dropping constructs the walk would have accepted.
The recipe (cheap, and it converges in 2–3 rounds). Do NOT reason about what the tool should have carried — compile and let cc1 enumerate it:
match_onethe generated sibling. cc1 names the first missing symbol/type.- Find it in the exemplar; carry that construct across, applying the tool's own printed substitution map to any per-overlay names inside it.
- Repeat. Each round clears one construct class, and the error text tells you which.
Behemoth #3's sibling func_8017F5B4 (1,511 ins) went CC1 FAIL → CC1 FAIL → CC1 FAIL → **MATCH**
across four rounds of exactly this, for ~0 agent tokens — the remap itself was correct from the
first invocation (52 per-overlay symbols substituted); only the preamble was short.
Rule: after any mechanical template/propagate step, diff the exemplar's full file-scope preamble
against what the tool emitted before concluding anything about the body. A CC1 FAIL on a
mechanically-remapped sibling is a preamble report until proven otherwise — it says nothing about
whether the remap was right.
§78 — A LENGTH drift can be a register grant in disguise; and fold never leaves a literal first in an | chain (Phase 29 SESSION-19, behemoth #2 func_8017D960 3,338 ins, 1806 → 0, pin-free)
Behemoth #2 was carried at "3,334/3,338, one fold OR-chain error left" — a summary that reads as four
instructions from done. Measured, it was 1,806 mismatched with a LENGTH-DRIFT/-4, and the
prior diagnosis was wrong in both halves.
The drift was an allocation decision, not missing code
The 4 missing instructions were 4 emit tails × 1 nop. The target's 0xFFFFFF OT mask lives in
$a1 — the same register as tp — so it cannot be materialised until tp's last load retires, and
the first tp[] load-delay slot therefore stays a real nop. In the draft the mask sat in $a0,
free early, so maspsx hoisted it into that slot and the nop vanished. Root cause one level up:
u32 *otp; at function scope has 4 deaths ⇒ fails local-alloc.c:472 ⇒ global allocno in $a2
⇒ via global.c:668-671 that pushes tp off $a1. Declaring otp per emit arm fixed the entire
drift in one edit (3334→3338, 1806→333).
This is the second time in one session that a residual which LOOKS structural was an allocno-class
choice (the first: a "scheduling" transposition on the sibling that fell out of a register grant,
§76). Practice: before treating a length or ordering diff as structural, check whether a register
grant explains it. A nop that exists in the target and not in your draft is very often a delay
slot the target could not fill because the register it wanted was still live.
fold never leaves a literal in the first term of an | chain
Seven parenthesisations of cb | c | (c<<8) | 0x800000 were measured; all reassociate — gcc-2.7.2's
fold-const.c will not emit or acc, K, var with the constant first. So if the target's asm shows
or acc, var, K as the FIRST term of a chain, K was a VARIABLE in the original source, not a
literal. That is a direct read from asm back to source shape, and it retires an entire family of
"try another parenthesisation" sweeps.
"Make it a variable" has TWO separable effects — and the wrong choice is catastrophic
Turning a literal into a variable changes (a) opacity to fold (fixes structure) and (b) adds an
allocno (changes registers). They are independent, and you usually want only (a):
- a fresh short-lived local gets the structure right and the allocation catastrophically wrong — measured 690 mismatched / 79.6%, with damage appearing ~300 instructions away in unrelated blocks;
- reusing an already-busy or function-scope variable gets both right. When you need opacity, spend an existing variable's ref count, not a new allocno.
The economics
Nine levers, each proven individually necessary by drop-one ablation against the matching draft:
otp per arm · no pins · dedicated vw/vzw vertex-word temps · an RC-15 zero-byte mny dial in
the tri cull block only · base = D_800AF630 first · f2,f1,f0 = 0 order · shared cb colour base ·
shared rgbw result temp · s32 za, zb; per case.
Five of the nine were read straight off the MATCHED relatives (func_8017F510 1,511 and
func_8017CA80 952, same renderer family) — worth more than every expression sweep combined.
Crack the smaller family member first; it is a lever library for the larger one.
§79 — For a 0-callee giant, fingerprint by DATA symbols (§71 cannot fire); and the STACK-SLOT ORDER is a declaration-order oracle (Phase 29 SESSION-19, func_8017BF14 4,763 ins, cold start → 45/4763)
A cold-start attempt on the project's second-largest function reached 4763/4763 ins, 45 mismatched (99.06% byte-identical, 99.94% structural, exact frame, exact opcode histogram) — not a match, but it produced two levers and refuted the premise it was given.
§71 has a blind spot, and this is it
The target was briefed as "no matched relative — a genuine cold start": h_norm/h_seq family
size 1, and §71's callee-set fingerprint returned jaccard 0.00 against every matched giant. That
premise was wrong. §71 fingerprints by callee set — and this function makes zero jal calls,
so the fingerprint is empty and cannot fire by construction. Grepping the target's data symbol
D_800A5E60 landed immediately on the matched func_8017BEBC: it is the 4-light-box member of the
same volumetric-light renderer family whose 3-box sibling (func_8017D960, 3,338 ins) was matched
hours earlier.
Rule: when §71 returns an empty or zero-overlap callee set, fall back to DATA-symbol fingerprinting
(lui %hi(D_xxxxxxxx) operands in the target .s). A leaf giant has no callees to fingerprint by, but
it still touches the same globals as its family. An empty fingerprint is a "cannot answer", not a
"no relative" — do not let it become a cold-start brief.
NEW LEVER — the frame layout reads back the original declaration order
gcc-2.7.2 assigns stack slots to spilled pseudos in pseudo-number order, and pseudo numbers are
issued in order of first use ≈ declaration order. Therefore the target's frame layout is a direct
readout of its source's declaration order. Compare your draft's slot assignments against the
target's and reorder declarations until they agree — moving a single line (f0..f3 after pkt) took
73% → 84% structural and brought all 127 slots into exact correspondence. Automatable; the
session's implementation is .run/giants/bf14_slots.py.
This is the counterpart to §78's "read the asm back to source shape": there, an |-chain's first term
tells you a literal was a variable; here, the frame map tells you the declaration order.
§76 confirmed at scale, and a pin nuance
- The entire −62 length residual was ONE allocno-class decision: declaring
s32 c0..c3inside the cull blocks (1 death ⇒ local allocno ⇒global.c:668-671removes those hard regs from the global pool) spilledr1loand moved the draft 52% → 93%. An__asm__ref-dial reached the same spill and scored worse — declaration scope beat the ref dial, again. - Pins are safe on a 0-
jalfunction — §74's caller-saved-across-a-call hazard cannot arise, so the usual suspicion is unwarranted here; 4 pins took 94% → 99%. But §72 still held: pins 5 and 6 made it worse. Pins remain a preference, and past a small number they fight the allocator.
The residual, and the honest read
45 mismatches, three register-grant ties, zero structural divergence. The one §76 lever class the
session never reached is variable REUSE across c0..c3 / a0v..a3v — that is the named next move.
Artifacts: .run/giants/s19_func_8017BF14_b1.c (45/4763), a pin-free fallback at 789/4763 that is
100% structural, and s19_bf14_report.md (~40-row do-not-re-buy table + 4 refuted diagnoses).
Cold-start economics, measured: a 4,763-instruction leaf giant with a findable matched relative reached 99.06% in one pass but did not close. Budget a second pass for anything this size; the first pass buys the decode, the frame, and the length — the last ~1% is register grants.
§80 — A do-not-re-buy entry is scoped to its BASE, not to the function; and the pin's hidden cost is an unconditional qty_phys_sugg (Phase 29 SESSION-19, func_8017BF14 45 → 0)
Round 2 closed the 4,763-instruction behemoth (45 → 37 → 33 → 21 → 11 → 3 → 2 → 0, reproduced 3×
from independent work dirs, banked whole-binary BYTE-IDENTICAL). The route matters more than the win.
⚠️ THE PROCESS CORRECTION: a measured negative is relative to the draft it was measured on
Round 1 left a careful ~40-row do-not-re-buy table. Three of its entries INVERTED on round 2's base.
The same edit (qsingle23) measured 1,040 mismatched on the 45-base and 11 on the 21-base.
Re-testing the round-1 negative list cost ~20 seconds and produced three of the seven winning
levers.
So: a do-not-re-buy table is a record of (edit, base) → result, NOT edit → useless. After any
lever that moves the base materially, re-run the negative list — it is seconds with a real harness
and it is where the next levers hide. This retroactively qualifies every such table in this cookbook
(§45, §60b, §75a, §76, §78, §79 and round 1 of this function): treat them as starting hypotheses at
the base where they were taken, not as closed questions.
Corollary already seen: round 1 measured "removing the va→$t2 pin costs 4% elsewhere" and concluded
keep the pin. On a base where c0..c3 sit at function scope, removing those pins is worth 21→13
— the opposite conclusion from the same experiment.
The pin's hidden cost, with the citation
combine_regs' hard-register branch (local-alloc.c:1795, reached from :1295 with
already_dead == 0) records the pinned register in qty_phys_sugg unconditionally — there is no
death guard. So a register __asm__ pin does not merely prefer a register: it actively invites
local-alloc to tie producer chains into it, which is exactly the residual-(a) tie round 1 diagnosed
but mis-cured. Three separable cures exist; the new one is worth knowing:
- R7 — a zero-byte
__asm__ref that keeps the pinned value LIVE PAST the temp, sofind_free_regcannot honour the suggestion. That closed the final 2 instructions, and was necessary becausec1→$a0proved uniquely load-bearing (it is what spillsr1lo; every alternative pin lost 64 instructions).
The flagged "#1 move" LOST — and why the failure is informative
Variable REUSE (§45-A / RC-14 MERGE) was swept in full: every merge lost, 43–3294 across 8 merges.
It was the right lever class for the sibling func_8017F510 (97 → 10) and the wrong one here, for a
structural reason worth carrying: the TRI and QUAD grants did not differ by RANK, they differed by
IDENTITY — two independent allocno sets. Re-ranking inside one set cannot fix a two-set problem.
Diagnose whether you have a ranking problem or an identity problem before reaching for a merge.
The actual fix was s32 c0,c1,c2,c3; at function scope (33 → 21), read off the two matched
relatives (b5:310, b4:338) and confirmed against the target itself: its TRI grants are identical
to its QUAD grants.
§78's attribution primitive, run and reproduced
Under -fno-schedule-insns, -fno-schedule-insns2, and both, the draft's order was unchanged ⇒
the rgb-accumulator transposition was never a sched.c decision. A 3-statement accumulator pins the
value to one register, so no scheduler could hoist the or above the sw. Changing the grant fixed
the order for free — §78 reproduced on a second function.
Cold-start economics, now complete
A 4,763-instruction leaf giant with a findable matched relative: round 1 = decode + exact length + exact frame + 99.06%; round 2 = the last 45. Two passes, and the second was far cheaper than the first. Budget two passes at this size and do not read a 99% round-1 result as a stall.
§81 — Banking a jr (jump-table) function: the 3-step carve chain, and why match_one cannot see the problem (Phase 29 SESSION-19, func_8017C954)
match_one masks jal/HI16/LO16 relocations. A jr (switch) function therefore reports a clean
MATCH while the whole-binary gate reports DIFF — and the gate is right. Matching the C makes
gcc emit that function's jump table into .rodata (floated to the FRONT by section_order) while the
raw copy still sits in the overlay's data tail ⇒ duplicate table at the wrong address. This is the
§53 carve law, and it is the one case all session where the candidate gate and the real gate disagreed
about something that was not plumbing.
Detect it before you spend a gate cycle: grep -cE 'jr\s+\$(v0|v1|a0|t[0-9])' the target .s
(a mid-function jr on a non-$ra register), and look for a jtbl_<addr> in asm/<ov>/data/*.s.
The chain (each step byte-gated on its own, BEFORE building the next on top):
1. tools/jr_isolate_all.py <ov> --only <func> # cut the fn into its own code subseg
make extract BINARY=<ov> && make build BINARY=<ov> -> must be BYTE-IDENTICAL
2. tools/jtbl_carve.py <ov> --func <func> # carve its jtbl into a dotted .rodata subseg
make extract BINARY=<ov> && make build BINARY=<ov> -> must be BYTE-IDENTICAL
3. tools/harvest_verify.py --binary <ov> --drafts <dir> --chunk 1 # now the bank
then a FULL R22 (config changed => T2).
Use --only. jr_isolate_all <ov> bare would have resegmented 47 jr-functions across 21
objects; --only func_8017C954 touched 2 functions in 1 object. Same result, a fraction of the
blast radius. (The tool automatically pulls in already-banked jr in the same object so their existing
carves get repointed — that is why --only is safe rather than partial.)
Step 2 fails loud when the subseg would host NON-CONTIGUOUS .rodata carves — one object can
contribute at most ONE contiguous .rodata run, so two matched jr-functions in the same code subseg
with a third, unmatched jtbl between them is unsatisfiable. That refusal is the instruction to run
step 1.
The defect this exposed: a shared type that is present but invisible
Step 1 refused with "2 file-scope decls matched _HOIST_RE but could not be placed" for
extern struct PW8017E6D8 D_801E1EC4; — while struct PW8017E6D8 sits in engine_types.h:658.
_engine_types() harvested shared type names with four patterns (typedef … X;, } X;,
forward-decl struct X;, fn-ptr typedef) and a tagged definition with a body —
struct PW8017E6D8 { int w; } __attribute__((packed)); — matches none of them. Measured: 77
such tags in engine_types.h were invisible to the check. One added pattern
(^\s*(?:struct|union|enum)\s+(\w+)\s*\{) fixed it.
Why this cost twenty minutes instead of a mystery byte-diff three phases later: the Phase-26 audit
had already converted this predicate's silent drop into a loud refusal. The original bug dropped
4,040 col-0 decls, 683 of them function prototypes — and a dropped prototype is a silent
byte-changer (C89 implicit int f(), and return type drives delay-slot fill in this codebase). The
refusal named the exact symbols and the exact remedy. A loud "I cannot place this" is worth far more
than a green build.
§82 — Two source-shape oracles from behemoth #6: a duplicated addiu $aN,$sp,K across a jal means the block was INLINED, and scalar-vs-aggregate decides WHEN a stack slot is allocated (Phase 29 SESSION-19, func_8017C730 1,061 ins)
Two findings that read the ORIGINAL SOURCE SHAPE off the asm — the same class as §78's |-chain rule
and §79's frame-slot oracle, and the pair that actually cracked this function.
1. The inlined-helper signature
&X for any non-first local always creates a pseudo, and CSE always merges two of them
(expr.c:6260, ADDR_EXPR → force_operand(..., NULL); the one exception is the
virtual-stack-vars offset-0 local). So if the target re-materialises the SAME addiu $aN,$sp,K
at two call sites separated by a jal, CSE was prevented from merging them — which means those two
sites were not in the same function body. The block was an INLINED function.
17 non-inline spellings failed to reproduce the prologue; a static inline helper reproduced it
byte-for-byte on the first try.
The reusable probe that produced the hypothesis: scan the built objects for that duplicated-addiu
signature in functions that are NOT INCLUDE_ASM (i.e. already-matched code known to come from real
source) — ~1,200 objects, cheap, and it tells you which shapes the original codebase actually used.
2. Scalar vs aggregate decides when the slot is allocated
A scalar's stack slot is allocated LAZILY, at its first &; an aggregate's is allocated AT ITS
DECLARATION. So six GTE result words must be six separate longs, not a struct — only then do
they land after an inlined helper's temps (here 0x118..0x12F) and the frame comes out at the
target's 0x270. Declaring the same six as a struct puts the slot in the wrong place and no amount
of reordering recovers it.
Second-order effect worth knowing: this also flips MEM_IN_STRUCT_P (§30's /s flag). With one
of those words a fixed-address scalar, ((PolyF3*)pkt)->rgbc no longer aliases it — so a store had
to be respelled *(u32 *)(pkt + 4) to keep the target's nop. A scalar-vs-struct choice is
simultaneously a frame-layout decision and an aliasing decision.
Also reproduced on this function
§78 (reuse an already-busy variable — t32 = mid matched where a fresh temp did not) · §80(i) (a
lever went from −8 ins to exactly neutral as the base moved) · §72 (a register pin made it worse).
And the banking footnote (§75a class A, one line)
The whole-binary gate rejected the first bank with conflicting types for 'ApplyMatrixSV': the draft
declared it (MATRIX2 *, SVECTOR2 *, SVECTOR2 *), the TU and the fleet canon use (void *, void *, void *) — 2,286 of 2,835 sites. Conforming the draft's decl to the canon is byte-neutral
(pointer args pass identically) and banked first try. On a jr function, expect BOTH gates to have
something to say: the carve chain answers the jump table, and §75a answers the declarations.
§83 — The parameterised-repeat law, the spill-area trap, and why a per-case edit cannot move a per-case symptom (Phase 29 SESSION-20, func_80183814 5,122 ins, cold-ish start → 36 structural / 99.3%)
The largest unmatched function in the project, a 21-case state machine. Round 1 reached 5,127 ins vs 5,122 target, 36 structurally-unmatched instructions under a register-blind mask (99.3% exact), 17 of 21 case bodies EXACT, args+locals byte-exact at 216 bytes — not a match, and the residual is one decision, not 36 problems.
§83a — READ THE HEADLINE NUMBER CORRECTLY: a LENGTH drift makes match_one's count meaningless
match_one compares strictly index-wise. A +5 instruction delta shifts every later index, so the
tool reported 4,622 mismatched where difflib-aligned comparison shows 36. Those two numbers
describe the same draft. When the class is LENGTH-DRIFT, the mismatch count is not a progress
signal — align first (difflib) and re-read, or you will abandon a 99.3% draft as a 10% one.
(Counterpart to §78: there, a length drift was a register grant in disguise; here it inflates the
score. Both say the same thing — never read a length-drifted diff literally.)
§83b — THE LEVER: find the parameterised REPEAT before decoding case-by-case
Three callees (func_8012EC04/func_8012F14C/func_8012C51C) each appear exactly 35 times as
one 72-instruction body parameterised only by (KIND, START, BOUND) — with the invariants
member == KIND*4, array == &D_801F61C0[START], and prev.BOUND == next.START. That is 2,625 of
5,122 instructions (51%) from a SINGLE definition. Writing it once as a template and generating the
35 sites (harness: .run/giants/s20_g14_*.py — edit the template in one place, re-propagate to all
35) is the whole game on a function this size.
Practice: on any large state machine, hunt for a parameterised repeated block BEFORE decoding case
bodies one at a time. The tell is a repeated call triplet at a fixed stride with monotone constants.
(A prior handoff asserted this repeat existed; it was carried forward flagged UNVERIFIED because it
appeared nowhere in the recon. It proved TRUE — but flagging it cost nothing and the discipline stands:
an unverified premise is a hypothesis to test first, not a foundation to build on.)
Three sub-levers made the template byte-exact:
- A running POINTER walk, not array indexing — under
-G0array indexing does not strength-reduce. rand() % (u32)xto forcedivu(the signed spelling emitsdiv+ the sign-fixup dance).(s32)((u8 *)p + (X + 0xC))— the cast barrier stopscombinereassociating the offset.
§83c — TRAP: a "dead local" in a prior draft may be gcc's OWN spill area
The inherited recon modelled a 128-byte s32 pad[32] local and called it dead. It is not a local at
all — it is gcc's spill area. Declaring it explicitly adds 132 bytes and corrupts the layout;
removing it made the 216-byte args+locals area byte-exact. If a draft needs an unexplained dead
block to reach the target frame size, suspect the spill area before inventing a variable. (The frame
being 8 bytes over here is a different cause — two extra callee-saved registers, §83d.)
§83d — CSE's quantity budget is WHOLE-FUNCTION, so a local rewrite cannot fix a local symptom
The stall: in cases 0 and 3, gcc CSEs three &D_8018E27C-class address constants (+0x30) across two
call groups that share a basic block, consuming 2 callee-saved registers the target spends on real
variables — so a0 lands in $s7 instead of $s2 and every register downstream renames.
Why no rewrite of case 0 moved it: cse.c:8340 sizes the quantity table as
max(nsets*2, 500) + max_reg — gated by the whole-function pseudo count. A per-case edit does not
change max_reg, so it cannot change a CSE decision, even one whose symptom is local.
⇒ The lever for a function-global CSE fork is a function-global quantity change — here, closing the
+5 length delta is expected to move max_reg and the fork together. Diagnose the SCOPE of a
compiler decision (function-global vs block-local) before choosing where to edit.
A diagnostic a0 → $s2 pin halves the residual and makes the prologue byte-exact — which confirms
the diagnosis — but it is a hand-placed dial, not plausible source, and it does not fix the frame. It
belongs in the do-not-re-buy table, not in the deliverable (§72/§74).
§83e — §80 vindicated again, on the same day it was written
Two case residuals (6 and 11, 8 and 7 diffs) had been written off as "pure allocation". They were not:
the morph/lerp loop walks copies of its two input pointers (pa = msa; pb = msb;), not the
originals — and spelling that took both cases to zero. A residual class you assigned on an
earlier base is a hypothesis, not a verdict (§80). Re-run the negative list after the base moves.
§84 — The DERIVED-OFFSET remap bug: a hand-computed literal that encodes the DISTANCE between two per-overlay symbols, and why match_one is structurally blind to it (Phase 29 SESSION-20, func_8013D53C)
A family_remap member draft reached match_one MATCH (240 ins) and failed the whole-binary
gate. The whole binary differed by ONE BYTE.
The construct
Cracking the exemplar produced a deliberate matching idiom — reach a symbol via a different symbol
plus a literal offset, so gcc cannot CSE the two %hi/%lo pairs into one:
/* EXEMPLAR (ov_SC01_077): 0x801DA998 + 0x20 == 0x801DA9B8 ✓ */
(*(S9 *)&D_801DAA78) = *(S9 *)(&D_801DA998 + 0x20); /* same addr as &D_801DA9B8;
distinct sym defeats cse, keeps %hi/%lo */
family_remap substitutes symbol NAMES correctly — D_801DA998→D_801A5778, D_801DA9B8→D_801A5790
— and leaves the literal 0x20 alone. But 0x20 is not a constant of the algorithm: it is the
distance between two per-overlay symbols, and that distance is per-overlay.
exemplar: 0x801DA998 + 0x20 = 0x801DA9B8 ✓
member: 0x801A5778 + 0x20 = 0x801A5798 ✗ (the real symbol is 0x801A5790)
member: 0x801A5778 + 0x18 = 0x801A5790 ✓ correct offset is 0x18, not 0x20
Changing 0x20→0x18 + running the full gate_stage ladder banked it byte-identical.
Why it survived every candidate gate
match_one masks HI16/LO16, so a wrong %lo is invisible to it — it reports a clean MATCH.
Only the whole-binary gate sees the byte. This is the §81 blindness in its DATA form: §81 is
match_one blind to a jump table; §84 is match_one blind to a mis-derived data address.
A match_one MATCH that fails the whole-binary gate by ONE BYTE is a masked-field bug —
diff the built image against the payload and read the differing word before assuming plumbing.
The diagnostic that found it in one step:
python3 - <<'PY' # built vs extracted payload, byte-diff, map file offset -> vram
a=open('build/<ov>/<ov>','rb').read(); b=open('<target_path>','rb').read()
d=[i for i in range(min(len(a),len(b))) if a[i]!=b[i]]
print(len(d), [hex(BASE+i) for i in d[:8]])
PY
THE FIX IS MECHANICAL — the tool already holds the answer
The remap knows both mappings, and the exemplar's own comment even names the aliased symbol (which
the remap has already substituted: "same addr as &D_801A5790"). So:
correct_literal = mapped(aliased_sym) − mapped(base_sym)Detect&SYM + LITERALwhereSYM_exemplar + LITERALequals another mapped symbol's exemplar address, and recompute the literal from the member's own addresses. Never carry the exemplar's literal through a symbol substitution.
Scope, measured (do not over-generalise — §80)
The idiom is rare: only 5 sites across the whole matched corpus
(ov_SC01_077.c ×1, ov_SC01_077_jr_8017AE2C.c ×2, engine_core.h ×2). But one of those five
gates an entire 123-member family — 133 staged member drafts all carry the un-recomputed + 0x20
with different per-overlay base symbols (D_801F3058, D_801A5778, D_8018F9B8, …), so the single
tool fix is worth ≈ 240 ins × 123 members ≈ 29,520 ins.
It does NOT explain the sibling pool generally: the other three families probed the same day
(func_80144090, func_8012CC88, func_8014D12C) have zero derived-offset sites and fail for a
different, still-undiagnosed cause.
Two ladder lessons banked with it
- Bare
harvest_verifyis the LAST rung, not the ladder.gate_stage.pyrunscanon_resident_calls → cast_call_sites → reconcile_tu → ARITY pre-pass → sig_unify → harvest_verify. A probe that callsharvest_verifydirectly measures the un-recovered rate and will report PLUMBING for everything the ladder would have cleared. The byte fix AND the ladder were each individually insufficient here; only together did it bank. reconcile_decls.pyis RETIRED (Phase 26-A, R33) — superseded byreconcile_tu.py, because asking "what does the FLEET call this symbol?" is wrong by construction in a loosely-typed engine (548 of its answers conflicted; it was rewriting 60 of 196 live drafts). Reach forreconcile_tu, neverreconcile_decls.
§85 — The RETURN-axis fleet widen is ALL-OR-NOTHING: widening the shared header alone guarantees a conflict in the source overlay (Phase 29 SESSION-20, func_8012CC88 / func_8014D12C)
§73 says a def-side self-decl conflict has two axes — RETURN (fleet widen, T2/R22) and PARAMS (casts at each use, T0). This is the RETURN axis measured end-to-end, including the way it fails.
The conflict
A remapped member's byte-true definition needs a return value the fleet declares as void:
draft def : s32 func_8012CC88(s32, s32, s32) <- the return keeps a local alive (§30#2)
fleet decl: extern void func_8012CC88(s32, s32, s32)
-> cc1: conflicting types for `func_8012CC88'
THE FAILURE MODE — widening only engine_core.h is worse than not starting
Widening the shared header banked the member in the target overlay and broke ov_SC01_077
(R22 139/140). Reason: the source overlay carries its OWN local extern void func_X(...) decls
(they predate the shared macro), so a shared-header-only widen puts the two in direct conflict.
The per-binary gate cannot see this — it passed on ov_SC01_000 while breaking a binary it never
built. §63/§61 exactly: a T2 write set is only provable by R22.
The precondition, and how to check it in one grep
The widen is byte-neutral iff no caller consumes the return value (a call compiled against void
discards it either way, so the emitted code is identical):
grep -rhoE "[A-Za-z_]\w* *= *func_XXXX\(" src/ | wc -l # must be 0
Measured here: 0 for both functions — then the widen is safe.
Do the WHOLE axis in one edit, then R22 once
grep -rl "extern void func_XXXX(" src/ | xargs sed -i 's/extern void func_XXXX(/extern s32 func_XXXX(/g'
make clean && make extract-all && make check-all # T2 => R22 MANDATORY
Measured scale: 3,668 decl sites across 2,688 files for two functions — and 140/140
byte-identical, tools-health green, dedup 1886/0. The site count is large because every overlay
re-declares the callee locally; that is normal, not a smell.
Reading, for the next person
- A half-done axis is a guaranteed break, not a smaller win. Widen every
extern voidsite for that symbol acrosssrc/, or widen none. 0 sites remainingis the completion assertion — count before and after (R32).- The other axis (PARAMS) still uses per-use casts (§17a-1/§73); do not widen params this way.
⚠️ A verification trap that cost me a false "BYTE-IDENTICAL" report
make build BINARY=X 2>&1 | grep ... | head then echo rc=$? reports the exit status of head,
not make — it is 0 even when the build failed. The output simply had no BYTE-IDENTICAL line,
which is the thing to check. Assert on the expected SUCCESS STRING, never on $? after a pipe
(or use set -o pipefail). A green-looking rc=0 on a failed build is the same class of
self-deception R32/R35 exist to prevent — the gate was honest; my reading of it was not.
§86 — Pinned-exemplar templatability is a PER-FAMILY property, not a per-member rate; and the §42e pin guard is now over-conservative (Phase 29 SESSION-20)
The guard refuses a class that largely works
family_sweep's §42e guard skips any family whose exemplar carries register __asm__ pins. Measured
on the zero-crack pool: of 1,083 candidate members in the top 8 FREE families, 680 (63%) were
refused BEFORE any gate ran. Re-run with --allow-pins (letting the byte-gate arbitrate, G3/P9):
268 of 680 banked, and ZERO cc1 crashes across hundreds of pinned compiles.
That confirms the guard's original motivation is gone: the SIGABRT it protects against was
Phase 27's extract_unit macro-drop, not a compiler limit (§42e-CORRECTION). The guard is now
protecting against a bug that no longer exists.
THE LAW: all-or-nothing PER FAMILY
The headline "37%" from a 19-member sample was an ARTEFACT OF MIXING FAMILIES. The real distribution:
| exemplar | members | banked |
|---|---|---|
func_801749C8 |
137 | 137 (100%) |
func_8014C6F4 |
137 | 137 (100%) |
func_80133AB0 |
136 | 4 |
func_8014CF04 |
137 | 0 |
func_80143D28 |
136 | 0 |
Two families at 100%, three at ~1%. Whether a pinned exemplar templates is a property of the
FAMILY (does its pin set survive symbol substitution into a sibling TU?), not a per-member lottery.
This matches SESSION-19's func_8017A4AC, which banked ×134 with pins — a whole family, not a
fraction.
⇒ THE PROCEDURE (do this, not a blanket sweep)
for each pinned family:
probe ONE member -> banks? yes: sweep the whole family
no : SKIP IT ENTIRELY
Cost measured: the blanket sweep spent ~412 futile gate cycles (≈60% of the run) grinding three families that were never going to bank. The 1-member probe reduces that to 5 probes + 2 sweeps. A sample that straddles families reports their AVERAGE and hides the bimodality — sample per-family, never per-pool. (Counterpart to §80: a measurement is scoped to what it was taken over.)
Why the two live families differ from the three dead ones — the open question
Not yet diagnosed. The likely axis is whether the pinned registers are caller-saved across a jal
(§74's corrupting form) in the sibling's register pressure, versus pins that only fix a local
allocno. Diagnose before extending --allow-pins fleet-wide — the byte-gate makes a wrong guess
free, but a wrong PROCEDURE costs a sweep.
§87 — match_one COMPILES but never LINKS, so an unresolvable data symbol reads as MATCH; and stored drafts go STALE against the tree (Phase 29 SESSION-20)
The autopsy's "integration" bucket — 315 entries reported match_one MATCH, "byte-correct
standalone, blocked only on plumbing" — banked 0 of 27 through the full gate_stage ladder.
Two causes, neither of which is plumbing:
- UNDEFINED DATA SYMBOLS. The gate fails at LINK:
undefined reference to D_801893xx. That symbol is defined in no overlay's symbol file.match_onecompiles a single TU and never links, so anexternthat resolves nowhere is structurally invisible to it — it reports a clean MATCH. (Checked and refuted the obvious alternative: the drafts were authored for the CORRECT binary, so this is not mis-targeting.) - STALE AGAINST THE TREE.
redefinition of struct S80172C50— the struct has since been lifted intosrc/shared/engine_types.h(§20/§64 type-lift), so the draft's own copy now collides. A draft stored months ago is scored against TODAY's tree; type-lifts, shared-header growth and canon-sig changes all age it.
The blindness ladder, now complete — FOUR classes match_one cannot see
| § | class | what it masks | who catches it |
|---|---|---|---|
| §81 | jump tables | the duplicated .rodata jtbl |
whole-binary gate + carve chain |
| §84 | mis-derived %lo |
HI16/LO16 are masked | whole-binary gate (ONE byte) |
| §87 | unresolvable symbol | it compiles, never links | the LINK step |
| §87 | stale draft | the tree moved under it | cc1, at re-compile |
**match_one MATCH means "this TU compiles to the right bytes with relocations masked" — nothing |
|||
| about linking, nothing about the current tree.** Treat a stored MATCH as a CLAIM WITH A TIMESTAMP. |
Consequence for the backlog ledger
The autopsy's integration bucket is overstated as ready-to-bank work. Combined with §83's
finding that 44% of the ledger (707 redraft) are partial drafts misfiled as near-misses, the
honest read is: docs/backlog.md's headline count is not a work queue. Before planning against
any stored-draft pool, RE-GATE A SAMPLE — the recompute is one CPU-second per entry
(autopsy collect) and it is strictly better than trusting a stored verdict.
The cheap discriminator, before spending a sweep
grep -oE 'D_[0-9A-F]{8}' <draft> | sort -u | while read s; do
grep -qF "$s" config/symbols.$OV.txt config/symbols.us*.txt || echo "UNRESOLVABLE $s"
done
An entry with any UNRESOLVABLE symbol will fail at link no matter how clean the ladder run is.
§88 — cross_jump will not merge a common suffix containing a CALL; and §78 is scoped to ORDERED comparisons only (Phase 29 SESSION-20, the behemoth close-out)
Three behemoths closed the same night (func_80183814 5,122 · func_8017D2DC 1,586 · func_8017DC1C
1,518), taking the fleet to zero unmatched functions >1000 ins. Their durable output:
§88a — repeated CALL-shaped blocks are left UNMERGED; call-free tails are merged for you
Measured on func_8017D2DC: ~34 byte-identical 6-instruction jal-bearing blocks were left
unmerged, and case 66's two byte-identical 12-instruction blocks likewise — while every
call-free tail (.L8017EB5C/EB64/EB70, .L8017E9A0/E9AC/E9B4, .L8017E06C, .L8017DAF0,
.L8017E804) was merged. ⇒ Write repeated call-shaped cases LONGHAND; never hand-factor the
call-free tails — the compiler does that itself, and pre-factoring them puts you off-target.
§88b — the slti literal-position law (extends §78 to comparisons)
MIPS slti can only carry its constant on the RIGHT, so mips.c rewrites LE/GT vs CONST_INT into
LT(x, K+1). Consequence, decidable from bytes alone: if the target materialises the limit into a
REGISTER on the LEFT of slt (li K; slt K,x; beqz), K was NOT a literal in the source. All eight
literal spellings were swept (> K, >= K+1, K < x, !(x<=K), ++x > K, early-return…) and
every one folds to slti x,K+1. Only a block-local variable holding the limit
(s32 lim = K; if (lim < X)) reproduces it — and per §76 that variable's block scope matters: if
its live range spans a call it takes a callee-saved register and the li hoists.
⚠️ §88c — THE MIRROR-IMAGE FALSE POSITIVE (this one costs 25 wasted edits)
addiu $vX,$zero,1; bne looks exactly like §88b's "the constant was materialised ⇒ it was a
variable" signature. It is not. MIPS has no beqi, so equality constants are ALWAYS
materialised. §78/§88b are scoped to ORDERED comparisons only — the ones mips.c rewrites into
slti. Believing the signature on an ==/!= sends you rewriting perfectly correct literals.
§88d — BANKING ORDER: run the §81 carve chain BEFORE banking, never after
Banking func_8017DC1C first broke the build. Its draft establishes the canon for 39
previously-undeclared externs; jr_isolate_all's re-partition (overlay_src_split) then dropped
all 39 across the new split boundary — D_801C1EB0 undeclared, present in NEITHER file. That is the
§77 preamble-drop class in a third tool. The fix is ordering, not patching: carve chain first
on a clean tree (each step byte-gated), then bank. Re-sequenced, both banked first try.
§88e — a wrong diagnosis, refuted properly (the model for how to treat an inherited lever)
Round 1 handed round 2 "close the +5 length delta to move max_reg", citing cse.c:8340. Round 2
refuted it three ways: a 15-line reproducer reproduced the case-0/3 CSE exactly (so it cannot be
max_reg-gated); max_qty only gates extension ACROSS blocks and both call groups share one block;
and the target leaves $s7/$fp unused, killing the pressure story. The real discriminator came
from a second direction — case C01 has the identical two groups over the identical symbols with
zero residual, because a break puts a CODE_LABEL between them. The +5 was a SYMPTOM.
Both biggest levers were pure DECLARATION SCOPE (§45/§76), not pins. ⇒ An inherited "named
lever" is a hypothesis with a citation, not a fact. Reproduce it small before spending a round on it.
§88f — the missing rung: a RELOCATION gate between match_one and the binary
match_one masks jal/HI16/LO16 and never links (§81/§84/§87). The behemoth run produced
.run/giants/s21_g21_reloc_verify.py, which resolves every relocation — including the implicit
MIPS-REL addend objdump -r does not print (§84's trap, in tool form) — against the target:
359/359 jal callees, 60/60 internal j destinations, 406/406 %hi/%lo addresses incl. all 6
derived-offset sites. Promote this to tools/ — it closes three of the four blindness classes
before a gate cycle is ever spent.
§89 — Two throughput rules the project already had written down and was not following (Phase 29 SESSION-20)
Neither of these is a new capability. Both are cases where the tool or the rule already existed and the practice had drifted — which is worth recording precisely because that is the hardest kind of waste to notice from inside a session.
§89a — MEASURE the write set; do not assert its tier (tools/blast_radius.py)
§63's T0/T1/T2 taxonomy has existed since Phase 26 and was never enforced. Both failure directions were measured in one session:
- Over-verification: ~13 full clean-fleet R22 runs (~15 min each), most for batches that were
provably T1 (
src/<binary>/**only). Hours of serialised wall-clock proving what the written rule already guaranteed. - Under-verification — the dangerous half: the §85 return-type widen was believed contained to
one overlay and broke
ov_SC01_077(R22 139/140);gate_stage's ARITY pre-pass silently rewrote caller decls in 40 TUs. In both cases the write set was LARGER than the belief about it. A tier is a CLAIM.blast_radius.py --expect t1 --binary <b>turns it into a MEASUREMENT and exits non-zero when the tree disagrees. Negative-controlled on all four cases, including the §85 shape. Coverage asserted (R32): an unclassified path exits 2 and names itself.
§89b — the parallel gate farm existed; the family path could not reach it (tools/sweep_parallel.py)
bulk_harvest Phase B has been a ProcessPoolExecutor over DISTINCT binaries (per-binary flock,
per-worker result files, compute_fleet=False) since Phase 23 — but welded to Phase A's LLM
drafting. Family sweeps stage their drafts a different way (family_sweep --stage-only), so the farm
was unreachable from that path, and SESSION-20 gated 389 + 268 + 104 members serially
(for ov in …; do gate_stage …; done) — roughly an 8-16x throughput loss on a 32-thread box, for no
architectural reason. sweep_parallel.py is a thin adapter: same gate_stage.run_gate, same
per-binary lock, no new gate logic.
It also filters the phantom-dir bug at the source: a bare .run/sweep/*/ glob matches
gate_stage's own intermediate ladder dirs (-cn, -cn-cast, -cn-cast-rc, -s2in, -s2in-uni)
and calls them as binaries — 24 phantom PARTIAL 0/1 lines that inflated one run's notbanked from
0 to 56. Require config/splat.<bin>.yaml to exist (R33/R36: derive the binary set, never glob it).
The standing sequence
family_sweep --stage-only → sweep_parallel.py -j 12 → blast_radius.py
T2? → R22 clean-fleet mandatory T1? → the per-binary gates already ran; commit
Parallelism changes THROUGHPUT, never the verdict — the whole-binary byte-gate is still the sole arbiter (G3/P9) and still reverts a wrong draft in its own binary.
§90 — Five tool-integrity laws from one session, each of which changed an answer (Phase 29 SESSION-21, 2026-07-27)
SESSION-21 set out to run a mass wave over the family exemplars. Before the wave returned a single draft, five tools had produced a confidently wrong answer, and each fix changed the number it gave. These are the generalizable laws, not the individual bugs.
§90a — A comparison tool MUST share its reference oracle's index space, exactly
tools/reloc_verify.py (promoted this session) compares "what the target names at instruction index
i" against "what my object relocates at index i". It used objdump -dr; masked_diff has always
used -drz. Without -z, objdump ELIDES runs of identical instructions, dropping them from the
listing — func_801330E0 read 104 instructions under -dr and 110 under -drz (6 elided
nops). Every index after the first nop run therefore compared against the wrong instruction, and the
tool manufactured 2 mismatches on a draft that is clean at those sites.
A second instance in the same tool: the splat .s word field is little-endian hex TEXT
(0080033C), not the instruction integer. masked_diff byte-swaps it (struct.unpack("<I", …));
the new tool did not, and reported "word differs" on three sites that are byte-IDENTICAL
(3C038000 vs 3C038000).
The law: when you write a tool that compares against an existing oracle, do not re-derive its extraction — reuse it, or byte-for-byte replicate its flags and encodings, and cross-check on a known-clean input before trusting a single verdict. Five false alarms preceded the first true one.
§90b — "Byte-neutral" is not "wanted": undo on the SUCCESS path too
Two ladder stages (family_sweep --normalize-self-decls, gate_stage's ARITY pre-pass) had a
backstop that restored their snapshot only when the edit proved NON-neutral. A run that banked
nothing therefore left every edit in the tree: --normalize-self-decls 0/123 left 123 files /
246 insertions / 246 deletions of dead diff, and a git add -A would have committed it as noise.
The law (§61 applied to the success path): an edit that bought nothing gets reverted, regardless of whether it was byte-safe. Restore the snapshot on a 0-bank group. It cannot cost a match — there is nothing to preserve — so the only thing at risk is the noise itself.
§90c — A library-callable function must FAIL CLOSED on an unconfigured module
progress.linked_subsegs() is gated on a module global that set_binary() assigns. Imported as a
library without that call it returned an empty set — "this binary has no linked library
subsegs", which for main is wrong by 49 and silently reclassifies ~960 already-byte-identical
PsyQ-linked stubs as outstanding game-code work. The CLI path always configured first, so the defect
was invisible for as long as nobody imported it. I hit it within five minutes of importing it.
The law: a function whose correctness depends on module state must raise when that state is absent. An empty result is indistinguishable from a true negative, and that is the R32 silent-skip shape wearing a different hat.
§90d — Do not measure a live wave's drafts (§87 in real time)
Cross-checking the wave's landed drafts mid-run gave a different verdict for the same function two minutes apart — its agent had rewritten the file 12 seconds earlier. §87 says a stored draft is "a claim with a timestamp"; during a live wave that timestamp is now.
The law: draft QA happens after the wave returns, never during. A measurement of a file that is still being written describes nothing.
§90e — An agent's CONCLUSION and its EVIDENCE fail independently — re-derive the premise, not the fix
A wave agent found a genuine jtbl_carve bug (spimdisasm splits one jump table across two dlabels,
so the carve reserves 112 B for an object supplying 200 B — §84-class, invisible to match_one).
Its conclusion was correct and confirmed three ways. Its stated evidence was not: it reported the
continuation label as having "ZERO xrefs anywhere in the tree"; the label has two, and the agent's
proposed remedy — delete it — would have removed a symbol two emitted words reference.
The repair therefore uses a gate that needs no judgement call: the owning function's own
sltiu N range check, which gcc emits immediately before the indexed load, so the program
declares its own table length. Absorb a continuation only when it is adjacent, its words are all
code addresses, and absorbing lands on an exact sltiu bound — the SET of bounds, never
max(), because a multi-switch function has several and no way to say which owns this table.
Three more errors surfaced only by testing the fix: the absorption fired and the trailing-pad trim
immediately undid it (it re-trimmed against the first dlabel's words); a continuation ends at its
own last .word, not the next dlabel (224 B further on here) — which is the very assumption being
repaired; and the shortfall warning fired ~90 times across 38 tables until it was scoped to an
unambiguous single-bound pairing. A warning that fires on ambiguity is noise, not a signal.
The law: when an agent hands you a diagnosis AND a fix, re-derive the diagnosis from the bytes and design the fix from what you can prove. Verified: the split table goes 28 → 50 words, and across 38 jtbls × 6 functions = 228 combinations exactly ONE range changes. Both halves of that sentence are the deliverable — the fix, and the negative control proving its blast radius.
§91 — A structure-TRANSFER is only valid where the structure corresponds: the --like role trap (Phase 29 SESSION-21, func_8012AAAC ×137)
jtbl_family_bank sweeps a matched jr exemplar across its family by calling
jtbl_carve <sibling> --func <fn> --like <exemplar_ov>. The --like role-transfer copies the
exemplar's .rodata span STRUCTURE (its tables= starts) onto the sibling, on the premise
"same family => same span structure". That premise is a claim about the OVERLAYS' split layout,
not about the family — and it fails silently whenever the exemplar has a code-subseg split the
sibling does not.
The measured case. func_8012AAAC lives in ov_SC01_077_a (role _a) in the exemplar and in
the MAIN subseg (role ``) in all 137 siblings. Role-transfer keys on the subseg role, so it looked up
ov_SC01_077 — an unrelated seven-table span owned by entirely different functions — and stamped
those starts onto a sibling span holding one table. jtbl_rodata_pads then correctly refused:
consumed 1 rodata .align(s) but 2 pad spec(s) given — table-count drift vs the carve
Why it presented as a mystery: jtbl_family_bank deliberately excludes the table-count-drift
error from its auto-isolate retry (it is genuinely not isolate-fixable), and that path discards the
message — so all 137 siblings reported a bare gate-fail with no cause attached. The sweep read
as "this family does not template," which is exactly the verdict this project has had overturned as
tooling three separate times.
The fix: transfer only when the exemplar's subseg for this function has the sibling's role;
otherwise derive the span from the sibling's own carve, which was already computing it correctly.
Fail-open is not acceptable here — a wrong table set corrupts the image — so the guard defaults
to local derivation. Result: 0/3 → 3/3 on the probe, then 134/134 on the remainder — the family swept 137/137, zero failures. func_8012AAAC is now stubbed in ZERO overlays.
And it moved the RE-completeness number, not just the display one: distinct-code 69.3% → 69.5%. A jtbl family is byte-VARIANT (each overlay's table holds its own addresses), so every member is a genuinely new unique function — the opposite of the h_seq PURE propagation families swept earlier the same session, which added 274 members and moved distinct-code by +0.0. That is SESSION-20's routing rule reproduced twice in one session, in both directions: target byte-VARIANT families to move RE-completeness; high-reach h_exact families move only the decomp.dev display number.
The three-hypothesis trail, because two of them were wrong and the wrongness is instructive
- Call-site casts (real, fixed, NOT the blocker). Every sibling TU has the stub, then
extern void f();, then a 0-arg call later in the same file — so splicing the definition puts a prototype in scope and gcc rejects the call. 137 TUs, exactly the member count. Cast one site per TU, R22-proven byte-neutral. Still 0/3. A real defect that had to be fixed anyway, and fixing it moved nothing — "the error I can see" is not the same as "the error that is blocking me." - My own carve-alone test (a false lead I generated). §81 step 2 says the carve alone must be byte-identical. For this shape it CANNOT be: the 2-entry spec describes a table the object only emits once the body is banked, and a stub object emits one. The tool splices the body BEFORE building, so its path is valid and my simplification was not. A diagnostic that departs from the tool's real sequence tests a different program.
- Reading the tool's ACTUAL invocation — which is where
--likewas visible at all. The fix took ten minutes once the command line was read instead of imagined.
The law: any "same family ⇒ same structure" transfer must state which structural fact it assumes and CHECK it against both sides. Role-keyed transfer assumes the two overlays split their code identically; overlays differ, so verify per function, not per family. And when a tool deliberately drops an error class it cannot act on, it should still SURFACE it — a bare
gate-failrepeated 137 times cost far more than printing one line would have.
§92 — Conforming a DECLARATION: pointer changes are caller-neutral, scalar-WIDTH changes are not (Phase 29 SESSION-21, tools/conform_decls.py)
When a draft reaches match_one MATCH its signature is byte-TRUTH (§58b) and the fleet extern
is a stub-era guess, so conflicting types for func_X is fixed by moving the DECLS. That is right,
and it was the single dominant gate-failure class this session — 7 of 7 plain drafts and most of the
jtbl drafts. tools/conform_decls.py automates it: derive the signature from the draft's
DEFINITION, rewrite EVERY site, and assert completion (R32) because a half-rewritten axis is a
guaranteed break (§85), not a smaller win.
But "decls are free" is only true for some changes, and the difference is byte-measurable:
| change | caller-neutral? | evidence |
|---|---|---|
pointer type (s16*/short* → u16*) |
YES | func_80179B74: 1,600 sites / 523 files / 3 distinct forms conformed, banked, R22 140/140 |
return widen void → s32 |
only if no caller consumes it (§85) | func_8014D2A0, func_8012CC88 — precondition checked, then byte-neutral |
scalar WIDTH (s32 → u16) |
NO | func_80175DA8: decls reverted → gate PLUMBING; conform applied → gate DIFF |
arity ((void) → takes params) |
NO — breaks every 0-arg CALL | func_8015B950 by hand: ov_SC01_077 gated BYTE-IDENTICAL, 138 of 140 binaries broke |
The scalar-width case is the subtle one. Narrowing a parameter changes argument promotion at every call site, so the callers emit different code. The conform did not fix the draft — it changed the callers. A DIFF appearing only AFTER a conform is the signature of this: examine the callers (the §17a-1 pair — keep their decl compatible and cast at the call site), never the body.
The arity case is the expensive one, and the guard now names its price. func_8012AAAC needed
137 call-site casts to absorb it; func_8015B950 looked like it needed ~926 and in fact needed
ONE — its only 0-arg call is in an engine_core.h DEFINE macro body that the preprocessor
expands into all 926 TUs. Count the call SITES, not the expansions.
The law: a declaration edit is a T2 fleet-shared write set, provable only by R22 (§63/§85). The per-binary gate authorises the binary you built; it says nothing about the 139 you did not.
§93 — set -o pipefail attributes a pipeline failure to the LAST stage, not the failing one (Phase 29 SESSION-21, func_8014D820)
The c-rule is cpp | cc1 | maspsx | [jtbl_rodata_pads] | as under set -o pipefail. When cc1
exits 33, make reports the failure against the object whose recipe ends in as — so the error
reads as an assembler problem. I recorded func_8014D820 as an "assembler-stage failure" on that
basis and left it undiagnosed for hours. It was conflicting types for 'Ent' against
engine_types.h:434, fixed by moving three types to BLOCK scope (byte-neutral, and collision-proof
across all 138 member TUs).
The diagnostic, ~2 minutes: run the stages by hand and print each rc.
cpp … > t.i ; echo "cpp rc=$?"
cc1 … < t.i > t.s ; echo "cc1 rc=$?" # <- the real failure surfaces here
maspsx … < t.s ; echo "maspsx rc=$?"
as … ; echo "as rc=$?"
Then re-run the failing stage alone with stderr visible — cc1 names the file, line and symbol in
one sentence. This turned an opaque Error 33 into a one-line fix twice in one session
(func_8012AAAC's too few arguments, and this).
Corollary (§88e, earned): when handing a stuck function to another agent, pass the failure as what it IS — an undiagnosed observation — not as a named cause. I flagged this one explicitly as "never diagnosed, re-derive," and the agent found the true cause immediately. Had I written "assembler-stage failure" as established fact, it would have inherited my wrong search space.
§94 — A family sweep's 0/N is a TYPE-CARRY failure until proven otherwise: lift the exemplar's local types, ALL of them, transitively (Phase 29 SESSION-21, func_8016B6BC 0/137 → 137/137)
func_8016B6BC failed its family sweep at 0/137, reproducibly, twice — once in the 274-member
propagation batch and again after the §91 --like guard landed. It looked exactly like the §86
bimodal case ("some families template, some do not"). It was nothing of the kind.
§59(1) says a remap drops the exemplar's LOCAL struct types. It does — and the failure it produces names the wrong thing. cc1 reports:
`c' undeclared (first use this function)
`v' undeclared (first use this function)
`off' undeclared (first use this function)
Those are ordinary locals and they ARE declared in the remapped body. cc1 says "undeclared" because
it aborted the declaration block at an unknown TYPE (Prim_8016B6BC prim;) and every later
declaration in that block fell out with it. Read the first error, not the loudest one — and if a
variable you can see declared is reported undeclared, suspect its type, not its declaration.
The lift must be TRANSITIVE, and one round is not enough. Lifting the type the body names
directly (M8_8016B6BC) changed nothing: still 0/137. The full set was four, discovered by
following each definition's own references —
M8_8016B6BC → Prim_8016B6BC → Vtx_8016B6BC (named only inside Prim's body) → DVec_8016B6BC.
tools/lift_types.py --types A,B,C --apply lifts them to engine_types.h and strips the local
copies. Byte-gate the lift ALONE first (it must be neutral — it was, d19c9580… unchanged), then sweep.
Result: 0/137 → 137/137, zero failures. R22 clean-fleet 140/140.
The law: a sweep
0/Nis an INTEGRATION signal (§59), and the cheapest hypothesis is that the exemplar's body references a type only its own TU defines. Diagnose by splicing ONE sibling and reading cc1 directly (§93) — the sweep's own summary tells you nothing about cause. Then lift the transitive closure of the local types, not just the one the body mentions.Cost of not doing this: this family sat recorded as a bimodal "doesn't template" case across two sessions. The whole diagnosis, once pointed at one sibling's real stderr, took under an hour and was worth 137 members.
§95 — reconcile_tu dropped the SIBLING declarators of a multi-symbol extern line (Phase 29 SESSION-21, func_80176218)
reconcile_tu conforms a draft's DATA declarations to what the target TU can see — the right
question (§ the retired reconcile_decls asked the fleet, which has no single answer). Its rewrite
replaced the draft's declaration LINE with the TU's declaration of the conflicting symbol. But a
declaration statement can declare SEVERAL symbols:
extern u16 D_80078EB2, D_8011F82A, D_8011F82C, D_80078EB4, D_8011F8C4; /* only EB4 conflicts */
became
extern s16 D_80078EB4; /* four symbols GONE */
Why it was hard to see: the draft does not fail at the declaration. It fails later with
D_8011F82A undeclared (first use this function) — pointing at a USE, several conflicts down a
peeling chain, nowhere near the cause. I peeled four separate "next conflicts" out of this one draft
before dumping ALL cc1 errors in a single build and seeing three undeclared symbols that the tool
itself had removed.
Fix: group the plan by STATEMENT rather than by symbol, then re-emit every declarator — the TU's version for the ones that conflict, the draft's own for the rest — and note when a multi-declarator statement is touched. When the statement cannot be re-parsed, say so loudly instead of emitting only the planned symbols. After the fix the same draft reconciles 3 symbols rather than 2: the dropped declarators had been hiding a further conflict.
The law (R32, again): a transform that REPLACES a syntactic unit must account for everything that unit contained. Line-granular rewriting of C declarations is wrong by construction — the statement, not the line, is the unit, and a statement can hold N declarators.
Diagnostic worth reusing: when a draft fails in a chain, stop peeling one error per gate cycle. Splice it once and dump EVERY cc1 error — the shape of the whole set names the cause (three
undeclaredsymbols that share one original declaration line is not three problems).
§96 — The same rewrite, one shape down: reconcile_tu matched statements to lines by TEXT, so every COMMENTED declaration was silently skipped (Phase 29 SESSION-22, func_80176218 banked)
§95 fixed what the rewrite emitted (every declarator of the statement, not just the conflicting one). It did not fix how the statement was located, and that was the second half of the same bug.
cdecl.split_statements returns comment-stripped text with spans:
Stmt('extern u8 D_80078E78;' @ 4171:4193) # the line reads
# extern u8 D_80078E78; /* cur base ($s5) */
The rewrite re-found each planned statement by stmt.text.strip() == line.strip(). For any
declaration carrying a trailing comment that test is false, so:
- the declaration was left unconformed, while
- the use-cast pass (a separate loop, keyed off the same plan) still fired,
producing a draft whose uses are cast for the TU's storage against a declaration that still has the
draft's — i.e. a guaranteed conflicting types, reported by cc1 at the very declaration the tool
had just claimed to fix, with the tool exiting 0 and printing reconciled: N symbols.
Fix: rewrite by SPAN. split_statements preserves start/end precisely because drafts get
rewritten — its own docstring says so. The primitive was there; the code re-found the text instead.
And assert it landed (R32). The old code even had a dropped_check counter — incremented in two
places and never compared, which is the R32 "a loud failure nobody counts is exactly as invisible
as a silent one" shape in miniature. Now: declarators-in vs declarators-out, plus a per-symbol check
that each planned tu.declaration() is actually present in the output, each emitting a !! note
(so --strict exits non-zero).
Measured: func_80176218 went 3 → 4 data symbols reconciled and then banked whole-binary
(327 ins, ×138 = 45,126 templated instructions), after two gate cycles that had each reported a
different "next conflict".
The law, sharpened from §95: locating a syntactic unit and rewriting it are the same obligation. If you have spans, use them — re-finding a unit by its own text silently reintroduces every lexical difference (comments, whitespace, line breaks) as a miss. And when a transform can partially apply (decl not rewritten / uses rewritten), the partial state is worse than no-op, because it manufactures an error at a location the tool believes it already handled.
Corollary for the ladder:
reconcile_tuowns DATA declarations only (it skipskind == 'func'by construction). Function-decl conflicts in the same draft arecast_call_sites' axis (§20), and the two compose cleanly in either order — they touch disjoint symbol classes.func_80176218needed both: 4 data symbols conformed, then 2 callees (func_80177AD4void (int, unsigned int),func_80178298(u32*, u8*, short, short)) decl-conformed + call-site-cast to the draft's intended widths.
§97 — The gate's own tree hygiene: a refused carve, an unchecked recovery, and a snapshot that captured a dirty tree (Phase 29 SESSION-22)
A 15-draft harvest_verify --chunk 1 batch reported CC1-FAIL=4 and ended final SHA None. Three
of those four were manufactured by the harness. The chain, in order:
_okwas computed and ignored._jtbl_prep_onereturns(ok, snapshot); the caller tested onlysnapshot. Whenjtbl_carveREFUSES a table (§59(3): a non-contiguous same-subseg carve),okis False andsnapshotis None — so the draft was spliced and built without its carve, which cannot link. The resultingError 33was filed as CC1-FAIL, a codegen-flavoured verdict for a pure plumbing wall. Now classifiedCARVE-REFUSEDand skipped (also one build cheaper).attempt()does not restore on failure. It writes the candidate render and relies on the NEXT attempt's render to overwrite it — so the tree is dirty between drafts._jtbl_snapshot()snapshots the tree as it finds it. Combined with (2), a later draft's carve captured an EARLIER FAILED DRAFT'S SPLICE, and its undo then faithfully re-applied it — after the final_write(baseline)had already run. A run that verified nothing therefore ended with a body spliced on disk and unable to rebuild the binary, which reads exactly like a byte regression and is not one.- The recovery's own
make extractwas unchecked._shdoes not raise (§93's sibling), so a failed re-extract inside_jtbl_restorewould report nothing — a recovery that silently did not recover, strictly worse than not attempting one.
The invariant, one line: the gate's tree is at baseline except while a specific draft is under
test. Restore after every failed attempt (atomic branch AND bisect branch), and the snapshot can
never capture someone else's failure.
Plus an R32 coverage assertion on the cleanup itself: at 0 verified, a non-empty
git status --porcelain src/<bin> config is residue, not a result — print the files and the recovery
command rather than making the operator go looking.
Measured recovery of the false verdicts (same 4 drafts, same drafts dir, clean tree):
| draft | contaminated | true |
|---|---|---|
func_8013B83C |
CC1-FAIL | CARVE-REFUSED (§59(3) wall) |
func_801789AC |
CC1-FAIL | PLUMBING — conflicting types for func_801789AC (actionable) |
func_8017C974 |
CC1-FAIL | DIFF — corroborates its agent's global_alloc spill diagnosis |
func_80140958 |
CC1-FAIL | CC1-FAIL (genuinely its own, on a different object) |
final SHA went None → d19c9580 BYTE-IDENTICAL; tracked diff empty.
The law (R34/R35 again, pointed at the gate itself): the whole-binary byte-gate is a perfect oracle for did this draft match and a NULL oracle for what state did I leave behind. It cannot manufacture a match — no banked result in this session was affected — but it can manufacture a verdict, and verdicts are what the backlog and the roadmap are built from. A failure class is evidence about the compiler only once the harness is proven not to be the cause (§53's carve law, generalized from "sweep with the right tool" to "gate from a clean tree").
§98 — conform_decls had three defects, and only the third needed R22 to find (Phase 29 SESSION-22, func_8014CF04)
The decl-conform axis banked two drafts and gated BYTE-IDENTICAL on ov_SC01_077, then R22 came
back 139/140. Three separate defects, found in order by following the bytes:
1. The regex crossed newlines (mechanical).
rf"(extern\s+)?[A-Za-z_][\w \t\*]*?\b{fn}\s*\([^;]*\);" # [^;]* matches '\n'
Starting at a DEFINITION line it ran past the { and through the body to the first ;, matching
s32 func_8014CF04(s32 param_1, void *param_2, void *param_3) {\n register u8 *q __asm__("$17");
as one "declaration" and replacing both lines with a prototype — deleting the definition (and a
register pin) and producing undefined reference. Fix: [^;{\n]*, which makes a definition
unmatchable by construction. A multi-line prototype is then simply not matched, and the completion
assertion reports it instead of mangling it.
2. It rewrote inside COMMENTS (H5). Same runaway match. Fix: scan cdecl._mask(txt) — a
length-preserving blank-out of comments/strings whose offsets are valid in the original — and rewrite
by SPAN. (R33: that primitive already existed for exactly this.)
3. IT ASSUMED ONE SIGNATURE FITS THE FLEET — and this engine is loosely typed. The real cause,
and no amount of code-reading would have found it. ov_SC07_006 carries its OWN banked definition of
func_8014CF04 with a different byte-true signature — (s32, s32, void*) where ov_SC01_077
needs (s32, void*, void*) — beneath a local decl explicitly marked
/* de-macroized: per-overlay-local decl (byte-true sig); do NOT re-macroize */. Conforming that TU
to the fleet canonical produced conflicting types against the definition directly below it.
The rule: a TU that DEFINES the function owns its own declarations. A fleet-wide decl axis is meaningful only for TUs that CONSUME the symbol. Skip defining files whole — their definition is byte-truth there. This case grows more common as banking proceeds: every overlay that banks a function becomes a defining TU and therefore an exception.
And the assertion then cried wolf on its own by-design behaviour. With the defining TU skipped,
the R32 completion check counted it as a surviving non-canonical decl and reported
*** HALF-AXIS — DO NOT BUILD *** for a rewrite that was complete and correct. An assertion must
be exact about its DOMAIN, not just its condition — the §85 all-or-nothing invariant is over the
CONSUMING TUs. Scoped it; the axis reports complete at 1,747 sites with 1 file excluded by design.
Also hardened: PLAN → VALIDATE → WRITE. The refusal path originally aborted mid-write while claiming nothing had been modified — which is precisely the HALF-AXIS §85 calls a guaranteed break. The plan is now built and validated in full before a single file is written, so a refusal costs nothing and leaves the tree untouched.
Verified: 1,752 → 1,747 sites (the 5 differences were 1 definition, 3 comment lines, and the defining TU's own decl); definition + register pin intact; both drafts re-banked; R22 140/140.
The meta-lesson (R22's whole premise, re-earned): after fixing defect 1 I expected R22 to pass. It failed again at 139/140 for an unrelated reason, and an individual
make buildof the failing binary succeeded — because it reused objects the clean run rebuilds. An incremental pass does not refute a clean-tree failure. Every step of this diagnosis came from reading the real cc1/ld error after a genuinely clean rebuild, never from reasoning about what the tool "should" do.
§99 — The narrow-param wall is a DEF-side problem with a ZERO-blast-radius fix: convert the definition to K&R (Phase 29 SESSION-22, func_80175AB8 + func_80175DA8)
§92 said these two need "the §17a-1 caller pair, NOT a bare conform". The diagnosis was right —
conforming the fleet's void f(s32) declarations to the byte-true void f(s16) narrows the
parameter, changing argument promotion at every call site, so the callers emit different code
(§92 measured exactly that: PLUMBING before the conform, DIFF after). But the prescribed remedy
was the expensive one. The actual fix touches no declaration at all:
void func_80175AB8(param_1) /* K&R: the narrow param PROMOTES to int (C89 6.3.2.2), */
s16 param_1; /* so this is ALREADY compatible with the fleet's `s32` */
{ /* prototype — while still emitting the narrow-param codegen. */
That is §43 applied to the definition side instead of the declaration side. Cost: a draft-only
edit (T0, zero blast radius) versus a 524-site fleet conform. Both banked, d19c9580,
R22 140/140 — 57,822 templated instructions for two draft-local rewrites.
The law: when the byte-true signature has a NARROW scalar parameter and the fleet declares it wide, do NOT move the declarations. Move the DEFINITION to K&R and let C's promotion rule make the existing prototype correct. Conform only when the disagreement is a POINTER shape (caller-neutral, §85) or an arity/return change.
Two reconcile_tu bugs found underneath, one introduced while fixing the other
(a) It was blind to BLOCK-SCOPE declarations. split_statements is depth-0 by design, so for
a draft whose body is one function definition it returns exactly ONE statement and every declaration
inside is invisible — and §8d (scope_data_externs) deliberately demotes the data externs to block
scope. C still requires a block-scope extern to agree with a file-scope declaration in scope, so
the conflict is real: the tool printed reconciled: 0 draft(s), 0 data symbol(s); coverage defects: 0
for a draft cc1 then rejected with conflicting types for D_8011F7BC. Fixed by descending one level.
(b) DESCENDING INTO ANY { CORRUPTS STRUCTS — a bug I introduced with (a) and caught by diffing
the tool's own output against its input. Struct MEMBERS parse as declarations and get "conformed":
- u32 code; /* 0x04 */ -> typedef void (*code)(unsigned short*);
- p->code = *(u32 *)src; -> p->(*(u32 *)&code) = *(u32 *)src;
Guard: descend only into a FUNCTION body (a parameter list before the brace, and not a
typedef|struct|union|enum head).
(c) And it exposed a LATENT one: _cast_sub's regex matched a bare identifier, so it rewrote
member accesses as if they were the global. Broken since the tool was written; only reachable
once block-scope descent started finding such names. Guard: (?<![.\w])(?<!->).
Process note worth keeping: (b) was caught because the transform's output was diffed against its input before the result was trusted — not by a gate. The byte-gate would have reported PLUMBING and told me nothing about why, and the corrupted draft looked plausible.
§100 — Prefer the DRAFT-LOCAL fix: a type only one function uses belongs in its BODY, not in a shared header (Phase 29 SESSION-22, func_80175DA8 0/137 → 137/137)
func_80175DA8 swept 0/137. Per §94 that is a TYPE-CARRY failure until proven otherwise — and
it was: the draft defines typedef struct {…} Sp_80175DA8; at FILE scope, and remap_hseq templates
the BODY but not the type, so every sibling compiled without it.
§94's remedy is the shared engine_types.h lift (correct for func_8016B6BC, whose four types were
transitively referenced). But the cheap remedy was already visible in the same draft: it carries
typedef struct {…} S_AF634; at BLOCK scope, inside the function body, and that one templates
fine — because a type declared in the body travels WITH the body. So:
- the type is used by that function only (measured: 7 mentions, 6 inside the body, 0 elsewhere)
- → move it into the function body. Byte-neutral (
d19c9580unchanged), T0, zero blast radius - → re-sweep: 137/137, 0 failed.
versus editing a header included by 140 binaries, which then needs --strip/uniquify care (§64a),
a name that cannot collide fleet-wide, and an R22.
The law: scope the fix to the smallest unit that makes it travel. File scope in a draft is the worst of both worlds — it does not travel with the templated body, and it pollutes the TU. Lift to
engine_types.honly when a type is genuinely SHARED across functions or transitively referenced by another lifted type (§94); otherwise put it in the body (§59(1)'s "carry it in the template", which this makes concrete).
This is the same shape as §99, an hour apart: in both cases the cookbook's named remedy was the expensive fleet-wide one (524-site decl conform / shared-header lift) and the correct fix was draft-local (K&R definition / block-scope typedef). Two data points, one rule: before editing anything shared, ask what the smallest scope is that still travels with the body.
§101 — The STALE DEFAULT class: a guard whose cause was removed is a silent skip wearing a safety label (Phase 29 SESSION-22, three instances in one session)
Three separate tools were suppressing real, already-correct work — not by being wrong, but by defaulting to a protection whose cause had since been fixed, opt-out only if you remember a flag:
| tool | the guard | cause, and when it died | measured cost |
|---|---|---|---|
family_sweep |
skip any pinned exemplar (§42e cc1 SIGABRT) | the SIGABRT was extract_unit dropping file-scope macros — our bug, fixed in Phase 27 (_carry_macros) |
137 skipped, 133 of them bank |
family_sweep |
gate serially; the parallel farm is opt-in via --stage-only → sweep_parallel |
sweep_parallel.py was built in SESSION-20 after measuring the loss, and never wired to the caller |
~8-16× throughput; 3 sweeps (543 members) ran serially this session |
conform_decls |
REFUSE when 0-arg call sites exist | correct — but it printed the exact remedy and could not perform it | func_801789AC blocked two sessions; unblocked by --cast-zero-arg-calls |
The shape, every time: correct when written · the cause was later removed or the remedy became
available · the default never changed · and the tool reports the skip as a clean, quiet number
(skipped {'pinned-exemplar': 137}) that reads exactly like "nothing to do."
The law (R32/R35, pointed at defaults): a guard is a claim about the world. When the thing it guards against is fixed, the guard becomes a silent skip — and a skip is invisible in exactly the way R32 warns about, because nobody counts what a protection declined to attempt. When you fix a wall, grep for the guard that was erected against it and retire it in the same change. Phase 27's roadmap delta did say the PINS class was "back on the mechanical-harvest table" — the knowledge was recorded and the default still wasn't flipped, which is why this needs to be a mechanical habit and not a note.
Corollary — how to spot one: any run reporting a large skipped {...} bucket deserves the same
suspicion as a 0/N (§59, §94). Both are the tool declining to try, and neither is evidence about
the compiler.
§102 — A PLUMBING verdict can MASK a DIFF; and K&R is not always a codegen change (Phase 29 SESSION-22, func_8016EC0C)
Applying §99 to func_8016EC0C produced three results worth separating:
1. §99 GENERALISES — it dissolved a 1,617-site narrowing axis. conform_decls flagged
SCALAR-NARROWING (s32 → u8) across the fleet. Converting the DEFINITION to K&R promotes u8 →
s32, so the computed canonical became void func_8016EC0C(s32, s32) — the narrowing warning
disappeared entirely, leaving only a RETURN change (s32 → void) whose §85 precondition (no
caller consumes the return) was already satisfied. A caller-hazardous conform became a byte-neutral
one because of how the definition was written.
2. BUT K&R IS NOT AUTOMATICALLY A CODEGEN CHANGE — measure it, do not assume. K&R promotes, so
the CALLEE narrows (§43's in-place sll/andi tell) where ANSI assumes the caller did. That can
change bytes. Here it did not: match_one on both forms returned identical results — closeness
8, 88 ins, same residual. So the K&R rewrite was free. The rule is not "K&R always matches" nor
"K&R always differs" — it is: convert, then compare both forms against the target. (For
func_80175AB8/func_80175DA8 the K&R form is what banked; here it is byte-equal to ANSI.)
3. THE REAL FINDING: its "PLUMBING" verdict was HIDING a DIFF. The T14 census recorded
PLUMBING: conflicting types for func_8016EC0C, which reads as "recoverable, not a compiler wall".
It is not recoverable: with the decl axis clean the true verdict is SCHEDULE-REORDER, closeness
8, bucket permuter. cc1 reports the declaration conflict and never reaches the byte comparison,
so the plumbing error fires FIRST and the codegen verdict is never produced.
The law: PLUMBING is a verdict about the DECLARATIONS, never about the BODY. A census's PLUMBING bucket is therefore an upper bound on recoverable work, not a count of it — some entries are DIFFs wearing a plumbing costume, and you only learn which by clearing the plumbing and re-gating. Size a PLUMBING pool as "worth diagnosing", never as "worth banking".
Consequence for the backlog: func_8016EC0C is genuine permuter fuel (close=8,
SCHEDULE-REORDER) — unlike func_80176734/func_8017C974/func_80177B5C/func_80140958, which
measured structural and which the grinder's admission rule correctly rejects (§60/§60a).
§103 — A FILE-scope extern in a shared overlay TU is a GLOBAL constraint on every LATER function; move the DECL, not the draft (Phase 29 T48/T51, func_80135260 — the fleet-wide half)
func_80135260 needs its three per-location data symbols declared as 4-byte pointers
(extern u16 *D_x;). The canonical extern u8 D_x; + (*(u16 **)&D_x) cast makes gcc-2.7.2 CSE
&D_x into two callee-saved registers, which costs a 7th saved register and +3 instructions —
measured twice, two independent ways (reconcile_tu conform, and cast-at-use), both 139 ins against
the target's 136 with 123 mismatched. The draft could not be bent. But every host TU already
carried extern u8 D_x; at FILE scope, and a file-scope declaration constrains every later function
in the TU, so the byte-true block-scope decl was a conflicting types error.
The asymmetry is what makes this look like a compiler wall when it is a scope problem:
BLOCK(u16 *) ... then FILE(u8) -> warns, builds (the TU's own pre-existing mix)
FILE(u8) ... then BLOCK(u16 *) below it -> conflicting types (ERROR — the draft's position)
The lever: move the TU's OWN declaration down into its consumers. The engine is loosely typed (§16), so per-function views of a symbol legitimately disagree — the file-scope decl is the anomaly, not the block-scope one, and the original per-function sources declare these symbols at block scope in exactly this way. Moving it is declaration-only: every consumer keeps the identical declaration text, only its scope changes, so no access can change opcode.
Verify in two steps, always (T48's structure, and the reason the fleet application was safe):
- the decl move alone must rebuild the binary byte-identical — that proves it is declaration-only;
- only then splice the byte-true draft and gate it.
tools/scope_tu_externs.py is the tool — the TU-side complement to scope_data_externs.py
(§8d), which fixes the incoming draft. §8d has a documented give-up branch: when the TU already
declares the symbol at file scope, it drops the draft's own decl and lets the TU's type govern.
That is right when the types agree and fatal when they do not — it is precisely how 132 byte-true
siblings gate-failed while looking like a codegen wall. The two tools are halves of one lever:
| what collides | tool | move |
|---|---|---|
| the DRAFT carries a file-scope decl the TU never had | scope_data_externs.py (§8d) |
demote the draft's decl into the body |
| the TU carries a file-scope decl the byte-true draft must contradict | scope_tu_externs.py (§103) |
move the TU's decl into its consumers |
Derive the contested set, never hand-list it (R33): it is the DATA symbols the remapped draft
declares at block scope, intersected with what the target TU declares at file scope above the splice
point. --family does this per sibling in one command.
Refuse rather than skip (R32). Three conditions abort a symbol loudly: more than one file-scope
decl above the splice point; a file-scope statement below the decl that references the symbol (an
initializer has nowhere to move to); an unlocatable body brace. The rewrite then asserts its own
coverage as a delta — file-scope decls -1, block-scope decls +len(consumers) — because these
TUs already carry many legitimate block-scope decls of the same symbols, so an absolute
"at least one exists" check would pass vacuously.
Use cdecl, not a new regex. cdecl.split_statements yields depth-0 statement spans (a function
definition flushes at its closing }, so "ends with }" is a reliable is-a-definition test — a
column-0 test is not, because m2c emits goto labels at column 0 inside bodies), and cdecl._mask
blanks comments and string literals length-preservingly so offsets stay valid. Scanning raw text is
the false-positive class that made gather_externs accuse func_80135D20 on all 137 siblings and
garbled gen_harvest_targets' hints in Phase 19.
AUTOMATED (T53). The lever is now a jtbl_family_bank stage, ordered
raw → scoped → **tu-scoped** → recovered → reconciled. After the two non-invasive stages (it edits
the TU outside the spliced body) and before the two recovery stages deliberately: those bend
the DRAFT, and T48 measured both at +3 instructions for exactly this class, so they cannot succeed
here. The stage re-runs scope_data_fix against the scoped TU rather than reusing the raw body —
composition-correct, because the contested symbols no longer have a file-scope decl to be dropped
against while every other symbol is still handled normally.
The counterfactual, byte-gated on a reproduced blocker (ov_SC01_000, pre-T51 TU restored):
| stage | result |
|---|---|
raw |
compile error — conflicting types |
scoped |
compiles, fails the byte check — §8d dropped the draft's decl, so the u8 form's CSE costs the +3 |
tu-scoped |
BANKED |
That is the whole 133-sibling "wall", reproduced and dissolved in one build cycle.
Corollary — a stage that edits outside the spliced body must re-find the splice point. The stub offset is an index into the ORIGINAL TU;
tu-scoped's base is a rewritten TU, so reusing it splices at the wrong place. Each stage now carries its own base and re-searches. The pre-existing stages all passorig, where the re-search returns the identical span — so they are the same operation as before the refactor, by construction.
The law: when a byte-true draft and its host TU disagree about a symbol's type, the question is never "which type is right" — the engine has no single right type. It is "which declaration is in the wrong SCOPE." A file-scope decl in a shared overlay TU is a fleet-wide constraint that was almost certainly never in the original source; scoping it to its consumers is free, and bending the draft to it costs instructions.
§104 — Two silent-skip defects in one scan: match on MASKED text, emit from the ORIGINAL (Phase 29 T53, gather_externs)
family_remap.gather_externs decides which of the exemplar TU's file-scope externs to carry into a
templated sibling. It scanned raw text, which gave it two defects of the §96 class (matching by
TEXT rather than by what the compiler sees):
- A symbol named only in PROSE counted as a reference.
func_80135260's header comment mentions its siblingfunc_80135D20twice, so the scan demanded a declaration for it, found none, and printed "1 referenced symbol(s) have NO file-scope decl … the sibling will not compile" on all 137 members. It was wrong every time — T52 banked 132/132 against that warning. Two tasks read it as a possible cause before it was measured. - A commented-out
externcould be selected as the carried declaration and spliced into the sibling as live code. Not observed in the corpus; closed by construction.
The fix is a two-text discipline, not a smarter regex: MATCH on cdecl._masked text, EMIT by span
from the original. _mask is length-preserving, so offsets index both. A masked declaration is all
blanks — emitting from the mask would splice whitespace, which is why "just mask it" is a bug and
"mask for the search, slice the original" is the fix.
stmts = [(mask[s.start():s.end()], orig[s.start():s.end()]) # (what you search, what you emit)
for s in EXTERN_STMT.finditer(mask)]
hit = next((emit for searchable, emit in stmts if word.search(searchable)), None)
Verified as a no-op on output: 20 (exemplar, sibling) draft pairs across 4 families, old code vs new, 20 identical / 0 differing. The only behavioural change is that a false warning stopped firing.
The law: a warning with a high false-alarm rate is worse than no warning — it trains you to ignore the channel that reports real causes. This one fired on 137/137 and was right 0 times. When a diagnostic disagrees with a measurement, fix the diagnostic in the same session (R30), or the next reader pays the same tax.
§105 — A gate's revert must survive an EXCEPTION, not just a failure (Phase 29 T53, jtbl_family_bank)
jtbl_family_bank.bank() was carefully revert-on-fail: every return path restored the overlay, and
the stage loop even caught a stage that raises while producing a candidate. Nothing covered the
stages never being reached. A wrong exemplar made remap_hseq raise after the carve had rewritten
config/splat.<ov>.yaml + overlays.mk and jr_isolate had created a region file — the exception
propagated out, the revert never ran, and the tree was left with:
- a rewritten carve config (recoverable by
git checkout), and - an untracked region file, which
git checkout -- src/does not remove.
In a 132-member sweep that residue rides silently into the next member's build. Found by being bitten by it while testing something else.
The fix is a wrapper, not a bigger try inside: snapshot the region set, call the real body,
and on ANY exception revert and return a per-member exception status. Loud (it appears in the
tally), non-fatal (the sweep continues), and the tree is provably clean afterwards.
def bank(...):
keep = region_files(to_ov)
try:
return _bank(...)
except Exception as e:
revert(to_ov, keep_regions=keep)
return "exception", repr(e)[:140]
Negative-control proven: the same crashing invocation now reports {'exception': 2} and leaves
git status -- config/ src/ at 0.
The law: "revert on failure" is not the same property as "revert on every exit". Enumerate the exits — success, gate-fail, refusal, and the throw — and put the restore where all four pass through it. Same family as §97 (the gate's own tree hygiene), and the reason it matters more in a sweep than in a one-off: a one-off's residue is visible in the next
git status; a sweep's residue is consumed by the next iteration first.
§106 — Persist the MEASUREMENT, derive the POLICY: a stored route let a stale file out-vote the live table (Phase 29 T54, residual_class._ROUTE)
residual_class answers two different questions in one pass. klass is a measurement — it comes
from comparing two instruction streams and is expensive. (profile, bucket) is a policy — a table
lookup over klass that says which tool should work on it. autopsy persisted both to
.run/autopsy/residuals.jsonl, and verdicts() read both back.
That made the corpus authoritative for a decision the table owns, with two consequences:
- a route correction was inert. Editing
_ROUTEchanged nothing until someone re-ran the whole collect — so the fix and its effect were separated by an expensive step that is easy to skip. - a weeks-old row could silently contradict the live table, and nothing would ever report the disagreement (there is no oracle comparing a stored policy against the current one — R34's blind spot).
The fix: re-derive the route at read time from the stored klass + detail. The measurement stays
persisted; the policy is looked up fresh on every read.
The subtlety that makes this a technique rather than a one-liner: one route is magnitude-dependent.
LENGTH-DRIFT is permuter-shaped only when |delta| <= 2 AND explains == "tail" (§60b). A naive
re-derivation from klass alone would silently demote those rows. Both inputs are already in detail,
so route_for(klass, detail) reproduces the override exactly — verified at 1610/1610 against the
stored corpus with the table UNCHANGED, before the table was edited. Prove the derivation is faithful
first, then change the policy; otherwise a bug in the derivation is indistinguishable from the intended
change.
The route change itself (the reason this came up): ADDRESSING was routed to the permuter, which
contradicted residual_class' own bucket definition — "structural — local mutation CANNOT introduce
it … it wants a C-level idiom." The §10/§20 hoist-vs-remat shape is a multi-instruction change with a
documented deterministic recipe (gcc-2.7.2-map/cse_expr.md §2). Measured: both admitted ADDRESSING
targets plateaued under a §31-directed permuter, and the class was 32% of the entire admission pool
(18 of 56). After the fix: 56 → 38, exactly 18 rows changed, all ADDRESSING, nothing else moved.
And the bound, kept in the comment where the next reader will hit it: T31's finding 4 byte-tested
the §2 recipe on func_80132F40 across six variants and it never closed. structural here does not
promise a free fix — it means "a search over local mutations is the wrong tool; try the documented
idiom", exactly what WIDTH / BRANCH-POLARITY / IMM-OFFSET already mean.
The law: persist what was measured; derive what was decided. If a stored field can be recomputed from other stored fields plus a table, storing it converts a future correction into a silent no-op — and the staler the file, the more confidently it lies.
§107 — A lever wired into ONE gate path is a lever most families cannot reach (Phase 29 T56, func_80144090 0/136 → 136/136)
The §103 tu-scope lever was wired into jtbl_family_bank (T53) and nowhere else. But that tool only
runs for has_mid_jr families; every other family sweeps through family_sweep, which gates via
plain harvest_verify by design. So the lever was unreachable from the path most families use, and
the symptom was indistinguishable from a compiler wall: func_80144090 swept 0/136 with
conflicting types for D_800A651C.
Wired into family_sweep's staging, the same family banked 136/136, 0 failed — +20,944
instructions — with no change to the drafts at all.
Why this does not violate the sweep's plain-harvest_verify rule. That rule exists because
gate_stage's transforms perturb a correct draft (the §19/T3 finding). The tu-scope never touches
the draft — it moves a declaration in the target TU. Different object, different risk. The test
for "may this run inside the sweep?" is not "is it a transform" but "does it change the draft?"
Reuse the existing undo, do not invent one. family_sweep already snapshots TUs it edits at
staging time (--normalize-self-decls) and reverts them on two conditions: a final MISMATCH (the edit
was not byte-neutral) and a zero-bank group (§61's undo law — an edit that bought nothing gets
undone, or a git add -A commits dead diff). The tu-scope shares that dict, so it inherits both
backstops for free. A staging-time TU edit without those two reverts is how a sweep leaves residue.
Default ON, with an opt-out. It is byte-neutral by construction, a no-op when nothing collides,
and auto-reverted when it buys nothing — so gating it behind a flag only means most runs silently
miss it (the T24 --allow-pins precedent). --no-tu-scope exists to A/B it.
The law: when a lever fixes a class, ask which gate paths can reach it before declaring the class handled. A class is only handled on the paths that can apply the fix — and the paths that cannot will keep reporting it as a wall, in language identical to a real one.
Related, from the same task: scope_tu_externs refused N>1 file-scope decls of a symbol as
"ambiguous". Duplicate-identical externs are legal C, so N identical decls are one declaration
written N times — delete all N. Now it compares whitespace-collapsed forms and refuses only on a
genuine disagreement. (Measured on D_800B9A02: 3 decls in 2 different forms — so that one was
correctly refused, and the relaxation did not paper over a real conflict.)
§108 — Diagnosing a family 0/N: the four causes, and the third opt-in lever (Phase 29 T59)
A family sweep's 0/N says nothing about the code until you splice ONE member and read the compiler.
Do it like this — the shape of the output actively hides the answer:
make -j1 build/<the one .o> BINARY=<ov> # -j16 interleaves the real error out of reach
# then filter: '.c:' in line AND 'warning:' not in line
The memcpy / type mismatch with previous external decl warnings (§58) are noise from unrelated
TU positions and will dominate any naive tail. And set -o pipefail (§93) attributes the failure to
the last pipeline stage, so "Error 1"/"Error 33" names the wrong stage — read cc1's own lines.
The verdict split that matters is PLUMBING vs DIFF: a compile error is a declaration problem (recoverable, and each has a named lever); a clean compile with differing bytes is codegen. Five families diagnosed this way resolved to four distinct causes, only one of which is a wall:
| cause | signature | lever |
|---|---|---|
| shared-header signature conflict | conflicting types for func_X, "previous declaration" points at a DEFINE_func_*() macro line |
--fix-def-sig (see the bug below) |
| PsyQ/library symbol conflict | conflicting types for ApplyMatrixSV |
unresolved — the draft's carried decl vs the TU's |
| genuine codegen | compiles clean, bytes differ | permuter / §31 lookup |
| no matched exemplar | remap: no matched unit |
not a blocker — the family has no fuel |
Read the "previous declaration" line number before theorising. For func_8014D610 it pointed at
line 1727, which is not a declaration at all — it is DEFINE_func_8014D438(), a shared-macro
instantiation whose expansion forward-declares the templated function with the canonical
engine_core.h signature. cc1 reports the conflict at the macro's line. That one line identifies the
whole class.
THE --fix-def-sig BUG (why the lever did not fire). reconcile_def_sig rewrites the draft's
definition to the canonical header decl wholesale — types AND parameter names:
canonical : void func_8014D610(s32 a0, void *a1, void *a2)
draft body: ... param_1 ... param_2 ... -> `param_1' undeclared
Its docstring calls this a "rare name mismatch" that "the gate rejects, never a false bank". It is
not rare — an exemplar drafted with the param_N convention hits it every time, and the whole
family books as a compile failure. The fix is to conform the TYPES and keep the BODY's names;
both are already in hand at the call site.
The law: three times in one session a family-wide
0/Nwas a lever that was unreachable (§107), off by default, or subtly broken — never the compiler. Before diagnosing a family as hard, enumerate the levers the invocation actually enabled, then read one member's real cc1 output. A sweep's0/Nis a statement about the harness.
§109 — Conforming a definition to a shared header: fix the NAMES, then check the RETURN precondition (Phase 29 T60)
reconcile_def_sig substituted the canonical header decl wholesale — types AND parameter names —
while the body kept the exemplar's names. Its docstring called this a "rare name mismatch" the gate
would reject; both halves were wrong. An exemplar drafted with the param_N convention hits it
every time, and the outcome is not a rejected match but the whole family booking as a compile
failure, indistinguishable from a compiler wall:
canonical : void func_8014D610(s32 a0, void *a1, void *a2)
draft body: ... param_1 ... -> `param_1' undeclared (first use this function)
Fix: conform the TYPES, keep the BODY's names — both are in hand at the call site. Parse with
cdecl (base = return type, params = types, pnames = names), not a regex. Two traps in the
re-render: void* + a1 must become void *a1 (cdecl glues the stars to the type), and an empty
parameter list must be handed back verbatim — (void) and () both parse to params == [], and
they are different declarations (§99: () is the no-prototype form).
The fix is real but it is not sufficient, and the proof is that the verdicts MOVED:
| family | before | after | what is left |
|---|---|---|---|
func_8016163C |
param_1 undeclared |
DIFF | plumbing fully cleared; genuine codegen |
func_8014D610 |
param_1 undeclared |
void value not ignored as it ought to be |
the header is wrong |
func_80156044 |
unchanged | conflicting types for func_80155FF8 |
wrong lever — a CALLEE conflict |
The §85 return-axis precondition applies here too, and nothing checks it. Conforming a definition's return type to the canonical
voidis only safe when no caller consumes the return.func_8014D610's callers do — so the header'svoidcontradicts the byte truth, and conforming to it producesvoid value not ignored. The header is the thing that is wrong; changing it is fleet-shared blast radius.reconcile_def_sigshould test that precondition before promoting or demoting a return type, exactly as §85 requires ofconform_decls.
And a verdict that changes is the signal to re-route, not to push harder. One of these three is now a codegen question, one is a header-correctness question, and one was never the def signature at all. They shared a symptom, not a cause — three families, three levers.
§110 — A unit must define exactly ONE function, and "ends in ;" does not tell you which line defines it (Phase 29 T65)
extract_unit located a definition with "the line matches <type> func_<addr>( and does not end in
;". That is wrong whenever one line holds both a declaration and a definition — which the
handwritten inline-asm wrappers do:
extern void func_80156044(int, int); int func_80155FF8(int, int) { __asm__ … }
The line does not end in ;, so func_80156044 — which appears there only in the declaration —
was taken as a definition head. extract_unit lifted the neighbouring wrapper instead of the real
definition seven lines below, every sibling already defines that wrapper via its shared DEFINE_
macro, and all 137 members failed with redefinition of func_80155FF8. Read as a compiler wall.
Ask what follows the parameter list, not what ends the line. ; after the closing paren is a
declaration; {, or end-of-line (the brace-on-its-own-line form), is a definition.
Then assert it (R32): a unit that defines a function other than its target cannot template — the
sibling already has that function, so splicing can only produce redefinition of …. Refuse loudly
instead of handing the sweep a unit that fails N times.
Two traps in writing that assertion, both hit on the way:
_def_head_atalone over-fires. A call whose arguments wrap (iVar4 = func_X(a,/ newlineb);) has nothing after the(on its line, which the "end of line ⇒ definition" rule reads as a definition. It refused three families that had just banked 137/137.- The type-prefix test alone under-fires — it is what missed the wrapper in the first place, because the text before the name is a whole preceding declaration.
The predicate needs BOTH: split the prefix on its last ; and require the remainder to look like a
return type (this excludes call sites and admits the shared-line form), then check what follows the
parameter list. Regression-check any such assertion against families that are known to bank —
all five here extracted byte-identically before and after.
The law: an extraction that silently takes the wrong thing is worse than one that takes nothing, because its output looks plausible and the failure surfaces N members later wearing the compiler's clothes. Assert the shape you require; and when the assertion is about "is this a definition", remember C lets a line be two declarations and a definition at once.
§111 — The distinct-code metric is not noisy: a family pays it only if its members are byte-VARIANT (Phase 29 T66)
Seven family sweeps moved distinct-code by +125, +125, +129, and +0 four times. It read as a
metric bug for four tasks. It is not — it is the metric reporting something the instruction-weighted
number cannot.
weighted_metrics counts dedup_fns = len(matched_cls), where a class is one distinct h_exact
and is marked matched if ANY binary has it matched. So:
Δdistinct = (distinct
h_exactclasses in the family) − (classes already matched)
Exact on all seven, with no residual:
| family | classes | already matched | predicted | observed |
|---|---|---|---|---|
func_80135260 |
131 | 6 | +125 | +125 |
func_80133AB0 |
131 | 6 | +125 | +125 |
func_80156044 |
130 | 1 | +129 | +129 |
| four others | 1 | 1 | +0 | +0 |
The meaning. A family whose 138 members are byte-IDENTICAL is ONE piece of distinct code. The
exemplar's crack already reconstructed it; the other 137 banks are real (each binary now builds that
function from source instead of pasted asm, so fleet/instr-weighted pays in full) but they add
no new reverse-engineering. A byte-VARIANT family is ~130 genuinely different functions and pays
both.
So the two headline metrics rank the same work differently, and you can predict both before
spending a sweep — count the family's distinct h_exact across overlays:
cls = collections.Counter(sig[ov][addr] for ov in sigs if addr in sig[ov])
instr_yield = live_members * nins
distinct_yield = len(cls) - len(classes_already_matched)
Measured over the 49 currently-eligible non-jr families: 194,416 instructions (~1.48 pp) total, of which 13 families / 80,085 ins are byte-identical and pay ZERO distinct-code, and 36 families / 114,331 ins pay 2,962 distinct classes. Pick by which number you are trying to move.
The law: before calling a metric noisy, model it. Two behaviours with no identified variable is not noise — it is a variable you have not found. This one took a
Counterand ten minutes, after four tasks of writing "still unexplained, still not guessed at" in the log. Logging the anomaly honestly was right; leaving it unmodelled that long was not.
(And the derived-not-persisted rule from §106 applies to the table above: it is two lines of code over the sigs, so regenerate it — do not commit a ranking that rots the moment a family banks.)
§112 — A macro-scoped declaration only collides where the macro is INSTANTIATED (Phase 29 T67/T69, audit_header_sigs.py)
src/shared/engine_core.h declares, inside each DEFINE_func_M() macro body, the functions that
body calls. When such a decl disagrees with the byte-true definition, every family templating that
function fails conflicting types — ×137 members, wearing a compiler wall's clothes. Three were
found one at a time in Phase 29, each worth ~137 members. tools/audit_header_sigs.py finds them all
in one pass: parse every header decl, find every DEFINITION in src/**/*.c (via §110's
_def_head_at), and report only where NO definition agrees — one overlay disagreeing is loose
typing (§16/T49); all of them disagreeing means the header is the outlier.
Three preconditions decide whether a correction is safe, and two of them were learned by failing:
| precondition | why | how it was found |
|---|---|---|
| §85 return axis | a consumed return makes the change non-neutral | already known |
| ARITY | the macro's OWN call site passes the header's arity — correcting a (void) decl for a 1-param definition breaks it with "too few arguments" |
measured before the batch |
| VISIBLE COLLISION | a TU that carries its own incompatible decl AND instantiates the macro | a failed gate, 2/140 |
The third one is the subtle one, and getting it right took two wrong models:
- "Any disagreeing decl in
src/blocks it" — compares type SPELLINGS, sos32vsintandu32vsunsigned intcount as disagreements. Usecdecl.compatible(type IDENTITY), not==. - "Any INCOMPATIBLE decl in
src/blocks it" — still wrong, and it blocked all six corrections that had just gated 140/140 and banked 685 members.func_80161774has 1,063 TUs carrying the old spelling; correcting it was still byte-clean.
The fix is to measure the INTERSECTION, not the population. A macro-body decl is only visible where the macro is instantiated, so a collision needs a TU that does both:
colliding = [tu for tu in decls_of(fn)
if (tu.macros_instantiated & macros_declaring(fn))
and not cdecl.compatible(tu.decl, byte_truth)]
Validated against the known outcomes: the six that gated clean → 0 colliding TUs each; the one
that failed the gate (func_80147364) → 272. Perfect discrimination, and the finding count drops
61 → 32 once spelling noise is removed.
The law: a precondition that blocks work you have already proven safe is not conservative, it is wrong — and it will read as prudence forever unless you test it against known-good cases. Every gate you write deserves a control: run it against something that passed and something that failed, and require it to separate them.
§113 — An ARITY blocker only exists if the macro CALLS the function; an address-taken use has no call site (Phase 29 T72)
audit_header_sigs.py's ARITY precondition (§112) refuses to correct a header decl whose arity
differs from the definition's, on the grounds that the macro's own call site passes the header's
arity and would break with "too few arguments". True — when there is a call site.
func_80144B14 is declared void func_80144B14(void) and defined int func_80144B14(int param_1),
so the precondition blocked it. But the macro body does not call it:
*(s32 *)((s32)a0 + 0xDC) = (s32)&func_80144B14; /* address-taken, never called here */
No call site ⇒ no arity constraint ⇒ the FULL correction is available, not the §99 no-prototype
workaround. Applied: extern int func_80144B14(int param_1); — R22 clean-fleet 140/140
byte-neutral, then the family swept 137/137 with zero failures.
So the precondition must ask what the macro DOES with the symbol, not merely that both appear:
| use in the macro body | arity constraint? | correction available |
|---|---|---|
func_X(a, b) — called |
yes, the call passes the header's arity | §99 no-prototype () |
&func_X / (s32)&func_X — address taken |
no | full retype |
| declared but unused | no | full retype |
The law: a precondition derived from one usage shape will over-fire on every other shape. When a gate blocks something, check why the reason applies here before accepting it — "the call site would break" is not a fact about the declaration, it is a fact about a call site that may not exist. This one had 137 members behind it.
§114 — The THIRD decl axis: a CALLEE the draft declares differently from the target TU (Phase 29 T76/T77)
Three axes can make a templated family draft fail conflicting types, and the sweep pipeline handled
only two of them:
| axis | whose signature | lever |
|---|---|---|
| the draft's own definition | func_X itself vs the shared header |
reconcile_def_sig (§109) |
| DATA externs | D_XXXX |
scope_data_externs (§8d) + scope_tu_externs (§103) |
| a CALLEE | func_Y that the draft calls |
cast_call_sites (§17a-1/§20) — was not in the pipeline |
The third axis was the whole reason the byte-identical family tier measured 0 of 682 (T76). Three independent diagnoses had already named it and none of them was the family's own function:
func_80173A60 -> conflicting types for func_80173B4C (a callee)
func_8012F40C -> conflicting types for RotTransPers (a PsyQ LIBRARY symbol)
0x80143d28 -> conflicting types for ApplyMatrixSV (likewise)
cast_call_sites already solves exactly this — rewrite the callee's decl to the TU's canonical
signature (killing the in-TU conflict while keeping the symbol in scope) and cast each call site back
to the draft's intended signature; gcc folds the cast of a known symbol to a direct jal, so it is
codegen-neutral. It lived only in gate_stage, which the family sweep deliberately does not use
(§T3: gate_stage's transforms perturb a correct remapped draft). Wiring it into the sweep took
func_80173A60 from 0/135 to 135/135.
Two details that matter when wiring it:
- Build the canonical map from the TARGET sibling's TU, via cpp (
canonical_map(ov, src_file=tu)→cdecl.tu_scope). A raw-text scan cannot see a macro-injected declaration, andengine_core.his ~23.5k continuation lines inside ~1,800 macros — so the map would return nothing for exactly the callees that conflict (§51g LAW 7). - Read the TU after any TU-side edit is on disk, so the map reflects the environment the draft will actually meet.
The law:
conflicting types for X— read X. When X is not the function being templated, no amount of work on the definition's signature or the shared header will help, and both will look like plausible next steps. Three diagnoses named a callee before anyone checked whether the pipeline could act on one.
§115 — A func_XXXXXXXX predicate rots by design: the same name-form assumption in THREE places (Phase 29 T78)
cast_call_sites is the lever for the callee-conflict axis (§114). It could not see a named callee
at all — RotTransPers, ApplyMatrixSV, any curated symbol — because three separate places assumed
the func_XXXXXXXX form, and fixing two of them changed nothing:
| # | place | assumption | symptom when wrong |
|---|---|---|---|
| 1 | canonical_map |
re.fullmatch(r'func_[0-9A-Fa-f]{8}'), keyed by parsed address |
the callee is absent from the map → if fn not in canon: continue |
| 2 | DECL_LINE_RE |
(func_[0-9A-Fa-f]+) as the name group |
the draft's decl line does not match → never considered |
| 3 | split_sig_string |
\bfunc_[0-9A-Fa-f]+\s*\( |
returns None → if not csig: continue |
All three are silent skips that look identical to "no conflict found". With 1 and 2 fixed, the
symbol reached 3 and was dropped there — the sweep still reported 0/547, and only a trace of
transform's internals (callees cast: 0 while the canonical map clearly held
s32 RotTransPers(s32, s32, s32*, s32*)) located it. After all three: func_8012F40C 0/137 →
137/137.
Curated naming is something this project does MORE of as RE quality improves, so any
func_-only predicate is a rot-by-design defect — the same shape as stub_map's (Phase 26-A), where
a curated stub name read as "already matched".
The law: when a name-form assumption is wrong, grep for every place that encodes it before testing. A partial fix produces the identical symptom as no fix, so the negative result reads as "the hypothesis was wrong" rather than "the fix is incomplete" — and that is how a correct hypothesis gets abandoned.
§116 — Optimization level is a property of the FILE, not the function: read a family 0/N against the member's stub HOME (Phase 29 T79)
family_sweep --hseq templates the exemplar's C into each member's stub file. -O0-ness is applied
by the Makefile per object (WHALE_O0B_OBJS, O0_CLUSTER_OBJS, build/src/boot.o, …), so a
family whose exemplar was matched at -O0 banks only where the member's stub happens to live in an
-O0 object too. Nothing in the sweep, the remap, or the byte-gate reports the mismatch: the draft
compiles fine, and the gate correctly rejects 137 subtly-wrong bodies.
0x801457a4 swept 0/137 on exactly this. In ov_SC01_077 the definition sits in
ov_SC01_077_o0b.c (the whale -O0 object); in all 137 other overlays the same function's stub sits
in <ov>_after.c, which is -O2. Same C, same remap, wrong flag.
The tell, before spending a sweep: find the exemplar's home file and ask whether the Makefile
gives that object a non-default CC1FLAGS. If it does, check where the members' stubs live. This is
the same class as the Phase-29 Task-1 swing verdict (0x8013c964/0x8013c938 compiled -O2 by the
sweep and masked-MATCHing only at -O0) — that one was diagnosed at the compile-flag level; this
one shows the flag is really a file-placement question.
The fix moves the DEFINITION, not the stub — and here is why the obvious shortcut fails
A carved -O0 object whose .text ends exactly at the target function's vram can absorb that
function by appending it, so the linker places it at the same address either way. That makes
"just move the member's INCLUDE_ASM(...) line into the -O0 file" look byte-neutral by
construction. It is not even buildable, and the reason is splat, not the linker:
INCLUDE_ASM("asm/<ov>/nonmatchings/<ov>_after", func_X)resolves to a.sfile that splat only emits whilefunc_XisINCLUDE_ASM'd in that segment's own.c. Delete the line from<ov>_after.cand the nextmake extractstops generatingasm/<ov>/nonmatchings/<ov>_after/func_X.s— so the relocated reference in<ov>_o0b.cfails assembly withcan't open … func_X.s. Verified across all 133 overlays at once (Phase 29 T80).
asm/ layout follows the segment; object membership follows the .c file. Moving a stub
line changes the second while silently invalidating the first. ov_SC01_077 gets away with the
_o0b placement only because it holds a real definition there — nothing references a .s.
So the rollout is: stage the remapped body into <ov>_o0b.c and drop the INCLUDE_ASM from
<ov>_after.c in the same edit, then gate. That is a two-file atomic substitution, which
harvest_verify/family_sweep do not do (they substitute a draft for the stub in the stub's own
file), so this class needs its own small driver. Still prefer it over a splat re-carve — a re-carve
is the Arm-A wall (+0x20 data-symbol shift on 3 of 4 sampled overlays).
The law: a family-wide
0/Nwhose exemplar lives in a flag-overridden object is a build-graph statement, not a codegen one. Check the object's flags and the members' stub homes before routing it to the permuter or logging it as intrinsic.The corollary, learned the hard way: "byte-neutral by construction" is a claim about the linker. The build graph has other stages, and splat's asm generation is keyed to a different partition (segment) than the one you are editing (object). Build it before you call it neutral.
§117 — Spell the sibling's symbol from the SIBLING's address, not the exemplar's kind (Phase 29 T82)
family_remap.symbol_map builds {exemplar_symbol → sibling_symbol} by zipping the two functions'
relocation slots positionally. Phase 26-A had already fixed the exemplar side ("name the symbol by
what the address IS, not how it was loaded" — an address-taken function loads via lui/%lo and so
looks like data). The target side kept copying the exemplar's kind:
if ke == "call": m[f"func_{ae:08X}"] = f"func_{at:08X}"
else: m[f"D_{ae:08X}"] = f"D_{at:08X}"
m[f"func_{ae:08X}"] = f"func_{at:08X}" # <- target spelled from the EXEMPLAR
For a same-address family the two sides always agree, so this was invisible for 20+ phases. It only bites cross-address families, where the same slot can be a function in the exemplar and data in the member:
exemplar func_80174784 |
member func_8017CFD4 (ov_SC01_000) |
|
|---|---|---|
| callback slot | 0x801747CC — a function |
0x80182688 — data (D_80182688) |
| map emitted | func_801747CC |
func_80182688 ✗ |
The body then materialized a name for an address that is not a function, and the whole-binary gate
refused all 251 members. Fix: spell the target by what the target address is in the sibling's
overlay — func_ iff it is in that overlay's sig set (the same boundary oracle nins_of trusts,
R33). Result: 0x80174784 2/255 → 251/251, +242 distinct classes.
Why it survived so long: a MASKED oracle will MATCH a wrong symbol
rtu_match/match_one mask jal/HI16/LO16 so relocation noise does not drown the diff. A body
pointing at the wrong symbol therefore reports a clean MATCH (10 ins) while the fleet gate
rejects it. That combination — masked-MATCH + whole-binary DIFF — is the exact signature of a
compiler wall, which is how this was booked for so long.
The law: when a masked oracle says MATCH and the whole-binary gate says DIFF, suspect a symbol before suspecting codegen — masking is precisely what hides a wrong relocation target. And any positional exemplar→sibling map must derive each side's kind from its own side.
§118 — Ordinal (positional) immediate resolution: compare C tokens to the DIFFERING asm uses (Phase 29 T87)
family_remap.imm_map_tier1 substitutes a member's immediates by value over the whole body, so it
refuses (asm-ambiguous) when the exemplar uses the same literal at a position that differs AND at one
that does not — a by-value swap would corrupt the fixed one. The refusal is correct; the safety test
was too strict.
It compared the C literal's occurrences against every asm use of that value. But gcc synthesises uses that no C token names, so that comparison can never balance. The canonical case is an array index:
D_80187044[*(u16 *)((s32)a0 + 0x2)](); /* one C literal `0x2` */
emits two uses of the value 2 — the 0x2 member offset (per-member) and a fixed sll ..,2
for the 4-byte stride. One C token, two asm uses => permanently unresolvable.
The fix pairs C occurrences with asm positions in order, accepting either balance:
len(spans) == len(asm_pos)— every asm use has a C token; pair 1:1, rewrite only those indiff_idx.len(spans) == len(diff_pos)— the extras are implicit (compiler-synthesised); pair the C tokens against the differing uses only. A swap cannot corrupt what no token names.
Anything else still refuses. The order assumption (C literal order ~ emitted immediate order) is a heuristic, so the whole-binary byte-gate stays the sole arbiter (G3/P9) — a wrong pairing is rejected, never banked.
Measured: func_801599A4 0 -> 137 drafts, 137 banked (family 0x80131eec, +12 singletons =
149). Blast radius is small: only 9 of the other 144 immediate-refusals converted, so this is a
targeted lever, not a second §117 — the remaining still-zero families are blocked by something else.
The law: when a safety check counts asm occurrences against source occurrences, it must exclude the ones the compiler synthesises — otherwise the check is unsatisfiable by construction and reads as an unresolvable member forever.
§119 — Two levers on the SAME axis, opposite directions: test the off-diagonal (Phase 29 T89)
family_sweep carries two declaration-axis levers that pull opposite ways:
| flag | bends | correct when |
|---|---|---|
--fix-def-sig |
the definition -> the shared-header decl | the header is right and the draft's def is wrong |
--normalize-self-decls |
the in-TU declarations -> the definition | the definition is byte-true and a decl disagrees |
0x80161c98 needed exactly one of each: its byte-true def is (int, u32) (target emits sltiu), while
engine_core.h says (s32, s32) and a TU decl disagreed. So:
- both flags (T79):
--fix-def-sigrewrote the def tos32->slti-> byte DIFF. 0/138. - neither flag (T84/T88): correct
sltiucodegen, butconflicting types for func_80161D20from the in-TU decl -> will not compile. 0/138. --normalize-self-declsonly: def stays byte-true, the divergent decl is dropped and its calls cast. 138/138.
Three sweeps across three sessions read as a compiler wall because all three tested the diagonal of the 2x2 (both on, both off) and never the off-diagonal.
The law: when two levers act on the same axis in opposite directions, "tried it with the flags" and "tried it without" cover half the matrix. Enumerate the off-diagonal before calling the family blocked — especially when one lever's own docstring says it rewrites what the other preserves.
Measured: 0x80161c98 0 -> 138/138 (130 distinct); +23 across the remaining still-zero
families. Corollary to §118's caution: a per-family blocker can be a flag combination, not a defect.
§120 — Uniquify draft-defined TYPE names; and check which of N staging sites you actually patched (Phase 29 T93)
A remapped draft may define a local typedef struct {...} S_AF634; whose auto-generated name also
names a type at file scope in the sibling's TU. gcc-2.7.2 rejects the redefinition (C89 has no
"compatible redeclaration" escape for typedef names) and cc1 dies before codegen, so the family
reads as a wall.
canon_sig_reconcile._uniquify_draft_types is exactly this lever and lived only on the
--reconcile-raw path — the 5th "lever unreachable from THIS path" of the phase. Wired into the hseq
staging path: func_801759D8 CC1 FAIL -> MATCH (56 ins) -> 137/137 banked.
Do NOT "strip the duplicate typedef" — it breaks the extern that uses it
The obvious fix is to delete the draft's typedef since the TU already has one. That fails: the
TU's copy sits below the spliced function, so extern S_AF634 D_800AF634[]; inside the draft no
longer parses and you get D_800AF634 undeclared / used prior to declaration — which looks like a
second, deeper blocker and is really the first fix misfiring. Rename, don't remove.
The wiring trap that cost two attempts
family_sweep has three staging sites (edit-remap, hseq, plain h_norm) that share the
identical two lines:
d = os.path.join(REPO, SWEEP, ov)
os.makedirs(d, exist_ok=True)
Patching by rindex lands on the plain sweep, so an hseq run shows the draft unchanged and the
lever reads as ineffective. Anchor on something unique to the path — the hseq site's write is
func_{to_addr:08X}.c (cross-address), the others use func_{addr:08X}.c.
The law: before concluding a lever does not work, prove it ran — diff the staged artifact for the change the lever is supposed to make. A patch applied to a sibling code path is indistinguishable from a lever that does nothing.
Blast radius: 0 beyond this family (74 further families re-swept, none moved). Like §118 and unlike §117, this is a targeted lever — a path-reachability gap, not a logic defect.
§121 — Synthesise externs for macro-DEFINED callees from the macro's own definition head (Phase 29 T95)
gather_externs carries a referenced symbol's declaration by copying a file-scope extern line out
of the exemplar TU. A shared engine function defined by a DEFINE_func_X() macro has no such
line anywhere — the macro defines it. The member TU instantiates that macro too, but frequently
below the splice point, so a draft that uses the symbol as a value (a cast call site,
((void(*)(void))func_80142C84)()) fails with undeclared (first use this function).
Guessing the type is worse than not declaring it. A no-prototype extern s32 func_80142C84();
turns the error into conflicting types against the macro's real void func_80142C84(s32 a0) — a
different error, which is the useful signal that the guess (not the diagnosis) was wrong.
The fix: macro_def_sig_map() parses the #define DEFINE_func_X() \ <ret> func_X(<params>) {
heads in engine_core.h (1,878 signatures) and the sweep prepends
extern <ret> func_X(<params>); for any referenced macro-defined callee the draft does not already
declare. Byte-verified on func_80142B2C: undeclared -> (bad guess) conflicting types ->
MATCH (34 ins) -> 136/136 banked.
Note this is the complement of header_sig_map(), which reads the extern decls a macro emits for
its own callees. Two different macro-derived signature sources; a symbol in neither is a real gap.
Blast radius: 0 beyond this family — like §118 and §120, and unlike §117, a targeted lever.
§122 — GATE RAW BEFORE TRANSFORMING; the undo belongs to the WRITER, as a per-edit journal (P30 T0a, 2026-07-30)
The defect pair this closes (carried from SESSION-22, reproducible): (1) the gate_stage ladder
FAILED drafts that bare harvest_verify VERIFIED — func_8013B6A0/func_8013B598 (_o0) and
func_80138C60 (jr split), rtu_match confirming real-TU MATCHes. Every casualty lives in a
split TU; the plain-TU draft banked through the same run. Root-cause hypothesis (still open, now
harmless): the transforms take ONE batch-wide --src-file while the gate derives each draft's home
TU per-draft (Phase 26-A) — a mixed-TU batch gets decls reconciled against the wrong TU.
(2) A bare-gate workflow left fix_arity_callers --any-proto residue in 17 unrelated TUs — the
ladder's snapshot/undo existed only inside the ladder.
Law 1 — stage 0: gate the RAW drafts before ANY transform. A recovery ladder's transforms are
for drafts that FAIL as written; running them on everything lets a "recovery" regress a byte-correct
draft, and the gate then reports the regression as the draft's failure. With stage 0, the
destroyed-good-draft mode is impossible by construction — no root-cause required first (the
right sequencing under R35: neutralize, then diagnose). GATE_NO_STAGE0 restores the old order.
Law 2 — undo is the WRITER'S job, recorded per edit, not the orchestrator's file snapshot.
The snapshot needed two measured special cases (restore-shared-only on a partial bank because a full
restore reverts fresh splices, Task-14; restore-everything on a zero-bank run, §61) because a
file-level restore cannot tell the pre-pass's edits from the gate's splices. A per-edit journal
(fix_arity_callers --journal / --undo-journal --keep <banked>) round-trips each substitution's
literal text: exact by construction, immune to interleaved splices, uniform for shared+local files,
and available to EVERY workflow — ladder or bare. A decl someone else edited since is reported
MISSING loudly (R32), never silently skipped. Negative-control-proven: apply→undo → byte-identical
tree; --keep retains exactly the banked set. Bonus closed: the undo now runs AFTER stage 2, so a
stage-2 bank no longer loses its arity edit before its own gate attempt (the old latent parity gap).
Generalizes to: any pipeline where deterministic "fixers" precede a truth gate — the gate goes first on untouched input, fixers touch only failures, and every shared-state fixer journals its own writes. (Same family as §19/§25 canon-first and the §61 undo law; this entry is their composition.)
§123 — PROPAGATE A FAMILY WITH THE TOOL ITS TIER NEEDS: dedup_propagate is h_exact-only; its refusals are statements about the TOOL (P30 wave 1, 2026-07-30)
The trap, walked into and caught. Wave 1 banked 8 fresh cores in ov_SC01_077. Propagating each
via dedup_propagate --addr, four landed and four refused:
0x80133298 [skip] not self-contained — missing file-scope extern (CARRY-FIXABLE): Blk32L,m 0x80175820 [skip] not self-contained — missing file-scope extern (CARRY-FIXABLE): D_800AE7BC,D_800AF634 0x8015FBE0 [skip] 1 reach<2 0x8016E9EC [skip] 1 reach<2
Read naively that is "two need an extern carry fix, two aren't shared." Both readings are WRONG, and
the second is absurd on its face — the family map says 138 members each. The actual fact:
dedup_propagate defaults to --tier h_exact — byte-IDENTICAL bodies. All four families are
diff_class PURE/IMM: members differing by relocations/immediates (h_seq). For an h_seq family the
h_exact reach genuinely IS <2, and the liftability check genuinely does fail — because the tool is
answering a question about a tier these functions do not belong to. The right tool is
family_sweep --hseq (remap per member). The four that DID propagate had byte-identical siblings.
The law (the §53 carve-law, generalized from the CARVE axis to the TIER axis): a propagation
refusal is evidence about the tool's tier, never about the function — check diff_class in
.run/family_hseq.json BEFORE routing, and never let a refusal message's vocabulary
("not shared", "not self-contained") name the function's property. §53 said sweep a family with the
tool its exemplar needed; this says propagate a family with the tool its TIER needs. Same failure
shape, different stage, and it is the shape that manufactured the "families bank ~0%" doctrine.
Routing table (memorize this, it is the whole entry):
family diff_class |
tool |
|---|---|
| byte-identical members (h_exact) | dedup_propagate --addr |
| PURE / IMM (reloc/immediate drift) | family_sweep --hseq |
has_mid_jr: true |
jtbl_family_bank.py (§53 carve) |
| exemplar in an -O0 TU | the -O0 rollout path (§116), not either sweep |
Corollary on the CARRY-FIXABLE label: it is real for h_exact bodies (hoist the externs/typedef to file scope above the def and it lifts), but seeing it on an h_seq family means you asked the wrong tool first — fix the routing before fixing the externs.
§124 — A "not matched" verdict can mean the definition is there under a DIFFERENT C NAME: the asm-label alias blind spot (P30 SESSION-28, func_8016191C ×137)
Symptom. A family sweep reports no matched unit for func_XXXXXXXX in <ov> and skips every member —
even though corpus.stubs says the exemplar is not a stub (i.e. the invariant says it IS matched).
The two statements look contradictory; they are both true.
Cause. family_remap.extract_unit looked for a definition head literally named func_<ADDR>.
But a body whose byte-true signature conflicts with the fleet-canonical declaration on both §73 axes
at once (return AND params) is banked zero-touch by the §37 asm-label alias — the C identifier differs
and a GNU asm label binds the emitted symbol:
extern void func_8016191C(void *a0, s32 a1); /* the fleet canon, in engine_core.h */
int aF8016191C(int param_1, unsigned int param_2) __asm__("func_8016191C");
int aF8016191C(int param_1, unsigned int param_2) { ... }
extract_unit matched nothing, returned None, and every caller reads None as "not matched."
Measured (P30). That single blind spot was the ENTIRE no matched unit skip class: one exemplar ×
137 same-address members, 24 ins each = 3,288 ins, every member still an INCLUDE_ASM stub and
otherwise sweep-ready. After the fix: 137 banked / 0 failed, R22 140/140. Fourth consecutive time a
"structural" residual resolved to our own tooling (R35).
The fix, and the two traps inside it.
- Resolve
<ident>(...) __asm__("func_<ADDR>");→<ident>and accept that as the definition head. Re-derive the pattern PER FILE — one file's alias must never leak into the next file's scan. - Carry the alias DECLARATION into the unit. Without it the sibling TU emits the symbol
aF8016191Cand the body never lands atfunc_<ADDR>— it would link, build, and be wrong. Let the preceding-decl backscan walk past the alias line (so the function's own externs are carried exactly as for a plain definition), then guardstart <= alias_ln <= endagainst double-emitting it. - R32: an alias declaration with no findable definition must refuse LOUDLY. Falling through to
_macro_unitre-reports "not matched" — the exact silent skip the fix exists to delete.
Do NOT "fix" this at the source. The tempting alternative is to widen the shared header
(extern void → extern s32), delete the alias, and rename the definition back. That addresses only
§73's RETURN axis while the decl and body also disagree on PARAMS, so it still conflicts — and it is a
T2 fleet-shared edit where the alias is T0 draft-only. Fix the reader, not the source.
The general law. corpus.stubs (the invariant) and a source scanner can disagree, and when they do
the scanner is wrong — the invariant is derived from the build, the scanner is a text model of it
(R33). Any place that turns "I could not find the text" into "it is not matched" is a silent-skip
defect waiting to be measured.
§124a — a family sweep's 0 matched-exemplar families may be a FILTER, not a wall
family_sweep --hseq defaults to --band substantial. A mid/tiny family therefore returns
[hseq] 0 matched-exemplar families (band=substantial); 0 candidate members — which reads exactly like
"nothing to do here." Pass --band all (or the family's band) before concluding anything. Same
shape as §53 (the missing carve) and §116 (the wrong opt level): a 0 from the wrong invocation is not
evidence. Check the band the family map assigned before you spend a probe on the residual.
§125 — Split the CARVE from the BODY before calling a jr residue a wall — and measure it by SHA from a CLEAN tree (P30 SESSION-28; this section's first draft was WRONG and the method caught it)
Three jr targets refused the whole-binary gate. I ledgered all three as tooling walls on the strength of a body-free "carve-only" probe. Re-measured properly, two of the three verdicts were false and the third had a different cause than I recorded. The method below is sound; my instrument was not. Both halves are the lesson.
The method (keep this)
Run the carve on a sibling with no body spliced at all, then rebuild:
tools/jtbl_carve.py $OV --func $FN # carve ONLY — no draft, no remap
make extract BINARY=$OV && make build BINARY=$OV
# byte-identical -> carve is neutral; the failure is the TEMPLATED BODY
# diverged -> the failure is the CARVE; the body was never fairly tested
It separates two failures that present identically at the gate, and it costs one build.
The instrument rules that make its answer trustworthy (this is where I failed)
- Compare the built SHA against
config/check.<ov>.sha. Do NOT grep the build log for[ OK ]. A log-grep cannot distinguish "wrong bytes" from "the build did not get that far", and it silently inherits whatever stale state the tree is in. - Re-extract after EVERY config change AND after every revert.
git checkout -- config/alone leavesbuild/holding objects from the carved config — the next build then links a mixture and reports a divergence that is purely your own. (Phase-20's R22 corollary; §42b's stale-object trap. I reproduced it exactly: a reverted config with no re-extract turned a byte-identical overlay into[FAIL] got 8f28aa77 / want 38a3d919.) - A driver that aborts a target MUST revert that target before the next one. v1 of my chain
continued without reverting;config/overlays.mkis SHARED, so target 1's half-applied isolate was still in the tree while target 3 was measured. Every verdict after the first abort is suspect. - Verify the baseline against the canonical SHA too, not just "it built". "Identical to the previous build" is worthless if the previous build was already wrong.
The corrected results (each SHA-verified, from a clean tree, restore re-verified)
| target | jr_isolate_all |
jtbl_carve |
true verdict |
|---|---|---|---|
func_8018057C / ov_SC01_009 (897 ins) |
NEUTRAL | not reached | my "isolate breaks bytes" was FALSE; the original failure is not reproducible |
func_80191C50 / ov_SC06_018 (710 ins) |
NEUTRAL | DIVERGED | REAL — the carve genuinely breaks bytes here, after a neutral isolate |
func_8017BEBC / ov_SC04_004 (group B, 13 members) |
n/a | NEUTRAL | carve is fine ⇒ the failure is the BODY/template, the OPPOSITE of my first claim |
So the tidy story I wrote first — "two apparent walls are one tooling problem" — was wrong. They are two different problems, and the third target has no demonstrated problem at all.
Two further notes worth keeping
jtbl_carveon ov_SC06_018 refuses loudly when run without the isolate: "subseg would host NON-CONTIGUOUS .rodata carves (0xab9f4 and 0xaba4c) — a single object can't leave a gap for the unmatched jtbl between them." That refusal is the documented §81/§8b instruction to run step 1 first — it is the tool working, not failing. Do not confuse a loud refusal with a byte divergence.- Ledger the STAGE, not the function (
JTBL-CARVE-BREAKS-BYTES), so one instrument fix reopens every target it covers — but only after the stage is verified by rule 1–4 above. A ledger full of misattributed classes is worse than no ledger: it schedules the wrong repair.
The meta-lesson
§53 warns that a 0% from the wrong tool manufactured a doctrine that steered two phases. This is the same failure one level up: a verdict from the wrong measurement manufactures a wall just as efficiently. R35 says fix the instrument before trusting its measurement — and my own diagnostic script is an instrument, subject to the same rule as the tools it audits. The saving grace is that the method in this section is what refuted the section's own first conclusion, one build at a time.
§126 — The carve-within-a-carve: an ADDRESS RANGE is not an OPTIMIZATION REGION (P30 T2, byte-proven end-to-end)
A 4th -O0 region was found inside an -O2 jr split (0x80183CF0..0x80184920, ov_SC03_014 +
ov_SC03_015). Banking it needs the containing object sub-split into pre/-O0/post — the
"carve within a carve" the roadmap had flagged as blocked on the Arm-A splat %lo +0x20 defect.
It is not blocked. Four probes, each isolating exactly one variable, SHA vs config/check.<ov>.sha
from a clean tree:
| probe | isolated | result |
|---|---|---|
| 1 | sub-split at arbitrary addresses, everything still -O2 |
BYTE-NEUTRAL — the re-carve does not shift %lo; Arm-A does not bite |
| 2 | same split, middle region routed -O0 |
diverged (two variables changed at once — inconclusive) |
| 3 | probe-1's name, only the -O0 flag added |
diverged ⇒ the FLAG, not the subseg name |
| 4 | -O0 regions cut to EXCLUDE matched bodies |
BYTE-IDENTICAL — route proven |
The finding: opt level is per FILE, so the file's contents must be opt-HOMOGENEOUS
§116 says opt level is a property of the FILE. The corollary nobody had needed until now: when you
select a region by address range, you get everything in that range — including functions that are
already MATCHED, whose bodies expand from engine_core.h as DEFINE_func_*() instantiations and
are compiled -O2. Flipping the file recompiles them, and they stop matching. Here two matched bodies
(func_80184440, func_801848E4) sat interleaved among the 15 -O0 stubs. Cut around them —
[lo..matched), matched stays -O2, [after..hi) — and the image is byte-identical.
So the region bound is: (address range) MINUS (already-matched bodies), and a range with K
interleaved matched functions needs K+1 -O0 sub-regions, not one.
The instrument trap that hid it (and it is §124's shape again)
I first derived "15 contiguous -O0 functions, clean cut" by scanning asm/<ov>/nonmatchings/**/*.s
for the frame-pointer prologue (addu $fp,$sp,$zero / 21F0A003). A MATCHED function emits no
.s — splat writes none, because its .c carries real C. So that scan is structurally blind to
precisely the bodies that break the flip, and it reported a clean run where the range was mixed.
Derive the region's contents from the SOURCE anchors (INCLUDE_ASM stubs and DEFINE_func_*()
instantiations, in address order), never from an asm-file scan. corpus.stubs gives the stubs; the
DEFINE_func_ instantiations in the region .c give the matched ones.
The mechanics
- Cuts:
jr_isolate_all'splan()/build_new_config()already accept arbitrary cut vrams — region naming is purely positional, so nothing new is needed for the split itself. Inject the cut list and reuse its source-repartition, carve-repoint and ascending/unique validation verbatim. - The one-carve-per-region law still applies: every already-banked jr in the object must ALSO be
a cut, or two carve owners share one object and its single contiguous
.rodatamust host both. - Naming + the Makefile: name each
-O0sub-region<ov>_o0<letter>and let ONE widened rule select them — the glob is now$(wildcard src/ov_*/ov_*_o0?.c)(was_o0b). A missed-O0rule is SILENT: the region compiles-O2and every residual it produces is a pure artifact (§116).corpus.o0_sources()parses this rule and resolves?via glob, so the-O0oracle stays honest. - Verify the routing, don't assume it:
corpus.is_o0("src/<ov>/<ov>_o0c.c")must return True before you read a single residual from that region.
Method note
Probe 2 changed the name and the flag and was therefore uninterpretable. Probe 3 — same name as the proven-neutral probe 1, flag only — is what produced the answer. One variable per probe, and keep the previous probe's proven-neutral configuration as the control.
§126a — a bare except: continue around a coverage-asserting oracle re-creates the silent skip (P30 S28)
Sizing this cluster, I reported "275 open stubs across 18 overlays". The true figure is 2,184 across 138 — an 8× under-count that would have mis-scoped the whole task.
The scan was:
for ov in overlays:
try: st = corpus.stubs(ov)
except Exception: continue # <-- the defect
and it ran while make extract-all was rebuilding in the background, so corpus.stubs() hit its
R32 coverage assertion ("N stub(s) have NO .s on disk — the tree and the source disagree") for most
overlays. The bare except turned every one of those loud refusals into a silent skip, and the loop
happily reported a total over the ~18 overlays that happened to be re-extracted already.
Two rules, both already ours, both violated at once:
- A measurement taken during a rebuild is not a measurement. Earlier the same session the same
mistake was caught because
corpus.pyrefused — the assertion worked. Here I wrapped the assertion inexcept: continueand threw its answer away. - R32 lives in the CALLER too. A coverage-asserting oracle only asserts coverage if the caller
lets it raise.
except Exception: continuearound it is precisely the silent-skip class R32 exists to delete — reintroduced one level up, where no audit looks.
Practice: in any scan that will SCOPE work, let the oracle raise. If some binaries legitimately have no data, filter them by an explicit predicate you can state, and print the count you skipped and why. A total is only trustworthy if the denominator was asserted.
(It also cost credibility in the other direction: I used the bad number to call the T0(f) pin of "2,192 open members" STALE. The pin was right. Re-derivation is only worth more than a carried number if the re-derivation is sound — R35 applies to the re-measurement as much as the original.)
§127 — The -O0 regime: the CONSTANT-OFFSET FOLD, and why -O0 needs its own idiom set (P30 T3 wave, 15 targets)
The -O0 population is now large (the P30 T2 routing put 2,200+ open stubs into -O0 TUs), and
a 15-target wave against it showed the cookbook is written almost entirely for -O2. The index
fired on only 3 of 15 targets, and multiple agents INDEPENDENTLY re-derived the same idiom — the
signature that a body of knowledge has a hole (§124's lesson applied to the index itself).
The idiom they kept re-deriving: the constant-offset fold
At -O0, gcc-2.7.2 folds a constant struct offset into the load's displacement but will NOT fold
an indexed one:
p->f /* -> lbu $v0, 3($s0) — offset folded into the load */
p[i] /* -> addiu $v0,$s0,..; lw 0($v0) — address computed FIRST, then a 0-displacement load */
So when the target shows lbu 3(reg) you must write the member access; when it shows
addiu + a 0-displacement load you must write the indexed form. At -O2 these converge and the
distinction is invisible — which is why nothing in §1–§126 covers it.
The rest of the -O0 regime (write PLAIN C, and mean it)
- Frame-pointer prologue
addu $fp,$sp,$zero(21F0A003) is the detector (§6/§116); every check needs--o0or you chase a phantom SIZE-MISMATCH. - Every local is spilled to the frame and reloaded at each use. A
0x18($fp)spill/reload pair is a real named local, not a compiler temp — declare it. - Load-delay
nops and redundantaddu rd,rs,$zerocopies are normal; do not "clean" them. - Do not hand-optimize, do not add temporaries to help the compiler. At
-O0the C maps almost 1:1 to the asm; the usual-O2steering levers (pins, live-length dials, statement reordering) are mostly inert and mostly a distraction.
§127a — §71 (sibling-first) is the strongest -O0 lever, and it beats the index
Several targets fell immediately to an already-banked sibling in the same TU. func_80184868
matched off the exact shape banked hours earlier in the same _o0d file:
s32 ret; ret = func_8001ABBC(0, 0, &D_xxxxxxxx, 0, 0); return ret;
Before drafting an -O0 function, read the banked functions in its own _o0* file. An -O0 TU
is a near-uniform code regime, so a sibling's shape transfers far more reliably than at -O2.
§127b — the knowledge was in a SOURCE COMMENT, not the cookbook
Two agents reported their decisive levers came from the header comment at the top of
src/ov_SC01_077/ov_SC01_077_o0.c, not from docs/. That comment is real, hard-won knowledge sitting
where only someone already editing that file will find it. When a lever is discovered in a source
comment, promote it to the cookbook and leave a pointer — otherwise every future agent pays to
rediscover it, which is exactly what happened here across 12 of 15 targets.
§128 — A raw NUL in C source makes grep SILENTLY SKIP the file (P30 S28, 137 files)
Symptom. grep -rn func_XXXXXXXX src/ returns nothing for a function that is plainly defined
in src/. corpus.stubs says it is not a stub (so: matched). The build is byte-identical. Every
statement is true and they look contradictory.
Cause. The file contains a raw NUL byte, almost always a control character written straight
into a character literal — the source reads == '<NUL>' where it should read == '\0':
if (*(char *)(p + 4) == '\0') { ... } /* correct */
if (*(char *)(p + 4) == '<NUL>') { ... } /* compiles the same; file is now BINARY */
file(1) reports data instead of C source, and grep treats any file containing NUL as binary
and prints nothing — no warning, no error, exit 0. The file disappears from every grep-based audit
and every hand search, invisibly.
Why no existing gate catches it. The byte-gate is structurally blind here (R34): the compiled
bytes are correct, so it has nothing to say. cc1 accepts the literal. check-all stays green.
The defect lives entirely in the readability of the source to tooling — a dimension no byte oracle
measures. Same family as §124 (a scanner that cannot see something reports it is not there) and
§126a (a bare except swallowing a coverage assertion), one layer lower: in the tool everyone
reaches for first.
Scope when it was found: 137 files. Every _o0c/_o0e region created in one session — a
templated body carried the NUL, so a single defective source propagated it fleet-wide in an
afternoon. A defect that is invisible to grep is also invisible to the review that would have caught
it spreading.
The oracle: tools/audit_text_sources.py / make audit-text-sources, in tools-health,
coverage-asserted over every tracked .c/.h (R32). Fix is '<NUL>' → '\0', then re-gate to
prove byte-neutrality (it is, but prove it).
§128a — a negative control must corrupt a SCRATCH COPY, never the tracked file
Proving the new guard fires, I injected a NUL into the real tracked file and restored it through
nested shell escaping. The restore left '\\0' — an escaped backslash, a multi-character constant,
not a NUL. That IS a semantic change, and R22 duly failed 139/140. Repaired and re-verified 140/140;
nothing was ever committed.
The rule: test a guard against a throwaway copy under .run/. A negative control that mutates
the artifact it is validating can introduce the exact defect it exists to detect — and the more
convincing the control, the more dangerous the restore. Corollary, learned the same minute: a
verification pipeline ending in grep -c PATTERN exits 1 when the count is 0, so the SUCCESS
case reports failure. Read the output; an exit status is not the oracle (§125's rule 1, again).
§129 — Post-carve, rtu_match/match_one COUNT THE JUMP TABLE AS INSTRUCTIONS; and a carve must never be committed without its owner (P30 S28, func_8013BD74)
Two independent traps, both hit banking one reach-138 jr function. Neither is a compiler wall.
§129a — the target instruction count is INFLATED after a carve
jtbl_carve moves the function's jump table into a dotted .rodata subseg, and splat then emits the
function's .s with a leading .rodata section holding the table, followed by .text:
.section .rodata
dlabel jtbl_801D828C <- 28 entries
enddlabel jtbl_801D828C
.section .text
glabel func_8013BD74 <- the actual 198 instructions
rtu_match reported mine=198 ins, target=226 ins, 206 mismatched — a catastrophic-looking DIFF.
226 − 198 = 28, exactly the table's entry count: the tool counted the data words as
instructions and diffed the body against them.
So a post-carve verdict from rtu_match/match_one is meaningless. A draft that verified
cleanly before the carve will read as a total mismatch after it, and the number will look like
evidence of a deep codegen problem. Verify the body pre-carve; after the carve, let the
whole-binary byte-gate arbitrate (it always was the arbiter — §52b). This is §81's warning one
step further on: §81 says match_one masks relocations so a jr fn's MATCH is not a bank; §129a says
that after the carve its DIFF is not a diff either.
§129b — never commit a carve whose owner is still a stub (it strands the carve)
harvest_verify refuses to run on a dirty tree (§97), and the carve necessarily dirties config/.
The tempting resolution — commit the carve, then bank on a clean tree — creates a stranded carve:
a .rodata carve piece with no matched owner. jr_inventory refuses immediately (R32):
committed .rodata carve ownership is not 1:1 (R32/R33) — a stranded/duplicated carve: [('UNOWNED', '0x801d828c')]
That is a coverage oracle correctly rejecting a state the commit created, and it blocks every later jr operation on that overlay until reverted.
The route for a jr function is the INTEGRATED one: tools/jtbl_family_bank.py, which does
carve → extract → remap → whole-binary gate per sibling inside one uncommitted transaction and
reverts on failure. The §81 hand-chain is for diagnosis; it is not a banking path, because its two
constraints (carve-before-bank, clean-tree-to-bank) pull in opposite directions.
The real blocker underneath, for the record
With the carve applied, splicing the draft fails cc1 with
jtbl_rodata_pads: more rodata .align directives than pad specs (2) — table-count drift vs the carve
— §8e's pad-spec filter failing loud, as designed: the object's committed spec
(0,4 tables=+0x0,+0x70) does not account for the table the newly-matched function emits. That is
genuine §8e work (re-derive the multi-table pad spec including the new owner), not a wall — and it
was only reachable after §129a stopped the phantom 206-instruction "diff" from misdirecting the
diagnosis.
§130 — An INCREMENTAL build can report BYTE-IDENTICAL for a change the CLEAN build cannot even LINK (P30 S28, the jr pair)
R22 has always said "verify from a clean rebuild." This is the sharpest instance yet of why, and it
cost two cycles because I used a fast in-loop gate that skipped make clean.
The setup. Two jr/switch functions, func_8013B83C (jtbl_801D8254) and func_8013BD74
(jtbl_801D828C), both in the SAME -O0 object ov_SC01_077_o0. Both bodies are byte-correct —
match_one --o0 and rtu_match --o0 each report MATCH (272 and 198 ins).
What the incremental gate said. Carve both tables in one jtbl_carve call (correct — it is
additive and re-derives the full span; the object's pad spec becomes [0,4,4]), splice both drafts,
make extract && make build → BYTE-IDENTICAL. I reported both banked.
What make clean said.
ov_SC01_077_o0.c:(.text+0x10f8): undefined reference to `$L105'
ov_SC01_077_jr_801588CC.o: in function `func_801596F0':
undefined reference to `func_8013C938'
It does not link at all — and note the second error: a previously matched cluster function becomes undefined. An incremental build reused objects that still satisfied those references; a clean one has nothing to reuse and the real state surfaces.
The rule, sharpened. A byte-gate result from an incremental build is not weak evidence — it can
be actively false, and false in the most convincing direction (a green SHA). §42b named the
stale-object trap for a FALSE FAIL; this is its mirror, a FALSE PASS on a change that is not even
linkable. Anything that touches config/ (a carve, a resegment, a split) MUST be gated by
make clean && make extract-all && make check-all before it is believed, let alone reported.
The residual class this exposes: two jr functions matched in ONE object. §8b already warns that a
single object contributes at most one contiguous .rodata run; the pad-spec machinery (§8e) handles
a multi-table span in principle, but matching both owners in the same -O0 object produced
unresolved local labels ($L105) from the C-emitted tables plus a collateral undefined symbol. The
integrated per-sibling path (jtbl_family_bank) banks ONE jr function per object per transaction and
has never hit this. Ledger class: JR-PAIR-IN-ONE-O0-OBJECT. The escape, untested, is §81 step 1:
isolate one of the two into its own code subseg first so each object owns exactly one table.
The diagnostic ladder that finally located it (reusable)
The function-level tools all said MATCH, so the signal had to come from the image:
- Build with the splice, build without, diff the two binaries.
- Compare each differing byte's vram against the function's own
[lo, lo+4*nins)range. Here: 3,749 of 3,791 diffs were OUTSIDE the function, first diff near the overlay's START, and the image was 57 bytes LONGER — the §8 signature of.rodatafloating to the front, i.e. "this function emits a jump table", not "this function's code is wrong." That size-and-location fingerprint distinguishes a codegen residual from an integration/layout effect in one build, and it is what redirected the diagnosis away from three wrong guesses.
§131 — The jtbl OVER-SPAN: sltiu N is ground truth in BOTH directions, and the zero-word rule only guards one (P30 S28, func_80191C50)
The carve had a symmetric blind spot. jtbl_range computes end = the next data dlabel, then:
- extends the span when the owning function's
sltiu Ndemands more entries than the dlabel supplies (§SPLIT-TABLE REPAIR — spimdisasm can cut one table in half); - trims trailing words that are zero, on the axiom "
0x00000000cannot be a jump target"; - warns when the span is shorter than an unambiguous
sltiubound.
Nothing handled a span that is too LONG for a non-zero reason. spimdisasm attributes to a dlabel
everything up to the next dlabel, and that remainder is not always zero — it can be ordinary data.
Then no trim fires, the carve reserves more words than the table has, the object supplies only the
real entries, and the .rodata piece under-fills.
The fingerprint (this is the reusable part). Under-fill does not look like a codegen bug:
image size: −4 (×N tables), often masked to −3 by the end-align TRIM
differing bytes: hundreds, in hundreds of 1-byte runs, spread over most of the overlay
position: ~95% at byte 0 (mod 4) — the LOW BYTE of a 16-bit immediate
value: every one changes by exactly −4
That is not "the function is wrong", it is every %lo in the image pointing 4 bytes low because a
data symbol moved. Bucket the differing bytes by offset % 4 and decode a few words: if the deltas
are uniform and small and land in the immediate field, you are looking at a layout/under-fill
problem, not codegen. (Measured here: 812 of 853 at pos 0 mod 4, all −4.)
The fix, and its authorization. Clamp end down to start + 4*N when the sltiu bound is
unambiguous (exactly one — a multi-switch function cannot say which table owns which bound) AND
the surplus words are not plausible code addresses. If any surplus word is in the overlay's text
range, REFUSE and say so: it might be a real entry, and silently dropping one corrupts the image
in the opposite direction. Same standard as the extension path — act only on the program's own
statement, never on a guess.
Why it mattered: this was the single instrument failure that survived §125's retraction round — the one case where "the tool is broken" was actually true. It blocked a 710-instruction behemoth and, because a carve is per-overlay, it would have blocked every future jr family whose table happens to be followed by non-zero data. One clamp, byte-identical, behemoth banked.
§132 — The JR-PAIR-IN-ONE-O0-OBJECT "wall" was TWO instrument defects: a merged-double span the carve could not see, and a truncated object no rule deleted (P30 S29, func_8013B83C + func_8013BD74)
S28 ledgered a new residual class: two jr functions matched in ONE -O0 object produced
undefined reference to $L105 + undefined reference to func_8013C938 from a CLEAN build, while an
incremental build reported BYTE-IDENTICAL (§130). The escape recorded was §81 step 1 (isolate one into
its own code subseg). Both the class and the escape are REFUTED. Neither function is on a compiler
wall; both banked from a clean fleet with no isolation at all. Class RETIRED — the fourth
consecutive "structural wall" to resolve to our own tooling (§124/§125/§126/§131 are the others).
Defect 1 — a pre-§8e MERGED DOUBLE is not a single-table predecessor
jtbl_carve reconstructs a touched span's table starts from the union {new fn's .s, surviving stub
.s, the committed tables=, --span-tables, --like}. When a span has no JTBL_PADS line, the
tool infers "SINGLE-table predecessor, so the span start IS the table start" (§8e-2, correct for the
case it was written for). ov_SC01_077_o0's carve at 0xb01a4 predates the tables= persistence and
is a merged double — func_8013C0F8 ($L75) and func_8013C414 ($L105), the second invisible
because a matched owner's stub .s is pruned by make extract. So the tool derived 3 starts where the
object emits 4 tables, wrote JTBL_PADS := 0,4,4, and jtbl_rodata_pads refused mid-stream —
correctly, with the exact message ("more rodata .align directives than pad specs").
Fix (single choke point, spec_from_starts): assert the starts EXPLAIN the span, and recover what
is missing from the payload — every zero word INSIDE the span is an original .align 3 pad (the same
axiom the pad rule already rests on: a zero can never be an ENTRY), so the word after it STARTS a
table. Recovery is a no-op wherever the structure is already known, so committed-green spans are
untouched. Honest limit: only PAD-SEPARATED boundaries are recoverable; a tight (0-pad) interior
boundary is indistinguishable from a continuing table in the payload — but it then makes the spec
SHORT, which the filter rejects loudly at build time. Never silent in either branch.
Defect 2 — as writes a corpse and nothing deletes it
as consumes a PIPELINE. When an upstream stage dies mid-stream, as has already assembled the
prefix and written a truncated .o (its only complaint is Warning: missing .end at end of assembly). make reports Error 1 correctly — and then leaves that object on disk, NEWER than its
.c. The next build considers it up to date and links it. Measured here: 12 of 16 T func_
symbols, and undefined $L57/$L59/$L63/$L75/$L76. That is the entire mystery: the link error is one
build DOWNSTREAM of a loud, correct, attributable compile error. Fix: .DELETE_ON_ERROR: in the
Makefile (negative-control-proven: make: *** Deleting file ...). Same family as §42b/§130 — an
artifact that outlives the command that failed to produce it.
The fingerprint, and the 30-second ladder that found it
An undefined $L<n> in a LINK error is never codegen. $L labels are gcc-local: the assembler
resolves them within the object, so one can only be undefined if the stream that defined it was cut.
Read it as "an object is truncated", not "a function is wrong".
The ladder — compile the ONE TU standalone through the real pipeline and let it name the owners:
cpp … | cc1 -O0 … | maspsx … > t.s # no filter, no carve: one variable
grep -n '^\.section \.rodata' t.s # how many tables does the object ACTUALLY emit?
# for each hit, the nearest preceding `.ent` is that table's OWNER; `$L<n>:` is its label
Four blocks (13/27/27/27 entries) against a 3-start derivation, and $L105 attributed to
func_8013C414, in one command — before any build, carve or isolation. Then confirm against the
ORIGINAL payload: table starts 0x801D8254 / 0x801D828C / 0x801D82FC / 0x801D836C, each preceded by a
zero pad word, span 0xb00fc..0xb0280 = 388 B = 52+4+108+4+108+4+108 — the arithmetic closes exactly,
so the C-side table count and the payload agree and the spec is 0,4,4,4.
Result: func_8013B83C (272 ins) + func_8013BD74 (198 ins) banked in ov_SC01_077
(d19c9580), R22 clean-fleet 140/140, with the 137-sibling sweep unblocked (their _o0c spans DO
carry tables=+0x0,+0x70, so the recovery is a no-op there — the blindness was ov_SC01_077-only).
The transferable rule
A fail-loud guard is only as trustworthy as the artifact hygiene around it. A guard that refuses correctly but leaves a partial artifact behind converts its own honest error into a lie one build later — and the lie is more convincing than the truth, because it points at a different subsystem. When a loud refusal and a mystifying downstream failure appear in the same session, suspect they are the SAME event, one build apart.
§132a — --like is for a sibling with NO record; against one that HAS a record it over-derives (P30 S29, ov_SC07_010)
The 2×137 sweep banked 136/137 twice and failed on the SAME overlay both times
(consumed 3 rodata .align(s) but 6 pad spec(s) given). Cause: jtbl_carve --like <exemplar>
transfers the exemplar span's table structure and matches donor→recipient by the subseg's ROLE
NAME (the name with the overlay prefix stripped). ov_SC07_010's -O0 region is named _o0 —
the same role as the exemplar's, and the only other overlay so named; the other 136 are _o0c,
whose role never matched, which is the only reason the sweep worked at all. The exemplar had
just banked two more owners than this sibling has, so the transfer unioned its rebased 4 offsets
with the sibling's real 2 plus the new table: six starts for three emitted tables.
The rule: a sibling's own committed tables= is AUTHORITATIVE; --like exists for a span with
no record. Suppress the transfer when a record exists (jtbl_family_bank.like_arg, deriving the
subseg via jtbl_carve.func_subseg — the same derivation the carve uses, R33). The
incomplete-record case that --like used to paper over is now covered honestly by §132's payload
zero-word recovery, so nothing is lost by preferring the local record.
The shape worth remembering: the transfer was wrong for 2 of 138 overlays and inert for the
other 136 — so a sweep can be 99% green and still be running a defective rule. A per-sibling
failure that repeats on exactly the same sibling across two independent sweeps is a property of that
sibling, not noise — probe it rather than ledger it: here it cost one probe and returned 2 banks
plus a real tool defect. And note the near-miss: had ov_SC07_010 been named _o0c like its 136
peers, the defect would have stayed invisible until some later family whose exemplar shares a role
name with a recorded sibling — a silent wrong spec instead of a loud refusal.
§132b — When the span's already-matched owner is ITSELF multi-switch: --span-rel (P30 S1, func_8014032C)
§132's payload zero-word recovery closes the pad-separated case. Here is the case it cannot see, met head-on while sweeping the zero-crack tier's largest family (183 ins × 137).
The sibling gate-failed. The §132 ladder named it in one command: the object emits four tables
because both functions in it are multi-switch (8+5 entries each), while the carve derived three
starts. The missing start belongs to the already-matched func_8013FFD8, and neither oracle can
reach it — make extract prunes a matched owner's stub .s, and its second table abuts its first
with no pad (8 entries = 32 B ≡ 0 mod 8, so .align 3 emits nothing).
Note what is NOT wrong: the true pads [0,0,4,0] are exactly what natural alignment produces. The
build breaks only because a short spec gets written and then enforced.
The lever: jtbl_family_bank --span-rel d0,d1,… — table offsets relative to the FIRST NEW table,
which func_jtbls reads from the sibling's own .s. The family's layout is invariant (same code,
same entry counts; only the base moves), so one measured offset list serves every sibling. Verify
that invariance on ≥2 siblings before trusting it — one command, and it is the whole premise.
It is a per-family FLAG, not a default, and the failure is symmetric. The same sweep needed it ON for 127 siblings and OFF for 10: where the span holds only the new tables, forcing the extra starts over-specifies and the carve refuses ("table starts … do not fit the span"). Two passes — sweep with it, re-run the residue without — banked 137/137. A residue that fails the opposite way from the majority is a signal to flip the flag, not to add another lever.
§133 — The DEFAULT-FILTER class: three times in one session, a tool silently answered a narrower question than the one asked (P30 S1–S3)
Not one of these was a broken tool. Each was correct for its own purpose, and each silently scoped down a question that was asked more broadly — which is the same defect class as §124/§126/§132, one level up: not a wrong measurement, a mis-scoped one.
- My own analysis filter. Sizing the remaining ×138 work I required
nins >= 80("substantial"), reported "only 2 crackable fleet-wide families remain — the ×138 era ends", and wrote it into the phase plan as a structural signal. Re-run without the size cut: 33 families / 238,478 templatable ins, of which 30 (137,186 ins) were below my line. The conclusion was an artifact of a threshold I chose and then forgot I had chosen. worklist.mdprices byh_exactreach. A per-location PURE family shares onlyh_seq, so the worklist prices its head at ×1. Byte-proof: the day's banked pair was priced 272 and 198 ins and delivered 37,536 + 27,324;func_80176734, the single largest item in the frontier, sits at rank ~50 there. Rank family work by.run/family_hseq.jsontemplatable weight; the worklist is correct only for genuinely h_exact-reach functions.family_sweep --hseqdefaults to--band substantial. A propagation run over nine freshly banked heads — all under 80 ins — reported0 matched-exemplar familiesand banked nothing. The sweep was right; the band was the question it had been asked.--band allis the fix.
The practice. When a scan returns "nothing" or "far less than expected", the FIRST hypothesis is your own filter, not the world. State the filter out loud in the same breath as the number ("33 families with ≥100 members, any size"), and re-run once with it removed before any conclusion is allowed to shape a plan. A number that scoped a phase deserves the same instrument-check R35 demands of a probe.
§134 — MULTI-LINE BLINDNESS: one root cause, four faces, in family_remap's preamble scanner (P30 S6, 190 zero-crack families)
extract_unit builds a templatable unit by walking UP from a definition line, accepting lines that
"look like" preamble (extern / // / /* / * / typedef / blank). That scanner is line-at-a-time
over a language whose constructs span lines, and every construct that wraps was misread. Four faces,
all found in one session by probing eight zero-banked families with ONE build each (.run/s6_diag.py
— remap → splice → build that overlay → read the compiler's own error → revert):
| face | the construct | what the scanner concluded | the symptom, in the sibling |
|---|---|---|---|
| D1 | a block comment containing { — e.g. * => { u16, u16, s32 } |
the {-guard (a real T65 fix for extern void f(int); int g(){…} on one line) fired on documentation |
carry stops MID-comment; the draft opens with * … and an unmatched */ → parse error before 'the' |
| D2 | a wrapped DECLARATION — extern void func_801466F0(s32 a0, …,⏎ s32 sp8); |
_def_head_at's fallback: "param list continues past this line ⇒ ANSI definition" |
line 457 accepted as a definition head → unit is a 16-line fragment with no body (a neighbouring DEFINE_func_*(), an #include, a comment), closed by the { u16, … } inside that comment → 0/137, reads as a compiler wall |
| D4 | a wrapped asm-label alias — extern void aF…(…,⏎ s32 sp8) __asm__("func_801466F0"); |
_alias_decl_for is a single-line rx.match |
the definition is invisible → "no matched unit" |
| D5 | a multi-line typedef struct {…} T; |
the backscan meets the CLOSING line } T; first; } is not an accepted prefix, so it stops |
the type never travels → T undeclared, measured at 17 families / 24,332 templatable ins |
Why it matters more than four bugs. D2 and D5 produce a silent, uniform, whole-family failure — exactly the shape a compiler wall produces. The tell is bimodality: S6's first sweep banked 842/2,735, and the residue split 57 families all-banked / 52 zero / 8 partial. Per-member codegen residuals do not cluster like that; one per-family blocker does. Bimodal bank rates are a tooling signature — probe one member before writing the family off (R35, and the fifth consecutive "structural wall" in this project to resolve to our own instrument after §53/§124/§126/§132).
The fixes (all in tools/family_remap.py, all blast-radius-verified against the pre-fix tool by
diffing extract_unit output over every affected exemplar — 157 of 181 byte-identical, the rest
changed only in the intended direction):
_def_head_at(ln, paren_idx, more=())— the caller passes the following lines, so a wrapped list closes and the same;-vs-{test answers correctly. With lookahead supplied and no close, it returns False ("unknown" is not "definition"); with no lookahead it keeps the historical answer, so un-updated callers cannot silently change behaviour.- the
{-guard exempts comment-only lines (*,//, or an unclosed/*), plus an R32 backstop that drops-and-announces a preamble that still opens inside a comment. - the forward body scan counts braces in
cdecl._masked text (R33: one masking oracle), so a brace inside a comment or string can never close a function body. _typedef_block_start()— on a} T;closing line, walk up to the matchingtypedef(both thetypedef struct {and brace-on-its-own-line forms) and carry the WHOLE block. Only blocks that literally begin withtypedefare carried: a struct VARIABLE (struct {…} g;) closes identically and defining it in the sibling would be a duplicate global. Safe becauseharvest_verifyalready strips a typedef the sibling TU provides (cdecl.strip_provided_typedefs), so a duplicate cannot break the sibling — and the whole-binary gate remains the sole arbiter (G3/P9).
Measured payoff, one command each: D1+D2 recovered +323 members from families that had banked
zero; D5 then banked the 139-member func_8012B77C family 139/139 (8,062 ins) and took
func_80128C98 to 137/275. S6 total: 1,582 members off a population that the pre-fix tool scored
at 842. D4 was measured (1 exemplar / 3,288 ins, with a second blocker behind it) and deliberately left
UNFIXED — but it now returns None, so the sweep reports it as skipped {'no matched unit for func'}
rather than failing 137 times. A known gap that announces itself is not the same defect as a silent
one (R32).
The general practice. Any scanner that classifies C by reading one line at a time is wrong on wrapped declarations, wrapped attributes/asm-labels, multi-line typedefs, and comments containing braces — and its failures will look like walls, not bugs. Give it lookahead, mask comments/strings before counting delimiters, and when a family banks 0/N with every member failing identically, read one compiler error before believing the compiler.
§135 — Six byte-verified gcc-2.7.2 idioms from the P30 S6f-h waves (and the two-lane wave shape that found them)
Distilled from ~100 agent-drafted functions gated whole-binary across three waves. Each idiom below CLOSED a specific residual — none is a hypothesis.
The codegen idioms
-
UNSIGNED switch index ⇒ pure equality chain (no range test).
switch (*(s32 *)(p+0x30))over cases {0,1,2,3} emits a balanced tree with ansltirange split;switch (*(u32 *)(p+0x30))emitsbeq 1 / beqz / beq 2 / beq 3with no comparison at all. For an unsigned index the case-0 leaf satisfiesnode_has_low_bound(0 == TYPE_MIN), soemit_case_nodesdrops the bound test. If the target's switch has no range check, read the index UNSIGNED. (func_8017C910, 92 ins.) -
ARRAY_REFvsINDIRECT_REFchanges ALIASING, therefore scheduling. Writing a field read asa0[0x46]makes it an ARRAY_REF, which setsMEM_IN_STRUCT_P; gcc-2.7.2'strue_dependence()then DROPS the dependence between an in-struct varying load and a not-in-struct constant-address store, and the load hoists. Writing the same read as*(s16 *)((s32)a0 + 0x8C)keeps it a plain INDIRECT_REF and restores the dependence. A 4-instruction "scheduling residual" that is really a type-form choice. (func_8017CFE0.) -
A constant store whose top bit is set in the STORED width needs an UNSIGNED destination.
*(u16 *)p = 0x8C00emitsori(viaforce_fit_type, the value stays positive);*(s16 *)p = 0x8C00folds to sign-extended −29696 andliemitsaddiu. If the target materializes the constant withori, the destination is unsigned. (func_8018A0F4.) -
The list scheduler PRESERVES the relative order of disambiguable stores. A store written late in source SINKS to the end of the block rather than hoisting into load-delay slots. Two field-zero stores had to be written ABOVE a three-halfword copy to land at the target's indices — 9 mismatches → 0, no permuter. When a store lands too late, move it earlier in SOURCE. (
func_8017CAB4.) -
A
shortloop counter blocks strength reduction on array indexing.for (s16 i…) p[i].f0emits a sign-extend + multiply chain; walking explicit pointers (p++,q++) reproduces the original's biv/giv set. The 1998 source walked pointers. (func_8017C738, 96 ins.) -
Frame size off by a constant ⇒ DEAD LOCALS, not a codegen bug. A draft that is structurally exact but yields a
0x40frame against the target's0x50shows everyspdisplacement off by exactly0x10— the original declared locals ahead of the live ones. Add the padding declaration. The tell is that ALL diffs aresp-relative immediates of one constant delta. (func_8017FD58.)
The integration idioms (these decide whether a byte-correct draft BANKS)
match_oneMATCH ≠ a bank. It compiles standalone and cannot see the TU's other declarations. Measured across the three waves: 83% → 93% → 71%(+reconcile to 89%) of agent MATCHes survived the whole-binary gate. Always finish on the whole-binary gate (G3/P9).- cc1 reports only the FIRST conflict, so a draft can look one edit away and hold three more. One draft had invented prototypes for six symbols the TU declares, two BELOW the splice point. grep the whole TU for every symbol the draft names, in one pass.
- An interior address has no symbol.
D_801DA0F0does not exist — it is offset0x6CintoD_801DA084. Alui/addiupair can build an INTERIOR pointer; declaring the interior address as its own extern givesundefined referenceat LINK, not a compile error. Find the containing symbol in the data.sand index into it. (func_8017C5F0.) - Never redeclare a C-library name. A draft's own
memcpyprototype collided with the TU's — which declaresmemcpythree times with incompatible signatures. (func_8018A860.)
The wave shape that produced these
Two lanes, and the reconcile lane is the reliable one (12/12 across two waves).
- Draft lane: cheap tier (Haiku ≡ Opus at ≤~50 ins, ~4.8× cheaper —
cheap-tier-ab-validated), Opus direct ≥90 ins, Opus escalation in between. 17 of wave-1's 20 banks were Haiku. - Reconcile lane: for every gate failure, the orchestrator captures the compiler error first and embeds it. Agents cannot run the gate, so without the error a declaration conflict reads to them as a codegen wall — S29's law, re-confirmed. With it: 12/12.
- Between waves, do all three: paste args from a DERIVED manifest (never typed — a hand-transcribed path list cost wave 1 three agents' time); capture blockers; and promote wave-N's Opus discoveries into wave-N+1's cheap-tier prompt. Bank rate 83% → 93% on that alone.
- Concurrency hazard: if N reconcile targets share ONE TU, FORBID agent builds — concurrent splice-builds clobber a tracked file. Allow the single permitted splice-build only when targets are spread across TUs. (Guard the campaign, not the process — §the S27 law.)
- An agent that rejects your premise is working correctly. Told a draft was byte-correct and only
declaration-blocked, one agent re-ran
match_onefirst, found a real 1-instruction DIFF, and fixed both. Hand agents the evidence, not the conclusion.
§136 — The LOCAL-VARIABLE lever: how many C locals, at what scope (P30 wave 4a, 25 byte-verified banks)
Distilled from the wave-4a index-gap reports (33 drafted, 23 banked whole-binary first pass). The wave's dominant finding, and the reason this section exists as a class rather than a list:
In the 60–120-instruction band, most "regalloc/scheduling residuals" are decided by HOW MANY C LOCALS YOU DECLARE AND AT WHAT SCOPE — not by register pins. gcc-2.7.2 allocates one pseudo per C local;
local-alloc.c:472refuses a local allocno whoseREG_N_DEATHS > 1, promoting it to a global allocno that is ranked by density and loses the low register. So splitting one reused local into two, or merging two into one, moves whole register assignments — deterministically, at zero blast radius. Reach for the local-count lever BEFOREregister __asm__pins.
One report makes the anti-case explicit: for a redundant move $sN,$sM the pin is the wrong lever
— it acquires the register but lets gcc reuse it destructively as the sign-extension scratch. The
right lever was hoisting the assignment above the call (func_80183394).
The splitting/merging rules (each closed a residual, byte-gated)
- One local reused across N arms/repetitions ⇒ SPLIT it per arm. A function-scope pointer used
in 3 if/else arms dies in 3 places, fails the local-alloc gate, becomes a global allocno and loses
the low reg to a block-local constant. Per-arm block-scoped locals fix it in one edit. Symptom:
the same
$v0/$v1pair swapped in ONE arm only, siblings byte-correct. (func_8018C96C; same mechanismfunc_801909D8,func_80183A14.) - A compound initializer holding two values ⇒ SPLIT into two statements when you have one
callee-saved register too many.
x = *(u8*)p << k;makes a load pseudo and a shift pseudo, both live across the call ⇒ two$sregs;x = *(u8*)p; x = x << k;reuses one ⇒ one. Symptom: an extrasw $sNin the prologue, frame size otherwise identical. (func_80184FA4.) - Two variables where you wrote one ⇒ the target keeps a copy you cannot reproduce. An extra
addu $vX,$v0,$zeroright after ajalplus a later copy of the same value = the source hadv1 = f(); rnd = v1;withv1pinned. An unpinned pseudo always coalesces the pair away — verified: removing the pin merged them and cost 27 instructions of drift. (func_8017BF88.) - A local's address in a callee-saved base register ⇒ write a POINTER local, assigned before the
loop and used only inside it. Writing
local.fieldeverywhere addresses$sp-relative, allocates no register and shrinks the frame. Symptom:LENGTH-DRIFTshort by anaddiu $sN,$sp,Kplus one save/restore pair. (func_8017DAC4.) - A symbol read at a constant offset, but built into
$s1bylui/addiu⇒ cache it in a pointer local (u8 *p = D_80078E78;). A directD_xxx[0x1A]folds%loper use, loses the pin and shrinks the frame 0x20→0x18. (func_8017C61C; extends §17.) - Local stack slots are assigned in DECLARATION order, ascending from the outgoing-arg area
(0x10) — independent of use order. Frame-offset drift with correct code is a declaration-ORDER
problem. (
func_8017D2B8; complements §135-6, where the delta is a dead local.)
The type-form rules
- A real
mult $rX,$rYwith a small constant ⇒ the multiplier is a NON-CONST LOCAL, not a literal. A literalx * Kalways goes throughsynth_mult(sll/addu/subu chain). Assigns32 r = K;as its own statement before the first multiply:expand_multthen sees a REG, and CSE cannot fold it back becausemulsi3has no immediate form. Bonus — thelilands in whichever basic block the assignment is in, so its position in the.stells you where to put the statement. (func_80183B04. The inverse of the existing synth_mult entry.) - A negative addend on a narrow field coming out as
li $sN,0xfff0+addu(target:addiu $vN,$vN,-0x10) ⇒ gcc narrowed the whole expression to HImode, where the negative constant is its 16-bit unsigned image and no longer fitsaddiu. Fix: hoist the call to its own statement and put the load+subtract in a block-scopeds32temp. A signed*(s16*)load does NOT fix it (it reassociates); a function-scope temp does NOT fix it either. (func_801840FC.) lhu+sll 16+sra 16+Non a stack local an out-param call wrote ⇒ the local isu16, read as(s16)x >> N— the combiner folds the sign-extendingsra 16into the user shift. Through a PsyQSVECTOR(short vx) you getlh+sra Ninstead. **A scratch vector read back
⚠ RULE 9's CURE IS BYTE-REFUTED — see §197-A (P31 S54).
u16 v[4]andSVECTOR vcompile BYTE-IDENTICALLY in rule 9's own stated context (lhu ; sll 16 ; sra 23in both); the declared type is inert. The residual is real, the tell is right, the cure is a zero-byte asm re-tie. sign-extended-then-shifted must beu16 v[4], NOTSVECTOR.** (func_8017D2B8.)
- An unexplained
addu $vX,$aY,$zerobefore a conditional branch, with the two feedinglhloads in the wrong order ⇒ the value is ans16LOCAL, nots32.LOAD_EXTEND_OPfolds the sign-extend of an already-lh-loaded HImode pseudo into a plain move, giving a second pseudo for the arithmetic while the comparison keeps the original. (func_80183EF8.) LENGTH-DRIFT +1with a narrow load of the SAME stack slot (lh 0x12($sp)besidelw 0x10($sp)) ⇒ gcc-2.7.2 narrowed a memory-operandlocal >> 16into a sign-extending halfword load at +2. Bind the local to ans32temp used twice to force onelw+sra. (func_801834BC.)andi $vN,0xffffright after ajalthat the target lacks ⇒ the TU declares the calleeu16/s16-returning and gcc re-extends the return value. Do NOT change the declaration — call through a cast. This is §135-9 applied to the RETURN axis, not the arguments. (func_8017E26C.)
The scheduling rules (refining §135-2 and §135-4)
- §135-2's
MEM_IN_STRUCT_Plever does NOT apply when the blocking store has a VARYING address.true_dependence()only drops the edge for a non-varying (constant-address) store. If the load must hoist above stores through a different register base, the only lever is source order — assign the load to a temp ABOVE the stores. This is the load-side dual of §135-4. Tell: one extranopin a load-delay slot plus constants landing in$v0instead of$v1. (func_80182F00.) memrefs_conflict_ptreats$sp-based and register-based MEMs as CONFLICTING, so a register load cannot hoist past an$spstore — but two$spstores at different constant offsets ARE disambiguable and reorder freely. Reading store order as literal source order will send you down a wrong path; the load/store base-class asymmetry is the discriminator. (func_80183B04.)- An unfilled load-delay
nopwhere the target fills it with a trailing call's argument setup ⇒ hoist a LOAD, don't chase the arg setup. Split a read-modify-write (*p = *p + 1) intov = *p + 1; … *p = v;so its load rises above an intervening pointer chase; the chase'slwthen fills the slot and the freed arg-setup instructions cascade into the earlier nops. Store order is unchanged, so it is byte-safe. (func_8019064C.) - Prologue
sw $sNstores in REGNO order where the target has DEF order ⇒ the saves are anti-dependent on each register's first def, so emission order tracks def order. Aregister __asm__pin on the incoming parameter turns itsmoveinto a schedulable body instruction that loses priority to the%hiaddress chain and reshuffles the whole prologue. Pin loop variables; NEVER pin the incoming parameter. (func_8018613C.) - An extra induction register (3 IVs where the target has 2) ⇒ do NOT write the second pointer.
Write ONE pointer and address every field as
p + const;combine_givsmanufactures the representative itself. And when the preheaderaddiu rIV,rBASE,Kbuilds the WRONG K, that is the combined-giv ANCHOR choice:record_givprepends andcombine_givstakes the list head, so the last-emitted reference in the body anchors — move the statement whose final reference sits at the target's K to the END of the body. (func_801862A8,func_8018B128; §3-Giv/§70 keyed by symptom rather than by the word "induction".)
The declaration surface (integration, not codegen)
conflicting typesfor aD_symbol whose declaration you cannot find in the split.c⇒ it lives inside aDEFINE_func_*()macro body insrc/shared/engine_core.h. Grep the macro bodies for everyD_symbol your draft names and reuse the canonical type verbatim. In particular an 8-byte-stride table declared there ass32 D_x[][2]must be indexed[i][0]/[i][1]— do NOT declare splat's interior label (D_x+4) as its own extern; that both conflicts and duplicates. Extends §135-7 to the shared-header declaration surface. (func_801854C4.)- A
lui+oripair whose halves are both small (e.g.0x8000A8) and which resolves to no symbol is a PACKED COORDINATE LITERAL, not an address. Confirm against a sibling TU's call. (func_8017CDB0.)
Wave economics (measured, for the next batch's sizing)
33 targets · 46 agents · 4.44 M tokens · 29 min → 29 claimed MATCH, 23 banked whole-binary (70%).
Bank rate by the tier that produced the FINAL draft (derived per-function from the journal + the
gate, NOT read off the workflow's by_tier, which counts claimed matches and therefore sums to 29
rather than 23 — R37):
| tier | banked / attempted |
|---|---|
| Opus direct (≥90 ins) | 10 / 14 |
| Haiku direct (≤89 ins) | 3 / 8 |
| Opus escalation after a Haiku miss | 10 / 11 |
The operative number is the escalation rescue rate: 10 of 11. On a 60–120-instruction pool the
cheap tier closes outright only ~3/8, so Haiku here is a triage stage, not a substitute — it is ≡
Opus at ≤~50 ins (cheap-tier-ab-validated), and this band is above that line. The two-lane shape
still pays because the escalation almost never fails; route ≤50 ins to Haiku and expect to pay for
an Opus pass on most of the 60–120 band.
index_hit was 13 true / 18 false — the index is now the bottleneck the cookbook itself was in
wave 1, which is why the 31 gap reports above are worth more than the matches.
§136a — Blocker capture: classify on the OUTPUT, never on the exit status
The reconcile lane only runs 12/12 because each agent is handed the compiler's own error line (§135, the S29 law). Capturing those lines needs one care point, learned the hard way this session:
make build runs check, so a draft that COMPILES PERFECTLY and merely produces different bytes
also exits non-zero. A capture tool that branches on returncode == 0 to mean "compiled fine ⇒
byte DIFF" therefore has an unreachable branch, and silently files every genuine byte-DIFF under
"unknown". Classify on what the build PRINTED:
| what the output shows | class | route |
|---|---|---|
a non-warning line matching error / conflicting types / undefined |
PLUMBING | reconcile lane — hand the agent the line verbatim |
no compiler error, but [FAIL] / got <sha> / want <sha> |
DIFF | redraft lane — the C is wrong, not the declarations |
| neither | UNKNOWN | investigate; do not route |
Note the filter must exclude warning: lines: the same conflicting types for … text appears as a
warning for built-ins (memcpy) and for benign external-decl mismatches, and those do NOT block
the bank. Only the hard-error form does.
Measured on wave 4a's 10 gate failures: 7 PLUMBING / 3 DIFF. That ratio is why the capture step is worth its ~10 builds before any reconcile fan-out.
⚠️ CORRECTION (earned the hard way — do not repeat my error). I first wrote that this meant "70% of the refusals were paperwork, not codegen." That is wrong, and a reconcile agent refuted it against the bytes. A PLUMBING verdict means only that a declaration conflict EXISTS — the conflict aborts the compile, so the byte question is never reached and the capture says NOTHING about whether the body is correct. Two of the three second-round PLUMBING drafts had a real codegen residual hiding behind the declaration conflict:
func_80188694wasDIFF/4 SCHEDULE-REORDERon the untouched draft (closed with a §21 zero-byte re-tie barrier after six other variants failed), andfunc_8018C638wasDIFF/6 ADDRESSING/cse(closed by hoisting a store above a call). Both agents ranmatch_oneon the unmodified draft FIRST, found the body defect, and said so instead of accepting my premise.So: PLUMBING ⇏ byte-correct. Route it to the reconcile lane, but tell the agent to re-verify the BODY before assuming only declarations are wrong — the wave-4b reconcile prompt's "do not rewrite the body unless you prove it is actually wrong" is the right instruction precisely because it leaves that door open. An agent that rejects your premise is working correctly (§135).
The classification was EXACTLY predictive, which is the point: all 7 PLUMBING banked through the reconcile lane; all 3 DIFF stayed stubs. Wave 4a therefore closed at 30/33 = 91% (23 first-pass
- 7 reconciled), and the reconcile lane is now 19/19 across three waves at ~13× lower token cost
than drafting (329 K for 7 fixes vs 4.44 M for the wave). Every agent found the reported conflict
PLUS a hidden second one cc1 never reached — which is the mechanical reason the "grep the whole TU
in one pass" instruction (§135-8) has to be in the reconcile prompt, not just the drafting prompt. Tool:
.run/s7_capture.py(any overlay, any draft dir; the ov_SC01_077-only.run/uc_capture.pyis its ancestor). It reverts the TU in afinally:— a killed process performs no undo (the S27 law).
Corollary — mandate a PRIVATE scratch path in the agent prompt. A wave-4a reconcile agent chose
.run/s7/scratch/spliced.c on its own initiative; a concurrent agent in the same wave overwrote it
mid-run, so its first verification compiled another agent's TU and returned a meaningless rc=0.
It caught the swap only because the emitted .s did not contain its own function. This is the
Phase-28 match_one fake-isolation defect recurring one level up — at the AGENT layer, where no
tool fix reaches it. Any parallel wave whose agents may compile must tell them to use a
process-unique scratch path ($$/pid-suffixed) and must never suggest a fixed shared one. A
shared scratch path does not produce an error; it produces a CONFIDENT WRONG VERDICT.
§136b — A prior wave's "genuine byte-DIFF" verdict is NOT reliable evidence (4 of 4 refuted)
FINAL TALLY: 8 of 8 DIFF-ledgered functions banked on redraft — wave 3's four, plus the three I classified DIFF from wave 4a's capture, plus one from wave 4b. The classifier is not wrong about what it measures ("this draft compiles clean and produces different bytes" is true and useful); it is wrong to read that as "this function resists matching." A DIFF verdict is a fact about one draft.
Phase-30 wave 3 ledgered four functions as genuine byte-DIFF — the class we treat as "real codegen residual, redraft is unlikely to help." Wave 4b re-drafted all four with fresh agents. All four banked. The recorded causes were not codegen at all:
func_801845B0— the prior draft readbeqz $v0, .L8018467Cas an inner early-exit when.L8018467Cis the epilogue, so it hoisted the whole tail out of the enclosingif. A control-flow misread. Resolve every branch TARGET LABEL to its actual instruction before trusting a prior draft's nesting: a branch to the label that beginslw $ra,K($sp)is a RETURN, not a join.func_80184A94— a D2 declaration conflict (D_801BBB78declared scalar in the draft, array in the TU at a line below the splice point). Fixed by copying an already-banked family sibling's declaration forms verbatim (§71 sibling-first).func_8017BEBC— the cached Ghidra seed was an entirely different body; the prior draft had followed it. The.swas the only usable source.func_8018480C— likewise re-derived clean.
The rule this establishes: a DIFF verdict describes the draft that was attempted, never the function's matchability. It is a statement about one attempt by one agent at one moment. So:
- Never retire a target on a DIFF verdict. Route it to the REDRAFT lane, not to a wall ledger.
- A RETRY note must be handed to the agent as a data point, explicitly labelled as one — the wave-4b prompt said "treat that as a data point, not a verdict; re-derive from the .s", and every retry agent did exactly that and refuted it.
- Re-GATING an unchanged draft is not a retry. Wave 4a's three DIFFs stayed stubs through a second gate purely because the same bytes were resubmitted; they still owe a redraft.
- Corollary for the backlog generally:
docs/backlog.mdentries carrying an old closeness/class are stale by construction (Phase-29 measured 77% of stored drafts had decayed). Re-verify before valuing one.
§136c — SIBLING-FIRST is a DERIVATION shortcut, not just a conflict fix (the fastest route in a family wave)
§71/§D1 are written as remedies for conflicting types. Wave-4b agents found their far higher-value
use: before deriving anything from the .s, grep src/shared/engine_core.h's DEFINE_func_*
macro bodies — and the target's own TU — for a byte-verified NEAR-TWIN. In a family wave the
twin usually exists, because that is what a family IS.
Measured instances this wave:
func_801859D8—DEFINE_func_80185978()inengine_core.his a near-twin: identicala0layout, identicalD_801B8748[D_801B8788[*(s16*)(a0+0x70)]][0]chain, differing only in three store values and a trailing call. Reusing its expression forms verbatim reproduced the schedule with no intervention — first-draft MATCH, and the same twin generalizes to the whole 10-member family.func_80184A94— copying an already-banked family sibling's declaration forms verbatim (extern u8 D_x[]used as(s32)D_x; the__asm__data alias) is what made it bank after a prior wave had ledgered it a genuine byte-DIFF.func_8018CB18—func_80180CC0/func_80185C6Cin the same TU fixed the whole tail shape and the u16-compare form before a line was written → first-attempt MATCH.
The rule: a byte-verified sibling is stronger evidence than the decompiler seed AND cheaper than
deriving from the .s, because its expression forms are already proven to produce the gcc-2.7.2
schedule and register assignment you need. Search order for a family target:
engine_core.h DEFINE_* near-twin → same-TU banked sibling → the .s → the Ghidra seed (last:
this session it was byte-proven to be an entirely different body twice).
§136d — Four gcc-2.7.2 levers the redraft lane found (each closed a residual no other lever moved)
These came from re-deriving four functions that had been ledgered "genuine byte-DIFF" (§136b). Each is byte-gated and none was reachable from the symptom index at the time.
-
RC-12, the
$0-add OPAQUE COPY — for a copy-pair whose COMPARE reads the wrong register. Symptom:REGALLOC-PERM, one register, on a copy pair — the target'sbeqzreads the SOURCE's register while every plain-Cb = a;spelling makes the compare read the COPY's. Cause:cse.c make_regs_eqvpromotes the longer-lived copy to canonical andcanon_regrewrites every use. Lever:register s32 zr __asm__("$0"); b = a + zr;— an opaque copy CSE cannot see through. Do NOT pin the copy's source or dest to a real hard register: pinning the source perturbs the prologue's sign-extend temp, pinning the dest lets gcc propagate the hard reg forward and delete the copy — both cost 2 instructions elsewhere. (func_8017E978, the last 1-instruction residual.) -
gcc-2.7.2
jump.cCOLLAPSES an if-then-else into a conditional overwrite.if (c) t = A; else t = B;— both arms single SETs of the SAME pseudo — becomest = B; if (c) t = A;, hoisting the else-arm's%hi/%lopair ABOVE thebeqzand shifting the whole tail (LENGTH-DRIFT -3, 20 mismatches). Symptom to look for: one arm's%hi/%lopair appears BEFORE the branch, and the target'sj-over-arm shape is missing. Lever: write the selector as TWO SEPARATE CALLS, not a ternary/select feeding one call — a call is not a simple SET so the transform cannot fire, and post-reloadcross_jumpthen merges only the common[move $a0,$s0; jal]tail, which IS the target shape. Corollary: a branch-delay slot holdingmove $a0,$s0that sits BEFORE the two arms is the fingerprint of cross_jump tail-merging, not of a hoisted argument. (func_80184494.) -
When a load hoists above a CONSTANT-ADDRESS store of a
D_global, fix the STORE, not the load.true_dependencedrops the edge between a/svarying-address load and a non-/sfixed-address store. Force/sonto the STORE via a COMPONENT_REF:((struct { s32 w; } *)&D_801E7998)->w = 1;— unconditionalMEM_IN_STRUCT_P, address stays constant, identicallui $at/sw %lo($at)codegen. REFUTED axis, recorded so nobody repeats it: §135-2's "reshape the LOAD to an INDIRECT_REF" is the wrong half of the lattice here —*(a[i] + j)still earns/s(a top-levelPLUS_EXPRgrants it) and merely re-folds the symbol into the load's%lo, going 2 → 32 mismatched. (func_80184960.) -
A branchless flag is
-(a != b) & 0xFF, never a ternary.xor / sltu $zero,x / negu / andi 0xFFisstore_flagnormalised to −1 followed by a u8 truncation. Acond ? 0xFF : 0ternary emits a BRANCH and can never reach that shape. (func_8017E978.)
Also confirmed here (§76's inverse, previously unindexed): when a narrow load lands in $v0 but
the target uses a mid scratch register, reuse an EXISTING global allocno as the destination —
but reuse one whose live range ALREADY spans the arm. Extending a SHORT allocno into the arm
lengthens its live range, drops its global.c:594 allocno_compare rank, and swaps two grants
instead of fixing one.
§136e — §136c's PRECONDITION, and two more symptom keys (wave 4b batch 3)
Sibling-first has a precondition, and an agent hit it honestly: func_801899AC's family has
all 13 members still unmatched and no DEFINE_func_801899AC in engine_core.h — so there IS no
byte-verified twin, and the search is a pure cost. Check that a banked sibling exists before
spending the greps; in an all-nonmatchings family, go straight to the .s. §136c is the fastest
route when the family has already been opened, which in a family wave is usually but not always.
Two symptom keys that had no index entry:
-
LENGTH-DRIFT -2, where MINE returns via a bare branch to the epilogue but the TARGET emitsj+addu $vX,$vY,$zero⇒ this is §136-L1 on the RETURN axis. An over-scoped function-level temp became a global allocno and swapped$v0/$v1with the returned local, so the return no longer needed a move. Scope the temp inside the loop. (func_801899AC.) -
A loop increment sitting in the loop-back DELAY SLOT plus a compensating negative
addiuon the fall-through is a SOURCE SHAPE, not areorgartefact — MIPS1 has no annulling, soreorgcannot invent the compensation. Write it asp += 2; if (t == cur) break; … p -= 2;.combine'sreg_n_sets == 1guard is what stops theaddiu -8folding into the followinglw 4($a1). The index's delay-slot entries point atreorg, which is a dead end for this one.
Also demonstrated (composition, func_8017D5F4, 46 ins): flat early-returns instead of nested
ifs to get the cross-jump layout → s32 pad[2] dead locals to sweep the frame size → s16 locals
so each load emits its lh+addu copy pair → mask-first or operand order → three register __asm__ pins on the mask temps (local-alloc otherwise takes $v0/$v1/$a3 and shifts the whole
global assignment) → two zero-byte __asm__ re-ties from the §30 toolkit → and finally
tools/permuter/run_masked.py on a pin-carrying base for the last 2. The toolkit composes; the
permuter is the LAST step on an already-pinned base, not the first.
Second correction to the capture classifier — DERIVE the class, do not pattern-match error prose.
The output-based rule above was still wrong in a third way: it decided PLUMBING by matching a regex
against cc1's diagnostic text, and cc1's vocabulary is open-ended. too many arguments to function 'func_80146C3C' — a plain arity conflict — matched none of error|conflicting|undefined|previous declaration|redeclar, so a trivially reconcilable function sat classified UNKNOWN through two
gate rounds. The closed, true fact is whether the compile produced an object:
compile_failed = bool(re.search(r'^make: \*\*\* \[.*\] Error \d+', txt, re.M)
and 'Deleting file' in txt) # .DELETE_ON_ERROR, added Phase 30 S29
Decide the class from that; keep the diagnostic lines only to hand the agent verbatim. Re-running the
fixed tool over six stubs moved the population from 5 PLUMBING / 1 UNKNOWN to 5 PLUMBING / 1 DIFF with no other change. That is three defects in one small tool in one session — an exit-status
branch that was unreachable, a regex that missed a common phrasing, and the prose-matching design
that made both possible — and each one silently mis-routed real work. R33 in one line: if an
invariant answers the question, never re-parse the output.
§136f — Two declaration sub-cases the reconcile lane surfaced (lane now 15/15 lifetime)
-
A symbol you are calling may be DEFINED — not merely declared — below your splice point.
func_8017CD9C's draft forward-declaredextern void func_8017D540(s32);, guessed from the asm (a barejalwith$a0 = 0and an unused$v0is consistent with several signatures). Butfunc_8017D540is defined in the same TU ~275 lines BELOW the splice, asint func_8017D540(int). cc1 took the draft's prototype first and rejected the later definition. D2's "grep the whole TU below the splice point" must look for DEFINITIONS, not justexternlines. The fix was to copy the definition's own signature; it was byte-neutral because the argument is a literal0and the return is discarded. -
An ARITY clash on the symbol you are DEFINING cannot be fixed by a cast — use lever (B).
func_801848DCis forward-declaredextern s32 func_801848DC(void);at three places above the splice, each used by a banked caller invoking it with no arguments, while the byte-true signature takes a pointer in$a0. Cast-at-use fixes a callee's type; it cannot change how your own definition is declared. The §37/§124 asm-label alias is the lever:s32 aF801848DC(void *a0) __asm__("func_801848DC");— define under the alias identifier, emit under the real symbol, zero blast radius, no shared header touched. In-TU precedent for this exact TU atov_SC04_018_jr_8017AE2C.c:8872(aF8018CB18). Verify the alias is byte-neutral by gating with and without it.
Lane record: 15/15 across five waves. The reconcile lane remains the most reliable stage in the pipeline and the cheapest per bank — but see §136a: it is not purely paperwork, and several of those 15 also carried a real codegen residual behind the declaration conflict.
§136g — When the index points at the WRONG lever: two byte-refuted routings (func_801863B4)
The last redraft of the session is the clearest case yet of why an agent must byte-test the index's
suggestion rather than trust it. It read the real tools/reference/gcc-2.7.2 source and refuted two
entries that the index confidently routes:
1. BRANCH-POLARITY where the return K block RELOCATES to the function tail.
The index routes BRANCH-POLARITY to §3-T4 ("invert the source condition") and §34 (the zero-byte
__asm__("") fence). Both were tried and byte-refuted here. The actual transform is
jump.c:1806 — /* Look for if (foo) bar; else break; */ — which SWAPS range1/range2 and
inverts. It runs long before reorg, so a fence instruction cannot block it. Its real
precondition is label2 = next_label(label1) being the RETURN label with
JUMP_LABEL(range1end) == label2. C lever: put ANY label between the if-join and the return
label — wrap the loop inside the guard, if (…) { loop } with ONE trailing return 0, instead of
an early-return 0 guard. The if-join label disarms the swap. (Closed the last 8 instructions.)
2. lh AND lhu of the SAME address, feeding an sll 16/sra 16 pair — not a weird cast.
MIPS LOAD_EXTEND_OP == ZERO_EXTEND (mips.h:1163), so a plain HImode local load emits lhu and
its later int use costs sll/sra, while an SImode use of the same lvalue (*p == -1) emits
lh. Source form: s16 v = *p; plus a separate *p == -1 test. combine collapses the pair
back into ONE lh unless the HImode pseudo has TWO reaching defs — so the shape only survives with
a hand-rotated guard (v = *p; if (*p != -1) { … do { …; v = *p; } while (*p != -1); }), which is
what makes both loads appear in the guard AND the loop-bottom block.
The generalizable point: the index is a starting hypothesis, not an answer. This agent tried both indexed levers, measured them at zero, went to the compiler source, and found the transform in a pass earlier than the one the index named. Record the refuted routing next to the correct one — otherwise the next agent re-runs the same two dead ends. (§136b tally: 9 for 9.)
§136h — CORRECTION: the zero-crack pool does NOT "refill with cheap work" (my error, byte-measured)
At the S7 close I recorded that the zero-crack (propagation-only) pool grew 120 → 147 families /
62,232 → 70,924 templatable ins even after ~1,400 members were propagated through it, and framed
that as a compounding cheap lever — "run the zero-crack sweep FIRST next session." I priced it off
the family map's byte_weight_templatable and did not probe a single member first.
Measured: the sweep banked 1 of 1,781. Diagnosing the top families (.run/s6_diag.py): two
compile clean and produce a byte DIFF, one fails at link (undefined reference to tail_8012F274). These are genuine per-member residuals — the remapped exemplar body does not
reproduce in the sibling.
Why the pool grows, correctly stated:
- It accumulates members that already failed earlier sweeps (S6a/S6b banked 1,582 out of this same population and left the rest).
- A fresh crack adds its family's members to the pool — but if you propagate behind every crack (which you should), those members are harvested at crack time. What accrues afterwards is the fraction that refused to propagate.
⇒ A growing zero-crack count is a residue signal, not an opportunity signal. Price this pool by
probing one member per family, never by summing byte_weight_templatable — that column counts
what could template if the bodies reproduced, which is exactly the thing in question. This is the
S28 worklist.md mis-pricing (§133) recurring on a different column: a weight column is a
prediction; the gate is the fact.
(Recorded against myself: this is R37 — probe before costing — violated in the scoping step of the very session that was correcting R37 violations elsewhere. The byte-gate cost was ~1,780 build cycles and zero tokens, and nothing wrong entered the tree; the loss was wall-clock and a wrong line in a checkpoint that a fresh session would have acted on.)
§136i — The drafter model LADDER: Haiku → Sonnet → Opus → Fable5 (Drew, 2026-08-03)
The two-tier rule from the 2026-06-29 A/B (cheap drafter ≤~50 ins, Opus for the 90+ tail) left the ~50–120-ins band unassigned, and every wave since defaulted it to Haiku-with-Opus-escalation. P30 S7 measured what that costs:
| tier that produced the FINAL draft | banked / attempted |
|---|---|
| Haiku direct (≤89 ins as routed) | 3 / 8 |
| Opus escalation after a Haiku miss | 10 / 11 |
Haiku on that band was expensive triage — a wasted draft plus a full Opus redraft — not a cheap drafter. The original A/B only proved parity ≤52 ins; everything above was extrapolation.
Route drafters by size:
| band | model: |
|---|---|
| ≤ ~50 ins | haiku (measured ≡ Opus, ~4.8× cheaper) |
| ~50–120 ins | sonnet ← the rung this section adds |
| ≥ ~120 ins, or escalation after any lower rung returns non-MATCH | opus |
| a genuinely NEW wall class nothing else cracks | fable (discovery only — never for applying known idioms) |
Never route Haiku → Opus directly, and never default a whole wave to Opus because the band "looks hard" — that is the same extrapolation in the other direction. Escalation is unchanged: any rung returning non-MATCH escalates one step up. The whole-binary byte-gate remains the sole arbiter, so a weaker drafter is a throughput risk, never a correctness risk (G3/P9).
Treat ~50 and ~120 as current best estimates, not constants — re-measure the boundaries whenever
a wave gives a clean per-tier signal (derive the split per-function from the journal + the gate, not
from the workflow's by_tier, which counts claims — §136).
§136j — The failure MIX flips with function size (measured across four bands, one session)
Blocker-capture classifications from P30 S7/S8, same tooling, same gate, four size bands:
| band | drafted | PLUMBING (declaration) | DIFF (genuine codegen) |
|---|---|---|---|
| ≤60 ins (volume lane) | 111 | majority | few |
| 60–120 ins | 33 | 7 of 10 failures | 3 |
| ≤120 aggregate, second round | — | 5 of 6 | 1 |
| 121–328 ins | 23 | 1 of 7 | 6 of 7 (86%) |
Small functions fail on paperwork; big functions fail on the compiler. The mix inverts almost completely across the range. Two operational consequences:
- Budget the lanes by band. Below ~120 ins, expect the reconcile lane to be the workhorse (it ran 15/15 lifetime and costs ~13× less than drafting). Above ~120 ins, expect the redraft lane and real gcc-source work — reconcile will have little to bite on.
- Do not read a low bank-rate on a big-function wave as a tooling problem. 16/23 (70%) on the 121–328 band with 86% of the failures being genuine byte-DIFFs is the expected shape, not a sign the pipeline is broken. The equivalent 70% on a ≤120 wave WOULD have been a tooling signal, because there the failures should be declarations.
This also re-frames §136a's correction: "a PLUMBING verdict says nothing about the body" is true everywhere, but the prior probability that a failure is paperwork at all is strongly size-dependent.
§136f addendum — the collider is often an ALREADY-BANKED SIBLING below the splice, and you can
locate it by arithmetic. func_801832A8's draft audit scanned only above its INCLUDE_ASM at
TU:4680 and concluded two callees "appear nowhere in the TU". They were declared at TU:4846-4847 —
inside the already-banked sibling func_8018389C, below the splice. The proof is arithmetic, and
it is worth doing before hunting: the draft grows the file by N lines, so a pre-splice TU line L
appears at L+N in the error output. Here N=184, and 4846+184 = 5030, 4847+184 = 5031 — exactly the
two conflicting types lines cc1 reported. If the reported line number exceeds the splice point,
subtract the growth and look there.
And a self-verification an agent can run WITHOUT the gate (stronger than match_one): splice into
a scratch copy of the real TU, run the Makefile chain cpp → cc1 → maspsx → as, then objdump the
function out of the resulting object and compare word-by-word against the target .s. The words that
differ should be exactly the unlinked relocation slots — set-compare the differing indices against
objdump -r, and require the differing-but-not-relocated set to be empty. That proves both the
declaration surface AND the codegen in real TU context (func_801832A8: 237 words, 25 differing,
all 25 relocations). It is the closest an agent can get to the whole-binary gate on its own.
§137 — REGALLOC-PERM is a TWO-COMPILE ARITHMETIC PROBLEM, not a permuter job
Symptom key: match_one reports REGALLOC-PERM — a clean swap of two registers, everything
else byte-identical ($t8/$t9, $s0/$s1, …). Historically this class went to the permuter or was
ledgered "unsteerable". It is neither: gcc-2.7.2 decides it by an arithmetic priority you can read
out of the compiler's own dumps and then target deliberately.
The mechanism. global.c:allocno_compare ranks by
pri = (int)( floor_log2(R) * R / L * 1e4 * size )
where R = times the register is used and L = the live-range length in insns. Both come out of cc1's own dumps:
cc1 -dl -dg … # t.i.lreg : "Register N used R times across L insns"
# t.i.greg : ";; Register dispositions"
The method (measured on func_801833F0, 328 ins):
- Compile with
-dl -dgand read R and L for BOTH contenders and their ranked neighbours. - Evaluate
prifor each ⇒ you get the exact admissible priority WINDOW that flips the pair. Here the two contenders were ONE unit apart —vtx1297 vs the giv 1296 — with window (1228, 1296). - R and L are both forced by the emitted code, so source reordering does not move them
(measured twice: moving a load's source position changed nothing, because
Lis recomputed post-sched1). This is why source-level levers are a dead end for this class. - Place a zero-byte
__asm__ __volatile__("" :: "r"(v))(§17/§21 primitive) at the source point that makesLland inside the window. Five probed placements gave L = 190/194/196/219/233/258; only L=219 → pri 1232 worked.
Why this matters: it converts a class we have been routing to the permuter (a random search that this session banked 0 from) into a deterministic two-compile calculation. Try it before the permuter on any clean 2-register swap.
(Companion finding, same wave, func_8017BFEC: a 5-instruction head rotation that was invariant
across every legal statement permutation — 8 head orderings × 2 store forms all identical — is the
diagnostic signature of priority / birthing-boost, not LUID order. Fix: make the pseudo
single-set by splitting a variable reused in two blocks into two locals (sched.md §1.7/§S2,
sched.c:2469/2490). An earlier agent had ledgered this exact function DIFF/7 "not steerable from
this decomposition" after ~70 source variants — refuted. Invariance under source permutation is
information: it says the lever is not in the source order.)
§137a — A gate verdict has a TIMESTAMP; re-check it against the draft's mtime
A redraft agent this wave found its handed-over "genuine byte-DIFF" was stale by 28 minutes: the
gate ran at 14:53, the draft was rewritten at 15:22 by an earlier lane whose result had been cached,
and it was never re-gated. The agent ran match_one on the file as it stood (D3), got MATCH, and
spent its budget proving the file would BANK instead of re-deriving a function that was already done.
The rule: before acting on any failure verdict — yours or a prior wave's — compare the verdict's time against the draft file's mtime. If the file is newer, re-verify before redrafting. This is the same family as the Phase-29 finding that 77% of stored drafts had decayed, but the opposite direction: a stored verdict can be stale because the draft got better, not just worse.
Two offline oracles that agent built, both worth reusing (they close the gap match_one's
relocation mask leaves open, without running make):
- Full relocation RESOLVE — resolve your
.o's relocations against the target.s(D_<addr>/func_<addr>symbol values, HI16/LO16 paired addends) and compare all words. This sees the class the mask hides: a wrongjaltarget or a wrong%hi/%losymbol or addend. Result there: 0/227 diffs including every relocation. - Collateral check — objdump the whole TU with and without the splice and require every OTHER
function to emit identical words, allowing only
.text-relativejaddends shifted by exactly your function's size (0x38C there). That catches file-scope declaration damage the target function's own bytes cannot show.
Symptom line for the index: "match_one/rtu_match say MATCH but the whole-overlay SHA still
DIFFs" has exactly three causes — a stale verdict, a wrong jal/%hi/%lo target the mask hides,
or collateral from file-scope decls — and the two oracles above discriminate all three offline.
§138 — The propagation lanes: a gate refusal is a DECLARATION, and which lever you owe depends on blast radius
Symptom key: dedup_extend or dedup_propagate plans N members and banks 0, or reports
PLUMBING: conflicting types for X / an undiagnosed DIFF. Measured this session: a 0/36 lane went
to 31/36, and every single blocker was a declaration — not one was compiler codegen.
The triage, cheapest first
Capture each failure's own compiler error (§136a — classify on the build's OUTPUT, never its exit status) and bucket by which symbol is named. The named symbols repeat: 9 failures per binary reduced to 4 distinct symbols shared across all four binaries. Then pick the lowest-blast-radius lever that is byte-neutral by construction:
| conflict | lever | radius |
|---|---|---|
macro declares (void), TU declares (ptr), use is cast |
relax the macro decl to () |
T2, 1 token |
| TU decl is unused boilerplate (zero uncast uses) | conform the TU decl to the fleet canon | T1 |
TU declares the symbol volatile, or any type the macro can't match |
asm-label alias on the DATA | T2 |
| the fn's own signature (return/arity/param) | asm-label alias on the DEFINITION | T2 |
Before relaxing to (), MEASURE the whole fleet's decl shapes for that symbol. () is illegal
against a prototype carrying a default-promotion param (s8/s16/u8/u16/char/short/float) — the
documented gcc-2.7.2 dead-end. It is legal against pointers and s32. One grep -rhoE over src/
answers it; 4,020 decls of func_80146C3C were all (void)/()/(u8*), so the relax was safe and
bought 8 of 36 for one token.
One conflict HIDES the next. A declaration conflict aborts the compile, so the error you see
says nothing about what is behind it (the S29 law). Fixing func_8012E5CC immediately revealed
func_80147364 at the same site. Re-run after every fix; do not price the lane off the first error.
Bucket by the (macro-shape, TU-shape) PAIR — NOT by the symbol. The same symbol conflicts in
BOTH directions across the fleet, and the lever differs. func_80146C3C cost this lesson twice in
one session: in the EXTEND lane the macro said (void) and the TU (u8*); in the PROPAGATE lane
the mirror — macro (u8*), TU (void). Relaxing only the (void) form (42 decls, byte-neutral,
R22-clean) fixed the first and did nothing for the second, which then banked 0/1 in all 134
overlays. One awk over the macro you are ACTUALLY fixing — not its sibling — shows the pair before
you spend a 134-build run. Read the declaration of the macro in front of you.
volatile in the host TU is a SCHEDULING BARRIER — and it looks exactly like a codegen wall
The four "undiagnosed DIFF"s in dedup_extend's own header were this. Its correctness argument says
an h_exact match guarantees byte-identity including relocs, so a DIFF should be impossible.
Both halves resolved against the bytes:
- The contract HELD —
func_80162FF4's original bytes are sha1-identical in the failing and the working overlay (af1aceb2…). Check this first: it splits "the registry is lying" from "the TU is different" in one command, with no build. - The TU differed — the host TU declared
extern volatile s32 D_80127090/94/98at FILE scope, which no working overlay's copy of that TU does. Volatile makes the macro's three stores a barrier, soaddu $a0,$s2,$zerocould not sink into thejal's delay slot: the build emitted it early plus anop, one instruction longer.
The tell: a diff that is a positional shift with a nop appearing at a delay slot is an
ordering constraint, not a wrong body. Look for a qualifier (volatile, const) on a symbol the
body touches before reaching for a codegen idiom. The fix is the data asm-label alias
(extern s32 aD_80127090 __asm__("D_80127090")): a distinct C identifier is immune to any TU's
declaration of that symbol, and is byte-neutral wherever the macro already worked.
The DEFINITION-side alias is the only escape when the fleet canon disagrees on a promoting param
func_80147364's byte-true definition is (u16, u16); 4,046 fleet decls say (u16, s32). ()
is illegal (u16 promotes) and conforming the decls would change caller codegen. Author the macro as
void aF80147364(u16, u16) __asm__("func_80147364"); + a definition of the aliased name: the real
symbol is emitted, every caller keeps its own declaration, blast radius is zero. In-tree precedent:
1,725 files already use this form. Banked ×137 first try.
Rank the lane by measured concentration, not by class count
A "45 classes / 20,837 ins" queue was really 5 classes carrying 89% of it. Re-split the ledger by
nins × n_stub before scheduling anything — and note the per-class outcomes diverge wildly
(4,110 banked ×137 · 3,288 banked ×138 · 3,973 dropped · 3,886 at 4/138 · 3,288 tool-gapped), so a
lane average predicts nothing. --recover is not a retry: the caller-extern reconcile that is
16/16 lifetime on drafts banked 4 of 138 on a propagation. Probe one excluded member's build
output before re-running any lever that already returned a bad number.
THREE carry variants hide in one "CARRY-FIXABLE" bucket — and they need different fixes
dedup_propagate reports missing file-scope extern (CARRY-FIXABLE) for all of them, but the
response differs and two are NOT tool bugs:
| variant | what extract_unit cannot carry |
response |
|---|---|---|
| a multi-line comment halts the preamble backscan | the externs above it | fix the tool (cdecl._mask) |
a draft-local struct Tag {…} in the preamble |
the type (refused by design: two macros defining one tag redefine it in a TU) | switch the exemplar to the shared engine_types.h type if the layout already exists there — byte-neutral |
a file-scope static inline helper |
the helper | hand-author the macro with the helper inlined, and exclude the source overlay |
The third is the sneakiest, because gcc-2.7.2 accepts implicit function declarations: the
extracted body passes compiles_standalone with the helper undeclared, so nothing complains until
a whole-binary byte DIFF 137 gates later. And the macro cannot be instantiated in the SOURCE overlay
— its file-scope helper is still there, so the macro's copy is a duplicate definition. The invocation
shape is --source-overlay X --binaries <all-but-X>; passing --binaries alone removes the source
from the scan pool and errors with "no source overlay has it matched".
A function defined under an asm-label alias is invisible to a name-anchored head regex. The def
is named aF<ADDR> in C and only BINDS the real symbol via __asm__("func_<ADDR>"), so any matcher
anchored on the literal func_<ADDR> returns None and every caller reads that as not matched.
family_remap._alias_decl_for resolves the form; make other tools reuse it rather than grow a
second matcher (R33). Two live consequences found together: dedup_propagate.find_site was blind to
the form entirely, and _alias_decl_for itself was blind to the wrapped (multi-line) declaration
— the §134 shape again. Measured before fixing (R37): 91 distinct alias decls fleet-wide, the
per-line matcher resolved 90, and the single miss was func_801466F0. That one function then took
all THREE fixes plus a type-lift — alias-aware find_site, wrapped-alias matching, and hoisting
its record typedef to engine_types.h — which is exactly why it survived four phases of being
written off: each blocker on its own looked sufficient to explain the failure.
Tool boundary worth knowing: once a group's members are DEFINE_func_*() sites,
dedup_propagate can no longer extend it — find_site never returns a def, so the auto-source
scan errors. dedup_extend is the tool for an already-macro-ized group. (And running
dedup_extend --check-only across ordinary overlays is a cheap census of how much wiring is
outstanding fleet-wide: measured here as exactly 1 group per overlay, i.e. no hidden backlog.)
§134 again, in a second tool — and the waiter rule corrected
dedup_propagate.find_site's preamble backscan had the SESSION-18 fix for blank / // /
single-line /* … */ lines, and still halted on a multi-line block comment (middle lines
start *; the last ends */ without starting /*). Same class S6b fixed three times in
family_remap. Decide skippability on cdecl._mask, not on line syntax — one oracle (R33),
every comment form, immune to a /* inside a string, with an R32 assertion that the mask is
length-preserving. Also: a body declaring a draft-local struct Tag {…} is unextractable by design;
if the identical layout already exists in src/shared/engine_types.h, switching the exemplar to the
shared tag is byte-neutral and unblocks propagation.
Waiter rule, corrected (three failures, one mechanism — the signal sampled is not the thing waited
for): pgrep -x make is right for ONE make and wrong for a campaign — dedup_propagate runs a
sequence of make build BINARY=<ov>, so a poll lands in the gap between two and reports a live
campaign finished. And pgrep -f <pattern> self-matches its own command line, so that waiter can
never exit. Wait on the campaign process by its real argv (ps -eo args | grep 'python3 tools/…') or
on tools/treelock.sh --status, which is a statement of intent spanning the gaps. Likewise never
wrap a campaign in nohup … & inside a backgrounded call: the harness then signals completion of the
wrapper — a fleet check "finished" at 63/140.
STEP 0 of sibling-first: grep src/ for a distinctive LITERAL from the .s
§136c's search order (engine_core.h near-twin -> same-TU banked sibling -> the .s) has a hole: both
of its first two steps are same-TU or shared-header scoped, so neither can reach a banked twin
that lives in a different overlay's TU — and the large template classes live cross-overlay by
construction. Measured: func_80188C04 (328 ins) was byte-identical to an already-banked
func_801833F0 in ov_SC02_028, and one command found it —
grep -rn "E100000A" src/ # a magic word lifted straight out of the target .s
— after which the body was reused verbatim with only the file-local type/macro suffixes renamed.
Put this ahead of engine_core.h. Pick a distinctive constant from the target: a magic word, an
unusual mask, an odd immediate. Corollary for the family map: it carries an in-family exemplar
pointer only, so a family whose twin is banked elsewhere looks un-cracked — and, separately, that
pointer can name an instance that is already banked, which hides the whole family from any
ranking built on it. Derive open sites from corpus.stubs over the member list. Measured on one
wave: ranking off the map's exemplar yielded 16,696 templatable ins; deriving from corpus.stubs
yielded 41,023 — including a 55-ins family open in 138 overlays and a 46-ins one open in 133.
Reconciling a gate-refused draft: which way you edit depends on WHERE the TU's decl is
A draft that match_one-MATCHes but the whole-binary gate refuses is declaration plumbing (the
agents cannot run the gate, so a TU-level conflict is invisible to them). Two opposite fixes, and
picking the wrong one creates the next error:
| the TU's decl is… | symptom | fix |
|---|---|---|
| ABOVE the splice point | redefinition of struct X / conflicting types for a TYPE |
DELETE the draft's duplicate — the TU already provides it (§100) |
| BELOW the splice point | 'X' undeclared (first use…) after you deleted yours |
KEEP a decl, in the TU's exact shape, and cast at the use (§17a-1 D2) |
Measured both in one session: func_8017D318 needed the deletions (the TU defines the type trio
above it — and a struct PW tag that a first pass missed, so it took two rounds), while
func_80181EE0 needed the opposite — I removed its decl, and the TU's own turned out to be ~180
lines BELOW the splice, leaving the identifier undeclared. Grep the TU for the symbol and compare
line numbers with the stub line before editing either way.
And apply §136a to your own capture tooling. My blocker-capture filtered the build log for
error|conflicting|undefined reference and so reported "no compile error" on a build that was
failing with redefinition of struct PW8017C290 and 'func_80143C74' undeclared — neither phrase
matched. A narrow keyword filter is exactly how a real error goes unseen. Keep any line naming a
source position (\.[ch]:\d+), minus the known SHB macro noise.
Symptom line for the index: "a propagation/extend lane plans N and banks 0" — read each failure's
compiler error, bucket by named symbol, and apply the lowest-radius byte-neutral alias; a volatile
in the host TU and a fleet decl carrying a promoting param are the two that masquerade as codegen.
§139 — A GATE THAT GREPS FOR VERDICTS MUST ASSERT 1:1 ACCOUNTING; and a --src filter must not survive a carve (P30 S38, wave 6: 10 of 16 drafts vanished)
The symptom was a gate run that looked ordinary: BANKED 5 / FAILED 1 — printed over 16
drafts. Six verdicts for sixteen inputs, and nothing said so. Nine of the ten missing drafts were
claiming MATCH.
The chain, both ends of which were ours:
harvest_verify._reload_corpus()exists, by its own docstring, to "re-derive the stub map after an isolation moved a stub to a new TU." It then re-applied the--srcfilter (x.path == a.src) to the reloaded map — deleting the very stub it had just followed to its new home. Line 136 of that same file already labels--src"an optional filter, not a location oracle";_reload_corpuswas the one place treating it as one. Downstream:_stubslosesfn→render()raisesKeyError→ uncaught →_jtbl_restore(snap)never runs → the carve is stranded inconfig/+src/→ and every later group in the same gate run then builds against a tree the earlier crashes mutated.- The gate driver (
.run/s6f_gate.py) captured the child's output, grepped it forVERIFIED:andFAILED :, and never looked at the returncode. "Neither line present" was booked as nothing at all, and the tally printed clean.
The fixes are both structural, not cosmetic:
- the filter never drops a draft under verification, wherever it now lives, plus an R32 loud report
if a working stub vanishes across a reload (which also repairs
_touched/baseline— a carved fn missing from_stubsleft its new TU unbaselined, so the revert path could not have restored it either); - the gate asserts
banked + failed + no-verdict == drafts, prints the child's rc and output tail for anything unaccounted, and exits non-zero — because a crashed child may have stranded a carve, so it must never look like success.
Proof it was not cosmetic: func_8017EA84 (579 ins) carves and banks BYTE-IDENTICAL under the
fixed path. The old tool reported it as nothing.
The generalisation — three corollaries worth more than the bug
(a) The byte-gate is a perfect CORRECTNESS oracle and a null COVERAGE oracle (R34, again). It cannot tell you about work it was never asked to do. Every wave-level tally is a coverage claim, and coverage claims need their own assertion. Any driver that classifies N inputs must prove it emitted N verdicts.
(b) A pipeline's exit status is the LAST command's. In the same session family_sweep … | tail -30
hid a non-zero exit (--only wants comma-separated; space-separated args were rejected). Use
set -o pipefail and read PIPESTATUS, or the tail is the error handler.
(c) A reverted CONFIG needs make extract, not just the revert. After undoing the stranded
carves I re-extracted one overlay of sixteen; the next gate run read three genuinely-banked functions
as failures because they were building against asm/ still partitioned by the old carve. This is the
Phase-20 R22 corollary, and it costs a whole gate cycle every time it is skipped. A gate result
measured against stale asm is not a measurement (R35).
And the inverse-lookup trap, same session
Propagating a just-banked head, family_hseq.json moves it out of its family's members list
and into exemplar with kind: "matched". A lookup that searches the member list for the head
therefore returns "no family" for every head — while family_sweep enumerates those same families
from the same file seconds earlier. Key on the exemplar (ov, addr) pair (two families can share
an exemplar address in different overlays). This does not contradict §138 rule 4 ("never rank off
the exemplar field"): that rule governs target selection, where an exemplar pointing at a banked
instance HIDES a family; here the freshly-banked exemplar is precisely what you are looking the
family up BY.
Symptom lines for the index: "a gate reports fewer verdicts than it had drafts" · "a tool reports NO FAMILY for a head another tool just enumerated" · "a gate result got worse after a revert" (→ re-extract, then re-measure).
§140 — A METRIC IS NOT A MEASUREMENT UNTIL IT IS REPRODUCIBLE FROM THE COMMITTED TREE (P30 S1e: a phantom regression that gated the session's best lever)
The alarm. S38's checkpoint recorded, in bold: "distinct-code FELL 89.3 → 89.2 across the last commit — UNEXPLAINED. Do NOT scale the alias lever until it is resolved. The BYTES are proven (R22); the ACCOUNTING is not." The def-side asm-label alias had just cracked a 208-conflict class 138/138 — the best-performing lever of the phase — and it was parked on that one line.
The regression never happened. True values, recomputed from each commit's own tree:
| instr | distinct | unique fns | |
|---|---|---|---|
commit:1426 true |
12,394,533 | 5,022,306 | 77,895 |
commit:1426 as committed |
12,402,412 | 5,029,324 | 78,025 |
HEAD true = committed |
12,405,402 | 5,025,082 | 77,952 |
The real delta over that span is instr +10,869, distinct +2,776 ins / +57 unique fns — everything
rose. The 843 digest was committed stale: generated from a working tree that still held work
REVERTED before the commit landed (overstating by +7,879 ins / +130 unique fns), and never
regenerated. The next honest digest was lower than the stale one, so the metric appeared to fall.
The three-line proof (do this before diagnosing any metric movement)
The weighted metrics are matched = sig − corpus.stubs. So if, between two commits, (a) the sigs are
identical (both denominators unchanged is sufficient evidence), (b) tools/ is unchanged, and
(c) git diff A B -- src/ | grep -c '^+.*INCLUDE_ASM(' is 0 — then HEAD's stub set is a strict
subset of A's, HEAD's matched set is a superset, and both numerators are mathematically forbidden
to fall. A reported fall is then a statement about the digest, not the tree. Three greps settle
it before a single hypothesis is formed; I burned two wrong mechanisms first (see below).
The two instrument defects it exposed
(1) progress.py stub_addrs swallowed the oracle's refusal. It wrapped corpus.stubs in
except Exception: return set() — and an empty stub set does not mean "no stubs", it means "the
oracle could not answer", after which matched = sig − stubs credits every function as banked.
Byte-witnessed: running the metric in a tree with no asm/ made corpus.stubs raise its correct,
coverage-asserted CorpusError for all 140 binaries, and the tool reported instr 100.00% /
distinct-code 100.00% — a complete decomp, out of a swallowed error. corpus.stubs is
deliberately fail-closed ("it refuses to answer rather than guess 'banked'"); a bare except
around a fail-closed oracle reinstates precisely the guess it refuses to make. Fixed: propagates.
(2) Nothing ever re-checked a committed digest. R34, again: the whole-binary byte-gate is a
perfect CORRECTNESS oracle and a null oracle for documents — a stale digest is byte-irrelevant,
so check-all stays 140/140 across it forever, and audit-corpus/audit-binaries assert things
about the CODE. Fixed: make audit-digest (tools/audit_digest.py, wired into tools-health
after report) recomputes the three headline metrics from the current tree and fails if the
committed digest disagrees. Negative-control-proven against the known-stale 843 digest. Note it
compares integers, not the printed percentages: the +7,879-instruction staleness rendered as
"94.4%" both before and after, so a percentage comparison would have seen nothing.
The same swallow, twice more, in the integration spine
cast_call_sites.tu_for and reconcile_tu.tu_for had the identical except Exception: pass around
corpus.stubs, falling back to the default src/<ov>/<ov>.c. cast_call_sites' own docstring, three
lines above, says the function exists because "the recovery passes were reconciling against a
DIFFERENT TRANSLATION UNIT than the one that would compile the code, and the heavy Phase-26 cores
live in exactly those jr files" — so the swallow silently reinstated the bug the function was written
to fix. A wrong-TU reconcile fails the gate, and this phase's base rate is ~24k PLUMBING vs 4,917
DIFF, so it would present as a codegen wall. Both now propagate CorpusError while keeping the
ValueError fallback for curated (non-func_ADDR) names.
Two wrong mechanisms I chased first, and why they were wrong
- The recorded lead —
progress.py:423'sSIGregex (it booksvoid aF80146A6C(…)under the alias name). Real blindness, but it feedsclassify(), which computes fn-count only. Neither weighted metric ever sees a C identifier. A lead that names a function must be checked against which metric that function actually feeds. - "The harvest reverted functions to INCLUDE_ASM" — plausible because reverting to
INCLUDE_ASMis byte-neutral (it pastes the original asm), so R22 would stay green over real coverage loss. Refuted in one grep: 483 stub lines removed, 0 added.
Symptom lines for the index: "a progress metric fell but the byte-gate is green" · "two metrics moved in opposite directions" · "a digest disagrees with the tree it describes" · "a coverage oracle reports 100%" · "a recovery pass reconciled against the wrong TU".
The law: a committed number is a claim about a tree; if it cannot be recomputed from that tree, it is not evidence — and it must never gate a lever. (R32/R34/R35; and R14 — I asserted two mechanisms before deriving either.)
§141 — The §134 class is CLOSED: every line-shape decision now routes through cdecl._mask (P30 S39)
§134 ("multi-line / comment / string blindness in a hand-rolled line scanner") had been fixed individually in six tools, each time by patching that tool. The standing note said the real fix is routing every line-shape decision through the ONE masking oracle rather than writing a seventh regex. Done — the last two holdouts are migrated:
progress.py.strip_comments — was a private two-line regex stripping /*…*/ and //…, not
string-aware. Every caller is a line-shape decision on its output: the {-vs-; scan that
separates a definition from a declaration, the count('{') - count('}') body-depth walk, and the
empty-vs-real body test. So a brace inside a string literal mis-buckets a function in the fn-count
metric. Demonstrated:
void f(void) { puts("}"); x = 1; }
old regex -> body-depth walk = -1 (unbalanced: the string's brace was counted)
cdecl._mask-> body-depth walk = 0 (correct)
Metrics were identical before and after on today's corpus (341,365 / 353,717; REAL 339,510,
empty 896, stubs 12,345) — i.e. no live source currently trips it. That is what a latent defect looks
like: harmless until the day someone banks a function containing "{", and then silently wrong.
lint_symbol_refs.strip_comments_strings — was correct (char-by-char, escape-aware) but was a
SECOND implementation of the same masking. Deleted in favour of cdecl._mask. One behavioural
difference existed and was checked rather than assumed: _mask blanks the quote DELIMITERS too,
where the private scanner kept them — irrelevant, because every token the linter hunts
(func_<ADDR>, D_<ADDR>, and the bare 2nd arg of INCLUDE_ASM("...", func_X)) lives outside the
quotes either way. Gated on the linter's own output being byte-identical across the change, not on
the two masks being byte-identical — the right gate is the tool's answer, not its internals.
The general law (R33): when the same defect class has been patched N times in N tools, the fix is not the N+1th patch — it is deleting N−1 implementations. A private copy of a shared decision is a divergence waiting to happen, and it diverges silently.
Symptom line for the index: "a scanner miscounts braces/semicolons" · "two tools disagree about what a line is".
§142 — An open stub whose h_exact class is MATCHED elsewhere is FREE. Propagate the body; do not gate a draft. (P30 S39, +7,710 ins in two commands)
The cheapest thing on the board is the thing nobody looks for: a stub that is byte-identical to a function already matched somewhere else in the fleet.
h_exact is the SHA1 of RAW INSTRUCTION BYTES (tools/sig_image.py), so two instances sharing one
are identical including their jal/lui/%lo reloc immediates — same callees, same data
addresses, same symbols. Therefore the body that compiles byte-identically at one member compiles
byte-identically at the other with no remap at all. This is why dup_report calls h_exact
"guaranteed byte-match" and h_norm "candidate-only", and it is the same correctness argument
dedup_extend is built on.
The measurement (do this before any wave; it is ~20 lines and needs no builds)
For every sig entry: is its address a stub in its own binary? Build class -> matched-anywhere?
and class -> [open instances]; the free pool is the intersection.
OPEN stubs whose h_exact class IS matched elsewhere:
215 function-instances / 8,763 instructions across 33 classes
...and ONE class was 86% of it:
func_801758FC — 55 ins, SAME address in all 138 overlays,
matched in ov_SC01_000 only, OPEN in the other 137 => 7,535 ins
tools/dedup_propagate.py --addr 0x801758fc
[ OK ] 138 overlays byte-identical after propagation
fleet instr +7,535 EXACTLY; R22 140/140.
The trap that hid it — SAME FUNCTION, TWO ROUTES, ONLY ONE IS FREE
func_801758FC had been sitting in the stored-draft backlog for two overlays, and was re-gated
"no" earlier the same night. Both facts are true and not in tension: gating a stored draft asks
"does this hand-written C reproduce the bytes?", which is a hard question with an ~8% yield. The
right question for an h_exact class is "who already matched this, and can I stamp that body here?",
which is free and gated at 100%. A function's presence in the near-miss backlog is not evidence
that it is hard — it may only be evidence that it was attacked from the wrong side.
Route selection (why --addr sometimes says "nothing changed")
dedup_propagate --addr needs a source overlay holding an inline definition to extract. Where
the source is itself a DEFINE_func_*() macro instantiation (the ~1,600 shared bodies), it refuses —
that is dedup_extend's job (extend an existing macro-backed group to a binary that lacks it).
Measured tonight: of 33 free classes, --addr reached 9 (+7,710 ins); the remaining 25 classes /
66 instances / 1,061 ins are all macro-backed and need the dedup_extend route.
And the report-vs-bytes lesson attached to it
The frontier report claimed the whale was "open only in SC07_006/007/010/011". Three of those four were already banked. Its pool numbers were carried with an explicit R14 caveat and the caveat was right. Re-measure the pool from the sigs before acting on any published count — the measurement above is cheap enough that trusting a stale number is never worth it.
Symptom lines for the index: "a stub is byte-identical to something already matched" · "dedup_propagate --addr says nothing changed" · "a backlog function turns out to be free".
§143 — cast_call_sites read a RETURN STATEMENT as a prototype and deleted it. A 0/39 sweep became 18/39. (P30 S40)
The observation. A family_sweep --hseq over 5 matched-exemplar families banked 0 of 39
members across 23 overlays. A total zero on PURE families with a matched exemplar is not a credible
codegen result, and §53 says exactly that: a 0% from the wrong tool is not evidence.
Read the payload first (.run/hseq_failed.*.classified.txt, the standing pre-probe rule): the 39
split 34 PLUMBING / 4 DIFF, and every PLUMBING was the same C89 error:
src/ov_SC05_017/ov_SC05_017_jr_8017AE2C.c:7690: parse error before `extern'
The mechanism. cast_call_sites.DECL_LINE_RE classifies a line as a declaration with
^([ \t]*)(extern\s+)?([A-Za-z_][\w \t\*]*?)\b([A-Za-z_]\w*)\s*\(([^;{]*)\)\s*;
Now feed it a return statement:
return func_8012CB64((s32)out, -0xC0, 0x40, -0x60, 0);
^^^^^^ captured as the return TYPE; func_8012CB64 as the DECLARED NAME
So the "rewrite this decl to the canonical signature" path replaced the statement with
extern s32 func_8012CB64(s32,s32,s32,s32,s32); — deleting the return. In C89 a declaration after a
statement in a block is a parse error, so the damage surfaced as a bare syntax error in the draft,
which reads as the draft's fault rather than the tool's. 9 of 9 staged members lost their return.
The fix — a keyword guard, because a declaration's type-specifier can never begin with a
statement keyword (return|if|else|while|for|do|switch|case|default|break|continue|goto|sizeof):
0/39 -> 18/39 (same families, same members, same gate; only the guard changed)
⚠️ AND THE TRAP INSIDE THE FIX — cdecl CANNOT ADJUDICATE THIS. The obvious R33 move is "route
it through the declaration oracle." Checked, and it is wrong: cdecl.parse() is a declarator
grammar parser that ASSUMES it was handed a declaration. It reports return func_X(…); as declaring
func_X, and if (f(a)); as declaring if. Statement-vs-declaration is a question cdecl does not
answer, so routing there would have been a silent non-fix that looked principled. §134's law still
holds — line-SHAPE masking goes through cdecl._mask — but "is this text a declaration at all" is a
different question with a different answer.
Blast radius (measured, not assumed). cast_call_sites is in gate_stage's DEFAULT pipeline
(canon_resident_calls → cast_call_sites → sig_unify → harvest_verify) and has been since Phase 20.
Across the 44,833 stored drafts, 318 (0.7%) contain a return f(...); line this would mis-read,
concentrated in 67 callees (worst: func_8014F468 ×41, func_8014F6F4 ×37, func_8014F74C ×32,
ratan2 ×25). Every one of those, every time it passed the gate pipeline, lost its return and failed
as PLUMBING. Some fraction of the historical "plumbing tail" is this bug.
The law: a tool that REWRITES source must be able to tell a declaration from a statement, and a
regex over <ident> <ident>(...)ï¼› cannot. When a whole sweep returns 0, suspect the tool that
touched every member — and read the per-member payload before believing any wall.
Symptom lines for the index: "parse error before `extern'" · "a sweep banked 0 of N" · "a draft lost its return statement" · "declaration after statement in a block".
§144 — THE LITERAL'S SPELLING PICKS THE IMMEDIATE ENCODING (P30 S40 wave 1, func_801822E0)
A new, byte-proven gcc-2.7.2 idiom, found by a wave agent on an 85-ins exemplar.
A byte counter stored through an sb was diffing on one instruction's immediate field only:
target : addiu $v0, $v1, 0xFF (imm 0x00FF)
ours : addiu $v0, $v1, -1 (imm 0xFFFF)
Both are arithmetically identical modulo 256 — only the low byte survives the sb — and both
compile to a single addiu, same instruction count, same registers, same schedule. The compiler
is not choosing between them on any semantic ground: it takes the immediate from how the literal
was SPELLED in the source.
cnt = cnt - 1; -> addiu $v0,$v1,-1 (0xFFFF)
cnt = cnt + 0xff; -> addiu $v0,$v1,0xFF (0x00FF) <- matches
Signedness of the counter (s8 vs u8) was tested and makes no difference; the spelling is the
whole lever.
When to reach for it: your diff is a single instruction, the mnemonic and both registers agree,
and only the immediate field differs — and the two immediates are congruent modulo the width of
the store that consumes the value (sb → mod 256, sh → mod 65536). Then re-spell the literal to the
form whose bit-pattern you need. Do NOT reach for pins, scheduling barriers or the permuter: nothing
about register allocation or ordering is wrong.
Why it is easy to misread as intrinsic: the residual is one immediate in one instruction, which looks exactly like the tail of a regalloc/scheduling wall. It is not — it is a pure source-text lever, and it is free.
Generalisation to test when it next appears (not yet byte-proven, so treat as a hypothesis):
the same should hold for any masked-then-truncated arithmetic where two literals are congruent modulo
the consuming store width — e.g. x - 2 vs x + 0xfe, or h - 1 vs h + 0xffff ahead of an sh.
Symptom lines for the index: "only the immediate field differs" · "addiu -1 vs 0xFF" · "one instruction off, same registers".
§145 — Three loop/combine levers from the S40 wave-2 drafters (16/16 match_one)
All three were derived by agents reading tools/reference/gcc-2.7.2/ against a real diff, and each
one moved a specific instruction count. They belong with gcc-2.7.2-map/loop.md and cse_expr.md.
(a) The combine_givs ANCHOR RULE — the group anchors on the LAST address-giv in SOURCE order
record_giv prepends to the induction-variable list and combine_givs takes the head, so the
address-giv group is anchored at the giv that appears last in source order. Consequence: which
store you write last decides which offset becomes the base, and a wrong choice spawns a third
induction register.
stores +0x5E and +0x62 in the loop:
… +0x62 written last -> anchor +0x62, extra IV, +5 instructions
… +0x5E written last -> anchor +0x5E, 2 IVs, MATCH
Reach for it when: you have one instruction-count too many and the diff shows an extra
addiu/addu maintaining a second or third pointer through a loop. Re-order the stores, not the
pointer arithmetic. (func_80192F64, 226 ins.)
(b) A bare p = r; is a COMBINE BARRIER
Between r = p + K and r's uses, an apparently-redundant p = r; stops combine folding r → p+K
into every MEM offset (can_combine_p / use_crosses_set_p — the set of p crosses the use). That
fold is exactly what makes a pointer-bump addiu vanish; the barrier keeps it.
Reach for it when: the target has an explicit pointer bump your C keeps optimising away, i.e. you
are one addiu SHORT and the offsets in your MEMs are larger than the target's. (func_8018C960.)
(c) Chained assignment emits stores RIGHT-TO-LEFT
a = b = c = 0; emits the stores in descending source order (c, then b, then a). This is what
produces target sequences like 0x52 / 0x51 / 0x50 or 0x4C / 0x44 / 0x34 from one statement —
writing three separate statements gives ascending order and a different schedule.
Reach for it when: several adjacent fields are zeroed/initialised and your store ORDER is reversed
relative to the target. (func_80183FE0, 171 ins.)
Related, same wave: the §76 merge-into-one-variable lever appeared twice more (func_8018E5DC,
func_80192F64) — three block-scope pointers vs ONE function-scope variable changes whether the
allocno is local or global, and therefore whether $a0 propagates across blocks. And in
func_80187320, not introducing a second walked pointer (*(T*)(p+k) off a single biv) let
combine_givs anchor all three field accesses on one giv; an explicit q = p + 0xE split it into 3
IVs. Same family of decisions: how many named pointers exist in the C is a codegen lever, not style.
Symptom lines for the index: "one extra induction register" · "a pointer bump addiu disappeared" · "stores are in the wrong order" · "one instruction too many in a loop".
§146 — RE-MEASURE A WALL BEFORE YOU RESPECT IT. Both "permanent" giants fell to drafts already on disk. (P30 S6, +50,094 ins)
The roadmap carried two functions as permanent walls since Phase 24. Between them they were 50,094 instructions — the largest single item on the board — and they had deterred a Fable5-scale attempt. Both were matched from stored drafts, in minutes.
| wall | recorded verdict | what it actually took |
|---|---|---|
func_80178004 (165×138) |
Phase 26: Fable5, ~477k tokens, "intrinsic 3-integer regalloc wall" | a stored draft, gated as-is |
func_801412A8 (198×138) |
close=29/110 since Phase 24 | 1 of 31 stored drafts + the §37/§124 alias |
Why a correct draft can read as an intrinsic wall
func_801412A8's TU declares extern int func_801412A8(int,int,int,int,int,int) and its callers
use the return value, while the byte-true definition is
Prim_1412A8 *(Prim_1412A8 *, int, int, int, u16, u16). Narrow params cannot agree with an int
prototype, and the () no-prototype escape is illegal precisely when a param promotes — so neither
side can move. The resulting byte difference is in the CALLERS, and match_one only ever compiles
the target function. The measuring instrument was structurally blind to where the difference lived,
so the residual was attributed to codegen. The §37/§124 def-side alias decouples them: the TU's
declaration keeps governing the call sites (their codegen untouched), the definition keeps its
byte-true signature.
Then propagation returned 0/137 TWICE — both times a missing TYPE
family_remap._carry_macros carries file-scope #defines, but (a) it does not carry typedefs at
all, and (b) it is not transitive — it brought addPrim_1412A8 and stopped, though that macro
calls setaddr/getaddr, and getaddr casts to PTag_1412A8. Lifting
Env_1412A8 / PTag_1412A8 / Prim_1412A8 + OT/getaddr/setaddr into engine_types.h took it to
137/137. A total-zero sweep on a PURE family with a matched exemplar is a tooling signal, not a
codegen one (§53) — here it fired twice in a row for two different missing symbols.
Two errors of mine, both instructive
Lift without strip. I added the typedefs to engine_types.h and left the originals in
ov_SC01_077.c. gcc-2.7.2 rejects a repeated typedef even when identical — the lesson already
recorded at the foot of engine_types.h — so R22 came back 139/140, [FAIL] ov_SC01_077, the
exemplar's own overlay. A proper lift strips the source; build_engine_types --strip does both.
The clean-fleet gate caught it before commit, which is exactly why fleet-shared edits are R22-gated.
Sampling instead of scanning. My first pass over the stored drafts used head -8 of 31 and
reported "best closeness 40" — the MATCH was in the 9th. And I checked whether a draft defined
Prim_1412A8 with a plain grep -c, which matches inside addPrim_1412A8, and briefly concluded
the carry worked. Same shape as reading a return as a declaration (§143): a pattern that is a
substring of the thing you are actually asking about.
The rule
A wall verdict is only as current as the instrument that produced it. Before spending
frontier-model tokens on a documented wall: re-run every stored draft through match_one (ALL of
them, not a sample), and check whether the tools that produced the verdict have changed since. Here
that cost four minutes and was worth 50,094 instructions. The corollary for the ledger: when a tool
is repaired, the verdicts it produced become hypotheses again, not facts.
Symptom lines for the index: "a documented wall" · "an old close= verdict" · "match_one MATCH but the whole binary differs" · "a sweep returns 0 of N twice".
§147 — The three-stratum FRAME LAW, and four "stop searching" verdicts (P30 S42, func_8017C294, serial run)
Five levers from one serial crack. It did not bank (NEAR 12/246, zero structural divergence, and
permuter_ils plateaus at exactly 12) — but the knowledge transfers, which is why the run was serial.
A. The frame has THREE strata, and stratum 3 is unreachable from C
gcc-2.7.2 lays out the frame in order: (1) declared locals, in declaration order, ascending from
the outgoing-args top · (2) reload spill slots · (3) a trailing block the function's ?:
chains allocate and never reference. §136-6 ("slots assigned in DECLARATION order") describes only
stratum 1.
Symptom → verdict: if the target's mystery slot sits at the TOP of the frame, adjacent to the
saved-register area, it is stratum 3 — and NO declaration-order, filler-array, volatile, or
inner-block edit can reach it. Stop looking for the missing local.
Measurement recipe (30 seconds, deterministic): delete the min/max tail, recompile, re-read
.frame … # vars=. The drop IS stratum 3's size. (Here: exactly 96 bytes.) Run this BEFORE
drafting anything large with a min/max tail — it converts an unbounded "which local am I missing?"
hunt into a yes/no.
B. A ?: on MEMORY operands costs ~16 bytes of invisible frame; on REGISTER operands, zero
minx = MIN(outp[0][0], outp[1][0]); /* memory operands -> +16 bytes of frame */
a = outp[0][0]; b = outp[1][0]; minx = a<b?a:b; /* register operands -> 0 bytes */
They are not interchangeable. The memory form is also what emits the target's lhu+lh
double-read of one stack slot (the frame-size counterpart to §136-9). So when a draft's frame is a
multiple of 16 too large around a min/max chain, COUNT THE ?:s before inventing a dead local
(§83c's trap, seen from the other side). Corollary proven here: a zero-temp tail is unreachable when
the target re-reads its operands — 219 ins (if/else) and 234 ins (operands bound to locals) vs 246.
C. Inner-block declaration does NOT delay slot allocation — BYTE-REFUTED
expand_function_start walks the whole BLOCK tree, so { … T x; … } lands at the same stratum-1
offset as a function-scope declaration. Do not spend a cycle on it.
D. A lone $t8/$t9 in the target is RELOAD SCRATCH — reproduce the spill, don't pin the register
MIPS defines no REG_ALLOC_ORDER (regalloc.md K3), so plain allocation never reaches $24/$25
unless everything below is busy. An inherited draft here pinned register short *dst2 __asm__("$24")
to force lw $t8; the pin then pushed mfhi off $t8 onto $t9 — a diff the pin itself created.
The correct lever was structural: delete the volatile "out" local and let the a1 parameter spill
naturally — reload picks $t8 for both store and reload for free, and mfhi $t8 comes right too.
Generalises §17/§72: before pinning a high register, check whether the target's value is a SPILL and
reproduce the spill instead.
E. A qty_compare TIE is not spelling-reachable — recognise it and stop
QTY_CMP_PRI = log2(nrefs)·nrefs·size / (death − birth). When two quantities have equal ref counts
and live ranges differing by one insn, the register grant flips with statement order and both
orders cost the same. Here (mw; xw; mh; yh) buys the target's emission order and mh → $a0 but
transposes w/h; (mw; mh; xw; yh) buys the registers and loses the order — both exactly 5.
Tell: structure exactly right, exactly ONE register PAIR transposed, and the alternative ordering
transposes a DIFFERENT pair. Swept here: 72 statement permutations × 4 declaration orders × 7
s16/s32 retypings × ?:-MAX spellings × ref-count shifts × $v0/$v1 pins, plus the permuter — floor
unchanged in every direction. Worth ~an hour to recognise early.
Consequence for the family (a real scheduling decision)
The blocker is a frame-layout fact about the body, not a per-overlay symbol thing, so all 15
siblings will hit it identically: the same draft remapped reaches 12/246 on each and none will
bank. Do not spend the 15 until stratum 3 is explained. When it closes, family_remap carries
all 16 in one pass.
Symptom lines for the index: "a mystery stack slot at the top of the frame" · "frame is a multiple of 16 too large" · "a lone $t8/$t9 in the target" · "exactly one register pair transposed" · "permuter and hand-search plateau at the same number".
⚠️ §147 CORRECTED BY THE BYTES (P30 S43) — A and E REFUTED, B re-explained. 12 → 2.
The function §147 was written from (
func_8017C294, and its 246-ins twin atfunc_8017CE58in ov_SC02_000/003) was re-attacked with ~70 named probes plus two permuter basins. Three of this section's verdicts were wrong, and the "stop searching" advice cost this project a parked family.
- A — "stratum 3, unreachable from C" is REFUTED. There is no stratum 3. gcc-2.7.2's frame is declared locals, then reload spill slots in strictly increasing pseudo-regno order. The mystery
0x108slot is an ORDINARY reload spill whose pseudo simply has the highest regno — becauseloop.ccreated it: writing the loop as an index loop (for (i=0;i<4;i++)overpos[i]/mat[i]/outp[i]) makesmaybe_eliminate_biv_1/emit_iv_add_multbuild the limit INSIDE the loop, landing the pseudo high; a pointer walk puts it in a low-regno expand pseudo at the BOTTOM of the reload block. That is the whole difference, and it is fully source-reachable — which is why every prior draft needed a fakevolatile pEnd+dead[7]to counterfeit the offset. (121 → 54.)- B — the unreferenced slot block is NOT
?:-on-memory frame cost. It is combine-orphaned sign-extension intermediates:(ashift (subreg (reg:HI)) 16)pseudos that combine folds into anlh, leaving(use (reg))+REG_DEADat a CODE_LABEL (combine.c:10839), soalter_regstill hands each an 8-byte slot that emits nothing. Ablation: min chains cost 4 slots each, max chains only 2 (cse1 elides two conversions per max pass), clamps/base-folds 0.- E — the
qty_compareTIE is breakable.__asm__("" : : "r"(w))aftermw = w— §148-C's zero-emission ref slider — flipsw/hback onto$v0/$v1. (30 → 25.) (Note §150 separately shows this class is often variable-identity, not an allocator tie at all — check pseudo COUNT first.)- D applied properly still holds and is the second-biggest lever: drop the
volatile outlocal and the$24pin, store through thea1parameter and let reload spill it — reload then picks$t8for both store and reload for free. (54 → 30, length exact.)Net: the recorded floor of 12 was a floor of the ANALYSIS, not of the function — it is now 2/246 (
sw $a1/lw $t8at one stack offset), permuter-confirmed from both basins, and one draft covers four instances. The residual is a cse1 elision-count fact (target 16 orphan slots, draft 12), not an allocator or spelling one.The process lesson, which is the expensive part: §147's "stop searching" verdicts parked
func_8017C294AND held back its 15 siblings pending an explanation of a stratum that does not exist. A confident negative verdict in this cookbook is a claim like any other — date it, name the evidence, and re-measure it before letting it park work. (Same shape as §146: re-measure a wall before you respect it.)
§148 — The loop.c hoisting THRESHOLD is arithmetic you can compute, and the ?: clamp that folds to MIN_EXPR (P30 S42, func_8017C6F4, 947 ins)
Serial run #2. Reached NEAR(63) of 947 (frame 0x120 exact, vars=232 exact, all opcodes,
immediates, stack offsets and branch targets correct; residual is one register rotation). Did not
bank. The levers below are the yield.
A. move_movables hoists iff threshold × savings × lifetime ≥ insn_count — and you can read it
loop.c:1631 sets threshold = 58 for this MIPS config and decrements it by 3 per movable
already moved (threshold -= 3, loop.c:1719/1904). This is the first quantitative handle we have
on gcc-2.7.2 invariant motion.
Measured here: the draft's prim loop was 513 RTL insns, so &g.flag (savings 3, lifetime 3 →
58×3×3 = 522 ≥ 513) got hoisted into a preheader register that the target recomputes inline —
costing a callee-saved register and cascading into 2 extra spills and +96 instructions.
Duplicating the if (za < g.sz2) za = g.sz2; tail into both arms of the sz0>sz1 test (which the
target's cross-jump reveals) pushed the loop to 523 insns → 522 < 523 → not hoisted → the
register came back.
Symptom → lever: an address (&x) hoisted into a loop-preheader register that the target
recomputes inline ⇒ raise the loop's RTL insn count by ~10, or lower the movable's ref count.
Read it directly: cc1 -dL writes <file>.i.loop, which prints
Loop from A to B: N real insns and, per movable, Insn K: regno R (life L), savings S moved/not desirable. Stop guessing which invariant moved — the dump names it.
B. (v < 0x40) ? v : 0x3F is folded to MIN_EXPR and expands to the WRONG SHAPE
fold-const.c:4948 — A < C1 ? A : C2 with C1 == C2+1 becomes MIN(A,C2), which expands as
copy-then-conditionally-overwrite (move t,v; slti; bnez; li). A <= C ? A : C folds too (the
A op B ? A : B rule at 4907).
(v < 0x40) ? v : 0x3F -> MIN_EXPR -> move/slti/bnez/li WRONG
(v > 0x3F) ? 0x3F : v -> no fold -> jumpifnot/store/j/store RIGHT
Same slti $v0,$v,0x40 is emitted either way — but the second keeps gcc's canonical branchy form.
This single respelling took the draft from close 827 to close 63 and fixed all four clamps
instruction-for-instruction. Corollary: (v < 0) ? 0 : X is safe — fold's "swap if arg1 is simpler"
rewrites it to (v >= 0) ? X : 0, which is exactly the target's bltz.
C. A zero-byte ALLOCNO-PRIORITY slider
__asm__ ("" :: "r"(a), "r"(b)); /* emits nothing */
Priority is floor_log2(n_refs)·n_refs·size / live_length, and REG_N_REFS is incremented by loop
depth — so an empty asm with "r" inputs inside a loop adds depth references per operand while
emitting no code. Used here it flipped {rowptr,y} ↔ {cx1,cy1} for the last two callee-saved
registers. This is the counterpart to §47's live-length slider: that one moves the DENOMINATOR,
this one moves the NUMERATOR.
D. Reproduce the original's BUGS verbatim
This variant's F3 arm bbox-tests the packet through PolyFT3 offsets (pkt+8/+0x10/+0x18, stride 8)
while writing F3 xy at stride 4; the FT4 arm reads tmpxy[3].vx where .vy is meant. Both are
original-source copy/paste bugs. Matching means reproducing them.
⚠️ E. A "these are all the same function" claim needs the DRAFT test, not a diff
The run reported all sixteen 947-ins instances as one identical body (→ "one crack banks 15,152 ins").
Checked and it does not hold: the draft scores 63 on ov_SC03_126 but 340 on
func_8017C59C and func_8017CF90 — with an identical first diff on both, i.e. those two match
each other but not the cracked one. That is consistent with the h_norm clustering (947×3, 947×2, plus
singletons): several multi-instance groups, not one group of 16. A normalized-stream diff can say
"same shape"; only running the actual draft against the sibling's asm says "same body". Cheap
test, do it before scaling a ×N claim (sed s/func_A/func_B/ the draft and match_one it).
Tooling note
permuter_ils cannot be aimed at this draft: run_masked reports "Function … not found in
base.c" because the gte_* #define block defeats make_base_c. Demacroize first — worth doing
generally, since any GTE-using draft hits it.
✅ FIXED, and the diagnosis above was WRONG (P30 S43, R14/R35). Nothing about the draft or the macro block defeats
make_base_c—cpp_expand_macrosexists precisely to handle it, and its detector fires correctly here. The real cause:make_base_cran the cpp expansion before#includelines were dropped, somipsel-linux-gnu-cpp -P -nostdinc -died on#include "common.h"(rc=1, empty stdout) and areturn cfallback handed back the unexpanded draft — silently re-creating the exact failure the function was written to prevent. No demacroizing is needed by hand. Fixes: strip#includeinsidecpp_expand_macros(byte-neutral — they are dropped downstream anyway) and RAISE on cpp failure; plusdefines_fn(), an R32 assertion insetup()that the function's definition survives intobase.c, which catches every swallow cause rather than this one (validated: 0 false alarms over 388 stored drafts). Blast radius: 63 stored drafts carried both a#define … __asm__and an#include, including the behemoth renderer drafts — the permuter lane was silently dead on the highest-byte-weight targets on the board. After the fix this draft loads at base score 65 and iterates.The class, third sighting (§G unterminated comment · §133 default-filter · here): a prep step that silently returns its input on failure turns "the tool broke" into "the search found nothing", and the two are indistinguishable at the call site. A permuter run that reports no match in ~0 seconds is a TOOLING verdict until proven otherwise — check
base.cbefore believing the floor. Corollary for §147/§148's hand-search floors: the ~40 probes on this function were run with the permuter unavailable, so "the permuter also plateaus" was never actually measured here.
Symptom lines for the index: "an &address hoisted into a loop preheader" · "a clamp expands as move-then-overwrite" · "MIN_EXPR" · "two callee-saved registers swapped" · "permuter says function not found in base.c".
§149 — Four instrument defects in one session, and the two questions they were hiding (P30 S43)
Not a codegen section. Every item here was recorded in a previous session as a property of the CODE; each turned out to be a property of a TOOL. That is now the fourth consecutive session with that shape (§53 carve · P28's B2 · P27's five · this), so the pattern below is the reusable part.
A. A prep step that returns its input on failure is indistinguishable from a search that found nothing
p16_permute.make_base_c ran cpp_expand_macros before #include lines were dropped, so
cpp -P -nostdinc - died on common.h, and return c handed back the UNEXPANDED draft. hide_asm
then ate the gte_* macro block and the function with it; decomp-permuter reported "Function not
found in base.c" and no-opped in 0 s. §148 recorded that as "the GTE #define block defeats
make_base_c — demacroize first." It was a two-line ordering bug, and it had silently disabled the
permuter on 63 stored drafts, including the behemoth renderer drafts — the highest-byte-weight
targets we have.
Rules: a permuter run that reports no match in ~0 s is a TOOLING verdict until base.c is
inspected. And prefer an assertion on the OUTPUT (defines_fn: the definition must survive into
base.c) over a fix for the one cause you found — the output check catches the comment-eating class,
this cpp class, and the next macro shape, for free.
Consequence worth generalising: a hand-search floor measured while a tool was silently broken is not a floor. Here the ~40-probe "unmovable 63" fell to 42 the moment the permuter could run.
B. Same address + same name ≠ same body — and the ledger keys on address
0x8017C6F4 is a 15-instruction function in ov_SC03_010/011/013 and a 948-instruction
renderer in ov_SC03_126/003, ov_SC04_021, ov_SC05_019. backlog.load_best() keyed on address
alone and kept the lower absolute closeness, so a 14-of-15-wrong draft (7% correct) masked a
hand-won 63-of-947 (93% correct) and the giant vanished from render, the grinder, and target
selection. Sub-key by known nins; different sizes are different bodies.
Two corollaries, both live:
closenessis an absolute mismatch count and is NOT comparable across sizes. 14/15 outranks 63/947 in every ranking we have. (A relative rank was probed and NOT built: only 24 of 836 live rows carry bothclosenessandnins. Revisit when coverage rises.)binary: null→ defaults toov_SC01_077→ "not an open stub there" was read as "banked". When the function does not EXIST in the defaulted binary, absent was being scored as done (R32/R34). Derive the binary from the draft path; drop a row only when the fn is closed everywhere it exists.
C. make: *** [...] Error N is a summary, never a diagnosis
classify_fail took errs[-1], and make prints its own failure summary last, always — so the
wrapper won every time. ~3,000 of ~4,000 CC1-FAIL labels in the committed .classified.txt corpus
say nothing but the TU name the record already stores, which is why diagnosing one cost a manual
splice-and-rebuild. Exclude make's lines; take the FIRST real diagnostic (cc1 cascades — error #1
is the cause, error #N the aftershock); label a wrapper-only failure CC1-FAIL(no-diagnostic) rather
than disguising it. Same family as the §58 warning red-herring guard directly above it in the source:
a label identical for every input carries no information.
D. "Cheap fuel" that was never probed: 0 of 31 templatable
Three sessions carried "26 unpropagated members, the recovery ladder has every lever, ~0 tokens."
Scanned all 31 (not sampled): a mechanical family_remap from every matched source binary fails
on 31 of 31, with gross reloc-count mismatches (2!=15, 11!=20, 2!=0). They are structurally
distinct bodies sharing an address — B's collision, one level down — and family_hseq independently
agrees (matched=0, several n_members=1). Nothing to template from ⇒ never plumbing, always
per-member drafting.
The estimating rule this proves (R37's own failure mode): a work item carried across sessions with a token price attached and no probe behind it is a guess wearing a number. Probe one member before it is scheduled — the probe here cost minutes and removed a phantom item from the board.
Symptom lines for the index: "the permuter found no match in 0 seconds" · "a wall that only one tool reports" · "a backlog row that outranks a better result" · "CC1-FAIL: make: *** Error N" · "cheap fuel nobody has probed".
§150 — A register ROTATION across symmetric blocks is VARIABLE-IDENTITY evidence, not an allocator tie (P30 S43, func_8017C6F4, 947 ins ×4)
The function that §147-E's signature fit perfectly — "structure exactly right, one register pair transposed, hand sweep and permuter plateau at the same number" — and the signature was wrong about the cause. Every lever aimed at the allocator was structurally inert because the draft had the wrong NUMBER OF PSEUDOS. Two coupled source-shape changes matched it pin-free.
The fix
- The X-pass and Y-pass min/max intermediates are DIFFERENT variables —
xmn1/xmx1/xmn2/xmx2andymn1/ymx1/ymn2/ymx2(8, not 4 reused). - The cell-level clamp temps did not exist. The original reuses the PRIM-LOOP variables:
mn/mxfor the cell X clamp,mny/myfor the Y clamp.
Neither half works alone — split-only = 63 mismatched (which is exactly why a previous session's "separate X vs Y variables" probe was recorded as a failure), reuse-only = 624 + length drift, the conjunction = MATCH. Ablate both ways before believing either half is wrong.
The method that found it (this is the transferable part)
The diff's matching regions are a pseudo→register ownership map. Read the mismatching region's target registers against that map before touching the allocator:
- Different target registers for different instances of a symmetric block ⇒ per-instance
variables. gcc-2.7.2 does no live-range splitting: one pseudo holds one hard reg for life. So if
the X pass uses
(a2,t1,a1,a3)and the Y pass(a3,t0,a1,a2), the original CANNOT have used one shared set — and no allocator steering on a shared-variable draft can ever reproduce it. - Contested registers that coincide with a KNOWN variable's register ⇒ the region reuses that
variable. Here the four clamp registers were exactly the prim-loop
mn/mx/mny/myregisters, already byte-matched elsewhere in the function, with the right semantics per axis. - A deleted self-move in ONE instance of a repeated block is a per-instance-variables tell.
global_conflictsprocessesREG_DEADbeforemark_reg_store(global.c:719vs:729), so a copy whose source dies at that point records no conflict and the destination may be granted the source's register by ordinary first-fit — the copy then vanishes. A single shared pseudo can never be "same register as its source" in one pass and "different" in the other.
Two corrections to the record
- §147-E named the wrong allocator. The contested values here are multi-block and non-call-crossing
⇒
global.callocnos, not localqty_comparequantities. With the right variable identities, plainallocno_comparedensity order + first-fit reproduces every grant deterministically (full priority table in.run/s43/fable/8017C6F4/NOTES.md). Check WHICH allocator owns the value before citing a tie in it. - §148's "residual is one register rotation, did not move under ~40 probes" is explained: the probes were all allocator-shaped, and the defect was arithmetic — the wrong number of pseudos. Register pins, priority sliders (§148-C), declaration order and the permuter are all inert against a variable-identity error, which is why they agreed on a floor.
Diagnostic order (adopt this)
Decode ownership → check pseudo COUNT and per-instance identity → only then reach for pins, sliders, statement order, or the permuter. A rotation that survives every allocator lever is evidence about the VARIABLES, not about the allocator.
Symptom lines for the index: "one register pair transposed" · "different registers in the X pass than the Y pass" · "a missing move / deleted copy in one of two symmetric blocks" · "every pin and slider is inert" · "hand sweep and permuter plateau at the same number".
§151 — THE GHOST WEDGE: when a load-before-store transposition is unreachable by ANY statement order (P30 S43, func_8017EF68, 969 ins)
The 2-of-969 residual that ~20 documented hand variants and the repaired permuter both floored at. Not a tie — a hardware-model constraint in the scheduler that no source order can escape.
The mechanism (read from cc1's own -dR trace, not inferred)
Target wants lw directly before a store; the draft always emits one ALU insn between them.
- The rival
srlis ready early and ranks first every tick, but loses to the boxshs viaschedule_select's potential-hazard rule (memory-unit users first). - The
lwbecomes ready exactly one tick after a store is picked (store-after-load anti-dep; MIPSADJUST_COSTzeroes anti cost, clamped to 1) — and is always unit-blocked there. The r3000 machine description gives the memory unit load-ready-cost 2, store 1, soblockage(load, store) = 2: a load can NEVER be picked in the tick immediately after a store pick. It queues one tick and wins the next. - Therefore sched2 always wedges one ready ALU insn between the
lwand thesh. Zero wedge is unreachable by any statement order or LUID assignment with that instruction set.
The diagnostic to look for: ;; blocking insn N for M cycles in a -dR dump. That line means the
schedule you want is not an ordering preference — it is forbidden by the machine model.
The lever — a zero-emission insn that absorbs the blocked tick
__asm__("" : "=r"(v) : "0"(v)); /* same-reg in/out re-tie: 0 bytes emitted */
Tied in/out on one pseudo, empty template ⇒ no code, but it IS a schedulable insn: it inherits the
rival's predecessors and the value's consumers, so it is ready in the blocked tick and takes the wedge
slot the ALU insn would have taken. The lw then launches on the next tick and the rival slides after
it. Bonus: it sets reg_n_sets(v) = 2, which also kills sched1's birthing boost — one instrument
acting on both scheduling passes.
The two fallouts, and how to close them (both measured, in order)
- A — register flip from the density rise. Adding refs to
vmoves it in the allocator and flipsv/rival off their registers. Cure: read the rival in the SAME asm —asm("" : "=r"(v) : "0"(v), "r"(rival))(the §S13 both-rivals lever). - B — allocno live-length PARITY. Every inserted in-loop insn adds +1 live length to every
loop-spanning allocno.
global.c'sallocno_comparepriority is integer-floored (floor_log2(refs)·refs/length·10000), so a trio of loop-invariant addresses sitting exactly on a floor boundary rotates its register homes when the parity changes. One ghost breaks it; a second re-tie restores it. Host choice is empirical and matters: a high-ref pseudo far from any boundary works (herepkt⇒ MATCH), a low-ref one does not (ot⇒ 705 off; doubling the first host ⇒ 10 off). Pick the host by ref count, then verify.
When to reach for it
A pure adjacent transposition of an independent load and a store, where statement order provably does nothing. Expect it across the ~950-instruction renderer siblings that share this unpack block. ⚠️ Corrects the draft's own §49 write-up, which read this as a sched1 LUID/sink story: that was incomplete — the LUID effect is real but secondary to the unit blockage.
Symptom lines for the index: "a load and a store transposed" · "blocking insn N for M cycles" · "no statement order changes the pair" · "two instructions apart after every lever".
§152 — BYTE SIZE is the family key that name- and h_seq-grouping both miss (P30 S43, the 0xECC family: 1 crack → 12 overlays → 11,364 ins)
Three isolated agents, working independently on what were filed as three different functions, all arrived here. The finding is worth more than the 11,364 instructions.
The finding
func_8017C6F4's 947-instruction body exists in 12 overlays under 5 DIFFERENT NAMES at 6 DIFFERENT
ADDRESSES, each differing by exactly two per-overlay symbols. It stayed invisible for ~30 phases
because every grouping we had scattered it:
- name-keyed → five separate "functions";
- address-keyed → six separate entries (and worse, the address collides with an unrelated 15-ins body in three other overlays — see §150/§148-E);
- h_seq-keyed → scattered as well, which is why the Phase-26 "h_seq is spent" sweeps missed it.
The key
grep -rl 'nonmatching .*, 0x<SIZE>' asm/*/nonmatchings/*_jr_<SPLIT>/
Byte size is an allocator-independent, name-independent, cache-independent family key, and it reads
the asm rather than a manifest (so it cannot go stale the way .run/family_hseq.json does). One
command, exact, no false positives on the case measured. Then family_remap's clean reloc pairing is a
free structural-identity oracle, and the per-member edit is a 2–3 symbol substitution.
This refines — does not contradict — the Phase-26 finding. h_seq mass-templating is still spent as a standalone harvest. What pays is exactly one size-keyed sweep behind each FRESH core crack. Here it returned 11:1 on the crack that preceded it. Run it immediately after banking any core.
Two cautions that must travel with this technique
- A masked tool CANNOT validate a remap.
match_oneandrtu_matchboth maskjaltargets and%hi/%loimmediates — exactly the fields a remap rewrites — so a WRONG symbol map still reports MATCH. Confirm each pair against the raw unmasked call-site pattern, and gate remaps by the whole-binary SHA only. (§53's carve law, restated for the remap path.) - A stale residual number is never evidence that two functions differ. A pre-fix draft scores nonzero against every member of its family including its own target, so "draft X scores 340 here" says nothing about body identity. Re-measure with the currently banked draft before accepting a "different body" verdict — one such stale number was briefed as fact this session and refuted in a single command.
The companion defect (open)
family_remap.py's raw output does not compile: its unit backscan accepts only blank/extern/comment/
typedef lines and therefore halts at the first #define, never chaining back past it. Measured:
16/16 gte macros carried, 0/10 typedefs carried, silently (R32). Prepending the typedefs by hand is
the workaround; the fix is to let the backscan skip #define continuation blocks.
Symptom lines for the index: "the same function under several names" · "a family that h_seq missed" · "one crack that should have propagated but didn't" · "a remap that gates MATCH but banks wrong".
§153 — THE ADDRESS-REMATERIALISATION LAUNDER: a third zero-emission asm lever (P30 S43, func_8018D98C, 710 ins)
(The producing agent proposed this as "§152"; that number was taken the same session by the size-key finding, so it lands here.)
Third member of the zero-emission-asm family, alongside §148-C (allocno priority NUMERATOR) and §151 (the blocked scheduler tick). This one launders an ADDRESS out of CSE's equivalence class.
Symptom
"One more sw $sN in the prologue than the target, and one la $sN,SYM + N × move where the
target has N × la $aX,SYM." I.e. your build hoists a symbol address into a callee-saved register
and copies it to the argument register per call; the target rematerialises la $aX,SYM at each site.
Mechanism (gcc source + RTL dumps, not inferred)
Every &SYM argument expands into its own pseudo. With ONE use, combine folds it back to a plain
la $a1,SYM. But when the same address is an argument ≥2× inside one CSE basic block, cse
unifies those pseudos into a single 4-ref pseudo; local-alloc.c:1080's rematerialisation path
requires reg_n_refs == 2 && reg_basic_block < 0, so it never fires, and global.c:388 then hands
the pseudo a callee-saved register — which cascades a rename across every other allocno.
What does NOT work (14 byte-measured probes)
plain · cast · array-decay · volatile · struct-typed · unprototyped · cast-through-fn-ptr ·
__asm__ __volatile__("") placed between the blocks — all hoist. do { … } while (0) splits
cse1 (cse_end_of_basic_block breaks at NOTE_INSN_LOOP_END only when !after_loop) and then
cse2 puts it straight back. This class is not reachable by respelling.
The cure — a fresh launder per site, each in its own block
{ s32 _m = (s32)&SYM; __asm__ __volatile__("" : "=r"(_m) : "0"(_m)); callee(x, _m, y); }
The volatile asm is never entered into cse's table and it SETS _m, which empties the symbol's
equivalence class; the single remaining use ties _m straight to the argument register, so the emitted
code is literally la $a1,SYM. Zero bytes.
Placement is load-bearing — #APP is a scheduling barrier, so the launder must be a statement
before the call, not inside the argument list. When a call takes two such addresses, launder BOTH,
symbol first; on the measured case that last step took the residual from 3 to 0.
Companion levers from the same function
- Branch polarity: the target's
beqzsays the!= 0arm is the one written first in the source. - §150 in its "how many pseudos" form: the same value wanted block-local scope inside a macro
(
$v1) but a single function-scope variable across the switch cases ($a0), with a per-case block-local timer temp. Both uniform extremes were wrong (176 and 42 mismatched). Scope is a per-value decision, not a file-wide style. - Full byte-measured ladder on this function: 483 → 444 → 363 → 333 → 176 → 42 → 28 → 3 → 0.
⚠️ And an integration caution that cost a gate cycle
The agent produced two variants: a plain one and one whose callee decls were "conformed to the TU's
existing prototypes." The TU-conformed variant gated DIFF; the plain one banked. rtu_match
MATCHing is not a promise that a decl-rewritten variant survives the real build — when two variants
exist, gate the PLAIN one first.
Symptom lines for the index: "an extra sw $sN in the prologue" · "la $sN,SYM + moves
where the target rematerialises" · "an address argument used twice in one block" · "a hoist no
respelling reaches".
§154 — Reading a disc payload: the module-id word, static base derivation, and "type 1 = uncompressed overlay" (P30 S44)
Not codegen — payload forensics. Three laws from the 78-unclaimed-payload analysis, each of which turns a former "needs the emulator" into a static read.
A. Payload word0 is a global MODULE ID; code starts after the header
75 of 78 unclaimed payloads begin with a small LE integer forming one dense id space across all discs
(0x13…0x73; the resident is 0x36). Some follow it with a function-pointer table (SC07/3: table to
0xF8; SC07/4: to 0x154) before code. Consequences: sig_image --bootstrap returns 0 functions
on any of them if run from offset 0 (its linear partition hits the header, finds no jr $ra, stops) —
always pass --text-lo past the header; and a header's own pointer table dates the base for free
(first table target − first prologue file offset = base). Only payloads that begin directly with code
(a 27bdffe8-class prologue at offset 0) may be signed bare.
B. Two static base-derivation methods that must AGREE (use both)
- h_exact voting: sign the payload at ANY nominal base; for every function h_exact-identical to a
corpus function,
delta = corpus_addr − signed_addrvotes for the true base. On real overlays the margin is decisive (~500:1 — 218,454 votes vs a 414 runner-up). - jal-alignment voting: collect distinct internal
jaltargets; the base under which the most land on actual prologue file offsets wins. Corpus-independent; the control (the resident) reproduces its known 0x800CEDF8 and ends 4 bytes under the overlay slot. A payload where the two disagree, or where votes are thin (script modules: 3–14 aligned jals, calls almost all outward), is loader-determined — park it for runtime confirm rather than guessing (P9). And check the LOADER first: the EXE'sloadDestPtrTable+ the resident's index tables route most payloads statically (memory-map.md§S44) — the vote is then the R34 cross-check, not the source.
C. PAC type 1 = the same payload class as type 4, just NOT compressed
The three biggest "mystery modules" were ordinary location overlays for the standard 0x80128158 slot, stored raw. Their first 192 bytes are byte-identical to built ov_ images; ~75% of their functions are h_exact-identical to the corpus. Before inventing a new class for a payload, diff its head against the classes you already own. (Corollary of §152: same-bytes is the family key — here at payload scale.)
Symptom lines for the index: "sig_image bootstrap finds 0 functions" · "a payload with a small integer first word" · "where does this blob load" · "a huge type-1 module".
§155 — hi/lo literal scanning MUST track base registers (S45)
A "find who references address X" sweep that pairs any lui with any later lo16-bearing op in a
window produces PHANTOM cross-references: the lo16 may ride a DIFFERENT base register (e.g.
lui $s2,0x800B … lui $at,0x8019; sw $s1,-0x1798($at) — the window-pairer reads 0x800AE868, the
truth is 0x8018E868). One such phantom steered an evening of MAIN/7 hunting (S45). Track the
register: record lui rt → hi, match only ops whose BASE is that rt (addiu rs==rt / mem-op
base==rt), invalidate on clobber. Register-blind results are candidates for triage only, never
evidence (G3/R14). The corrected pattern lives in the S45 rescan (checkpoint p4 → tools).
§155a — the same failure class, one level up: SHAPE-blind table scanning (S45 p5)
§155's lesson generalizes past instructions. Hunting a data table by its shape alone
("-1-terminated s16 run whose values are all valid indices") produces the identical brand of
phantom, and it will pass a coverage assertion while doing so. The S45-p5 scan re-found its
known-good control table exactly (R32 green) and still returned 664 "tables" across 212 payloads
whose parked-index hits were transparently (offset, count) pair data — [44, 2, 48, 7, 62, 6, 74, 7, ...] "contains 7". The discriminating power of a shape predicate collapses when the value
you are hunting is small and common (a global index of 7 or 9 looks like every other small
integer in the binary).
The law: a coverage assertion (R32) proves the scanner ran over everything; it says nothing about whether the predicate discriminates. Those are two different oracles (R34). Before trusting a shape scan, ask: would a random data region satisfy this predicate? If yes, the scan is a triage filter, never evidence — derive the table from the code that indexes it (register- tracked, §155) instead of from the values it holds.
Cheap test to apply first: compute the predicate's hit-rate on the corpus. 664 hits where the truth is ~1-per-overlay is itself the refutation — a discriminating predicate is rare.
§155b — check the TYPE your oracle returns before comparing against it (S45 p5)
A membership test against the wrong key type fails silently and always, and it looks exactly
like a real finding. corpus.stubs(binary) returns a dict keyed by integer address
(2148696600), not a set of names. Testing "func_80132018" in corpus.stubs(b) is therefore
always False — and it produced, in one session, two confident and completely wrong conclusions:
"none of the wave's matches are live stubs" and "the target pool was never filtered". The pool
was in fact 160/160 and 166/166 correct, and 9 of the matches were genuinely bankable.
This is the R32/R35 family's blind spot: those rules make a tool assert its own coverage and its own correctness, but neither catches an interface mismatch at the call site. A silent always-False comparison has no coverage gap to detect and no instrument to repair — the tool is fine; the caller is wrong.
The law: before using any oracle's return value in a comparison, print one element of it.
print(type(x), next(iter(x))) costs one line and would have caught this instantly. Corollary
for this repo: corpus.stubs is address-keyed — convert with
{f"func_{a:08X}" for a in corpus.stubs(b)} before comparing against names.
Smell test: a membership test that returns 0/N — exactly zero, across the whole corpus — is far more often a type error than a discovery. Real negatives are usually ragged. When a check comes back perfectly empty, verify the comparison before believing the conclusion (R14/R37).
§155c — the ZERO-REFERENCE trap: gcc splits a global-array address across the lui and the LOAD (S46)
§155a's law says a shape scan needs a discriminator, and names the obvious one: require a register-verified code reference to the candidate's address. Run exactly that against the two byte-proved IDXTABs and it returns zero references — and the naive reading ("nothing references these tables") is wrong in the most expensive way, because it looks like a discovery.
The tables are read by gcc's indexed global-array form:
lui $at, 0x8019 ; hi half
addu $at, $at, $a0 ; + index <- $at is WRITTEN here
lh $v0, -0x2844($at) ; lo half, in the LOAD -> 0x8018D7BC
The address exists only as (lui imm, load offset) — the index add sits between them. Any tracker
that invalidates a register when it is written (which §155 correctly demands!) kills $at at the
addu and can never rejoin the halves. So the strictness that makes §155 sound also creates a blind
spot for the single most common way a compiler reads a table.
The fix (in tools/find_addr_refs.py): carry the hi half through an index addu — still strictly
register-tracked, never window-paired — and label what it feeds -indexed so the two shapes stay
distinguishable. That one change took tools/idxtab_map.py from 0/2 controls to 2/2 and produced the
fleet load map (docs/idxtab-map.md).
The law: "no code references X" is a claim about your DECODER, not about the binary, until you have shown the decoder recognises the addressing forms the compiler actually emits. Before believing a zero, hand-disassemble ONE known-good case and check your tracker sees it (§155b's smell test: exactly zero is more often an instrument gap than a fact).
§156 — an ORPHANED reconcile poisons the fleet: dedup_propagate's kept edit (S45 p6/p7)
ATTRIBUTION CORRECTED (R14). This section first blamed
gate_stage's arity pre-pass (finding F1 ofdocs/concurrency-design.md). That was wrong. No arity journal from that session mentionsfunc_80146A6C(checked: 74/26/4 entries), and the arity undo reported success in every log. F1 is real and still worth guarding — it just did not cause this. The commit message oncommit:1519carries the same wrong attribution; corrected forward here, history not rewritten.
The real mechanism. dedup_propagate --recover's Part B reconciles an overlay's conflicting
caller extern and, when that buys the byte-match, deliberately leaves the edit on disk
(dedup_propagate.py, "keep the reconcile on disk"). That is correct while the function survives.
But a function can still be dropped by a later iteration against a different fail_ov, and when
plan finally empties, the sys.exit("[error] all candidates dropped …") fired with no restore.
Observed live: reconciles kept for ov_SC07_001..009, then everything dropped, then exit — leaving
no-proto'd caller externs for functions that were never propagated →
ov_SC07_010: passing arg 2 of 'func_80146A6C' makes pointer from integer → 141 of 213 binaries
failed check-all.
Fix (landed): a reconcile ledger — every kept reconcile is recorded against its function,
undone the moment that function leaves plan, and all outstanding reconciles are restored before the
failure exit. Proved by tools/test_reconcile_ledger.py: applying a real reconcile for the exact
overlay+fn (35 edits across 18 files) then driving the ledger undo restores all 25 files
byte-identical.
The generalizable law: a tool that deliberately leaves an edit on disk pending an outcome owes a ledger for it. "Keep it if this succeeds" is only half a transaction — the other half is undoing it on every path that can later invalidate the success, including the exit paths.
What it does NOT do: it cannot false-bank. The gate compares against config/check.<bin>.sha
(the original retail bytes, written by no pipeline stage) and INCLUDE_ASM pastes the original
assembly, so wrong C always diverges. The failure is loud and fail-closed — it costs time, never
integrity.
The trap it sets: a broken tree makes EVERY subsequent gate report near. Two batches
(4/4 and 20/20 "near") were read as verdicts about the drafts when they were verdicts about the
tree. A gate result measured on a tree you have not just verified is not evidence (R35).
Standing practice:
- Gate with
GATE_NO_ARITY=1unless you specifically want the arity lane; take the arity-needing drafts through a separate serial pass. Measured cost of the guard: 2 banks of 24 — cheap. - Assert the bracketing invariant after every gate batch:
git status --porcelain src/shared configmust be empty. This is the only cheap detector. - On breakage, do NOT surgically patch:
git checkout -- src/ config/and replay from the on-disk drafts. Replay is deterministic (7/9 and 22/39 reproduced exactly), so recovery costs build time only.
§157 — the cheap-tier size cliff, measured (S45 p6)
Two controlled Haiku waves, same prompt, same pool construction, same independent verification — the only variable was function size:
| band | close rate | tokens/match |
|---|---|---|
| 4–27 ins | 43/50 = 86% | ~44k |
| 30–39 ins | 9/17 = 53% | |
| 40–49 ins | 5/8 = 62% | |
| 50–59 ins | 2/9 = 22% | |
| 60–69 ins | 1/8 = 12% | |
| 70–85 ins | 2/8 = 25% | |
| ≥50 combined | 5/25 = 20% | ~177k (4× worse) |
The documented "Haiku ≤~50 ins" band is optimistic. The cliff starts around 30 and collapses past 50. Route ≤30 → Haiku (unbeatable cost), 30–50 → Haiku only when targets are plentiful, ≥50 → Sonnet.
Agent honesty at the cheap tier is excellent and should be relied on as a FILTER (never as the
gate): across 100 drafters, 63 MATCH claims, 63 confirmed by independent match_one re-runs,
0 false. The one apparent false claim was the verifier's own fault — an -O0-cluster function
(func_8013C360) checked without --o0. Always retry a failed verification with --o0 before
calling an agent wrong (§116).
§158 — The RANGE-EXTENDER: a fourth zero-emission asm lever completes the allocno toolkit (P30 S46 tier-3, func_8017CE58, 733 ins)
Fourth member of the zero-emission-asm family: §148-C moves the priority NUMERATOR (refs),
§47 slides the DENOMINATOR window (+1 slot, no refs), §153 launders an address out of cse,
and this one MOVES A DEATH — __asm__ __volatile__("" : : "r"(var)) placed AFTER var's
natural last use extends var's live range to the asm, growing its reg_live_length by the whole
gap (every insn crossed, +1 each) at the cost of +loop-depth-weighted refs. It reaches the one fork
§47 declares one-directional: a chained order↔registers fork on a USER-VARIABLE pair.
Symptom
"Source order buys the ORDER or the REGISTERS but never both" on a same-source pair (here
mny = wy; my = wy >> 16; — copy/shift of one loaded word straddling the next load), with the rest
of the function matching. SCHEDULE-REORDER/2 by the classifier, but the resolution lives in
global.c, not sched.c.
Why the fork is chained (gcc source, validated insn-by-insn against -dS/-dR dumps)
- sched.c schedules each block BACKWARD; rank = priority → independence-from-last-scheduled
(an insn ANTI-dependent on the just-placed one is class 2 and waits a tick — that is what slots
the next
lwBETWEEN the pair) → INSN_LUID = source order.adjust_priority's REG_DEAD cases are dead code (the???comment is accurate); only the birthing boost runs, and only forreg_n_sets==1pseudos — single-set load temps get boosted, multi-set user vars never do. Net: the pair's final order follows source statement order, full stop. - global.c
allocno_compare:pri = floor_log2(refs)·refs·10000·size / live_length; equal refs → shorter life allocated FIRST → LOWER free register; exact tie → lower allocno = declaration order. Live length is counted on SCHED1's output order (sched.csometimes_live, +1 per insn per segment) — so the earlier-born pair member is always longer → always loses the low register. Order and allocation are chained to the same source order; inside the block the fork is unwinnable.
The method (dump-arithmetic first, then place — no probing)
-dlthe current best: read both pseudos'Register N used R times across L insns. Compute both priorities. Work out the needed inequality (here: extendmysopri(my) < pri(mny)).- The extender adds ~
+depth+1refs and+gaplength; solve for the required L before placing (here refs 46→49 ⇒ need L ≥ 111 from 102 — the first placement gave 110 and flipped back; ONE slot deeper was exact). Knife-edge is normal, the dump re-check costs seconds. - Placement rules: after the pair's rival is DEAD (so only the extended one grows); adjacent to an existing volatile asm (§47's rule — no new barrier); NEVER before a call-crossing gap or a loop-entry (an upward-exposed use makes the var live-on-entry/call-crossing → it loses its caller-saved register entirely and the cascade is catastrophic).
- Expect §47-plateau collateral and compose the levers. The extender's +1 slot shifts EVERY
pseudo spanning it; here it pushed the two loop-invariant
&gszaddress pseudos (refs 7, 560/559) off theirint(140000/L)=250plateau → $s5/$s6 swapped. A bareasm volatile("")immediately after the extender (+1 slot, ZERO refs) put them back on a tie (562/561 → 249==249 → allocno order). One lever per priority relation: refs-carrying extender for the pair, ref-free slider for the plateau.
Bonus facts worth keeping
- USE insns are the mechanism, and gcc plants its own: loop.c leaves
(use (reg))insns in the stream that exist at sched1/alloc time and vanish before final — live length is measurably a function of insns final never emits. The keep-alive asm is just a plantable one. - The sched1-vs-sched2 uid-sequence diff (
-dSvs-dR, ~14 windows on this function) is a fast map of WHERE zero-byte freedom exists; both my failed "swap the min/max arm statements" probes sat OUTSIDE any window and predictably broke bytes (arm order + the y0/y1 $a0/$a1 tie flip together — measured, 6 mismatches). - Declaration order = allocno tie-break is a real lever (
s16 mny, my..won a 103/103 tie in a probe) but it only fires on EXACT length ties; the extender makes the inequality strict instead.
Symptom lines for the index: "order or registers, never both" · "copy/shift pair swapped around a load, registers correct" · "first-born always gets the higher register" · "keep-alive flipped an unrelated $sN pair" (→ compose with §47's bare slider).
§156 — THE PREFERENCE-DONOR MERGE: cross-region variable reuse is what fills a0-a3, and a call-arg use in ONE region steers the fill in ALL of them (P30 S46 tier-3, func_80186E24, 611 ins: 236-off "S11 regalloc-order" → MATCH, zero new pins)
Symptom: a multi-loop function where every loop's caller-saved map is permuted the same way —
target consistently {a0: index scalar, a1: dst ptr, a2: src ptr, a3: char/bound}, yours fills
v1/a0/a1 "correctly" by density first-fit. Per-loop locals can NEVER reproduce it: the copy-loop
pointers' density (loop-weighted refs / tiny range, e.g. 13/12) beats the index var's (10/32) in
allocno_compare, so your dst/src allocate first and sit in v1/a0. No conflict, no decl order, and
no S11 verdict fixes an ORDER gap that size.
The two coupled mechanisms (read off -da greg/lreg + real-2.7.2 global.c):
- MERGE pole, function-wide (extends RC-14 beyond one block): ONE
char *d; u8 *s;reused as every phase's dst/src (incl. secondary pointers: the ph6 ent/d2, the digit-render write pointer) sums their loop-weighted refs into 100+ → the merged allocnos allocate FIRST and first-fit lands them in a1/a2 for the WHOLE function (v0/v1 blocked by block-temp conflicts). One edit moved 113 → 26 mismatches. Same move for scalars: one s16 scratch serving {phase-A cnt, phase-B fl, phase-B cnt, filter n} — union range crosses a call somewhere → K4 → $s0 everywhere. Diagnostic tells: (a) the same caller-saved reg hosting the same ROLE in every loop; (b) a callee-saved reg hosting values that individually never cross a call — both mean ONE reused source variable, THE 90s-dev frugality signature. Merge macro locals too (_mask/_kboth in a0, disjoint segments = one variable passed for both macro params). - The preference DONOR (new; extends RC-10): the merged index var
bis a call arg in ONE region (cnt = f(b + 0x62)→set_preferencethrough(set a0 (plus b 0x62))gives the ALLOCNO an a0 hard-reg preference). In find_reg pass 0, every LOWER-priority-loser's preferred reg is skipped by conflicting allocnos (regs_someone_prefers, global.c:952) — so in the OTHER region, higher-density char/pct SKIP a0 and take a3/a1, leaving a0 forbeven though it allocates near-last. A per-loopu8 idhas no call use → no pref → char grabs a0 → the whole permutation. The preference travels with the allocno across regions; reuse is what carries it. Corollary: an unwanted pref-override (a copy landing in a0 you can't explain) is often inherited via expand_preferences' death-merge (A dies in insn setting B, no conflict → prefs IOR both ways: oursubinherited raw's a0-pref throughraw = q - sub); blocking it needs a conflicting a0 OWNER at override time, which the donor merge provides for free.
Also byte-settled on the way (each is a one-line lever):
x = 1; if (c() == 0) x = g();can NEVER putli $s2,1in the bnez slot (multi-set ⇒ no S2 boost; dbr's backward scan stops at the call). Theif (ret != 0) x = 1; else x = g();spelling gives the D3 own-thread MOVE steal + relax inversion = thebnez; lishape.- A second pseudo copy (
sub = pct) survives cse ONLY placed in the LOAD's bb before the branch: cse's follow-jumps extends the ebb along the TAKEN edge (label-used-once), so a use two fallthrough-branches down is in a fresh table; combine can't reach across bbs; K8 global never coalesces. In an arm (same bb as use) every spelling dies. dbr then backward-fills the copy into the branch slot — thebeqz; addu $v1,$a1,$0idiom. - Guard shape
i = 0; if (n != 0) do {...} while (i < n);=beqz+i=0in the slot + signed slt bottom. A rotatedforemitsblez;i = 0INSIDE the if leaves the slot as nop (the eager steal fails mark_target_live_regs' conservative liveness). for (k = 0; k < n; k++, q >>= 4)— comma-increment order is LUID order: addiu/addu(k) before andi 0xFFFF, srl in the loop slot.- RC-12 $0-add (
id = b + zr) reconfirmed for a cross-call s32 copy the target keeps asaddu $s1,$a0,$zero(plain copy: cse kills; u8 source: andi).
Route: map the target's per-loop caller-saved ROLES first; if the same reg repeats a role
across loops, REUSE one variable before touching pins or densities — the §17 pin is the fallback,
not the opener. Pins tried here (sub→$3, mask→$4) each half-worked and leaked new prefs
(set_preference sees a pinned var as hard → its expression partners inherit prefs); the reuse
form needed zero pins.
§159 — THE DECLARATION AXIS: conform to byte-truth, and make every guard state its COVERAGE (P30 S47; ~10,930 sites across 8 axes, fleet byte-identical)
The class is bigger than "a few symbol conflicts". dedup_extend's 129 failures over three
binaries split 106 conflicting types / 21 CC1-FAIL / 4 undefined-reference / 3 DIFF. Real byte
divergence is 2%; everything else is declaration plumbing. memcpy — the symbol the checkpoint
named — is 17 of 106. The tail that actually dominates is ordinary deduped callees: func_80128ED8
(19), func_8012F14C (14), ApplyMatrixSV (12).
Direction: the HEADER is usually the liar, not the target. For func_80128ED8 the byte-true
definition is s32 (s32, s32*) — exactly what the overlay .c files declare — while
engine_core.h's macro-local extern says (void*, void*). §58b already says the draft's
signature is byte-truth; the corollary is that a macro-local extern is just another stub-era guess
and carries no more authority than a TU's.
A deduped function has NO definition in any .c — its body lives inside a
#define DEFINE_func_X() \ macro, where backslash continuations and indentation defeat every
definition parser. conform_decls therefore refused (correctly) the entire largest class it was
built for. tools/macro_draft.py bridges it: emit the macro body verbatim, dedented, and the
existing conformer reads the signature it already knows how to read.
§85's all-or-nothing is LITERAL. Conforming the 10 header sites alone broke ov_SC01_000: that
binary carries the old spelling in its own file (_jr_80140608.c:2422). Header and fleet move
together or not at all.
Three guards that asserted completeness over a population they had silently narrowed:
- The file-level "defining TU" skip.
engine_core.his not a TU — it holds ~1,600 macro definitions plus thousands of unrelated macro-local externs. Skipping it whole left 10 stale externs while 1,514 fleet sites moved, and the run still printednon-canonical declarations remaining: 0 OK (axis complete)— a completion assertion blind to the file it had skipped. Fix: scope the skip to the defining macro's span, not the file. - The return-axis comparison. It matched the literal spelling
extern <ret>, sotypedef int s32made 11intsites look like a return change and 2 sites omittingexterndefeated the prefix — refusing a conform whose return type wass32on both sides, citing 165 consumers. Fix: compare NORMALIZED return types. - The §85 consumer scan — the dangerous one, because it under-reported. Three surface patterns
(
= fn(,return fn(,if (fn() miss every consumer that is not immediately after the operator, and this codebase casts constantly:s0 = (s32 *)func_80144A04((s32 *)a1);— cleared as "0 callers consume", then the build failed withvoid value not ignored as it ought to be. Fix: invert the test — a call is DISCARDED only when it stands alone as a complete statement; everything else consumes. Validated both ways: finds exactly the site that broke the build, still returns 0 for the two return-axis changes that gated clean.
Arity conforms: cast the handful of call sites, do not revert the axis. Widening (s32) to
(s32, s32, s32) fleet-wide produced too few arguments at exactly 6 sites (5 real, 1 in a
comment). The §17a-1 fn-pointer cast ((void (*)())func_X)(a) is byte-neutral — the callee address
is a compile-time constant, so gcc still emits a direct jal — and it preserves a 2,843-site axis
that reverting would have thrown away.
Prediction check, recorded because it was wrong: the documented hazard (SCALAR-NARROWING s32 -> u16, byte-proven on func_80175DA8) was benign here across 2,052 sites; the breaks came
from arity and from the under-reporting consumer guard, neither of which the tool warned about.
memcpy is NOT conformable this way. It has no C definition to be byte-truth, and the build
emits warning: conflicting types for built-in function 'memcpy' — the declaration changes gcc's
builtin handling, which is why ov_MAIN_012.c:14333 records an extern memcpy turning an inlined
block-move into a CALL. That class needs a chosen canonical + its own gate, not a mechanical conform.
THE GENERAL LAW (worth a rule): an assertion that excludes part of its own population reports
success over the gap. All three defects printed a clean verdict while skipping a file, a typedef
alias, or a syntactic position. A guard must state its COVERAGE, not just its verdict — the same
finding as _open_stubs vs corpus.stubs (T0(d)) and the family_hseq scope stamp (T0(c)).
§160 — THE REACH-15 WAVE HARVEST: an align-1 block move, and the instrument that called it a wall (P30 S47)
§160a — UNALIGNED 8-BYTE COPY = an ALIGN-1 STRUCT ASSIGNMENT. Target shape:
lwl $v0,0x3($a1) ; lwr $v0,0x0($a1) ; lwr $v1,0x4($a1)
swl $v0,0x13($sp); swr $v0,0x10($sp); swr $v1,0x14($sp)
lwl/lwr + swl/swr is gcc-2.7.2's emit_block_move when the moved type has alignment 1. A u32
copy gives aligned lw/sw and is wrong. The C that produces it, byte-proven on func_801EDC18:
typedef struct { char c[8]; } Blk8;
extern Blk8 D_801ED98C; /* see §160c — this one needed a DEFINITION, not an extern */
Blk8 buffer;
buffer = D_801ED98C; /* struct assignment, NOT memcpy, NOT a u32 loop */
Generalisation: read the move width off the target, not off the data's apparent type. Any lwl
in a target means the source expression's type had alignment 1 at that point — a char[]/u8[]
struct, never a u32* cast. The in-tree note at src/ov_MAIN_012/ov_MAIN_012.c:14333 records the
same mechanism from the other direction (an extern memcpy disabling the builtin turned an inlined
block-move into a CALL).
§160b — match_one WAS BLIND TO DATA-BUNDLED .s FILES; a byte-perfect draft read as a wall.
A splat .s may bundle a leading data symbol with its function: .section .rodata re-emitting
D_801ED98C as two .words, then .section .text. Those .word lines carry the SAME
/* off vaddr HEX */ comment shape as instructions, so masked_diff.insns_from_s counted them as
target instructions — while insns_from_object (objdump -j .text) can never emit them. Result on a
byte-perfect draft: mine=26, target=28, 26 mismatched, every position shifted by a constant +2,
classified SIZE-MISMATCH [redraft]. The wave agent correctly abandoned it at "closeness 6"; the
whole-binary gate then banked that very body. 116 of 12,583 .s files in the corpus have this
shape — every one mis-measured, one at -29 instructions.
Fixed: insns_from_s now tracks .section and counts only .text. Controlled over the full
corpus: 12,467 files unchanged, 116 corrected, zero regressions.
Same artifact class as §129a (post-carve jtbl inflates the target count). One shape, two causes:
when mine is SHORTER than target by a small constant and every position looks wrong, suspect
the INSTRUMENT's section scope before redrafting.
§160c — THE .s RODATA BLOCK IS THE DEFINITION, and the fleet's externs are only consumers.
Four sites declare extern short D_801ED98C; and NOTHING in src/ defines it — from which I
concluded the binary owned the data and shipped an extern-only draft. The gate refuted it:
undefined reference to D_801ED98C. The symbol was defined by the very .s the draft replaced, so
banking the function deleted the data. The variant that emits it —
const Blk8 D_801ED98C = {{0x00,0x00,0x7E,0xFF,0xB0,0x00,0x00,0x00}}; (the little-endian
decomposition of the two .words) — banks clean.
Law: when the target .s contains a data symbol, that draft OWNS the data. Grep for a definition,
never infer ownership from the presence of externs.
§160d — THE ASYMMETRIC INDEX RELOAD (func_801EDED4). Two consecutive table lookups indexed by a
byte field that was JUST stored must NOT both read the same C variable. The target emits sb, then
sll reusing the stored register for index #1, then re-loads lbu + a second sll for index #2.
The source is deliberately asymmetric — a local for the first use, a memory re-read for the second:
u8 t = (*(u8 *)(p + 5) + 1) & 1;
*(u8 *)(p + 5) = t;
*(s32 *)(p + 0xC) = TBL_A[t]; /* reuses t -> no lbu, one sll */
*(s16 *)(p + 0x2E) = TBL_B[*(u8 *)(p + 5)]; /* re-reads -> the second lbu + sll */
Both-t loses 2 instructions (cse collapses index*2); both-memory risks cse substituting a
(subreg:QI reg) and emitting a spurious andi 0xff. Read the lbu/sll COUNT off the target and
distribute local-vs-memory to match it — do not assume the source is uniform.
§160e — STACK-LAYOUT SOURCE ORDER, now byte-proven on a SECOND function (func_8017D364 +
func_801EDED4, so this is a rule, not a coincidence). With MATRIX m1 + SVECTOR in + SVECTOR out
on the stack, writing m1.t[0]; m1.t[1]; m1.t[2]; in.vx=0; in.vy=0; in.vz=…; in that source order
lands the two sh $zero stores in the load-delay window after the a1 setup and before the t[2]
store. Any other order moves them.
§160f — ADDRESS-ONLY GLOBAL STORE: to store a symbol's ADDRESS (not its value), declare it as an
array — extern u8 SYM[]; — so array-to-pointer decay emits lui/addiu with no following load.
§160g — PROCESS: sibling-search keyed on the CALLEE SET should be STEP 0 of every wave prompt.
One grep for two callee symbols returned an already-banked body that turned a 126-instruction crack
into a copy-edit. The existing wave template greps cross-overlay magic literals; extend it to
"grep the callee symbols across src/ for a non-INCLUDE_ASM body". Engine-state globals like
D_80126948 are shared across many overlay TUs, so a matched sibling is often already sitting there.
§161 — THE RETRY-WAVE HARVEST: a jump table indexed from zero, and two allocator traps (P30 S47)
§161a — case 0: break; IS LOAD-BEARING WHEN A JUMP TABLE IS INDEXED FROM ZERO. Target shape:
lhu $v1,0x34($s0) ; sltiu $v0,$v1,6 ; sll $v0,$v1,2 <- NO `addiu $v1,$v1,-1`
Writing the natural case 1: … case 5: makes gcc-2.7.2 pick minval = 1, so it emits
addiu $v1,$v0,-1; sltiu $v0,$v1,5 and every table index shifts one slot. The body can be perfect
and it still reads 58 of 77 mismatched — a near-total mismatch produced by a one-line source
difference, which is exactly the shape that gets a whole family written off as a codegen wall.
Fix: add an explicit empty case 0: break; as the FIRST case. minval drops to 0, the subtract
disappears, and gcc's jump optimizer threads the empty body straight onto the epilogue.
THE DIAGNOSTIC TELL (family-wide): if a member's jump table has its first entry pointing at
that function's own epilogue/end address, it needs the case 0 construction.
⚠ CORRECTED BY §162a (P30 S48): this tell is NOT exhaustive — check BOTH edges of the table.
As written it reads as a complete test on entry[0], and an agent that finds a real body there stops
looking. The upper edge is equally source-controlled: entry[N-1] == the epilogue needs a TRAILING
empty case N-1: break; to pin maxval, and its symptom is the opposite of the one below —
maxval-wrong shifts NOTHING (two bytes: the sltiu immediate and a table one word short), so it is
functionally invisible and surfaces only as image drift.
Byte-proven on
func_8018CC40 (10-member family); emitted table [$L2(end), $L4, $L7, $L9, $L10, $L12] matches
jtbl_801E59F8 = [0x8018CD60(end), 0x8018CC7C, 0x8018CCC4, 0x8018CCEC, 0x8018CCFC, 0x8018CD40].
Corollary: a bnez inside a case arm that jumps to a label which is NOT a jtbl entry is a plain
if/else, not a case fallthrough — do not model it as one.
§161b — ALIASING A PARAMETER INTO A LOCAL CAN COST A SECOND CALLEE-SAVED REGISTER.
void *s0 = a0; before three mutually-exclusive uses forced gcc-2.7.2 to allocate a SECOND
callee-saved register (+8 bytes of frame, +3 instructions) even though the uses never overlap.
Using the raw parameter directly at every site — *(s32 *)(a0 + 0xE4) = …, typed s32, not
void * — collapsed it back to the single $s0 the target uses. When your frame is 8 bytes too
big and you have one more sw $sN than the target, look for a pointer alias before touching pins.
§161c — LOOSE-PROTOTYPE ENGINE HELPERS AND THE DECL THAT FIGHTS THEM. Some engine helpers are
declared (void) at file scope in an overlay yet every jal to them carries addu $a0,$s0,$zero
in the delay slot — they really take an argument. Declaring extern s32 f(); in the draft to model
that collides with the TU's (void) and the gate reports too many arguments to function
(byte-witnessed, func_80178970). Do NOT fight the file-scope decl: drop the draft's extern and
cast at the call site (§17a-1) — ((s32 (*)(s32))func_80178970)(a0) — which gcc folds to a
direct jal, so it is codegen-neutral. Six call sites converted; the gate then banked it.
Process note: the crack agent PREDICTED this failure in its report before the gate ran. Read the
agent's integration notes before diagnosing a gate failure — it has already seen the TU.
§162 — S48 WAVE-1 HARVEST (P30, 2026-08-11): the reach-ordered sibling campaign's first 12 targets
Provenance: 12 zero-crack sibling exemplars cracked by parallel agents against match_one, every claimed
MATCH re-gated by an independent adversarial verifier, then gated whole-binary. 8 of 12 banked
(commit:1619, R22 213/213); 3 NEAR and 1 that passed match_one + verification and STILL failed the
binary gate (func_8017F2D4 — the §52b gap in one line: the per-function gate is a candidate filter,
the binary gate is the arbiter). Every entry below was deduped against the whole cookbook by a
skeptic agent before it was written; each carries its own scope caveat, and the ones resting on a
single instance say so. Where an entry CORRECTS an existing section, that section has been amended
in place — a reader who lands there first must not be taught the superseded rule.
An entry marked SHARPENS is not a duplicate: it names the section it sharpens and what that section does NOT say. NEW means no section stated the law.
§162c — THE | CHAIN, TWO SEPARATE RULES (written by the orchestrator; this candidate's dedupe agent
died mid-response, so treat it as the least-audited entry here). Two findings from func_8017D730
and func_80185F58, both byte-measured on the way to a banked MATCH:
- The constant MOVES. Source
(f(a)|0xB00000)|f(b)emitsf(a)|(f(b)|0xB00000); sourcer|(f(b)|0xC0000)emits(r|0xC0000)|f(b). Write the MIRROR image of the target's tree, not the tree you read off it. - Do NOT hand-fold two constant ors into one.
| 0x40000000 | 0x20000000must stay twoors; the hand-folded| 0x60000000emits a singlelui/orand loses an instruction. Tension to resolve before leaning on this: the existing entry "foldnever leaves a literal in the first term of an|chain" reports that all seven measured parenthesisations REASSOCIATE. That measurement was about operand ORDER of variable terms; this one is about whether two literals MERGE. They are compatible as stated, but nobody has byte-swept both at once. Do that before generalising.
§162a — SHARPENS (sharpens §161a, §131, §8a-pad, §129a)
§162a1 — THE MIRROR: A TRAILING EMPTY case N: break; IS LOAD-BEARING WHEN THE LAST JTBL ENTRY IS THE EPILOGUE. §162a2 is the minval half. gcc-2.7.2 emits the table over [minval, maxval] and indexes it expr - minval, so the upper edge is equally source-controlled. Target shape (func_80186270, ov_SC06_018, 281 ins, banked):
lhu $v1,0x34($s0) ; sltiu $v0,$v1,6 ; sll $v0,$v1,2 <- bound is 6, table is 6 words
Write only case 0: … case 4: and gcc picks maxval = 4 → sltiu $v0,$v1,5 and a 5-entry table. Add an explicit empty case 5: break; and maxval becomes 5 → sltiu …,6, a 6th word appears, and the jump optimizer threads the empty body onto the epilogue exactly as it does the leading case 0.
THE SYMPTOM IS THE OPPOSITE OF §162a2's — and that is the trap. minval-wrong shifts EVERY slot (58 of 77 mismatched, a near-total DIFF that screams). maxval-wrong shifts NOTHING: cases 0..4 keep their indices, and the empty case and the out-of-range default both land on the same epilogue, so the code is functionally identical. The whole error is two bytes: one sltiu immediate, and a .rodata table one word short. Expect a near-clean .text and a mystery image-size/%lo drift (§131's under-fill fingerprint) — and remember §129a, where a post-carve rtu_match folds the missing table word into the instruction count and reports a catastrophe.
AMENDS §162a2's TELL — CHECK BOTH EDGES, INDEPENDENTLY. §162a2's family-wide tell reads as exhaustive on entry[0]; it is not. The rule:
- entry[0] == this function's epilogue/end → leading
case 0: break;(§162a2) - entry[N-1] == this function's epilogue/end → trailing
case N-1: break;(here) - entry is a real body → write a real arm at that edge; add nothing.
Take the entry count from the sltiu bound, never from the dlabel span (§8a-pad L394, §131).
NEGATIVE CONTROL (banked, same TU): func_80187AEC (ov_SC06_018), also lhu 0x34 ; sltiu …,6, also 6 entries — but entry[0]=0x80187B3C and entry[5] are BOTH real bodies, so cases 0..5 are all real arms and neither empty-case construction applies. Two 6-entry tables, same dispatch shape, opposite C. The tell is the entry ADDRESS, never the entry count.
Byte evidence: src/ov_SC06_018/ov_SC06_018_jr_80186270.c (empty case 5: break;), carve config/splat.ov_SC06_018.yaml 0xab968→0xab980 = 0x18 = 6 entries, jtbl_801D3AC0 last entry 0x801866C0 = the function's own epilogue; banked in commit:1619, R22 213/213. Negative control src/ov_SC06_018/ov_SC06_018_jr_80187AEC.c, carve 0xab998→0xab9b0. Scope: one banked positive against one banked negative — the mechanism is the proven §162a2 half read at the other edge, not an independently swept law.
§162a3 — A SUBTRACT SEPARATED FROM ITS sltiu BY sll 16 ; sra 16 IS THE SOURCE'S, NOT THE DISPATCH'S. (target-reading discriminator; the C-side form below is UNPROVEN — no gate has banked it.) Contrast:
minval=1 dispatch (§162a2): addiu $v1,$v0,-1 ; sltiu $v0,$v1,5 <- adjacent, temp dies at dispatch
a real source decrement : addiu $s0,$s0,-1 ; sll $s0,16 ; sra $s0,16 ; sltiu $v0,$s0,0xA
expand_end_case never puts a HImode sign-extension between its own bias subtract and the range test, so the second shape says the decrement happened in the SOURCE, into a short local. Second, stronger tell: the decremented value sits in a callee-saved register and is re-read long after dispatch (beq $s0,$v0 at 8017F4C8/8017F4D0) — a compiler bias temp is dead the moment the table is indexed. Read it as s16 idx = a0 - 1; switch (idx) { case 0 … case 9 }, not switch(a0) with cases 1..10 — the latter would be the §162a2 minval bug and would emit no sll/sra. Target: asm/ov_SC01_006/nonmatchings/ov_SC01_006_jr_8017C340/func_8017F2D4.s:13-16, jtbl_801CC504, sltiu 0xA = 10 entries. Status: the instruction order and the $s0 liveness are facts read off the target; the C reconstruction is a prediction. func_8017F2D4 is still INCLUDE_ASM in ov_SC01_005 and ov_SC01_006 — bank it before promoting this to a law.
§162b — SHARPENS (sharpens §48-A3, §156, §150, §76)
§162b1 — TEMPORARY SCOPE IS A combine LEVER, NOT ONLY AN ALLOCNO LEVER: the same decl choice that sets the allocno class also sets whether a truncation mask survives. Target shape:
jal func_8017DC48 ; andi $a0,$a0,0xFFFF <- six of these, one per switch arm
Where the prior art stops. The allocno half is already written: §76 (scope/reuse is the ONLY way C reaches the local-vs-global allocno class), §136-1 (one local across N arms ⇒ split), §44-L3 (block-scoped-pointer-split), §156-1 (a merged scratch whose UNION range crosses a call goes $s0 everywhere; tell (b)), §150 (per-instance registers ⇒ per-instance variables; split and reuse are a conjunction — ablate both ways). None of them says the same edit also moves the INSTRUCTION COUNT, through a different pass.
THE LAW (combine.c:718-788 set_nonzero_bits_and_sign_copies). combine keeps a whole-function known-zero-bits record for a pseudo only when reg_n_sets > 1 && reg_basic_block < 0 (:725-727) — i.e. exactly a MULTI-BLOCK, MULTI-SET pseudo, which is what a function-scope temp assigned in N arms is. The record is the UNION over every set: reg_nonzero_bits[r] |= nonzero_bits (src, ...) (:776). So one wide assignment in ONE arm makes the value's range unprovable in ALL arms, and every (u16)/(u8) truncation on it survives as a real andi. Split the same temp per arm and each pseudo is single-block/single-set ⇒ excluded from the record ⇒ combine reads the narrow source directly, proves the mask redundant, and DELETES it. Same predicate family as local-alloc.c:472, opposite consequence — the two passes can want opposite answers, and the mask is the one that changes the length.
This is §12's "masked compare andi survives only if the value's range is unprovable" reached through declaration scope. §12 and §1/I2 name only the load-type / cast lever.
BYTE EVIDENCE — four functions, both directions.
- SHARED required —
func_8017D730(ov_MAIN_012, 326 ins, 7-entryjtbl_80184094;ov_MAIN_012_jr_8017CF3C.c:4010). ONE function-scope set of clamp tempsa/b/rfor all 7 arms, two effects at once: (i) it pinsb→$s1,r→$s0— case 6 has no second call yet the target still copies the return into$s0, which only a pseudo whose range spans cases 3/4 explains (§156-1 tell (b)); (ii)ais also set from a 32-bitlwin case 5, so the union is all-ones and sixandi $a0,$a0,0xFFFFsurvive. Per-case locals: every set is provably ≤16 bits, combine deletes all six, −6 ins. - SHARED required —
func_80185F58(ov_SC06_018, 198 ins). One shared scratchtfor threesll 16compares. Three separate temps let local-alloc TIE the short temp toiVar1and theaddu $s1,$v0,$zerocopy vanishes entirely (197 ins); the sharedtconflicts withiVar1, so the copy survives. - SPLIT required —
func_80188E1C(ov_SC04_018 /_jr_801878E8, 254 ins). A function-scope& 0xFFtemp is one pseudo whose range crosses the calls in cases 13/14 ⇒$s0. The target keeps it in$v1/$v0(dead before the call) and spends$s0only on the value that genuinely is call-live. Block-scoping it per arm restores that. - MIXED required —
func_8017D2A4(ov_MAIN_012 /_jr_801789AC, 291 ins). One shareds16 *pmerged every live range and handed every site$s0where the target uses$a0inside the arms: 134 mismatched. The fix is NOT uniform — a function-scope pointer for the call-spanning arm plus per-case block-scoped pointers for the rest → 4. §76 sweeps its three granularities uniformly; here the answer is heterogeneous within one function.
⚠ THIS BYTE-REFUTES §48-A3's ABSOLUTE. §48-A3 closes "In a jr-switch dispatcher, NEVER share a scratch across arms." func_8017D730 (7 arms) and func_80185F58 (3 compares) are jr-switch dispatchers where sharing is REQUIRED. The §48-A3 mechanism (local-alloc.c:1765 refuses to tie a multi-block pseudo) is real; the imperative is not. Read it as: splitting buys the local-alloc tie — it costs you the callee-saved pin and it costs you the mask.
THE DIAGNOSTIC TELL — decide per ARM, not per function, before you edit:
- Does the value live across a
jalin ANY arm? Yes ⇒ that arm's temp is shared/function-scope (union range ⇒ callee-saved, §156-1). No ⇒ block-scope it; the target will hold it in$v0/$v1. - Target emits an
andi 0xffff/0xffyour draft folds away? Look for an arm that assigns the SAME C variable from a full-widthlw— merging one in is what makes the range unprovable. Inversely, aLENGTH-DRIFT +Nwhere N equals the count of surviving masks means you SHARED a temp the original split. - A callee-saved register hosting a value that individually never crosses a call, in an arm with no call ⇒ one reused source variable (§156-1 tell (b)).
- Ablate both ways (§150): split-only loses the pin, share-only loses the tie, and neither number tells you the other half is wrong.
Honest scope. The regalloc half is proven on four functions and is mostly restatement of §76/§150/§156. The scope→nonzero_bits→mask coupling — the part that is new — is byte-proven on one function (func_8017D730, −6 ins, measured in its banked header). Falsifiable prediction for the next wave: any dispatcher whose target shows a truncation mask on a value that one arm loads with lw needs a SHARED temp, and per-case locals will read as LENGTH-DRIFT −N.
§162d — SHARPENS (sharpens §31, §21, §30, §55a)
§162d1 — THE ANONYMOUS TEMP IS A SINGLE-SET PSEUDO: how to fire S2 when your named locals MUST stay multi-set. Target shape, a call pair feeding one consumer:
jal func_8017DC48 <- call 1
...
addiu $a1,$zero,0xC <- call 2's arg setup, ABOVE its jal
jal func_8017DC48 <- call 2
addu $s0,$v0,$zero <- call 1's result save, IN THE SLOT
This is S2 + D1 (§31 row; sched.md S2, byte-proven on func_801770E0): the save of call 1's
result only sinks below call 2's arg setup — where dbr can slot it — if the pseudo holding it is
REG_N_SETS==1. The documented lever is a fresh single-set local. The new case is when you
cannot have one. In func_8017D730 the accumulators a/b/r are deliberately SHARED
function-scope locals: a is also set from a 32-bit lw in case 5, so combine's reg_nonzero_bits
stays unknown and andi $a0,$a0,0xFFFF survives (per-case locals delete it, −6 ins), and the shared
r is what pins $s0 across cases 3/4/6. Splitting them to fire S2 breaks both.
LAW: an expression's anonymous temp is single-set by construction, so nesting the pair into ONE
expression fires the birthing boost with zero declarations and zero effect on your named locals.
Write h((f(a) | K1) | f(b), …), not x = f(a); y = f(b); h((x|K1)|y, …). Applies to a
straight-line pair only — where a null-test forces the first result into a named local
(r = f(a); if (r == 0) r = K;), the target itself carries the save as an ordinary insn and only
the SECOND call stays inside the expression (same function, cases 3/4).
DIAGNOSTIC TELL: the insn sitting in the second jal's delay slot is that call's own argument
constant — addiu $a1,$zero,0xC where 0xC is the literal you passed — and the addu $sN,$v0,$zero
the target has in the slot appears in your draft above the arg setup. Same tell as sched.md D1's
"li/move swapped between just-before-jal and in-the-slot", keyed on the arg literal.
Evidence: func_8017D730 @ 0x8017D730, ov_MAIN_012 (326 ins, MATCH), cases 1 and 2 —
func_8017DCB0((func_8017DC48((u16)a,0x18) | 0xB00000) | func_8017DC48(b,0xC), 5, …).
Do NOT read this as "a call pair must be one expression." sched.md S2's func_801770E0
(53→49) reaches the same schedule with the pair as two statements, because its temps (u4/u5)
are fresh and single-set. Statement-vs-expression is not the dial; REG_N_SETS is. Reach for a
fresh local first; use the expression only when the local must stay multi-set. (Single instance,
and the edit was not gated independently of the | operand-order fix in the same draft — the
schedule delta is asserted from the drafter's intermediate, only the combined result is a verified
MATCH.)
§162e — NEW
§162 — THE LICM PAIR: what makes an address a movable AT ALL, and why the preheader order is the body order (P30 S47, ov_MAIN_012)
§162e1 — INDEX UNIFORMLY WITH THE LOOP VARIABLE, EVEN WHERE THE INDEX IS PROVABLY CONSTANT. Target shape:
<preheader> lui $s6,%hi(D_8018251C) ; addiu $s6,$s6,%lo(D_8018251C)
<body> lw $a1,0x4($s6) ; jal strcpy
Inside a guarded arm (if (i == 1) … strcpy(dst, D_8018251C[i]);) the index is knowable, and the literal
D_8018251C[1] is the obvious spelling. It is wrong, and it costs a callee-saved register. A literal index
makes the whole address a CONSTANT: expand emits the 3-instruction lw $reg,SYM+4(idx) gas macro at the use,
there is no base pseudo, scan_loop has no movable to find, the hoist never happens — and the
loop-invariant constant 4 takes $s6 instead. Written [i], expand builds SYM + i*4 off a la base
pseudo; scan_loop records that la (loop.c:769-800), move_movables hoists it (:1708), global-alloc
gives it a callee-saved reg. cse folds i to the branch constant afterwards (record_jump_equiv), so the
body bytes are IDENTICAL either way — the offsets still come out 0x4/0x10. Uniform indexing is pure LICM
steering at zero byte cost.
Scope: a SYMBOL-based array. A base already in a register (p[1] off a pointer param) has a base pseudo
either way and is unaffected. This is §48-B's corollary read in the positive direction — there a constant
offset lets fold_rtx collapse SYMBOL_REF into CONST(sym+k) and the la dies; here keeping the index
VARIABLE at expand time is what keeps it alive long enough for loop.c to move it.
(func_8017D730, 326 ins, ov_MAIN_012/jr_8017CF3C.)
§162e2 — PREHEADER ORDER IS BODY ORDER. scan_loop appends each movable to the movables chain in its
forward walk (loop.c:796-800); move_movables walks that chain emitting every hoist with
emit_insns_before (…, loop_start) (loop.c:1550, :1708). Both in order ⇒ the preheader emits hoists in
the order their defining insns appear in the loop body = source order of each value's FIRST mention. Two
byte-proofs, opposite vehicles:
func_8017D730:&D_801827A8is first mentioned in aj-loop insidecase 0,&D_8018251Cin the later else-chain ⇒ preheader$s7=D_801827A8then$s6=D_8018251C, the target's order, for free — once §162e1 made the second movable exist at all.func_8017CBC8(188 ins,ov_MAIN_012/jr_801789AC): target loop-1 preheader is[addiu $s5,$sp,0x18 ; addu $s6,$s7,$zero]. Writing the sp-relative store address as an explicit pointer local —u8 *bp = sp18;— makes that address the FIRST movable discovered, so thedim = flagcopy lands after it. The local exists only to order the preheader.
This GENERALISES §36 / gcc-2.7.2-map/loop.md "Movables EMISSION ORDER", which states the same law for
CONSTANT movables only and offers just the store_fixed_bit_field expansion order as a lever. It holds for
address and copy movables too, and the lever is ordinary source order: to move a hoist earlier in the
preheader, mention its value earlier in the body.
§162e3 — A CONDITIONALLY-EXECUTED INVARIANT WHOSE DEST IS LIVE AFTER THE LOOP IS NEVER A MOVABLE.
scan_loop:688-701 needs one of three things before an insn is even a candidate: (1) the reg is used only in
the set's own bb, (2) it is not a user variable and not in the exit test, (3) !maybe_never — the set is
guaranteed to run once the loop starts; maybe_never goes to 1 the moment the scan passes a conditional jump
(:923-930). A user variable assigned inside an if in the body and read after the loop fails all three and
STAYS IN-LOOP however favourable §148-A's threshold arithmetic is. (m->cond/m->global at :787-788 are set
only AFTER the filter passes — cite the filter, not the fields.) Two independent gates keep a value in-loop
and they need different fixes: §52-3's n_times_set != 1 (assign in both arms) and this one.
THE DIAGNOSTIC TELL. The target holds a global base in a callee-saved register your draft never
materialises, and your $sN is occupied by a small loop-invariant constant instead ⇒ you wrote a literal
index where the original wrote the loop variable (§162e1). The right two hoists in the wrong preheader ORDER
⇒ reorder the first mentions in the body, or name the earlier one in a local (§162e2) — do NOT reach for
§47/§158's allocno sliders, which split priority TIES; this is an emission-order fact upstream of them.
An invariant you expect hoisted stays in-loop ⇒ read the candidate filter (§162e3) before the threshold
(§148-A). cc1 -dL settles all three: <file>.i.loop names every movable, in order, moved / not desirable.
Symptom lines for the index: "a global base in $sN my draft never emits" · "a small constant
occupying a callee-saved register" · "two preheader hoists in the wrong order" · "an invariant that
refuses to hoist".
Honesty note: §162e1 has ONE exemplar — the mechanism is corroborated in the opposite direction by §48-B's byte-proven corollary, but the "index uniformly" lever itself is not yet replicated. §162e3 is source-cited, not ablated here.
§162f — SHARPENS (sharpens §42d, §41d, §73, §10)
§162f1 — A NON-VOID RETURN TYPE IS OBSERVABLE IN DELAY SLOTS. This is §42d#1 read BACKWARDS, and it bounds §41d. Target shape — look at every branch whose target is the epilogue:
beq $v1,$v0,.Lepilogue ; nop <- $v0-setting fill REFUSED
beq $v1,$v0,.Lepilogue ; move $s1,$zero <- non-$v0 fill ACCEPTED
reorg.c:4274 (init_resource_info) seeds end_of_function_needs from current_function_return_rtx
("Registers used to return the function value are needed"), so a non-void return type marks $v0
live at the end of the RTL chain; a jump-to-return inherits it (*res = end_of_function_needs,
line 2458) and fill_simple_delay_slots then refuses any candidate insn that SETS $v0. The
constraint is $v0-specific, not fill-forbidding — an unrelated move $s1,$zero lands in the same
kind of slot fine.
THE LAW: the return type is a SCHEDULING DECLARATION, and a function can need s32 even though it
never returns a value. s32 f(void) whose every return is a bare return; emits ZERO extra
instructions — no path sets $v0, the epilogue gains no move $v0,… — and buys only the $v0
liveness that keeps reorg's hands off the slot. §42d#1 already names this mechanism but states only
the void-ward flip, and its diagnostic ("Read the asm: does $v0 carry a value out?") answers NO
here and sends you the wrong way.
Byte evidence — func_8017CF3C (ov_MAIN_012, banked). Same source, return type alone flipped,
through the real cpp → cc1 → maspsx 2.56 → as:
| decl | ins | first epilogue-bound beq |
|---|---|---|
s32 func_8017CF3C(void) (banked, MATCH) |
218 | beq $v1,$v0,$L1 ; nop |
void func_8017CF3C(void) |
217 | beq $v1,$v0,$L1 ; li $v0,0xA |
The void build is one instruction SHORT: reorg steals the li $v0,0xA into the slot and every
later branch target shifts −4, so it presents as a whole-function cascade rather than a one-slot
diff. In the cc1 .s the tell is visible before the assembler ever runs — the void build wraps the
branch in .set noreorder / .set nomacro (gcc filled the slot itself); the s32 build leaves it in
reorder mode. The body has three bare return; statements and its epilogue (lw $ra…; jr $ra) never
writes $v0. The SAME function fills a different epilogue-bound slot with move $s1,$zero in both
builds — that is the discriminator.
THE DIAGNOSTIC TELL: your draft is 1 instruction short, and it fills the delay slot of a branch to
the epilogue with a $v0-setting insn where the target has nop — while other slots in the same
function are filled normally. Declare the function non-void and leave every return bare.
Precondition for it being free: no path may set $v0 (if one does, you owe a real return value).
Do NOT "fix" the warnings this produces. cc1 under -Wall emits warning: `return' with no value, in function returning non-void once per site. The project build never shows it — -Wall is
on CPPFLAGS (Makefile:636) and never reaches CC1FLAGS (Makefile:637) — but a standalone harness
that passes -Wall to cc1 will, and "cleaning it up" with return 0; re-clobbers $v0 and
re-breaks the match.
This BOUNDS §41d. §41d ("void→s32 is NOT always byte-neutral: gate the RAW draft FIRST",
func_80182268 31→32 ins) is the same mechanism with the opposite sign; its corollary — "a function
with no canonical decl anywhere should be banked exactly as drafted" — is not a licence to skip
this. Both directions cost exactly one instruction. Whenever an epilogue-bound delay slot differs,
gate the draft RAW and return-type-flipped; the sign is not predictable from the decl layer.
§162g — NEW
§162 — CROSS-JUMP DIRECTION: the surviving copy is always the LATER one, so a BACKWARD j into a sibling arm is a source goto (P30 S48)
Target shape. N arms each end j .LX, and .LX sits inside the body of the last-emitted arm — not in a tail block placed after every arm:
/* 8017F39C */ j .L8017F6D0 <- arm "case 3"
/* 8017F3DC */ j .L8017F6D0 <- arm "case 4" ... 10 sites total
.L8017F6D0: lui $at,%hi(D_801CD9A8) ; sw $v0,%lo(D_801CD9A8)($at) <- inside case 0's body
.L8017F6D8: addiu $a0,$zero,0x45F
.L8017F6DC: jal func_8002D4C8
THE LAW (read out of jump.c, not inferred). do_cross_jump (insn, newjpos, newlpos) (jump.c:2537) deletes the stream preceding insn — the jump being processed — and keeps the stream preceding the target: redirect_jump (insn, get_label_before (newlpos)), then while (newjpos != insn) delete_insn (newjpos). insn comes from a forward walk of the insn chain, while its partners come from jump_chain, built by a forward scan with push-front (jump.c:219-221) — so the chain head is the LAST jump to that label. Earliest jump pairs with latest copy; the latest copy survives. The minimum=1 path (jump.c:1978) says the same structurally — it compares against the code before the jump's own target label, i.e. the fall-through predecessor of the exit. The conditional path is forward-only by construction: jump_back_p (jump.c:2635) demands a mutual pair whose labels straddle both jumps. The RETURN chain (jump.c:2018, jump_chain[0]) is push-front too.
Two consequences:
- Compiler tail-merge is ALWAYS a forward
jinto a LATER block. A backwardjinto the middle of an earlier sibling arm's body therefore cannot be cross-jumping — it is agotothat was in the source. - A merged tail you must hand-write goes ONCE at the LAST-EMITTED arm, reached by forward
gotos from the earlier arms. Put it at the first arm (or longhand in every arm) and the canonical copy lands in the wrong place, shifting every label after it.
Byte evidence.
func_8017CF3C(ov_MAIN_012, banked, whole-binary byte-gated): case 8 — emitted after cases 4/5/6 — reaches theret = 0x13; D_801150D4 = 0; D_8011512C = 8;body of case 4/5/6 from two sites, both backward. Spelledreset_state:inside the 4/5/6 arm +goto reset_state;twice from case 8. No duplicate-and-let-it-merge spelling can produce that edge.func_8017F2D4(ov_SC01_005, 279 ins,match_oneMATCH — gate-blocked on TU plumbing, NOT yet banked): the target has 10 forwardj .L8017F6D0plus 3 refs to.L8017F6D8, all landing inside the last-emitted arm (case 0; emitted block order1/2, 3, 4, 5, 6, 8, 7, 9, 0). Writing the tail —D_801CD9A8 = sel; func_8002D4C8(0x45F, 0); return 0;— once at that arm withgoto set_sel;/goto call_45F;from the earlier arms took 224 → 12 mismatched in one edit. Draft:.run/backlog_drafts/func_8017F2D4.c.
THE DIAGNOSTIC TELL. Read the direction of every intra-function j whose target is neither a jtbl entry nor the epilogue:
- forward, ≥2 sources, target inside the last arm's body ⇒ a merged tail. Write it once, at that arm.
- backward, target inside an earlier arm's body ⇒ a source-level
goto. Do not chase it by duplicating code and hoping cross_jump merges; the pass cannot emit that edge.
⚠️ Bounds — three, all load-bearing.
- This BOUNDS §88a. §88a says "never hand-factor the call-free tails — the compiler does that itself." The 8017F2D4 tail contains a
jal, and by §88a's own byte finding call-bearing suffixes are left unmerged — which is exactly when you must hand-factor, forward, at the last arm. Decide from whether the target shows the merge, never from the rule of thumb. - The tail must clear §50-B's floor (
find_cross_jump(..., minimum=2),jump.c:1993, jumps not counted): a 1-instruction tail reached by twojs never merges. - Scope. Survivorship is a theorem for the N-jumps-to-one-label and RETURN chains. It is not a theorem for a loop back-edge — a backward
simplejumpcan still be redirected slightly earlier by theminimum=1path, keeping the EARLIER copy. Restrict the tell to sibling arms.
⚠️ One session claim deliberately NOT carried: "written longhand, gcc keeps the FIRST copy." jump.c keeps the LAST on every path. The longhand failure at 8017F2D4 is explained by §88a (call-bearing suffix ⇒ no merge ⇒ 14 live copies), not by first-copy survivorship. The 224→12 delta is real; that causal story is not.
(Supersedes and promotes the orphaned "L2 (NEW §36) — cross-jump fall-through law" in docs/gcc-2.7.2-map/t7g-giant-harvest.md:324, which stated the survivorship half for one function and was never written into cookbook §36.)
§162h — SHARPENS (sharpens §88, §88a, §50-B, §8)
§162 — The cross-jump "CALL veto" is a COUNT law, not a CALL law (BOUNDS §88a; P30 S48, func_80189540)
Target shape. A staircase of labels one instruction apart feeding a shared call — func_80189540
(ov_SC04_018 / ov_SC04_019, 551 ins):
.L80189DA8: addiu $a0, $zero, 0x472
.L80189DAC: addu $a1, $zero, $zero
.L80189DB0: jal func_8002D4C8
nop
.L80189DB8: addu $v0, $zero, $zero
.L80189DBC: lw $ra,0x30($sp) … # shared epilogue, entered from ~10 sites
THE LAW. find_cross_jump (tools/reference/gcc-2.7.2/jump.c:2371) has no CALL veto. Its only
CALL clause is conditional — jump.c:2428-2431 sets lose = 1 only when the two calls'
CALL_INSN_FUNCTION_USAGE differ (different arity / different argument hard regs). Otherwise a
CALL_INSN decrements minimum exactly like any other insn (2524-2528 excludes only USE/CLOBBER).
What actually decides a […][jal f][j L] tail is §50-B's floor:
- two
js to the SAME label →find_cross_jump(insn, target, **2**, …)(jump.c:1993) → needs a ≥2-insn common suffix, and the jumps themselves are not counted; - one side FALLS THROUGH into the label →
find_cross_jump(insn, JUMP_LABEL(insn), **1**, …)(jump.c:1978) → 1 insn is enough, call or not.
⇒ §88a's "repeated CALL-shaped blocks are left UNMERGED / write them longhand" is right in practice and wrong at the boundary. A call-bearing tail with ≥1 further matching insn merges normally.
Byte evidence, both directions.
- CALL LAST, 2-insn tail, MERGES: §136d-2
func_80184494— post-reloadcross_jumpmerges the common[move $a0,$s0; jal]tail (banked,src/ov_SC02_026/ov_SC02_026_jr_8017C180.c:6312). - CALL is the ONLY tail insn, MERGES via fall-through: §8
func_80159BE4(40 ins, matched in every overlay) — twojal hinsns merged into one shared site, per-arm arg setup left duplicated. Theminimum=1path. - 1-insn tail reached by two
js, does NOT merge:func_80189540—[jal func_80189E14][j p5EE]stays separate, while the longhand[jal func_80189E14][li snd,0x5EE][j play]merges (−4 ins). - The RETURN-label half is REFUTED.
jump.c:2005-2031cross-jumpsRETURNinsns against each other atminimum=2; §5a'sLzssDecodeSectorneeded the volatile-asm barrier because gcc merged two save/return epilogues (111 vs 122). Infunc_80189540's own target,.L80189DBCis the redirect target of ~10 sites. The one non-merge —80189DA0 j .L80189DBCduplicatingaddu $v0,$zero,$zeroinstead of targeting.L80189DB8— is a 1-insn suffix. The floor again.
DIAGNOSTIC TELL. At the two candidate blocks, walk backward from the jump and count matching insns,
excluding the jump. ≥2 with both sides reached by j → gcc merges it for you, write it longhand. 1
with both sides reached by j → it will not, and the duplicate is target-true. One side falls through →
1 suffices. A staircase of labels one insn apart is what a SUCCESSFUL merge looks like — it is not
evidence of source-level gotos.
⚠ THE FALSE POSITIVE this bounds. "The insn before the jump is a jal, therefore gcc can't have made
this — write gotos." [call][j] (count 1) and [call][set][j] (count 2) do not discriminate adjacency
from count; both readings fit the data. Before spending a round on gotos, run the discriminator: a
2-insn tail whose LAST insn is the call ([li $a1,0][jal f][j L] in both blocks). §136d-2 already says
it merges.
Open, and NOT closed by this. §88a's 34 byte-identical 6-instruction jal blocks in
func_8017D2DC are far over the floor and still went unmerged. The floor does not explain them. Live
hypothesis: the CALL_INSN_FUNCTION_USAGE gate (jump.c:2428) — same callee, different arg-register USE
lists — unprobed. §88a's advice stands for repeated switch cases; only its mechanism is wrong.
Provenance caveat (R37). func_80189540 is not banked — INCLUDE_ASM at
src/ov_SC04_018/ov_SC04_018_jr_80188E1C.c:3280 and src/ov_SC04_019/ov_SC04_019_jr_801878E8.c:3655; all
four size-0x89C siblings likewise; best draft 553 vs 551. Its "byte-proven twice" is two
whole-function variants moving the count by the predicted ±4 inside a 551-ins function, with no isolated
reproducer (.run/wave1/_scratch_80189540/probe*.c test unrelated shapes). The LAW above rests on the
gcc-2.7.2 source plus the two matched functions cited; the func_80189540 numbers are corroborating,
not gating.
§162i — SHARPENS (sharpens §135, §21, §42, §32)
§162i1 — THE DEAD-LOCAL FRAME PAD HAS AN 8-BYTE FLOOR: only a BLKmode local reserves anything. Amends §21, §32-5, §42-3, §135-6 (P30 S47, byte-proven on the pinned cc1).
Target shape (unchanged from §135-6). Instruction count exact, every diff an $sp-relative immediate off by ONE constant, prologue saving the same registers in the same order.
THE LAW. An unreferenced, non-&-taken local reserves frame space iff its type is BLKmode — and gcc-2.7.2's layout_type collapses a one-element array (any depth) and a one-member struct to the element's mode, making it a register candidate that -O2 deletes. Measured, -O2 -G0 -mips1 -mcpu=3000, reading .frame … # vars= straight off cc1:
declared, never read, never &-taken |
Δvars |
|---|---|
s32 p0; · s32 p[1]; · s16 p[1]; · char p[1]; · s32 p[1][1]; · struct{s32 a;}p; · double p; |
+0 — INERT |
s32 p[2]; · char p[2]; · char p[8]; · struct{s32 a,b;}p; |
+8 |
char p[9]; · s32 p[3]; |
+16 |
s32 p[5]; |
+24 |
⇒ ≥2 elements gets a slot at declaration time; sizes sum, then round up to 8 (MIPS_STACK_ALIGN(get_frame_size())). There is no 4-byte pad. Two corrections follow: §32-5's "structs/arrays … regardless of use" is over-broad (a one-element array is exempt), and §21/§42-3's mandatory (void)&pad is over-strict — address-taking is required only to force a SCALAR (s32 p0; (void)&p0; → +8). Prefer the plain s32 pad[N≥2]: & sets TREE_ADDRESSABLE and drags MEM_IN_STRUCT_P/aliasing (§30, §82-2) in with it.
BYTE EVIDENCE — src/ov_SC06_018/ov_SC06_018_jr_80187AEC.c, real cpp | cc1 pipeline:
func_80187DD0—s32 pad[2];, no&: 0x18 → 0x20. Load-bearing.func_801894F0—s32 unused[2];, no&: 0x28 → 0x30. Shrink tounused[1]and it snaps back to 0x28 — the 4-byte form buys nothing.- ⚠️
func_80187AEC— itss32 pad[1];is INERT. Delete it and cc1 emits a byte-identical.s(frame 0x20,vars=8,regs=2/0; only the.fileline differs). Its in-source key (2) — "without it the frame compiles to 0x18", "4 bytes → 0x20, 8 bytes → 0x28" — is byte-refuted against the banked source: the 0x20 is a real 8-byte vars area, and the 0x28 was that base plus a genuine 8-byte pad. Linear addition with 8-byte rounding, not an exact-size law. A pad that an intermediate draft needed survived into the bank wearing a "do NOT clean up" comment; correct the comment, do not copy the pad.
DIAGNOSTIC TELL. regs= equal to the target's save set — one more sw $sN is §162i2's pointer alias, one fewer is a missing callee-saved value — and everything but $sp immediates exact. Then do not probe: compute. Read vars= off your own draft (cpp … | cc1 … | grep '\.frame') and compare with target_frame − args − 4×regs. Δ is always a multiple of 8 ⇒ declare s32 pad[Δ/4] in the target's declaration-order position (§136-6). If Δ is 4, you do not have a dead local at all — go to §83c (it is gcc's spill area), §147-B (?: on memory operands), or §162i2.
§162j — SHARPENS (sharpens §25, §136d-1, §48-B, §46-L2)
§162j1 — DEFEATING local-alloc's optimize_reg_copy_1 WITH AN IN-PLACE SHIFT (this COMPLETES §25's triage rule, whose "→ pins" prescription cannot fire here). Target shape — a one-register diff on a truthiness sll, where the TARGET reads the register the value already arrived in and MINE reads the register of a copy made above it:
mine: addu $s1,$v0,$zero ; … ; sll $v0,$s1,16 ; blez $v0
target: addu $s1,$v0,$zero ; … ; sll $v0,$v0,16 ; blez $v0
The mechanism. At -O2 only (flag_expensive_optimizations, toplev.c:3387-3391 — this whole class is impossible in an -O1/-O0 file, §116/§127), update_equiv_regs calls optimize_reg_copy_1 (local-alloc.c:700, call site :1007) for every reg-reg copy whose SRC does not die in the copy (:1005 ! find_reg_note (insn, REG_DEAD, SET_SRC (set))) — i.e. precisely §25's "a copy emitted before its source's other use". It scans forward to SRC's REG_DEAD (:740) and validate_replace_rtxes SRC→DEST at every use in between (:761), then moves the death note onto the copy. The later use therefore reads the COPY's register. It is invisible in -da's .sched dump and present in .lreg — byte-witnessed on the exemplar: (ashift (reg 75) 16) in t.i.sched, (ashift (reg 73) 16) in t.i.lreg.
LAW — a pin does not EXEMPT this copy; an in-place SET aborts the scan. The hard-reg escape (sregno < FIRST_PSEUDO_REGISTER || dregno < FIRST_PSEUDO_REGISTER) sits inside #ifdef SMALL_REGISTER_CLASSES (local-alloc.c:712) and config/mips/mips.h never defines it (0 occurrences) — so register s32 t __asm__("$2") changes nothing (byte-verified). The reachable break is :732 reg_set_p (src, p), tested before the death test. Make the use insn also SET src:
t = t << 16; if (t <= 0) /* NOT: if ((s16)t <= 0) */
The sll now SETs t, the scan breaks at :732, no substitution happens, and the use stays on SRC's register. Companion fact: flow emits no REG_DEAD for a reg set in the insn that last uses it (§45 Lever B). Precondition: the unshifted t must be dead afterwards — in the exemplar iVar1 = t; is taken first, and that copy is the very thing that created the situation.
THE ASYMMETRY IS LOAD-BEARING. Only the site whose value was also copied has a copy insn for the pass to visit; every other (s16)x <= 0 compare in the same function must keep the natural cast. A uniform spelling loses bytes at the other sites.
Evidence: func_80185F58 / ov_SC06_018 — MATCH 198/198, banked, pin-free (src/ov_SC06_018/ov_SC06_018_jr_8017C24C.c:7305). Residual before the fix was a single byte-diff, everything else clean; sites 2 and 3 (0xFE / 0x76) correctly keep (s16)t <= 0.
DIAGNOSTIC TELL — and how to tell the THREE mechanisms apart. Tell: a one-register diff on a single use where the target reads the incoming register and yours reads a register some nearby addu $sN,$vX,$zero wrote, with the rest byte-identical. Confirm in two commands — cc1 -da, then diff the operand pseudo across the dumps:
- changed between
.sched→.lreg⇒ this section. Lever: shift/update IN PLACE at that site. Pins are not the lever. - changed in
.cse/.cse2⇒ §136d-1 / RC-12 (make_regs_eqv+canon_reg). Lever: the$0-add opaque copy. - never changed at all ⇒
combine_regstying (local-alloc.c:1825) — §25's own case, the one where the §17 pin genuinely worked.
§25 amended: its triage rule names the right symptom and one of three mechanisms; only that one answers to pins.
Caveats (stated because the sample is n=1 and the strength is the source reading, not the count). "Pins cannot block it" is exact only as no early return: a hard-reg SRC/DEST still takes the two conservative paths (:766 failed = 1 when DEST is mentioned in the same insn; :851 the dead_or_set_p break), so a pin may incidentally abort the rewrite in other shapes — read the law as "pin-it-and-the-copy-survives is not a lever here", not "a pin can never change this outcome". Second, untested, defeat: the scan also breaks at any CODE_LABEL / JUMP_INSN / NOTE_INSN_LOOP_BEG|END between the copy and the use (:723) — a do { } while (0) wedge. Reasoned to in .run/near6/wave23/r10.py, never byte-gated for this mechanism, and such a barrier is a loop-depth ref inflator with its own regalloc side effects (§55a). The in-place SET is the zero-side-effect form.
§162k — SHARPENS (sharpens §1-I2, §12, §160d, §21)
§162k1 — QImode-vs-SImode LOCAL WIDTH IS LOAD-BEARING, AND IT IS ASYMMETRIC WITHIN ONE FUNCTION.
Target shape (func_80188E1C — two lbu-into-a-local sites ~50 instructions apart in the SAME
function, same TU, same flags, same load width):
case 13: lbu $s0,0x0($v1) <- ZERO andi
jal func_800291B4
addiu $a0,$s0,0x62 <- the byte used RAW
case 14: lbu $a0,%lo(D_801E77B8)($at)
addiu $v0,$zero,0xFF
andi $v1,$a0,0xFF <- re-widen #1 (the `!= 0xFF` compare)
beq $v1,$v0,.L801891F0
addiu $a0,$a0,0x1
andi $s3,$a0,0xFF <- re-widen #2 (the `+ 0x62`)
addiu $s0,$s3,0x62
LAW: a u8 local is a QImode pseudo and gcc-2.7.2 RE-WIDENS IT AT EVERY SImode USE — one
andi $x,$src,0xFF per use, with NO & 0xFF anywhere in the source. An s32 local fed by an
lbu is provably ≤0xFF, so an explicit & 0xFF on it folds away and emits NOTHING. It is a
trap in both directions: (e & 0xFF) != 0xFF on an s32 local is silently optimized out (no
andi, no warning — you are short an instruction and the source looks right), and declaring a byte
local u8 buys you an andi at every use whether you wanted one or not.
Byte evidence — src/ov_SC04_018/ov_SC04_018_jr_80188E1C.c:3149 (banked ×N; the still-unbanked
twin's target is asm/ov_SC04_019/nonmatchings/ov_SC04_019_jr_801878E8/func_80188E1C.s:135-156).
case 13: s32 e = D_801E76FC[(short)param_2]; → lbu $s0 consumed raw in the jal delay slot.
case 14: u8 v = D_801E77B8[(short)param_2]; → two andi …,0xFF. A controlled A/B inside one
compilation, not two anecdotes: the only source difference is the local's declared type.
THE DIAGNOSTIC TELL: count the andi $x,$y,0xFF on each lbu-fed value, PER BLOCK. N andis ⇒
that value lives in a u8 local used N times in SImode. ZERO andis ⇒ it is s32, and any & 0xFF
you write there is dead source. Do it per case arm — the original author was not uniform and
neither should the draft be. This is §160d's "read the COUNT off the target" on a second axis:
§160d distributes local-vs-MEMORY off the lbu/sll count; this distributes QI-vs-SI LOCAL WIDTH
off the andi count.
Corollary (same function, the last 2-instruction residual): REUSE the one QImode pseudo for the
bump. v = v + 1; gives the target's in-place addiu $a0,$a0,0x1; andi $s3,$a0; a second local
(u8 n = v + 1;) gives two pseudos and addiu $v0,$a0,1; andi $s3,$v0.
Corollary (the compare): v != 0xFF on a QImode local costs THREE instructions —
andi $v1,$a0,0xFF; addiu $v0,$zero,0xFF; beq — where an s32 local would compare against one
materialized constant. If your draft is short at a byte compare, the local is too wide.
AMENDS §1/I2 AND §12 — read this before either. §1-I2 says the explicit & 0xff "survives as
andi" because gcc "does NOT prove the upper bits zero across pseudo-registers", stated
unconditionally; case 13 refutes it for an SImode local. §12 (L1049) corrects it halfway — "a clean
u8 load lets gcc-2.7.2 prove a0∈[0,255] and DROP the andi" — but it is keyed on the LOAD and
predicts case 14 backwards: case 14 is a clean lbu and emits two andis. The controlling
variable is neither the load nor the written mask; it is the declared width of the LOCAL holding
the value. docs/cookbook-index.md's "Start here" line ("hold the masked byte in a u16 local
so only a QI->HI extend survives", P30 wave 2, byte-tested) is this same axis at a third width and
has never been in the body — QI/HI/SI are one law.
Mechanism — HYPOTHESIS, not source-verified (§136g): gcc-2.7.2 expand_decl does not apply
PROMOTE_MODE to automatic scalars, so a u8 local keeps DECL_MODE == QImode and each SImode
use needs a zero-extend, while nonzero_bits on an lbu-fed SImode pseudo lets combine fold the
mask. Nobody read tools/reference/gcc-2.7.2 for this, and both locals here also cross a call —
the andi COUNT is the law, the RTL story is the guess. Refute it, do not cite it.
§162l — SHARPENS (sharpens §48-B, §48-C1, §20, §21)
§48-B corollary — AMENDED (P30 S47,
func_80189540, ov_SC04_018_jr_80188E1C, 551 ins ×5). The corollary is a statement about what your C can emit, not about what a target may contain — and its escape hatch is closed for NEGATIVE displacements.
Target at 0x801896E8 — 2 address insns, ONE MEM at offset 0, THREE at −0xA:
lui $a1,%hi(D_8011514C)
addiu $a1,$a1,%lo(D_8011514C)
lbu $a0,0x0($a1)
lbu $v1,-0xA($a1) ; = D_80115142
sb $v0,-0xA($a1) / sb $a0,-0xA($a1)
So the base survives on ONE offset-0 use. From plain C you cannot reach that: u8 *bp = &D_8011514C;
emits 4 separate luis (+2) — fold_rtx folds the SYMBOL_REF into CONST(sym−0xA) at every k≠0 use
(a legal MIPS address), the la loses its users, local-alloc rematerializes it away, and each MEM grows
its own lui … %lo. The delta is exactly (#distinct symbol+const forms) − 1.
NEW — the struct escape does not reach a negative displacement. §48-C1's "for offset uses declare a
struct" assumes fields ≥0 from the declared symbol. Re-declaring at the lower symbol (D_80115142, a real
symbol here) does give la + 0xA($b)/0($b) — but it flips the addiu immediate 0x514C→0x5142 and
the reloc symbol, so it is never byte-equal. For a base with negative displacements, BOTH of §48-B's
documented escapes are closed.
The $0-add extends from values to ADDRESSES — and it is a probe, not a cure. bp += zr;
(§36/§42c-4 register s32 zr __asm__("$0"), applied to a POINTER) makes the base (plus reg $0), which cse
never ties back to the SYMBOL_REF → the shared base survives and all four MEMs address off it. But it
emits an addu the target does not have and swaps $a0/$a1. .run/wave1/func_80189540.c sits at
553 vs 551 carrying it; the function is still INCLUDE_ASM
(src/ov_SC04_018/ov_SC04_018_jr_80188E1C.c:3280). Confirms the mechanism; does not bank.
DO NOT RE-BUY (§80 form) — 9 byte-measured refutations on this shape
(.run/wave1/_scratch_80189540/probe{4,5,7}.c): two separate pointers · in-place bp -= 0xA ·
register u8 *bp __asm__("$5") pin · struct-typed pointer · S1 * at the +1 symbol · s32 base + casts ·
a dead bp[0]=bp[0] second offset-0 use · a bp==0 liveness guard. All fold. Not reachable by
respelling — the same verdict §153 reached after 14 probes.
UNTRIED, and it is the next probe. The escape for a cse address fold is an EBB SPLIT, not a
spelling (§48-B's own rule; gcc-2.7.2-map/cse_expr.md §H-1): put the base's def and its offset uses in
different extended basic blocks — a balanced if/else diamond so the join label is barrier-preceded, or a
use reached only through a jtbl case label — so find_best_addr/fold_rtx start on a fresh table.
Zero bytes, unlike the $0-add.
DIAGNOSTIC TELL. Count address insns, then count offsets. Target = one lui+addiu pair feeding MEMs
at MIXED displacements (any negative one especially); yours = one lui per MEM. That is the fold — not
regalloc, not scheduling. Do not open the permuter on it.
⚠ INDEX DEFECT, fix with this entry. tools/cookbook_index.py:89 publishes "target reuses ONE address
register across two different offsets of the same global → §20 … take T *p = &D_x; and index off p".
§20's evidence (func_80186938) is lw 0($v1) / sw 0($v1) — the same offset, 0, twice. That line
prescribes the exact spelling this corollary refutes for k≠0, at the first place an agent looks. Add the
offset-0 precondition and point it at §48-B/§48-C1.
§162m — SHARPENS (sharpens §36, §158, §148, §153)
§158a — THE FIFTH LEVER IS NOT AN ASM: do { } while (0) is a REGION ref-multiplier you MINT (P30 S48, func_8017CBC8, ov_MAIN_012 / jr_801789AC, 188 ins → MATCH)
§158 called the allocno toolkit complete at four zero-emission asm levers. There is a fifth, and it
is plain C syntax: a never-iterating do { … } while (0). §36 already named the mechanism — but only
as a HAZARD to delete. It is also a lever to add, and it is the only one in the family that
reweights a REGION instead of a named operand.
The law
flow.c:434 starts depth = 1 and each NOTE_INSN_LOOP_BEG increments it (flow.c:456/471 →
basic_block_loop_depth), and every mention is then counted reg_n_refs[regno] += loop_depth
(flow.c:2067/2315/2501/2711). So one never-iterating wrapper DOUBLES every mention inside it,
for zero bytes, feeding global.c:594 allocno_compare pri = floor_log2(refs)·refs·size/live_length.
The minimal unit is ONE assignment statement. x = f(…, x, …); mentions x as both def and use,
so wrapping that single line buys x exactly +2 refs. That is finer-grained than §148-C's
asm("" :: "r"(v)) (needs an existing loop; +depth per named operand) and cheaper than §37's
block-top ref-boost (+1, and v must already be live-through).
Size it before you write it (§158 step 1-2, applied)
floor_log2 is a step function, so the answer is almost always 2, not 1. Read cc1 -dl, rank the
contenders, then solve for the refs you must buy. On func_8017CBC8:
| pseudo | refs / live_length | floor_log2(r)·r/L |
|---|---|---|
ot (param 0) |
16 / 129 | 0.4961 |
s (param 1) |
21 / 159 | 0.5283 |
q |
10 / 58 | 0.5172 |
s outranked ot, so s allocated first and the two incoming params landed in swapped
callee-saved registers. 17 refs still loses (4·17/129 = 0.5271); 18 wins (4·18/129 = 0.5581).
Wrapping exactly the loop-1 call —
do {
ot = func_800D27DC(mode, ot, sp60, 1, 0);
} while (0);
— bought ot 16 → 18 → ot allocates first → $s2/$s3 as the target. One statement, two refs,
zero bytes.
The wrap BOUNDARY is the dial — and it is indiscriminate
Everything mentioned inside gets +loop_depth, wanted or not. Proven both ways in this one function:
mode = dim ? 3 : 2; was hoisted OUT of the wrapper specifically to deny dim the same +1, which
would have swapped $s5/$s6 between bp and dim. Wrap the smallest statement span containing
only the mentions you want counted — that discipline IS §36's hazard, read forwards.
Not a pure dial
Unlike the asm levers, the loop notes also split cse1 at NOTE_INSN_LOOP_END (§153 — cse2 normally
puts it back) and open a fresh basic_block_loop_depth block. Re-gate; never assume byte-neutrality.
DIAGNOSTIC TELL — two faces, one law
- Constructive (this section): two long-lived values with near-tied
allocno_comparepriorities sit in swapped registers — here two incoming params across$s2/$s3. A density gap under ~0.04 is the signature that a SINGLE ref separates them. Compute the table; if the winner needs +2, wrap one def+use statement. - Hazard (§36,
func_8013AF20): a preheader-const register identity is off by one and an inherited wrapper is the cause — a wrap around three0x3dstores took refs 7→10, pri 4166 > 4000, and stole$a2from the mask; deleting it restored the tie → MATCH.
Compute the ratios before you add OR remove a wrapper. Same arithmetic decides both.
⚠️ Strength of evidence: the law rests on verified gcc-2.7.2 source plus two measured instances
pointing opposite ways, and is solid. The recipe ("wrap the call statement") is ONE constructive
datum: func_8017CBC8 also carried a $16 pin, a bp = sp18 hoist-order fix and a & 0xFFFF mask in
the same draft, so the wrapper's contribution is attributed by the 16→18 dump reading and the
mode-hoist counter-experiment, not by a controlled A/B. Treat the sizing arithmetic as the durable
part.
Symptom lines for the index: "two incoming params in swapped $sN" · "the density ratios are
almost tied" · "a register identity off by one ref" · "I need +2 refs and no asm fits".
§162n — NEW
§162n1 — A CONDITIONALLY-ASSIGNED ALIAS POINTER KILLS A SPURIOUS GIV. This is the off-diagonal of §135-17. Target shape:
addu $a2,$s0,$zero <- a plain COPY of the loop base, not a reduced IV
…
sh $v0,2($a2) <- literal offset off the copy; no 4th `addiu` in the body
Every prior giv law in this file runs one way — §30-2, §52-2, §135-17, §145, §36 all say do not write the second pointer; address every field as p + const and let combine_givs build one representative. That law has an off-diagonal, and this is it. When the target does NOT strength-reduce a near-field access at all, p + const off the single biv is exactly what manufactures the extra IV, and the fix is to introduce the second pointer — conditionally.
THE LAW. A pointer assigned INSIDE an if is recorded with always_computable = 0 (loop.c:4264, :4389 — v->always_computable = ! not_every_iteration). At the join label, update_giv_derive sets cant_derive = 1 (loop.c:4742: if (GET_CODE (p) == CODE_LABEL && ! giv->always_computable)). Thereafter simplify_giv_expr returns 0 for any expression containing that pseudo (loop.c:5271-5272), so find_mem_givs records no DEST_ADDR giv for p[k] — the address stays a base copy plus a literal MEM offset, and the induction register is never minted.
PRECONDITION — GET THIS WRONG AND THE LEVER DOES NOTHING. In the strength_reduce scan, find_mem_givs runs at loop.c:3632 and update_giv_derive at :3640, after it. cant_derive therefore only bites uses that follow a CODE_LABEL. The conditional assignment must be in the if; the p[k] uses must be after the join label. Both inside the same arm, with no label between, and the giv is still derivable and nothing changes.
/* draft: w[1] off the biv -> 4th induction reg, +1 $sN, arg2 spilled */
w[1] = v;
/* target: alias set on one path only, used after the join */
if (cond) p = w;
… /* join label -> cant_derive */
p[1] = v;
BYTE EVIDENCE. func_8017C294 (ov_SC02_000, 246 ins — §147's function, and its 246-ins twin func_8017CE58). w[1] (offset 2 off the biv) was strength-reduced into a 4th induction register, costing a callee-saved register and spilling arg2. Moving the assignment to p = w; inside the if reproduced the target's addu $a2,$s0,$zero + 2($a2), removed the giv, and dropped the frame 0x140 → 0x138. Verified MATCH. This is the lever §147 was missing; its S43 correction blamed the residual on a cse1 elision count and parked the 15-sibling family — family_remap now carries them.
DIAGNOSTIC TELL. One more addiu/addu maintaining a pointer through the loop than the target, and the target reaches the field through a bare copy of an existing base register (addu $aN,$sM,$zero) with a small literal MEM offset rather than through its own walked register. §135-17's tell ("extra induction register") is the same symptom with the opposite cure, so read the target's addressing before applying either: a reduced register ⇒ §135-17 (collapse to one pointer); a base copy + literal offset ⇒ this section (add a pointer, conditionally). Secondary tell, shared with §162n2: frame 8 bytes too large with one extra sw $sN.
⚠ READ WITH §162n2, WHICH POINTS THE OTHER WAY. §162n2 says an unconditional void *s0 = a0; alias costs a second callee-saved register and the cure is to use the raw parameter everywhere. Both are true and they are not in conflict: an alias's cost depends on whether it is conditionally set. Unconditional ⇒ a live pseudo across the uses (§162n2). Conditional ⇒ cant_derive, and the giv it would have spawned never exists. Do not delete a conditional alias on §162n2's authority.
HONEST SCOPE. One function, one instance. The gcc path is deterministic, so the mechanism generalises; the trigger does not yet. Byte-proven here: the emission, the frame delta, the MATCH. Read from source rather than from a dump: that cant_derive specifically is the flag that fired. If a sibling resists, cc1 -dL on both spellings settles it in 30 seconds — do that before writing a wall verdict (§147 is the cautionary tale, on this exact function).
Symptom lines for the index: "one extra induction register but the target uses a base COPY" · "addu $aN,$sM,$zero followed by a small literal offset" · "frame 8 bytes too big with one extra sw $sN" · "collapsing to one pointer made it worse".
§162o — SHARPENS (sharpens §158, §136-1, §136-6, §79)
§162o1 — WHEN A REGISTER PAIR IS SWAPPED IN EVERY ARM, THE LEVER IS DECLARATION ORDER; IN ONE ARM, IT IS DECLARATION SCOPE. (The missing discriminator between §158's tie-break and §136-1's split. Both present as "a swapped register pair".)
Target shape — the same pointer/value pair, transposed identically in all N repeated blocks:
lhu $v1,%lo(D_8011511A)($a0) <- target: ptr = $a0, val = $v1
lhu $a0,%lo(D_8011511A)($v1) <- draft: swapped, and swapped the SAME WAY in all four
LAW (an instance of §158, not a new one): allocno_compare breaks an exact priority tie by
allocno number, and pseudo numbers follow first use ≈ declaration order (§79) — so at a tie the
FIRST-DECLARED local takes the lower hard reg. Declaring the four pointer locals
(s32 *p; s32 *p2; u16 *ph; s16 *q;) BEFORE the value locals buys the target's $a0/$v1
assignment; moving them to the end of the decl block transposes the pair in all four
D_8011511A blocks at once. Byte-evidence: func_8017C3BC (ov_MAIN_012, jr_801789AC,
407 ins) — A/B preserved at .run/wave1/final/func_8017C3BC/t.c (pointers first) vs
.run/wave1/w3/func_8017C3BC/t.c (pointers last), identical bodies otherwise.
THE DIAGNOSTIC TELL — COUNT THE ARMS. One pair swapped in ONE arm with the siblings
byte-correct is §136-1: a function-scope local with REG_N_DEATHS > 1 fails
local-alloc.c:472, becomes a global allocno and loses the low reg — split it per arm. The same
pair swapped in EVERY arm is declaration ORDER — do not split, reorder the decl block.
SCOPE — respect the standing negatives. Decl order moves registers only at an exact
allocno_compare tie (§158: "only fires on EXACT length ties"; the range-extender makes the
inequality strict instead). §72 and §65 both byte-measured decl-order permutations as INERT on
their functions, and §150 records them inert against a variable-identity error — so run §150's
order first (decode ownership → pseudo COUNT and per-instance identity → then reorder).
"Pointers before values" is a prior about the ORIGINAL author's source style, not a compiler
rule — gcc reads order, never type. Use it as the first guess when reconstructing a decl block
cold; verify against the frame map, which reads the same order back (§79).
§162o2 — A POINTER COPY SPILLS OR COALESCES BY ITS DISTANCE FROM THE BASE'S INIT — and that
retires §147's volatile counterfeit. (Statement order, NOT declaration order. Do not merge
with §162o1.)
Target shape: addiu $vX,$sp,K ; sw $vX,off($sp) ; … ; lw $vY,off($sp).
base = world; o = out; c = cam; w = base; /* separated -> addiu/sw/lw = the target */
o = out; c = cam; base = world; w = base; /* adjacent -> coalesced away, -1 ins */
LAW: initialize the base FIRST, let the other pointer inits intervene, then take the copy. An
init-and-copy pair that is adjacent is folded away; separated by intervening statements the base
is spilled and the copy is a reload. Byte-evidence: func_8017C294 (ov_SC01_077 / ov_SC03_030,
jr_8017AE2C, 246 ins) — this is §147's long-parked function, and the reorder produces NATURALLY
the stack offset that every earlier draft counterfeited with short *volatile pEnd; + s32 dead[7]; (.run/s42/ov_SC01_077/func_8017C294.c). Delete the counterfeit and move the
assignment. Distinct from §156's sub = pct bullet, which is a register copy surviving cse
ACROSS bbs; this is a spill/reload inside one bb keyed on statement distance.
Citation care: func_8017C294 is a per-overlay address with at least three different bodies in
the tree (952-ins renderer in ov_SC06_008, 76-ins in ov_SC03_006, this 246-ins one) — always name
the overlay (§148-E).
Symptom lines for the index: "the same register pair swapped in EVERY arm, not one" · "pointer locals declared after the value locals" · "an addiu/sw/lw of a stack base the draft coalesces away" · "a draft that needs a volatile local to move a stack offset".
§162p — SHARPENS (sharpens §48-B, §46-L2, §156, §136d-1)
§48-B4 — THE EBB RULE IS A PLACEMENT LADDER: one value, N copies, N different constructions (P30 S48, func_8017CBC8, ov_MAIN_012 / jr_801789AC, 188 ins, MATCH — src/ov_MAIN_012/ov_MAIN_012_jr_801789AC.c:5879).
Target shape: a flag computed once, held in $s7, and re-emitted as three plain addu $sD,$s7,$zero copies — into $s6 at each of two loop preheaders and into $s0 for the tail call pair.
§48-B says the boundary must exist. It does not say what to do when the SAME value must be copied at several positions and only some of them have one. Work the positions in this order:
- Use inside a loop, and the loop already hoists something → write the copy IN THE LOOP BODY as a loop-invariant and let
loop.c move_movablescarry it to the preheader. §48-B documents this route only for a held address (la $sN,&G); it works for a plain reg-reg copy too, and it is the answer to §46-L2's "right copy, wrong place" parenthetical — the preheader is reachable, you just must not hand-write the copy there. Hoisted movables are emitted in BODY order, so the copy's position in the preheader is set by where it sits among the other invariants:bp = sp18; dim = flag;(5910-5911, the top of thekloop) makesaddiu $s5,$sp,0x18the first movable and lands the copy after it, as the target has it. Reverse the two source lines and you get the right copy in the wrong slot. (Companion to §148-A: that one tells you WHETHER an invariant hoists; this tells you WHERE it lands.) - Use inside a loop with no invariant to ride → write it in the preheader itself.
dim2 = flag;(5930) directly abovefor (;;)(5931): the loop-top label has 2 preds (fall-in + backedge), cse starts a fresh table there, the copy survives verbatim. This is §48-B's loop-top instance restated as a placement choice. - Straight-line tail, no boundary anywhere → opaque copy, then pin. Nothing separates
dim3 = flag;(5950) from thefunc_800D29F8/func_800D27DCargument pair, so every plain C spelling dies. Try RC-12 first (§136d-1):register s32 zr __asm__("$0"); dim3 = flag + zr;— the pin-free opaque copy built for exactly this position, and §136d-1 warns that pinning a copy's dest lets gcc propagate the hard reg forward and delete it. Only if that fails, pin:register s32 dim3 __asm__("$16");is what banked this one, and the matched siblingfunc_8013FAF8(ov_SC06_008) needed a pin for the same call pair. Cost: aregister __asm__pin failsdedup_propagate.compiles_standalone(§37) — rung 3 banks ×1 and forfeits the family, so exhaust rungs 1-2 first.
DIAGNOSTIC TELL: the target holds one value in a callee-saved register and emits N plain addu $sD,$sS,$zero copies of it into other callee-saved registers. That is never compiler redundancy — it is N source copies, each of which must be placed independently. Count the copies, map each one's position onto the ladder, and place them one at a time. Writing all N next to their uses gives N dead copies and a residual that looks like a whole-function regalloc wall; it is a placement problem.
Evidence scope (honest): the "same-bb copy always dies" half is corroborated across §46-L2, §48-B and §156. The ladder and the loop-body-hoist route for a reg-reg copy rest on this ONE function; rung 3 has a second sighting but of the same call pair. RC-12 was not tried on the tail copy here, so "needs a pin" is untested-alternative, not proven necessity.
§162q — SHARPENS (sharpens §30, §30a, §135-2, §136-13)
§162q1 — THE /s GRANT IS A PER-SITE EDIT, NOT A SHAPE REWRITE (bounds §30 / §135-2). Target shape,
case 3 of func_8017D2A4 (ov_MAIN_012, 291 ins, jr exemplar):
sh $zero,%lo(D_801150D4)($at) <- the target's order is LOAD FIRST
lh $v1,0($a0)
sh is a fixed-address non-/s store, lh is a varying-address load; the bare *q spelling is
non-/s, so sched.c true_dependence keeps the edge and the load will not rise. §30's grant fixes
it — ((struct { s16 h; } *)q)->h != 0xC is a COMPONENT_REF, /s unconditional (expr.c:4888),
/s+varying vs non-/s+fixed, edge dropped, load hoists.
THE LAW (this is the new half; the grant itself is §30 / §30a-1 / §135-2 and needs no restating):
the /s flag edits the DEPENDENCE GRAPH, not the schedule. Whether dropping an edge moves an
instruction depends on the rest of the block's ready list, so a second site with the same store/load
pair usually needs nothing, and rewriting it costs you. Grant /s at the site the diff names and
nowhere else.
Byte evidence. In the banked func_8017D2A4
(src/ov_MAIN_012/ov_MAIN_012_jr_8017CF3C.c) case 3 (L3936-3941) and case 6 (L3954-3960) open
identically — s16 *q = &D_8011512C; D_801150D4 = 0; — and only case 3 carries the COMPONENT_REF
(L3939). case 6 keeps three bare *q compares (L3955-3957) and is byte-exact. The blocks are not
otherwise alike: case 3 is one compare and one store; case 6 adds two more compares, a
4-iteration loop and a jal, and its scheduler had no tie left to break. Corroborated on the negative
side by §136d-3, where reshaping the load where the diff did not demand it went 2 → 32 mismatched.
THE DIAGNOSTIC TELL. Read the ORDER, not the shape: a bare-deref load sitting below a
sh/sw $zero,%lo(D_xxx)($at) that the target puts above it. Grant /s there. If a sibling arm has
the same pair and no order diff, leave it bare — you are looking at a shape, and the shape is not
the residual. Prerequisites still bind: the blocking store must be constant-address (§136-13) and the
access must not be QImode (cse_expr.md §4b — u8/s8 never get the escape).
Scope, honestly: two arms of one function. Nothing here was measured across a structural family, so read this as "site-selective within a body", not as a claim about family sweeps.
§163 — S48 WAVES 2-3 HARVEST (P30, 2026-08-11/12): the five that were byte-probed and are actionable
Provenance: 67 exemplar cracks across waves 2-3 (22 + 27 banked on the whole-binary gate). The agents flagged ~40 candidate laws; these FIVE were selected as byte-probed, generalizable, and immediately actionable, and were deduped by hand against the file. The remaining ~35 are catalogued at the end of this section with their function names — they are NOT lost, but they are NOT vetted either.
§163a — DECL-CONFLICT SEVERITY IS SCOPE-DEPENDENT, AND BLOCK SCOPE IS THEREFORE A CONFLICT
SOLVENT. (sharpens §8d and §161c: §8d PROVES the phenomenon on D_801812A4 — BLOCK→BLOCK→FILE
builds, FILE-first errors — but never states the rule or its lever half.) In gcc-2.7.2 an
INCOMPATIBLE extern redeclaration is:
either declaration at FILE scope -> HARD ERROR "conflicting types for X"
BOTH declarations at BLOCK scope -> WARNING ONLY "type mismatch with previous external decl"
Consequence for banking: the block-scope-extern pattern is not merely hygiene, it is a genuine
SOLVENT. A draft whose struct-typed view of a global disagrees with the host TU's scalar view can be
banked AS-IS by moving both the typedef and the extern into the using block. Byte-probed both
directions on the pinned cc1 (func_80189540, ov_SC04_018, 551 ins): the file-scope form is a hard
error, the block-scope form warns and still gates MATCH at 551 — codegen unchanged.
Corollary already paid for elsewhere: you cannot simply DELETE the draft's decl to dodge a conflict,
because match_one compiles the draft STANDALONE and the symbol is then undeclared — which
gcc-2.7.2 reports with no error: prefix, so it reads as a mystery CC1 FAIL (func_80187960).
§163b — THE SWITCH-INDEX PARAMETER-WIDTH ORACLE: read the EXTENSION, not just the bound.
(extends §161a/§162a from the table's edges to the dispatch's operand.) For switch (p) with
minval != 0:
s16 param: addiu $a0,$a0,-MINVAL ; sll $a0,$a0,16 ; sra $a0,$a0,16 ; sltiu $v0,$a0,MAXVAL
s32 param: addiu $a0,$a0,-MINVAL ; sltiu $v0,$a0,MAXVAL
The sll/sra pair straddling the minval subtract is a 2-instruction signature of the switch
parameter's DECLARED WIDTH — it is the HImode re-extension of the truncated subtract result and
exists only for a short. Tell: a jump-table function off by ±2 ins with the drift starting at the
sltiu — read the extension around the subtract before touching anything else. (func_80189540.)
§163c — case_values_threshold IS 5: AN EMPTY CASE LABEL GLUED TO default: CAN BE THE ONLY THING
THAT EMITS A TABLE AT ALL. (extends §162a from the table's BOUND to its COUNT.) A switch with
four live cases {0,1,3,4} emits a DECISION TREE, not a tablejump — so a 5-entry table in the target
is unreachable until an explicit case 2: label is added onto the default body. Read the table's
ENTRY VALUES, not just its length: jtbl[k] == the default label is the fingerprint of a case
label sharing the default body, and that empty label is LOAD-BEARING. Took a draft from 43 mismatches
to 1. (func_80181BE4, ov_SC01_077.)
§163d — cse DELETES A REG-REG COPY BY REWRITING THE PREVIOUS INSN'S DESTINATION. (the missing
sibling of §162j — same symptom, DIFFERENT PASS. §162j is local-alloc's optimize_reg_copy_1; this
one fires earlier, in cse, and the two need different levers.) Mechanism read out of the pinned gcc
source, not inferred: cse.c:7440-7477 (cse_insn, "special handling for (set REG0 REG1) where
REG0 is the cheapest") validates a change of the PREVIOUS insn's SET_DEST to REG0 and rewrites this
insn to (set REG1 REG0), which becomes a dead store and disappears — so both the copy and its
defining insn's destination change, and the source pseudo vanishes from the function entirely. The
canonical-quantity choice is make_regs_eqv (cse.c:826-855), and the dial is last-use order, not
set count. A prior agent had declared this residual "unsteerable — 30 variants all ≥17"; it was a
false wall, closed by source-shape edits alone with NO pins (229 → 15 → 8 → 0). (func_8017C3BC,
ov_MAIN_012, 407 ins.)
§163e — THE FRAME IS A PSEUDO-NUMBER ORACLE, AND DEAD-LOCAL SLOT ORDER IS NOT DECLARATION ORDER.
(sharpens §162i, the unreferenced-local frame oracle — that entry gets the SIZE right and the
PLACEMENT wrong.) reload1.c:658 runs alter_reg(i,-1) over pseudos in NUMBER order, so slots fall
out of pseudo numbering, not source order. Two independent measurements: a BLKmode local is 8-ALIGNED
with its size CEIL_ROUNDed to 8 (assign_stack_temp → assign_stack_local(mode,size,-1)) while a
scalar s32 gets only 4-byte alignment — which is why a bare s32 sz can never land on 0x30 and
must be written s32 sz[1]; and the dead pad is allocated BEFORE a later-declared live aggregate, so
sz[1]; sv; pad[6] and sz[1]; pad[6]; sv both emit slot order sz,pad,sv. Practical rule: place
the §162i pad IMMEDIATELY BEFORE the local you want pushed DOWN, then VERIFY with grep '\.frame'
plus the sp-relative store offsets — never by reasoning about declaration order. (func_80184BD8
ov_SC02_000; func_8017C294 ov_SC01_077.)
§163z — THE UNVETTED REMAINDER (do not cite as law; each needs a dedupe pass)
~35 further claims sit in the wave note-sets (.run/jr48/wave2_result.json,
.run/jr48/wave3_result.json) and in the run transcripts. The ones whose stated mechanism looked
strongest, by function, so a future harvest can go straight to them:
func_8018FF98 (cross-jump reconstruction is NOT optional even when a shared tail "obviously" wants
a goto — the §162g lever half) · func_8017FFD0 (same axis, "the missing LEVER half") ·
func_80183324 (N distinct jtbl labels pointing at the SAME block = a positive tell) ·
func_80192B60 (fold's PLUS/MINUS re-association is source-form invariant — only a statement
boundary breaks it; two-armed constant select; "a re-read is not redundant") · func_8017DEFC
(2-D vs 1-D spelling kills/creates a LICM movable — sharpens §162e in the OPPOSITE direction; loop
FORM as a delay-slot declaration; a shared temp as a SCHEDULING barrier) · func_8017DD28 (three
spellings of one subtraction give three codegens) · func_80185EF8 (fold_range_test defeats ||
bound-chains) · func_8017FDF8 (the mult/LO tie-break) · func_8017CA18 (found via gdb-on-cc1 in
sched.c priority()/rank_for_schedule()) · func_8018E8A0, func_8017D7C0, func_8017F83C,
func_80184944, func_80186C4C, func_801823E8, func_80192768, func_8017EB44, func_8017EB70.
R14 applies to every one of them: they are one agent's reconstruction until re-measured.
§164 — S48 §163z SKEPTIC PASS (P30, 2026-08-12): 190 claims vetted, 82 banked
The pass itself is the finding. The 34 catalogued wave-2/3 note-sets claimed 190 distinct laws. One independent skeptic per function, each required to read the full notes, grep the whole cookbook, classify, and GRADE THE EVIDENCE:
| verdict | count | evidence | count | |
|---|---|---|---|---|
| NEW | 20 | byte-probed | 114 | |
| SHARPENS | 62 | single-instance | 51 | |
| COVERED | 80 | asserted | 25 | |
| UNSOUND | 28 |
57% of what the crack agents flagged as novel was already in the cookbook or does not survive scrutiny. A crack agent is the right instrument for FINDING a lever and the wrong one for judging whether it is new — it has just spent hours inside one function and has not read the other 497 sections. Never bank a wave's flags directly; always run the skeptic pass. (The five entries hand-vetted as §163a-e came through this same filter and stand.)
Entries below carry their verdict and evidence grade. byte-probed = an A/B was actually run on the
pinned cc1 and the result quoted; single-instance = it worked once and the mechanism is inferred.
Several skeptics also CORRECTED the mechanism the crack agent proposed while confirming its effect —
those corrections are in the entries, and they are the most valuable part of this section.
(NEW; evidence: byte-probed; from func_8017DEFC)
§164-01 — pointer_int_sum CANONICALISES POINTER-FIRST, SO Residual A's Fix A1 CANNOT REACH A POINTER ADD. (bounds Residual A / Fix A1.) Target shape — an address computed with the INDEX in the first operand slot:
addu $a0,$a0,$s1 <- target: index + base
addu $a0,$s1,$a0 <- everything you write in C: base + index
THE LAW. Residual A's premise — 'gcc 2.7.2 has NO swap_commutative_operands, so the source order survives' — holds only for operands the FRONT END leaves alone. For pointer arithmetic it does not: c-typeck.c build_binary_op's PLUS_EXPR arm normalises the pointer into the ptrop argument whichever side it was written on (ptr + int → pointer_int_sum (PLUS_EXPR, op0, op1); int + ptr → pointer_int_sum (PLUS_EXPR, op1, op0)), and pointer_int_sum finishes with result = build (resultcode, result_type, ptrop, intop); — pointer unconditionally at operand 0. &p[i*K] funnels through the same path. So all three natural spellings produce the identical RTL and Fix A1 is a no-op you will mistake for a refutation.
THE LEVER. Do the addition in INTEGER space and cast the result: (char *)(i * 36 + (s32)p). Two integer operands go through build_binary_op in source order, fold.c reorders a commutative pair only when one side is a CONSTANT, and the MIPS addu pattern prints the RTL operands in order — so the index lands in operand 1 and you get addu $a0,$a0,$s1.
Byte evidence. func_8017DEFC (ov_SC01_005, 124 ins, MATCH; .run/wave2/func_8017DEFC.c:121). &p[i*36], p + i*36 and i*36 + p all emit addu $a0,$s1,$a0; (char *)(i * 36 + (s32)p) emits the target's addu $a0,$a0,$s1.
Bound. Only for two NON-CONSTANT operands. With a literal on either side fold canonicalises it to operand 1 and there is no dial left.
THE DIAGNOSTIC TELL. A one-register or operand-order diff on a single addu that forms an ADDRESS, where reversing the C operands changes nothing at all. 'Fix A1 did nothing' on a pointer add is the fingerprint — the canonicalisation happened before RTL. Cast to s32, add, cast back. Do not open the permuter and do not reach for pins.
(NEW; evidence: byte-probed; from func_8017ED5C)
§164-02 — THE qty_const OPERAND-ORDER SWAP: cse decides which register is rs, and SOURCE ORDER IS INERT AGAINST IT. (BOUNDS §10-Residual-A's "Fix A1 — operand order"; a second consequence of §153's launder.)
Target shape — a symbol table reached twice, once through the gas macro and once through a materialised base:
lh $3,D_801BB948($5) ; index in $5
la $4,D_801BB948 ; base in $4
beq $2,$0,$L5
addu $2,$4,$5 <- BASE is operand 0
LAW. cse.c:5278-5304 (fold_rtx): for any commutative rtx, if operand 0 has a constant equivalence and operand 1 does not, the operands are SWAPPED. equiv_constant (cse.c:5699-5705) reports one for every pseudo whose quantity carries qty_const, and insert (cse.c:1374-1397) sets qty_const on any register whose class contains a CONSTANT_P member — i.e. on every register loaded with la sym. So <symbol base> + <index> is forced to addu rd,<index>,<base> whatever the C says. §10-Residual-A's "gcc 2.7.2 has no swap_commutative_operands, so source order survives" holds only while NEITHER operand is constant-equivalent; do not apply its Fix A1 here.
The lever — zero bytes. After e = (T *)SYM; write __asm__("" : "=r"(e) : "0"(e));. e is now SET by an ASM_OPERANDS, which is not CONSTANT_P, so no qty_const is recorded, the swap never fires, and the base stays operand 0. Non-volatile is enough — it must be a SET, not a barrier. Same instrument as §153, aimed one pass later.
Byte evidence — func_8017ED5C (ov_SC01_005, 147 ins, banked commit:1652, src/ov_SC01_005/ov_SC01_005_jr_8017ED5C.c:2792); five A/Bs on the pinned triple:
- banked:
la $4/sll $5/addu $2,$4,$5— MATCH 147. - drop the asm:
la $5/sll $4/addu $2,$4,$5— index is operand 0 and the two registers trade. - drop the asm AND write the base first in source (
off + (u8 *)e + 2): output byte-identical to the previous line. Source order is inert. - keep the swap, pin the base (
register s16 *e __asm__("$4"), no asm):addu $4,$5,$4— the pin fixes the REGISTER and leaves the swap in place. A pin cannot buy this.
Second, separable effect of the same asm — §148-C, not new: its extra refs lift e's allocno_compare priority above the index temp's, which is what puts e in $a0. §163h's probe shows the two effects are independent, so budget for both.
DIAGNOSTIC TELL. Your addu/or/and has the right two registers on the wrong sides, and one of them was just loaded with la sym. §10's A1 (source order), a register pin, and the permuter are all inert. Read cse.c:5278 first: if the operand that must come first is a symbol address, the answer is the self-set asm.
(NEW; evidence: byte-probed; from func_8017ED5C)
§164-03 — ptr + i*K AND off = i*K; ptr + off ARE DIFFERENT RTL: expand puts a MULT first in an ADDRESS sum. (the expand-time half of §163f; the file's only operand-order section, §10-Residual-A, assumes source order reaches RTL untouched.)
expr.c:5288-5290, inside both_summands:
/* Put a constant term last and put a multiplication first. */
if (CONSTANT_P (op0) || GET_CODE (op1) == MULT)
temp = op1, op1 = op0, op0 = temp;
SCOPE — this is an ADDRESS-only path. both_summands is reached only for modifier == EXPAND_SUM (expr.c:5237-5248); a plain arithmetic + takes goto binop and keeps tree order. EXPAND_SUM is what INDIRECT_REF uses for its address subexpression (expr.c:4563). So:
*(T *)((u8 *)p + i*4 + 2) -> (plus <index> <ptr>) addu rd,<index>,<ptr>
off = i*4; *(T *)((u8 *)p + off + 2) -> (plus <ptr> <index>) addu rd,<ptr>,<index>
It is NOT "the MULT operand is expanded first" — expand_expr's binop path expands operands in tree order, and fold-const.c:3179-3190 reorders only literal constants. It is the explicit canonicalisation above, firing on the RTL SHAPE — which is why hoisting the scale into its own local (making op1 a plain REG) is the entire lever.
Byte evidence — func_8017ED5C (ov_SC01_005, 147 ins, banked commit:1652), both variants compiled WITH §163f's asm so cse's swap is out of the picture: off = idx * 4; … (u8 *)e + off + 2 ⇒ addu $2,$4,$5 (base first, MATCH); the same line as (u8 *)e + idx * 4 + 2 ⇒ addu $2,$5,$4. Operands flip, registers do not move.
DIAGNOSTIC TELL — this is also the discriminator against §163f. Sides wrong, registers right, on an address sum whose index is a scaled subscript ⇒ split the scale into its own local (this section). Sides wrong AND the two registers traded ⇒ the cse swap (§163f); the expand-time edit alone will not reach it, because cse re-canonicalises whatever expand produced.
(NEW; evidence: byte-probed; from func_8017F83C)
§164-04 — A signed x = x * 15 / 16 compiles to sll k / subu / bgez / addiu (2^k - 1) / sra k; write it literally.
§1-I5 — Signed divide by a power of two -> shift / bias / shift.
Target: sll $v0,$v1,4 ; subu $v1,$v0,$v1 ; bgez $v1,+2 ; addiu $v1,$v1,0xF ; sra $v1,$v1,4.
C: write it literally — x = x * 15 / 16;. The bgez + addiu (2^k - 1) pair is gcc's round-toward-zero bias for a signed / 2^k and exists for no other reason; an unsigned operand emits a bare srl with no branch, and a non-power-of-2 divisor goes to §1-I3's magic multiply instead. The sll k ; subu ahead of it is the source's multiply (*15 = <<4 - 1), not part of the division.
TELL: a bgez whose only job is to skip an addiu of 2^k - 1 immediately before an sra k. Do not respell it as >> 4 (loses the bias) or as a helper call.
Example: func_8017F83C (ov_SC03_108) at 8017F9F0 and 8017FA10; source src/ov_SC03_108/ov_SC03_108_jr_8017F83C.c:2835-2836.
(NEW; evidence: byte-probed; from func_8017F9AC)
§164-05 — THE INLINE-EXPANSION FRAME ORACLE: frame − args − 4×regs COUNTS THE EXPANSIONS. IT IS NOT A DEAD-LOCAL PAD. (bounds §162i1 — its 'Δ is a multiple of 8 ⇒ declare s32 pad[Δ/4]' procedure misfires on exactly this shape; also the honest reading of §21 / §32-5 / §42-3 / §135-6 for inlined bodies.)
Target shape. A large frame with zero $sp-relative references anywhere in the body — no spill, no &local, nothing.
THE LAW. Each in-place expansion of a static inline is given its OWN copy of the helper's block-scope local area. So vars = N × sizeof(helper locals) and the frame is free — writing the helper N times pays for it exactly, and a hand-rolled pad is both wrong and unnecessary.
BYTE EVIDENCE — two functions, two different N, arithmetic exact both times.
func_8017DC1C(ov_SC07_006, 25 expansions, 0jal⇒ no outgoing-arg area, no$ra): frame 0x258 = 600 = 25 × 24, all vars. Measured incrementally while building: 1 body = 24, 2 expansions = 48, 25 = 600.func_8017F9AC(same TU, 4 expansions, 4jal):addiu $sp,$sp,-0x80, saves$s0/$s1/$s2/$raat0x70/0x74/0x78/0x7C. 0x80 − 16 outgoing − 16 saved = 96 = 4 × 24.
⚠️ It looks exactly like §162i1's signature (frame too big, no $sp refs, Δ a multiple of 8). Refuted on DC1C: 'the 0x258 frame with zero stack references is dead storage / needs a padding array — No. It is 25 × 24 of per-expansion local slots and comes out free. Never hand-pad a frame before checking whether the body is expanded N times.'
DIAGNOSTIC TELL. Frame too big by a multiple of 8 with no $sp references ⇒ before reaching for §162i1's pad, count the repeated bodies and divide (frame − args − 4×regs) by that count. An exact integer is your answer: that quotient is the helper's local size, and you need N expansions, not a pad. Read it the other way too — the quotient tells you how many expansions the original wrote, before you have decoded a single block.
(NEW; evidence: byte-probed; from func_8017F9AC)
§164-06 — PARALLEL BIVS: THE PREHEADER GIV-INIT ORDER IS THE REVERSE OF THE INCREMENT ORDER. The mirror of §162e2. (§162e2 is an APPEND law and does not apply here; §145a/§135-17 use the same prepend idiom on a DIFFERENT list — record_giv's — and answer which giv anchors a group, not the order of the classes.)
Target shape — three pointers walked by one loop, three +4 inits in the preheader, and your draft has the right count but the wrong registers:
addiu $a2,$a3,0x4 <- pa
addiu $a1,$t1,0x4 <- pb
addiu $a0,$t0,0x4 <- d
.Lloop: … d->vx = pb->vx + (((pa->vx - pb->vx) * t) >> 12) …
THE LAW, from the pinned source. scan_loop appends movables (loop.c:796-800) ⇒ hoisted invariants come out in body order (§162e2). record_biv prepends its class (loop.c:4295-4297: bl->next = loop_iv_list; loop_iv_list = bl;), and strength_reduce then walks for (bl = loop_iv_list; bl; bl = bl->next) (loop.c:3717) emitting each class's init with emit_iv_add_mult (bl->initial_value, …, loop_start) (:3879-3880) — successively immediately before loop_start. ⇒ preheader init order = loop_iv_list order = the reverse of the order the increment insns appear in the body. Register grants follow emission order, so the increment order is the register assignment.
body increments: d++; pb++; pa++;
preheader emits: pa+4 ; pb+4 ; d+4 <- reversed, and this is the MATCH
BYTE EVIDENCE. func_8017DC1C (ov_SC07_006, 1,518 ins) — all 6 permutations measured: the natural read order pa++; pb++; d++; gives 1518 ins, 295 mismatched (right length, permuted giv registers); the four other permutations score 44-49/61 on the 2-block probe; d++; pb++; pa++; scores 57/61 and is the only one that MATCHes at full size. Second instance, byte-visible: func_8017F9AC 8017FA24-8017FA2C emits addiu $a2,$a3,4 (pa) / addiu $a1,$t1,4 (pb) / addiu $a0,$t0,4 (d), four times over — exactly the reverse of d++; pb++; pa++;.
DIAGNOSTIC TELL. Correct instruction count, correct loop body, but the walked pointers hold each other's registers and the preheader addiu …,4 lines are permuted ⇒ reverse the INCREMENT order; do not pin, do not permute the statements, do not touch §47/§158's allocno sliders — this is an emission-order fact upstream of them. If instead only ONE pointer's base constant K is wrong, that is §145a's combined-giv anchor (different list, same prepend idiom).
Symptom lines for the index: "the preheader +4 inits are in the wrong order" · "three walked pointers holding each other's registers" · "right length, ~N mismatches per loop, all register-local".
(NEW; evidence: byte-probed; from func_8017F9AC)
§164-07 — srl 2 ; sll 2 ; addiu K ; addu IS WORD-INDEX ARITHMETIC OFF A FIELD'S OWN ADDRESS, NOT A MASK — AND THE srl NAMES THE FIELD'S SIGNEDNESS.
Target shape (4 instructions, always in this order, always after the field load):
lw $v0,0xC($a0)
nop
srl $v0,$v0,2
sll $v0,$v0,2
addiu $v0,$v0,0xC
addu $t0,$a0,$v0
THE LAW. That run is (u32 *)&o->field + (o->field >> 2) — a pointer built by indexing the field's own address by the field's value in words (the sll 2 is the pointer scale, not a user shift, which is why gcc-2.7.2 leaves the shift pair standing). The arithmetically-equal mask spelling is one instruction shorter per site and does not match. And srl vs sra is a direct readout of the field's declared signedness — the field must be unsigned.
BYTE EVIDENCE — func_8017DC1C (ov_SC07_006, 25 sites), two independent rows of its do-not-re-buy table, both off the 1518 MATCH base:
(u8 *)&o->dst + (o->dst & ~3)instead of the index form → 1493 ins, 1456 mismatched (−25, exactly one per site).dstdeclareds32instead ofu32→ 1518 ins, 25 mismatched —sravssrl, exactly one per site.
Second instance: func_8017F9AC emits the identical 4-instruction run 4× (8017FA04, 8017FB1C, 8017FC34, 8017FD24), one per expansion.
⚠️ The DC1C report reads the mask form's emission as 'one andi 0xfffc'. Do not carry that mnemonic — ~3 = 0xFFFFFFFC is not an encodable andi immediate (andsi3 takes uns_arith_operand only), so the actual emission was not what the report names. What is measured is the direction and the size: −1 instruction per site, 1456 mismatched.
DIAGNOSTIC TELL. One instruction short per site, with an and-family instruction where the target has a shift pair ⇒ respell as index arithmetic off &field. Exactly one mismatch per site and it is sra where the target has srl ⇒ flip the field to unsigned and change nothing else — this is a declaration fix, not a codegen wall.
(NEW; evidence: byte-probed; from func_8017FDF8)
§164-08 — N unrolled expansions writing successive slices of one table must be spelled &SYM[k*STRIDE] off ONE array symbol — cse
§NNNd — N UNROLLED EXPANSIONS OVER SUCCESSIVE SLICES OF ONE TABLE MUST BE SPELLED &SYM[k*STRIDE] — NOT N SYMBOLS, AND NOT A WALKED POINTER. Target shape — ONE %hi/%lo pair for the table, then a bare constant bump per later expansion, with no %lo reload in between:
lui $s0, %hi(D_801F60A0)
addiu $s0, $s0, %lo(D_801F60A0)
... jal ...
addiu $s0, $s0, 0x20 <- x4, one per later expansion
THE LAW. Spell every site as &SYM[k*STRIDE] off ONE array symbol. cse hashes the address constant SYM+K, finds SYM already live in a register, and use_related_value (cse.c:1781, called at :6535) rewrites the later constants as reg + K — the bare addiu. Dump-confirmed, not inferred: the .combine dump carries (set (reg 138) (plus (reg 134) (const_int 32))) with REG_EQUAL (const (plus (symbol_ref "D_801F60A0") (const_int 32))).
Both natural alternatives lose, and neither loses loudly:
- N separate externs —
D_801F60C0,D_801F60E0, … , which is the shape splat's symbol map hands you. Each is its own unrelated constant, cse relates nothing, every site rebuildslui/addiu: 319 vs 317,LENGTH-DRIFT/+2. And the relocations mask identically —match_oneand the whole-binary gate cannot see WHICH symbol you used, only the count drift. There is no diff line that names the mistake. - A caller-local walked pointer —
u16 *p = &SYM[0]; … p += 16;.pstays live across the calls and the per-expansion address rebuilds vanish: 312 vs 317,LENGTH-DRIFT/-5.
DIAGNOSTIC TELL. A bare addiu $sN,$sN,K between two call sites where K is exactly the byte stride of the data those sites write, with no %lo reload in between ⇒ ONE symbol, indexed at each site. A LENGTH-DRIFT of a few instructions on an N-way unrolled body whose blocks each touch a table slice is this and almost nothing else.
Tension to know about: §136-5 ("a symbol read at a constant offset, but built into $s1 by lui/addiu ⇒ cache it in a pointer local") pulls the other way. That rule is for repeated reads at ONE offset inside one body; this one is for N sites at ASCENDING offsets, where the pointer local is byte-refuted. Read the target's bump: %lo reloads ⇒ §136-5, bare addiu ⇒ this entry. (func_8017FDF8, ov_SC07_006, 317 ins, banked.)
(NEW; evidence: byte-probed; from func_8017FFD0)
§164-09 — THE §5a CROSS-JUMP BARRIER HAS A PLACEMENT WINDOW: inside the last minimum insns before the jump, or the merge still fires. (NEW. §5a gives a working placement by EXAMPLE and never says placement is load-bearing; §50-B/§162h state the floor for a merge to FIRE — this is that floor inverted into the lever.)
Target shape. Two byte-identical blocks the original KEEPS separate, each ending … ; sw ; slti $v0,$v0,0x10 ; beqz $v0,.LA ; j .LB — func_8017FFD0 (ov_SC03_108, 196 ins) at .L8018017C and 0x801801FC, 9 slots each, identical to the word (asm/ov_SC03_108/nonmatchings/ov_SC03_108_jr_8017F83C/func_8017FFD0.s:117-126, 151-160). Written plainly gcc merges them: 189 ins, LENGTH-DRIFT −7.
THE LAW. find_cross_jump (tools/reference/gcc-2.7.2/jump.c:2371) walks both streams BACKWARD from the jumps, --minimum per matching non-USE/CLOBBER insn (2524-2528), breaks at the FIRST mismatch (2412 insn code, 2469 pattern code / rtx_renumbered_equal_p), and merges iff minimum <= 0 when the walk stops (2532). A difference deeper than minimum insns is therefore too late — minimum already reached 0 and the merge fires on the shorter suffix. The window is minimum insns: 2 for two js to the same label (1993) and for the RETURN chain (2024), 1 when one side falls through (1978). Only the trailing js are excluded from the count — the block's own conditional branch counts as one of the two.
⇒ To PREVENT a merge, the barrier goes IMMEDIATELY BEFORE the trailing goto/return. Earlier gets you a partial merge, not a match.
Byte evidence (one function, one target, five variants; .venv/bin/python tools/match_one.py func_8017FFD0 --c <v> --asm-subdir asm/ov_SC03_108/nonmatchings/ov_SC03_108_jr_8017F83C):
barrier position (insns back from the j) |
result |
|---|---|
none (.run/wave3/func_8017FFD0/vA.c) |
189, LENGTH-DRIFT −7 |
1 — last stmt before goto TAIL, in case 3 (vB.c) |
MATCH 196 |
1 — same, in case 1's tail instead (vC.c) |
MATCH 196 (either side; the walk is symmetric) |
3 — between the += 1 store and the >= 0x10 test |
192, −4 — merge TRUNCATED to the 2-insn slti;beqz suffix, not prevented |
| 6 — top of the case body | 189, −7 — no effect at all |
THE DIAGNOSTIC TELL. You added a barrier and the LENGTH-DRIFT moved but did not close (−7 → −4) ⇒ the barrier is OUTSIDE the window. Move it down to the last statement before the goto; do not add a second one (see §163f-2).
⚠ Bound — the barrier sits inside dbr's fill window. Between a conditional branch and a j it can block reorg from filling either delay slot. It is byte-free in func_8017FFD0 because the target leaves both slots nop; if the target fills either, expect the barrier to cost you that fill and pick the other twin block.
(NEW; evidence: byte-probed; from func_80181A30)
§164-10 — A setlen BYTE STORE AND THE TAG-WORD READ ARE THE SAME MEMORY, AND THERE IS NO ESCAPE — SO SOURCE ORDER IS THE SCHEDULE. (new; cse_expr.md §4b gives the QImode escape-denial, §162q1 gives 'the flag edits the graph, not the schedule', neither states the overlap window or the order consequence.)
Target shape — the second prim of a two-prim GPU builder:
li $3,0x1
sb $3,3($2) <- setlen(q,1): QImode store at +3
lw $3,28($18) <- an UNRELATED load
lw $4,0($2) <- the tag word read, BELOW both
THE LAW. memrefs_conflict_p (sched.c:614) reduces (mem:QI (plus q 3)) against (mem:SI q) through its PLUS arm to (1, q, 4, q, c = -3); the equal-base arm returns c < 0 && ysize + c > 0 -> 4 - 3 = 1 > 0 -> CONFLICT. Neither /s escape clause in true_dependence/anti_dependence (sched.c:830/856) can drop it: both are guarded on GET_MODE (...) != QImode and both additionally require one side to be FIXED-address, and here the store is QImode and both addresses vary. The edge is unconditional, so its direction is whatever you wrote:
q[3] = 1; t = *(u32 *)q; -> TRUE dep: the `lw` is nailed BELOW the `sb`
t = *(u32 *)q; q[3] = 1; -> ANTI dep: the `lw` is free to float ABOVE it
Byte evidence. .run/wave3/func_80181A30/{o,e}.c differ by exactly that statement swap and nothing else. e.s (read first) hoists lw $4,0($2) above BOTH the sb $3,3($2) and the unrelated lw $3,28($18), and pushes ori $5,$5,0x0200 down four slots — a 2-insn displacement against the matched fin.s. o.s (store first) keeps it directly below lw $3,28($18), as the target does. (func_80181A30, ov_SC03_117, 148 ins, MATCH — src/ov_SC03_117/ov_SC03_117_jr_8017BEBC.c:4717.)
The window is exact. A byte at +4 gives ysize + c = 0 -> no conflict -> order is free. Only sub-fields INSIDE the word alias it. Corroborated in the same function's FIRST addPrim: poly[3] = 8; poly[7] = 0x38; are both written far above *(u32 *)poly = (*(u32 *)poly & 0xFF000000) | ... and need no lever.
DIAGNOSTIC TELL. Your tag-word lw sits at the TOP of a prim-emit cluster where the target has it below sb <len>,3($p). Do not reach for a scheduling lever — you inverted the dependence in the source. Every PSY-Q setlen/setcode byte store (p[3] = n, p[7] = code) aliases the tag word at p[0] that addPrim reads. (Scope: the edge direction is determined by the source; whether the load actually MOVES is block-dependent — §162q1's bound applies.)
(NEW; evidence: byte-probed; from func_80183324)
§164-11 — THE JUMP TABLE IS PER-SWITCH AND SURVIVES TAIL-MERGE: count the TABLES to recover the source's switch count. (the COUNTING half of §162g/§162h — those read the j edges in .text; this reads .rodata, the one observable cross_jump cannot touch. Distinct from §163c, which reads entries WITHIN one table.)
Target shape. One function's rodata span holds N tables; k of them have byte-identical entry vectors, and those entries point into a single copy of the code:
jtbl_801D9340 8 entries, identity 0..7 <- the OUTER switch
jtbl_801D9360 {0,1,2,3,7}->A0 {4,6}->B0 {5}->C0 <- case 0's inner switch (did NOT merge)
jtbl_801D9380 {0,1,2,3,7}->A {4,6}->B {5}->C ┐
jtbl_801D93A0 ...the same three addresses ├ THREE tables, ONE copy of the code
jtbl_801D93C0 ...the same three addresses ┘
THE LAW (read out of the pinned gcc, not inferred). expand_end_case emits exactly one ADDR_VEC/ADDR_DIFF_VEC per switch that becomes a tablejump (stmt.c:5030-5041), and no pass merges, dedups or deletes a duplicate vector — do_cross_jump (jump.c:2537) redirects and deletes INSNS only. The table count is therefore conserved: it equals the number of tablejump switch statements the SOURCE contained, even after every body they dispatch to has been cross-jumped into one copy. k tables with identical entry vectors ⇒ k textually duplicated switches. Write the body out k times; do NOT collapse them into one switch reached by goto. The table whose vector DIFFERS is the copy that did not merge; and per §162g the surviving merged copy sits in the last-emitted of the k arms.
BYTE EVIDENCE — func_80183324 (ov_SC01_077, 324 ins, banked, src/ov_SC01_077/ov_SC01_077_jr_80183324.c:3115).
- The banked source spells the inner switch four times (
seq0/seq1/seq2/seq4in cases 0/1/2/4). cc1 emits five tables —$L67(outer),$L19,$L34,$L48,$L63— and$L34/$L48/$L63are word-identical, all three pointing at$L56/$L58/$L59, one copy of the code, physically inside case 4's arm = the last of the three.$L19alone points at its own$L12/$L14/$L15. (.run/wave3/func_80183324/raw.s:118,162,297,341,435,445.) - Whole-binary confirmation: the committed carve
config/splat.ov_SC01_077.yaml:156[0xb11e8, .rodata, ov_SC01_077_jr_80183324]runs totail21at0xb1288= 0xA0 = 40 words = exactly 5 × 8-entry tables, and the binary gates green.
THE DIAGNOSTIC TELL. Before writing any C for a multi-switch jr function, read its rodata span and count the tables, then diff their entry vectors. N tables ⇒ N switch statements, however few copies of the code you can see in .text. Identical vectors ⇒ duplicate the body that many times and let cross_jump merge it. A single shared body reached by goto gives the right .text and the wrong table count — invisible to match_one (it masks HI/LO and never compiles the carve), surfacing only at the whole-binary gate as §131/§129a image-size / %lo drift.
⚠ Bounds. (1) The oracle counts SWITCH STATEMENTS, not duplicated bodies — N distinct switches each goto-ing one common block look identical from the tables. Confirm the duplication from the arms' own prologues first. (2) A switch below case_values_threshold emits a decision TREE and contributes no table (§163c), so N is a lower bound on the source's switch count. (3) gcc-2.7.2 at -O2 does not inline non-inline functions, so a table can never be a duplicated inlinee here.
(NEW; evidence: byte-probed; from func_80188B84)
§164-12 — A ?: INSIDE A COMPARISON IS SPLIT INTO ONE BRANCH PER ARM; STORE THE FLAG INSTEAD. (NEW. §148-B, §147-B, §76 and §136d-4 are all about the VALUE a ?: produces; none is about a ?: used as a comparison OPERAND.)
Target shape — two sltis from one if, converging on a single test:
slti $v0,$v1,4
j …
slti $v0,$v1,3
beqz $v0,.Lskip <- ONE test, both arms in the SAME register
THE LAW. fold-const.c:3276-3333: for any node with TREE_CODE_CLASS == '2' || '<' whose operand is a COND_EXPR, fold rewrites X op (c ? A : B) into c ? (X op A) : (X op B). In a JUMP context that tree reaches do_jump's COND_EXPR case (expr.c:9124-9151), which emits do_jump(test) + a branch for the THEN comparison + a label + a branch for the ELSE comparison — one conditional branch per arm, +4 ins. if (x < (c ? 4 : 3)) can never converge on one beqz.
Write the flag as a value: s32 ok = c ? (x < 4) : (x < 3); if (ok) …. Nothing binary encloses the COND_EXPR, fold's distribution never fires, and expand emits one slti per arm into the same target followed by the single beqz.
⚠ DO NOT RE-BUY: fold's VAR_DECL/PARM_DECL guard (:3310-3313) only skips the SAVE_EXPR wrapper; :3324 builds the distributed tree regardless. Making the compared value a plain local does not stop the split. Related: the compared operand must be an s32 temp taken once before the test — an s16 local is re-extended in each arm (§162k1, the HImode instance) and an inlined (s16)argN duplicates the whole sll/sra pair per arm.
Byte evidence. func_80188B84, default arm — 166 ins, banked whole-binary in ov_SC04_018 and ov_SC04_019 (src/ov_SC04_018/ov_SC04_018_jr_801878E8.c:3754). Jump form measured +4 ins; the banked spelling is s32 ok = ((s16)mode == 5) ? (v1 < 4) : (v1 < 3); if (ok).
THE DIAGNOSTIC TELL. +4 ins concentrated in ONE if, and the target puts both comparison results in the SAME register (two slti, one beqz) where you emit two conditional branches. Then hunt for a ?: — or any conditionally-selected constant — sitting inside a comparison, and hoist it to a named flag.
(NEW; evidence: byte-probed; from func_80188B84)
§164-13 — IN AN ADDRESS, (v + C) * K IS DISTRIBUTED AND THE CONSTANT VANISHES INTO THE %lo; ONLY A NAMED TEMP BLOCKS IT. (NEW. §48-B/§162l cover the SYMBOL_REF+const fold that kills a la; §145(b) covers a pointer bump combine folds into MEM offsets. Same −1 tell, different pass, and none of their cures — struct, EBB split, barrier copy — reaches this.)
Target shape — the add kept as its own instruction:
target : addiu $v0,$s0,1 ; sll $v0,$v0,3 ; … ; lw …,%lo(SYM)($at)
mine : sll $v0,$s0,3 ; … ; lw $s0,8($at) <- one ins SHORT
THE LAW. expand_expr, MULT_EXPR case, address context only (modifier == EXPAND_SUM && mode == ptr_mode, expr.c:5359-5375) — the comment is literally "Apply distributive law if OP0 is x+c": (mult (plus r C) K) is rewritten to (plus (mult r K) (C*K)), and that constant then merges into the symbol's displacement, so the addiu is never emitted. Route the sum through a VAR_DECL — s32 e = k + 1; … SYM[e * 2]; — and op0 comes back a REG at :5366, the :5369 guard fails, control falls to :5377-5382, and the add is emitted as its own insn: the target's addiu ; sll.
One level up, a bare SYM[v + C] (no inner multiply) is folded the same way by pointer_int_sum's own distributive law (c-typeck.c:2650-2678). Two routes, one symptom.
SCOPE — this is an ADDRESSING law, not an arithmetic one. The :5362 guard is EXPAND_SUM; the identical (k + 1) * 2 assigned to an ordinary s32 keeps its addiu.
Byte evidence. func_80188B84 case 14 (166 ins, banked ×2). D_8010F468[(k + 1) * 2] inline → sll $v0,$s0,3 + lw $s0,8($at), one instruction short; s32 e = k + 1; ret = D_8010F468[e * 2]; → MATCH (src/ov_SC04_018/ov_SC04_018_jr_801878E8.c).
THE DIAGNOSTIC TELL. LENGTH-DRIFT −1, the missing insn is an addiu rX,rY,C, and your MEM displacement is exactly C × element-size larger than the target's. Generalises to any arr[(v ± C) * K] and arr[v ± C] whose target keeps the add separate: name the sum. Do not reach for §145(b)'s barrier copy — that one is downstream, in combine, and fixes a pointer bump, not an index.
(NEW; evidence: byte-probed; from func_8018E8A0)
§164-14 — combine_givs NEVER CROSSES IV CLASSES: two overlapping base registers in the target mean TWO BIVS in the source, and no store order can fake it. (the partition case §145a/§135-17/§162n1 do not cover — all three operate inside ONE class)
Target shape — two callee-saved pointers walking the SAME 12-byte-stride record, reaches overlapping:
addiu $s6,$s3,0xCC <- group A, offsets +0/+2
addiu $s4,$s3,0xD4 <- group B, offsets -4/-2/0/+2, bumped separately
THE LAW. combine_givs is called once per iv class (combine_givs (bl), loop.c:3770, inside for (bl = loop_iv_list; …)), so givs of different bivs are never merged. §145a's anchor rule only chooses WHICH member of one class becomes the base — it can never yield two bases. When the target holds two registers whose reaches overlap, the C had two induction variables, and the only move is to split the field groups across them. Second half, to get ONE register carrying k literal offsets instead of k reduced registers: make that group's base a DEST_REG giv whose add_val is a loop-invariant PSEUDO — zb = base + K; before the loop, q = zb + i*3; inside. express_from (loop.c:5418) returns 0 unless GET_CODE (g1->add_val) == CONST_INT, so the k dependent q[j] DEST_ADDR givs cannot combine with one another, each carries benefit 2 - add_cost = 0, all are ruled not worth while (§163f2), and they survive as literal MEM offsets off q's one reduced register.
⚠ COST — PAY IT KNOWINGLY. A pseudo add_val must be materialised, so emit_iv_add_mult emits the giv init as addiu $v1,$base,K ; move $q,$v1 — TWO insns where the target has one addiu. That is the §34 giv-init-fence / §70 giv-init-base residual, and it drifts every index after it. Read §70 before shipping this.
BYTE EVIDENCE. func_8018E8A0 (ov_SC02_011, 211 ins, NEAR 138 — not banked). Single-pointer spelling (p[0..2], .run/wave3/func_8018E8A0/dump/t.c): cc1 -dL prints giv at 288/294/297/303/306/313 combined with giv at 320 → ONE register addu $19,$20,214 (0xD6) reaching 0xCE as -8($19), 210 ins. Two-biv spelling (wk/t.c, p walking 0xCC + q = zb + i*3): addu $22,$19,204 and addu $3,$19,212 ; move $20,$3 — the target's 0xCC/0xD4 pair with the target's exact offsets — 211 ins, the target's count.
DIAGNOSTIC TELL. Count the target's IV REGISTERS before you touch its anchor constant. Two pointers into one record whose displacement ranges OVERLAP (both can address the same offset) ⇒ two bivs; §145a/§135-17 will burn a sweep and never get there (12 pre-loop and ~30 induction variants were spent here before the split). One pointer with a wrong base constant ⇒ §145a. A bare addu $aN,$sM,$zero copy with small literal offsets ⇒ §162n1.
Scope, honestly: one function, NEAR not MATCH. The per-class call site and the express_from CONST_INT gate are read from the pinned tools/reference/gcc-2.7.2/loop.c; the two emissions and the dump are measured.
Symptom lines for the index: "the target keeps two pointers into one struct" · "no store order produces the target's IV base" · "one register where the target has two" · "reordering the stores moved the anchor but not the count".
(NEW; evidence: byte-probed; from func_80192768)
§164-15 — fold CANONICALISES (x − K) − r INTO x − (r + K); ONLY AN UNSIGNED NARROWING CAST BUYS THE UN-REASSOCIATED FORM FOR FREE. Target shape — a constant bias landing ON the loaded field, before the runtime term arrives:
target: lhu $5,6($16) ; addu $5,$5,-48 ; … ; subu $5,$5,$2 ; sh $5,6($16)
yours: … ; subu $2,$2,$3 ; lhu $3,6($16) ; addu $2,$2,48 ; subu $3,$3,$2
THE LAW. fold (fold-const.c:3700-3757, entered through split_tree at :882) rewrites (VAR±CON) ± ARG1 into VAR ± (ARG1 ± CON). (x − K) − r and x − (r + K) are therefore the SAME tree after fold — no operand order, parenthesisation, or constant-sign spelling separates them, so an order sweep is wasted budget. Byte-measured on the pinned cc1, six int-mode spellings ((x−0x30)−r, x−(r+0x30), x−(0x30+r), (x+−0x30)−r, and s32 r = rand()%320; hoisted first): all emit the identical addu $2,$2,48 ; subu (.run/wave3/func_80192768/mt/a.s). A statement boundary on the RUNTIME term alone does not break it.
THE FREE ASYMMETRY — try this first. (x − r) + K does not reassociate: split_tree cannot decompose x − r (no INTEGER_CST operand). Same function, x − rand()%224 + 0x10 matched first try while (x − 0x30) − rand()%320 did not.
THE LEVER — an UNSIGNED narrowing cast on the inner arithmetic. (u16)(*(u16 *)(e+6) − 0x30) − rand()%320. Byte-free when the result is consumed by an sh (no andi, no extra allocno). Banked: src/ov_SC06_018/ov_SC06_018_jr_80191C50.c:3264 (func_80192768, 254 ins).
⚠ DO NOT GENERALISE TO "A HImode CAST" — the first write-up's mechanism is falsified by its own harness. (s16) is exactly as HImode as (u16) and still reassociates. Measured, 8 spellings, pinned cc1:
| cast on the inner subtract | reassociates? | cost |
|---|---|---|
(u16) |
NO — blocks | free |
(s16) |
YES | — |
(u16) over a signed *(s16*) load |
NO | free |
(s16) over a signed *(s16*) load |
YES | — |
(u16) over a plain s32 local |
NO | free |
(unsigned) / 0x30U |
YES | — |
(u8) |
NO | +1 andi 0xff |
(s8) |
NO | +2 sll 24 ; sra 24 |
Write (u16). Never reach for (s16). (split_tree's mode-preserving strip at :892 is real but is not the discriminator here; the signedness of the cast is. Mechanism open — the LEVER is byte-proven, the WHY is not.)
SECOND LEVER (when a cast is unavailable): TWO temps, not one. The sibling func_80192B60, same TU, hit the PLUS half and cracked it with s32 rr = rand(); s32 xx = *(u16*)(iv+6) + 0x30; *(u16*)(iv+6) = xx + rr%0x140; — the constant add in its OWN statement and the call hoisted to its own temp. Hoisting only the call leaves the fold intact (harness f4); binding the whole inner subtract to a u16 local (harness g6) breaks the fold but buys a second callee-saved register and +8 bytes of frame. Banked at :3446-3448.
DIAGNOSTIC TELL. Read where the constant sits relative to the lhu. lhu ; addiu −K ; … ; subu rT (constant ON the field, before the runtime term) ⇒ un-reassociated source, you need a lever. … ; lhu ; addiu +K ; subu (constant folded onto the runtime term, materialised after the jal) ⇒ plain C already gives it, write nothing special.
Corrects §163z's carried claim for func_80192B60 ("only a statement boundary breaks it"): the unsigned narrowing cast is a second and cheaper lever, and a statement boundary on the call alone is not one.
(NEW; evidence: byte-probed; from func_80192B60)
§164-16 — fold's associate: PATH IS SOURCE-FORM INVARIANT FOR +/-: PARENTHESES, OPERAND ORDER AND SAME-MODE CASTS ARE ALL INERT — ONLY A STATEMENT BOUNDARY MOVES THE CONSTANT. (generalises §78 and §162c off | onto the shared code path; answers §162c's open 'tension to resolve'.)
Target shape — a two-term sum with one literal, where the target's addiu rides the term the literal was written next to:
target: lhu $v0,0x6($s0) ; addiu $v0,$v0,0x30 ; … ; jal rand ; … ; addu $v0,$v0,$v1 ; sh
mine : jal rand ; … ; addiu $v0,$v1,0x30 ; lhu … ; addu <- literal rode the OTHER term
THE LAW (read out of the pinned fold-const.c, not inferred). PLUS_EXPR (:3642) falls through to associate: (:3685); MINUS_EXPR (:3804 → :3850), MULT_EXPR (:3897), BIT_IOR_EXPR (:3930), BIT_XOR_EXPR (:3937), BIT_AND_EXPR (:3967) all reach the same label. There split_tree (:882) decomposes either operand into (VAR, CON) and the tree is rebuilt as VAR op (ARG1 op CON) (:3735-3737) — the literal is pushed into the SECOND term, always. split_tree strips every same-mode NOP_EXPR/CONVERT_EXPR (:892-897) and takes the constant from either position (:909, :919, :941). So re-parenthesising, swapping operands, putting the constant first, and wrapping a same-width cast around the sub-sum are ALL no-ops. split_tree fails only on a leaf — a value that arrives as a DECL. A separate statement is the lever, and at this level it is the only one. (§83b-3's (s32)(…) cast barrier is a combine barrier on an ADDRESS, one pass later; it cannot fire here, and the strip loop at :892 says why.)
BYTE EVIDENCE — func_80192B60 (ov_SC06_018, 257 ins, banked; src/ov_SC06_018/ov_SC06_018_jr_80191C50.c:3440-3454). Two sites, x + 0x30 + rand()%0x140 and x - 0x20 - rand()%0x100. TEN spellings — parens moved, operands swapped, constant grouped with either side, constant first (.run/wave3/func_80192B60/variants.py, v_X0-v_X4 / v_Y0-v_Y4) — emitted byte-identical wrong code, 26 mismatched every time. The target's shape appears only with BOTH values in their own statements:
{ s32 rr = rand(); s32 xx = *(u16 *)(iv + 0x06) + 0x30; *(u16 *)(iv + 0x06) = xx + rr % 0x140; }
26 → 4 mismatched. Two ordering constraints inside the block, neither of them fold's:
rand()first.preexpand_calls(expr.c:8672, called from every binary-op arm ofexpand_expr) expands thejalbefore the rest of the statement, and no pass moves a load back across a call — so writingxxfirst puts thelhuabove thejaland fails.- Keep the
%in the FINAL expression.s32 rr = rand() % 0x140;as its own statement regressed to 26 (v_X6,v_Y5). Not a re-fold — both spellings build the same tree. Thediv/mfhiexpand where their statement sits, one statement earlier than thelhu. Read it as statement placement (§42 'statement position drives sched2'), not as fold. - Hoisting
rand()alone is not enough:v_X8keeps(mem + 0x30)inside the expression and still loses. Both temps are load-bearing.
THE DIAGNOSTIC TELL. The target's addiu $rX,$rX,K is adjacent to the load it modifies while your draft emits the same addiu adjacent to the other term (usually just below a jal's return value or fused onto a % result). ⇒ Stop sweeping spellings — the original had a temp. Read backwards: if the target's literal add rides the second term, the source was ONE expression and you must NOT add a temp. Same function, same wave: sites at :3385-3386 are one-expression and match as written.
BOUNDS §78 / §162c, and closes §162c's open question. §78 ('fold never leaves a literal first in an | chain', 7 parenthesisations) and §162c ('the constant MOVES' / 'do NOT hand-fold two constant ors') are this same associate:/split_tree path seen through BIT_IOR_EXPR. §162c flagged the two findings as an unswept tension; the source settles it — operand ORDER is normalised by split_tree, whereas two literals merge on the earlier wins (both-operands-constant) branch. Compatible, and neither generalises to the other. §88c's warning still stands: none of this family applies to ==/!=.
Scope: one function, two sites, ten spellings, plus the source read. Not replicated on a second function — but the source citation is the load-bearing half.
(NEW; evidence: single-instance; from func_8017DF40)
§164-17 — CONTIGUOUS PER-OVERLAY DATA IS AN INDEPENDENT ORACLE ON EVERY ARRAY TYPE AND TRIP COUNT YOU READ OFF THE INSTRUCTION STREAM. (a target-reading cross-check; nothing in the file states it. Same family as §163e — a second oracle that is not derived from the thing it is checking.) Target shape: a family whose per-overlay "shadow" globals sit in one contiguous block.
Read each copy loop's element type and count off the stream as usual (lhu/sh ⇒ u16, stride 2; lbu/sb ⇒ u8, stride 1; lwl/lwr+swl/swr ⇒ §160a align-1 4-byte struct; the slti bound ⇒ the trip count). Then subtract adjacent symbol addresses and check the sizes agree — and check they agree THE SAME WAY in at least two family members. The cross-member check is the load-bearing half: it is what rules out padding or an unreferenced symbol sitting in a gap, which a single overlay cannot distinguish from a real buffer size.
func_8017DF40 (ov_MAIN_012, 187 ins, reach 5): five shadow symbols per overlay, deltas +0x00 / +0x98 / +0x118 / +0x158 / +0x258 ⇒ sizes 0x98, 0x80, 0x40, 0x100 — exactly Blk152 (152 B), u16[64], u8[64], u8[256]. The same five deltas hold in md_SC03_076 / md_SC03_135 / md_SC04_027 / md_SC05_026. The stream-derived types were right first compile (MATCH, zero iterations), banked ×5.
TELL / when to spend it: any time a draft's element type or loop bound is a guess, look at the next symbol before you compile. It costs one subtraction, it is independent of the instruction stream, and a disagreement localises the wrong element type before you pay for a build.
(NEW; evidence: single-instance; from func_8017F9AC)
§164-18 — AT AN INLINE EXPANSION, THE SAME ACTUAL PASSED TO TWO FORMALS EMITS ONE LOAD PLUS A BARE COPY — AND THE BLOCK IS ONE INSTRUCTION SHORTER, NOT LONGER. (the positive-tell counterpart of §163d/§162j1, which both cover a copy being DELETED.)
Target shape — compare two expansions of the same helper:
distinct args: lui/lw $a2,D_X ; lui/lw $a1,D_Y (4 ins, 2 symbols)
same arg twice: lui/lw $a1,D_Z ; addu $a2,$a1,$zero (3 ins, 1 symbol)
THE LAW. cse folds the two identical global loads into one register; the inliner still materialises a separate pseudo per formal (integrate.c's copy_to_mode_reg of the second argument), and that copy survives — §163d's cse copy-deletion cannot fire because both registers are still live into the body, so the defining insn's SET_DEST cannot be rewritten. Write the duplicate argument literally; it is not a redundancy to clean up.
BYTE EVIDENCE. func_8017F9AC block 4 passes D_801C1E4C as both b and a: at .L8017FCEC the target emits one lui/lw $a1,%lo(D_801C1E4C) then addu $a2,$a1,$zero in the bne delay slot — 5 instructions of block head against block 1's 6 for three distinct symbols. Same shape on 10 of func_8017DC1C's 25 blocks.
DIAGNOSTIC TELL / REMAP CAVEAT. A bare addu $aN,$aM,$zero at the head of ONE expansion where its siblings load two symbols ⇒ that call passes the same expression twice. Before mechanically remapping such a family to another overlay, check the sibling's symbol pair at that site: two different globals there costs +1 instruction and family_remap will hand you a silent LENGTH-DRIFT.
Honest scope: shape byte-observed 11× across two functions in one family; no A/B run, and integrate.c is absent from tools/reference/gcc-2.7.2/, so the pass attribution is inferred, not source-checked.
(NEW; evidence: single-instance; from func_80185EF8)
§164-19 — ONCE THE CHAIN IS SPLIT, THE FLAG'S MATERIALISATION SITE PICKS WHICH SPLIT. NESTED ifs AND A goto LADDER ARE NOT INTERCHANGEABLE. (the missing second half of §163f/§21 — those say "separate statements" and stop.)
(a) The value FALLS OUT of the last test ⇒ nested ifs.
hit = 1;
if (x < 0xF2) { if (x >= -0x101) { if (z < -0x106) { hit = z < -0x4AA; } } }
emits li $a0,1 in the FIRST branch's delay slot, three branches to one common nest-end label, and slti $a0,$v1,-0x4AA writing the flag register directly — no separate materialisation of the last result.
(b) The true path has a BODY and the flag is 0/1 ⇒ goto ladder.
if (A) goto hit; if (B) goto hit; if (C) goto hit; if (D) goto hit;
f = 0; goto done;
hit: <body>; f = 1;
done:
jump.c's invert-a-cond-jump-that-skips-an-uncond-jump (§48-D's ratchet, used here as a LEVER instead of a trap) flips the last test, giving beqz $v0,done with addu $a1,$zero,$zero (f = 0) in the LAST branch's delay slot and fall-through into the body.
THE LAW. With the same N comparisons, the discriminator is where the flag constant is materialised:
flagreg = 1in the first branch's delay slot + aslt*writingflagregas the last test ⇒ (a).flagreg = 0in the last branch's delay slot +flagreg = 1scheduled inside the true-body ⇒ (b).
Writing (a) where the target wants (b) puts the body on the wrong side of the fall-through and costs the whole block layout, not one instruction. Do NOT reach for §2-T4's polarity invert first — the polarity here is a consequence of the shape.
Byte evidence: func_80185EF8 (ov_SC03_014, 304 ins, MATCH, banked commit:1671) uses both, 30 lines apart, in one gated function: (a) at src/ov_SC03_014/ov_SC03_014_jr_801848E4.c:2962-2968, (b) at :2996-3015. Neither position could be written in the other shape.
Scope (honest): n = 1 per shape. The pair is banked and each was necessary in situ, but the two-way oracle is a prediction over two instances, not a sweep. Re-measure on the next split-chain target before treating the tell as exhaustive.
DIAGNOSTIC TELL. Read the delay slots of the first and last branch of the comparison run. li reg,1 at the top ⇒ nested ifs. move reg,$zero at the bottom ⇒ goto ladder. If the flag appears in neither slot, you are not in this family — go back to §163f and check you are not still emitting a folded range test.
(NEW; evidence: single-instance; from func_80192768)
§164-20 — AN addu rD,rS,$zero IN A BRANCH DELAY SLOT AT A NULL-GUARD ⇒ THE GUARD TESTED THE EXPRESSION AND THE BODY RE-READ IT. Target shape:
lw $v0, 0xCC($aN)
beqz $v0, .Lskip
addu $s0, $v0, $zero <- the copy IS the second read, cse'd down from a load
THE LAW. e = *(s32*)(p+K); if (e != 0) { … e … } loads straight into the callee-saved register and is 1 instruction SHORT. Write the guard on the EXPRESSION and re-read it inside the arm — if (*(s32*)(p+K) != 0) { e = *(s32*)(p+K); … } — and cse rewrites the second load as a reg-reg copy off the branch's pseudo, which dbr then fills the beqz delay slot with. Two banked instances, one TU, and in BOTH the two spellings coexist in one body at different sites — so this is a per-site, target-driven choice, never a whole-function style: func_80192768 (src/ov_SC06_018/ov_SC06_018_jr_80191C50.c:3273-3274 re-read form vs :3221 plain form) and func_80192B60 (:3463-3464 vs :3399, where it was "+1 ins — the entire length drift").
DIAGNOSTIC TELL — three neighbours share this symptom; the delay slot's OWNER discriminates.
- copy missing from a branch delay slot at a guard ⇒ this entry — change the guard's operand.
- copy missing from a
jaldelay slot ⇒ §43 — a narrow prototyped (plain ANSIs16) param. - a copy off a PARAMETER that vanished entirely ⇒ the cse
make_regs_eqventry (index: "youre = param_1copy VANISHED") — reuse the same variable in a later block.
And read whether the target's second read is a LOAD or a COPY. lw/lbu a second time ⇒ §160d (keep the memory re-read; the load survives). addu …,$zero ⇒ here (the re-read cse's down to a copy). §160d's "do not assume the source is uniform" is the same discipline; this is its copy-valued half.
Provenance (R14): mechanism is inferred, not source-cited; two banked instances but no isolated reproducer. The source note's pairing with "the ov_SC03_099 func_8017EBC0 note, which needed the OPPOSITE" is unsupported — no such note exists in the corpus; func_8017EBC0 is only an unmatched stub. Do not carry it.
(SHARPENS — sharpens §30 (#1, store-vs-load is a /s aliasing flag), §30a-1 (/s grant refinement, expr.c:4577/4888), §37 (the /s-DEP LATTICE, load+store dual, func_80176D94), §135-2 (ARRAY_REF vs INDIRECT_REF changes ALIAS; evidence: byte-probed; from func_8017CA18)
§164-21 — A bare trailing store to a fixed-symbol global (D_801151D0 = ot;) floats up past a preceding /s+varying array-of-struc
§16Xy — SHARPENS (sharpens §136d-3, §37 /s-DEP LATTICE, §135-2, §136-13, §162q)
The /s drop clause is in ALL THREE dependence predicates, so the FIXED-ADDRESS STORE is what floats (P30 S48, func_8017CA18, ov_MAIN_012)
Target shape. A GPU tail: an addPrim read-modify-write through a global array-of-struct, then a bare global store — in THAT order, store LAST:
lw $v0,8($v1) <- D_800AE7BC[i].ot[2] (/s, VARYING)
and / or / sw <- the RMW finish, LAST in the target
addiu $v0,$s0,0x28
lui $at,%hi(D_801151D0) ; sw $v0,%lo(D_801151D0)($at)
Every plain-C spelling emits the same 7 instructions with the sw %lo(D_801151D0) hoisted to sit
immediately after the addiu that computes it, AHEAD of the RMW's lw/and/or/sw.
THE LAW. The /s drop clause — "a MEM_IN_STRUCT reference at a non-QImode varying address can
never conflict with a non-MEM_IN_STRUCT reference at a fixed address" — is duplicated verbatim in
all three predicates: true_dependence (tools/reference/gcc-2.7.2/sched.c:831-841),
anti_dependence (:855-863), output_dependence (:873-881). §136d-3 / §135-2 / §162q read it
only in the read-after-write direction, where the LOAD moves and the fixed store is the anchor.
Because anti- and output-dependence carry it too, the non-/s fixed-address STORE is equally free
to float UP past a /s+varying load or store. When the anchor is an array-of-struct global RMW
there is no statement-order lever to reach for: the edge is simply absent from the graph, so no
amount of moving the store's source line down can hold it. Same lever as §136d-3, applied because
the STORE is the insn that moved:
D_801151D0 = ot; /* non-/s + FIXED -> floats free */
((struct { s32 f; } *)&D_801151D0)->f = ot; /* COMPONENT_REF -> /s (expr.c:4888, uncond.)
both sides /s => clause arm 1 fails on
!MEM_IN_STRUCT_P(x), arm 2 on !varies(x)
=> edge restored, store stays last */
The address stays a bare symbol_ref, so rtx_addr_varies_p is still 0 and the emitted
lui $at / sw %lo(...)($at) pair is byte-identical.
Byte evidence. func_8017CA18 (ov_MAIN_012, 108 ins, banked on the whole-binary gate;
lever at src/ov_MAIN_012/ov_MAIN_012_jr_8017CA18.c:3795). The A/B was run on the RMW side FIRST
and every arm reproduced the wrong order identically — PTag-bitfield store vs raw pointer cast,
block-scoped temp vs inline expression, volatile, register pins. One edit on the STORE side
closed it. Source corroboration re-read this session at sched.c:855 and :873.
THE DIAGNOSTIC TELL. A trailing sw $vN,%lo(D_xxxxxxxx)($at) that your build emits directly
after the addiu that computes its value while the target emits it last, with a
lw / and / or / sw RMW through a global array-of-struct sitting in between. Do NOT read it as a
sched1 priority problem and do NOT reach for a barrier — grant /s to the store. This is the
reciprocal of §136d-3: same clause, same one-line lever, the other insn moving.
Scope, honestly: one function. The anti/output generalisation is read out of the pinned source, not
measured on each predicate separately. One axis was never tried: denying /s on the RMW side
with a cast-wrapped PLUS (*(u32 *)((s32)p + 8), §30a-1) would also restore the edge — §136d-3's
refuted *(a[i] + j) reshape is NOT that spelling, since a bare-typed PLUS still earns /s. Prefer
the store-side grant; it is the half that is now byte-proven three times.
(SHARPENS — sharpens §136d-1 (RC-12, 'Do NOT pin the copy's source or dest'), §72 (a pin is a preference, not a reservation), §42c-6 (durable lever 6, func_80168828), §163d; evidence: byte-probed; from func_8017D1E0)
§164-22 — A ONE-SIDED PIN CANNOT RESURRECT A COPY cse HAS ALREADY DELETED — on EITHER end. (sharpens §136d-1, whose warning predicts the wrong failure mode, and bounds §42c-6.) Target shape — a value computed into a scratch reg, copied to a callee-saved reg, the copy sitting in a branch delay slot:
lw $a0,0x30($s0) ; addiu $a0,$a0,0x6000 ; addu $v0,$v0,$a0
… bgez/blez … ; move $s1,$a0 <- the copy, IN the delay slot
LAW. When §163d has fired (cse rewrote the DEFINING insn's SET_DEST and the copy became a dead store), a register __asm__ pin on either end is not a lever, because there is no copy insn left for the allocator to place. A source pin is a total no-op — the build is BIT-IDENTICAL to the unpinned one and the pinned value does not even occupy the named register. A dest pin IS honoured — the value lands in the named callee-saved reg — but it arrives there by retargeting the definition, so the copy stays deleted and the delay slot stays nop. Neither costs instructions; both cost nothing and buy nothing, which is why the symptom reads as "the pin was ignored". The lever remains §136d-1/RC-12 (register s32 zr __asm__("$0"); t = tmp + zr;), which is pin-free and delay-slot-safe (§42c-4).
Byte evidence (func_8017D1E0, ov_SC03_014 _jr_8017AE2C, 78 ins, banked; pinned triple, masked_diff vs the gated body). Baseline = the banked build, 0 diffs. t = tmp + zr, no pins: 0 diffs, MATCH. Plain t = tmp;: 4 diffs (lw v1/addiu s1,v1/addu v0,v0,s1, slot = nop). Source pin register s32 tmp __asm__("$4") alone: 4 diffs, word-for-word identical to the plain build. Dest pin register s32 t __asm__("$17") alone: 4 diffs (lw s1,0x30($s0); addiu s1,s1,0x6000), slot still nop.
BOUNDS §42c-6. That lever's source-only $4 pin DOES yield addu $s1,$a0,$zero (func_80168828) — because its source is the ABI-fixed INCOMING argument, which has no defining insn for cse to retarget. Discriminator: does the pinned source have a def insn inside the function? If yes, a source pin is dead weight; if it is the incoming $a0, §42c-6 applies.
DIAGNOSTIC TELL. You pinned one end of a copy and the diff count did not move AT ALL (not by ±2 — by zero). Do not escalate to a second pin: diff the pinned and unpinned objects. Bit-identical ⇒ cse deleted the copy upstream; go to §163d's triage and reach for RC-12.
(SHARPENS — sharpens §28a ('Load coalescing' bullet), §3-T3 (pick the load width that matches the asm), §162k1 (in-function width asymmetry, by analogy); evidence: byte-probed; from func_8017D1E0)
§164-23 — gcc-2.7.2 DOES NOT COALESCE AN ADJACENT FIELD PAIR; a single lw over two u16 fields is the SOURCE's wide read. (REFUTES §28a's imported 'Load coalescing' bullet, which claims the opposite direction and had never been probed on our cc1.) Target shape — the same two offsets read NARROW earlier in the function and WIDE at the guard:
sh $v0,0x10($s0) … sh $v0,0x12($s0) <- ordinary s16 field stores
lw $v0,0x10($s0) ; beqz $v0, epilogue <- ONE 32-bit read covering 0x10+0x12
LAW. if (t->a || t->b) / if (t->a == 0 && t->b == 0) over two adjacent u16 fields does NOT fold into a word load — gcc-2.7.2 -O2 emits lhu; bnez; lhu; bnez, two loads and two branches. A single lw at the pair's base means the source read 32 bits there: write if (*(s32*)((s32)p + 0x10) == 0). The choice is PER SITE — the same struct offsets keep their narrow s16 spelling everywhere else in the same function (cf. §162k1's asymmetry law for local width).
Byte evidence (func_8017D1E0, ov_SC03_014 _jr_8017AE2C, 78 ins, banked; pinned triple, masked_diff vs the gated body). Wide read: 78 ins, 0 diffs. *(u16*)(a0+0x10) == 0 && *(u16*)(a0+0x12) == 0: 82 ins, 14 diffs. !(*(u16*)(a0+0x10) || *(u16*)(a0+0x12)): 82 ins, 14 diffs. Both narrow forms emit lhu $v0,0x10($s0); bnez; lhu $v0,0x12($s0); nop; bnez; nop where the target has lw $v0,0x10($s0); beqz — +4 ins and a displaced epilogue.
DIAGNOSTIC TELL. LENGTH-DRIFT +4 with a lhu/bnez pair in yours against a lone lw/beqz in the target, at an offset your field map says is two 16-bit members. Widen the READ at that one site; do not widen the field declarations, and do not expect the || spelling to fold.
(SHARPENS — sharpens §78 (L6193-6205, fold never leaves a literal first in an | chain), §162c (L11026-11035, THE | CHAIN, TWO SEPARATE RULES — 'the constant MOVES … write the MIRROR image'), §17 (L1395, 'explicit te; evidence: byte-probed; from func_8017D7C0)
§164-24 — gcc-2.7.2's fold associate block moves an integer addend to the OPPOSITE operand of a +/- node, exactly once and botto
§16x — THE PLUS/MINUS CONSTANT MIRROR: fold moves an integer addend to the OTHER operand, ONCE, so write the constant on the side the target does NOT put it. (sharpens §78 and §162c, which state the mirror for | CHAINS ONLY — and §78's headline is that all seven | parenthesisations COLLAPSE to one codegen. For +/- they do NOT: three spellings of one line give three codegens. Neither section names the gate that makes 'make it a variable' work.)
Target shape — the constant applied to the LOADED accumulator:
lhu $v1,0x0($s0) <- the memory accumulator `a`
sll $v0,$v0,0x4 <- the variable term `J`
addiu $v1,$v1,-0x200 <- K lands on the ACCUMULATOR, not on J
addu $v1,$v1,$v0
THE LAW (read out of the pinned fold-const.c, not inferred). The associate: block (:3685) runs ONCE, bottom-up, and splits the FIRST splittable operand — arg0 (:3703) is tried before arg1 (:3759):
(VAR ± CON) ± ARG1 -> VAR ± (ARG1 ± CON) [arg0 split, :3703]
ARG0 ± (CON ± VAR) -> (ARG0 ± CON) ± VAR [arg1 split, :3759]
So the constant ends up on the operand OPPOSITE the one you wrote it beside, and the rewrite is not iterated to a fixpoint. split_tree (:882-950) fires only when the node is PLUS/MINUS and an operand is INTEGER_CST or TREE_CONSTANT; a << term, a call, or a plain VAR_DECL is opaque. A MINUS whose constant is FIRST comes back through the varsign == -1 code flip (:3766) to the same tree as the PLUS form.
Five of the 16 measured spellings of one line (a = an s16 struct field, J = ((rand()&0x3f)<<4), K = 0x200):
a = (a - K) + J -> folds to a + (J - K) K rides the jitter (addiu after the sll) 9 ins off
a = (a + J) - K -> neither operand splits; K applied to the SUM 6 ins off
a += J - K -> folds to (a - K) + J K on the accumulator MATCH
a -= K - J -> same tree via varsign==-1 MATCH
r = K; a = a - r + J -> `r` is a VAR_DECL: split_tree refuses; cse re-folds K into the addiu MATCH
TELL: a +/- statement off by 2-3 instructions PER SITE with the constant attached to the wrong operand (an addiu after the sll where the target has it after the lhu, or vice versa). Do NOT sweep parenthesisations — MIRROR the constant onto the other term. The compound op= is merely the shortest spelling of the mirror; it is not special (see the UNSOUND note: neither compound-ness nor a memory lvalue is load-bearing). Reading asm back to source: an addiu on the loaded accumulator means the ORIGINAL wrote that constant beside the OTHER term.
When you use the variable-K form, SPEND AN EXISTING VARIABLE (§78's two-effects rule): in this same sweep the fresh-temp forms (t = a - K; a = t + J, and its s16 twin) get the tree right and still fail on allocation, while reusing the already-live r matches.
Evidence: func_8017D7C0 (ov_SC02_041, 181 ins, zero-crack family exemplar ×4), 16-variant sweep in .run/wave3/func_8017D7C0/try.py; banked whole-binary as rot.vx += ((rand() & 0x3f) << 4) - 0x200; at src/ov_SC02_041/ov_SC02_041_jr_8017BEBC.c:3404 (commit commit:1652). Supersedes the unvetted §163z claim on func_80192B60 that PLUS/MINUS re-association is source-form invariant.
(SHARPENS — sharpens §45 Lever D (L3309) — goto-shared-return isolates the exit li: a BB boundary stops the exit li $v0,K hoisting into a last-element load-delay slot ('use when a return-constant materializes one inst; evidence: byte-probed; from func_8017DD28)
§164-25 — OCCUPY $v0 THROUGH THE LAST STORE TO KEEP THE RETURN-VALUE INSN OUT OF A LOAD-DELAY SLOT. (a second lever for §45-Lever-D's class; the exact inverse of §136-15; distinct from Residual-B's $2 pin, which pins the return value ITSELF and pushes it earlier.)
Target shape — the LAST of several identical RMWs, and the only one whose load-delay slot stays a real nop:
lw $v1, %lo(D_800AE7BC)($at)
nop <- stays a nop
lw $v0, 0x8($v1) <- loaded word lands in $v0 …
and $s0, $s3, $s0
and $v0, $v0, $s1
or $v0, $v0, $s0 <- … and is REUSED for the or-result
sw $v0, 0x8($v1)
addiu $v0, $s3, 0x8 <- return value, computed only AFTER the store
THE LAW. return p + K; is one independent addiu $v0,… waiting on nothing, so it is the cheapest thing available to fill an earlier load-delay slot; if $v0 is FREE there it goes there and you are exactly one instruction short. Keeping the last memory-RMW's loaded word IN $v0 and reusing it for the or-result makes $v0 live across that slot, and the return insn cannot be placed before the sw:
register u32 v __asm__("$2");
v = op[2]; v = (v & 0xFF000000) | ((u32)out & 0xFFFFFF); op[2] = v;
Byte evidence. func_8017DD28 (ov_MAIN_012, 124 ins, banked whole-binary commit:1629, src/ov_MAIN_012/ov_MAIN_012_jr_8017CF3C.c:4343-4352): 16 mismatched → MATCH. The plain bitfield spelling put the loaded word in $v1/$a0, the nop vanished and the epilogue shifted −1. Target read off the still-unbanked family twin asm/md_SC03_076/nonmatchings/md_SC03_076/func_801F1520.s:114-121. The discriminator lives in the same function: its first two RMWs (:100-106, :83-89) have their load-delay slots filled by ordinary work (lw $v1,0x0($s3), and $v1,$s3,$s0) and need no lever — only the LAST one, where the return insn is the sole remaining candidate, does. Apply it to that RMW and no other.
THE DIAGNOSTIC TELL. LENGTH-DRIFT −1; a missing nop immediately after the last lw $rN,%lo(SYM)($at); the return-value addiu $v0,… sitting in that slot instead of after the final store; every other slot in the function filled exactly as the target has it.
⚠ Reach for the pin LAST, and expect to pay (§162p rung ladder). A register __asm__ pin forfeits the family: this exemplar has reach 5 and its h_seq sweep banked 0/4 (.run/jr48/w2prop_func_8017DD28.log) even though every typedef was correctly block-scoped per §94 — func_801F1520 is still INCLUDE_ASM. Cheaper levers for this same class were not tried here and should be exhausted first: §45-Lever-D's BB isolation (goto ret;, the OPPOSITE sign of this lever), a §17 zero-byte input barrier on v after the store, or RC-12's $0-add opaque copy (§136d-1). Read "a pin is the only thing that works" as untested-alternative, not as proven necessity.
(SHARPENS — sharpens §162e1 — INDEX UNIFORMLY WITH THE LOOP VARIABLE, EVEN WHERE THE INDEX IS PROVABLY CONSTANT (L11142), §162e2 — PREHEADER ORDER IS BODY ORDER (L11162), §48-B / §48-B4 (address folding, L11490-11524), §1; evidence: byte-probed; from func_8017DEFC)
§164-26 — THE MOVABLE DIAL IS THE OUTERMOST SUBSCRIPT, NOT 'CONSTANT vs VARIABLE INDEX'. (sharpens §162e1; supplies the codegen reason for §135-7#18's canonical s32 D_x[][2] decl.) Target shape — a symbol-based table read INSIDE a loop with NO preheader hoist and the gas macro left inline:
.Lbody: lui $at,%hi(D_8010F468) ; addu $at,$at,$v1 ; lw $s1,%lo(D_8010F468)($at)
THE LAW. expand_expr's case ARRAY_REF (expr.c:4589) branches on the index of the outermost ref only. Non-constant ⇒ it rebuilds the tree as *(&array + index*size) (expr.c:4620-4679) and that ADDR_EXPR becomes a real la base pseudo — scan_loop records it, move_movables hoists it, global-alloc pays a callee-saved register for it. Constant ⇒ control falls through the /* Treat array-ref with constant index as a component-ref */ comment (expr.c:4745) into the COMPONENT_REF arm, get_inner_reference walks down through the INNER variable-index ARRAY_REF folding b*stride into offset, and expr.c:4809-4817 builds the address as (plus (symbol_ref) (reg)) inside the MEM. No address insn is ever emitted, so there is nothing for loop.c to move. §162e1 gives you 'variable index ⇒ movable' and 'literal index ⇒ no movable, at zero byte cost'; this is the third position — variable OUTER subscript, constant FINAL subscript ⇒ no movable while the index stays variable. It is the only spelling that reaches it.
Byte evidence. func_8017DEFC (ov_SC01_005, 124 ins, match_one MATCH, .run/wave2/func_8017DEFC.c:85,138). Target keeps %hi/%lo inline in the loop. *(char**)(D_8010F468 + b*8) and extern char *D_8010F468[]; D_8010F468[b*2] both emit la+addu+mem(reg), loop.c hoists the la, and the function grows a SEVENTH callee-saved register: frame 0x34 vs the target's 0x30. extern char *D_8010F468[][2]; D_8010F468[b][0] ⇒ the address folds into the MEM and the frame returns to 0x30.
THE DIAGNOSTIC TELL. The target shows %hi/%lo of a global INLINE inside a loop, your draft hoists a base into $sN, and your frame is one callee-saved register heavy. Do not reach for §148-A's threshold or §162e3's candidate filter — those explain an invariant that REFUSES to hoist. This is one that hoists and should not. Re-spell the access so the LAST subscript is a literal.
⚠ Two side effects, both load-bearing. (1) The constant-index path also runs MEM_IN_STRUCT_P (op0) = 1 (expr.c:4855), so the 2-D spelling grants /s for free — read §30 / §162q1 before assuming a schedule change elsewhere was unrelated. (2) The 2-D decl must match the canonical DEFINE_func_* form in src/shared/engine_core.h (§135-7#18) or you have swapped a movable for a §161c conflicting types.
(SHARPENS — sharpens §158 'Also byte-settled on the way' guard-shape bullet — 'A rotated for emits blez' (L10811), §L1 — a loop's break must NOT land on the loop's own fall-through label / duplicate_loop_exit_test; evidence: byte-probed; from func_8017DEFC)
§164-27 — A TAUTOLOGICAL ENTRY BOUND DELETES duplicate_loop_exit_test's COPY: how to keep for shape with ONE guard. (sharpens §158's guard-shape bullet and §L1.) Target shape — a counted loop whose ONLY guard is the source's own null test, with the induction init stolen into its slot:
beqz $a0,.Lskip
addu $s0,$zero,$zero <- `i = 0` in the guard's delay slot
.Lloop: … <- NO second `blez`
THE LAW. stmt.c expand_end_loop rotates a top-tested loop so the test sits at the bottom, leaving NOTE_INSN_LOOP_BEG followed by a bare j to it; jump.c:599 then fires duplicate_loop_exit_test (jump.c:2131), which copies that test back in front of the loop. That copy is the second guard — the blez $sN §158's bullet reports as the cost of the for form. cse runs after this jump pass, so if the copy's operands are constants at the entry it folds and both it and its dead li vanish. Reach that by initialising the bound to a tautology and assigning the real bound INSIDE the body:
count = 1; /* entry test becomes 0 < 1 -> folded away */
for (; i < count; i++) { …; count = n; … }
§158's bullet concludes 'a rotated for emits blez, so write do/while'. That is a rule of thumb, not a law — the blez is only the un-foldable case.
Byte evidence. func_8017DEFC (ov_SC01_005, 124 ins, MATCH; .run/wave2/func_8017DEFC.c:133-134). The naive if (n != 0) { for (i = 0; i < n; i++) … } emits BOTH guards: +1 ins, visible as a blez $sN immediately after the beqz $a0. The count = 1 form emits only the beqz, with i = 0 in its slot, and matches.
Preconditions (both load-bearing). The bound must be a LOCAL whose entry value is a constant reaching def — a bound loaded from memory or taken from a parameter cannot fold, and you are back to §158's do/while. And the in-body count = n; is simultaneously §164b's rung-1 movable (it is what produces the preheader addu $sD,$a0,$zero), so the two edits are one edit — re-gate together.
THE DIAGNOSTIC TELL. LENGTH-DRIFT +1 and the extra instruction is a blez/bgez sitting directly under the guard branch you DID write. That is duplicate_loop_exit_test's copy, not a mis-written condition. Either fold it (this entry) or drop to the do/while spelling (§158) — but read §164d first, because the two forms are not interchangeable in the delay slots.
(SHARPENS — sharpens §17 STEERABLE idioms — 'for-loop vs do-while controls delay-slot scheduling' (L1412-1415, func_801399A8), §158 guard-shape bullet (L10811), §162f1 — A NON-VOID RETURN TYPE IS OBSERVABLE IN DELAY SLOTS; evidence: byte-probed; from func_8017DEFC)
§164-28 — for-vs-do-while HAS NO FIXED DIRECTION, AND IT REACHES INNER BRANCHES. (amends §17's 'for-loop vs do-while controls delay-slot scheduling' bullet, which states one direction from one exemplar.)
THE AMENDMENT. §17 (L1412, func_801399A8, do-while 7 → for-loop 2 → MATCH) says the for form is the one that gets the useful fill and the do-while dumps an increment into the slot. A second byte-measured exemplar runs it exactly backwards: func_8017DEFC (ov_SC01_005, 124 ins, MATCH) needed the for, and its do-while variant was exactly 1 instruction SHORT because reorg filled an inner if's beqz from the fall-through with the call's first argument (addu $a0,$s2,$zero); the for form instead duplicates addiu $v0,$s0,0x1 into that slot and pushes the join label past the original copy — which is the target. Read §17's bullet as 'the loop form is observable in the delay slots', never as 'write it as a for'.
Second generalisation: §17's instance is the LOOP TEST's own slot; this one is an inner conditional branch inside the body. The reach is the whole loop, not the back-edge.
THE DIAGNOSTIC TELL. LENGTH-DRIFT ±1 with the diff anchored on a branch delay slot anywhere inside a counted loop, everything else byte-identical. A/B both loop forms before anything else — it is a two-character edit and it is the cheapest probe in the file. Do NOT reach for §162f1's return-type flip, continue, ++i-in-the-condition, a goto respelling, or an argument reorder first: all five were ablated on func_8017DEFC and none reproduced the fill; only do/while ⇄ for did.
Evidence scope (honest): two functions, opposite directions, no mechanism. The reorg.c thread-selection reason is NOT established — treat the form as a dial to try, not a predictor.
(SHARPENS — sharpens §162b1 — TEMPORARY SCOPE IS A combine LEVER, NOT ONLY AN ALLOCNO LEVER (L11069), §30 #1 / §30a-1 / §135-2 / §136d-3 / §162q1 — the /s MEMORY-dependence lever, §158 — 'an insn ANTI-dependent on the; evidence: byte-probed; from func_8017DEFC)
§164-29 — TEMPORARY SCOPE IS A SCHEDULING LEVER TOO: reuse one pseudo to mint a register WAR edge. (§162b1's third pass; the register-side twin of §30/§162q1's /s memory lever.) Target shape — a store and an unrelated load in one block, where the target keeps the load BELOW the store and your draft hoists it into the preceding load-delay slot:
sh $v1,%lo(D_801BB730)($at) <- target order
lh $v1,%lo(D_801CD878)($at)
THE LAW. sched_analyze_1 (sched.c:1714-1715) emits, for every SET of a pseudo, for (u = reg_last_uses[regno]; u; u = XEXP (u, 1)) add_dependence (insn, XEXP (u, 0), REG_DEP_ANTI);. So re-setting a variable that an earlier insn READ is a hard anti-dependence, and the setter can never be scheduled above the reader. Reusing ONE local for two values that the target keeps in order is therefore a zero-instruction scheduling fence you spell in C:
s32 t;
t = D_801BB730[k]; D_801BB73C[0] = t; /* the sh READS t */
t = D_801CD878; …use t… /* the lh RE-SETS t */
§162b1 documents scope→allocno class and scope→combine's nonzero_bits union. This is scope→sched, and it changes the ORDER rather than the count.
Byte evidence. func_8017DEFC (ov_SC01_005, 124 ins, MATCH). Split temps let sched hoist lh D_801CD878 into the preceding lbu's load-delay slot, losing the target's nop; one shared t pins it below the store and matches. Independently reached on func_8017C3BC (ov_MAIN_012, 407 ins, MATCH — see §163d's function) where a shared-tail constant written into its own local was hoisted between a li 9 and its dependent sh, and writing it into the SAME variable (t = 9; D_8011512C = t; t = 0xFF;) pinned it. Two functions, two shapes, same edge.
⚠ Two bounds. (1) sched1 is per-basic-block. If the store and the load are separated by a label or a branch, the edge does not exist and this buys nothing. (2) You cannot buy the sched barrier alone. Sharing the temp fires §162b1's other two consequences at the same time — the merged live range changes the allocno class (both exemplars show a register move: $v1→$a0 here) and the multi-set pseudo makes reg_nonzero_bits unprovable, so any (u16)/(u8) truncation on it survives as a real andi. Ablate both ways (§150); neither exemplar isolated the scheduling axis from the allocno axis.
THE DIAGNOSTIC TELL. Two adjacent memory ops swapped relative to the target, everything else byte-identical, and the values are register-independent — no aliasing edge to grant. §30/§162q1's /s grant is for the MEMORY edge; when both sides already have the aliasing they need, look at whether the two values share a C variable in the target's reading and yours split them.
(SHARPENS — sharpens §43, §99, §73, §29, §161c, §163b, cookbook-index L29; evidence: byte-probed; from func_8017DF40)
Addendum (P31 S64 t5j-t5m, func_8017F10C): §164-29's ⚠ bound (2) — "you cannot buy the sched barrier alone; sharing the temp fires §162b1's other two consequences at the same time … both exemplars show a register move ($v1→$a0)" — has a third instance where the merged-allocno pair is the entire residual, and where the reuse is self-reuse: a divide whose own dividend is its destination. Target shape — two unrelated constant divisions in one block, each a lui/ori magic + mult + mfhi/sll/subu chain. Written the way m2c and every drafter defaults to, with the quotient in a brand-new local (s32 av = (d * 0x7F) / 0x900;), d dies at the divide, global-alloc hands it the first free register ($a0) instead of the target's $a1, and the two mult/mfhi chains come out in the opposite order — because av dies inside the block while the other chain's bne keeps it on the critical path. Re-spelling it d = (d * 0x7F) / 0x900; extends one allocno across the $a0-clobbering tail, which forces $a1 end-to-end and lifts the /0x900 chain's priority, in one token. THE TELL — read the residual's SHAPE, not its size: one long contiguous run from the first magic-multiply through the end of the block (here idx 56–73 of 91), never a few scattered mismatches; match_one presents it as a structural break and it is not one. Confirm it by reading which register the quotient lands in: the target holding the quotient in the DIVIDEND's own register (subu $a1,$v1,$v0 / sltiu $v0,$a1,0x901 / sll $v0,$a1,7, and the divided value still in $a1 two blocks later) says the source reassigned the dividend — the third selector alongside §167-18's "a register a nearby dead temp just vacated" and §176-B1-addendum's "an already-live different local". The barrier is a decoy, and the ablation says so. An __asm__("" : "=r"(av) : "0"(av)) launder on the fresh local buys the schedule alone and leaves d on $a0 (closeness 4, [53,54,58,59]) — which is what makes the true defect invisible; strip it and the honest closeness is 18. The same launder retargeted onto the reused d is byte-neutral (still MATCH), so variable identity, not the barrier, is the dial. Byte evidence (func_8017F10C, ov_SC06_010, 91 ins, single-axis A/B on the pinned triple): w5 fresh av, no barrier → near 18 91 [56…73]; w8, the only change being s32 av = → d = → match 0 91 []; w9 same plus the launder on d → match 0 91 []. Banked at src/ov_SC06_010/ov_SC06_010_jr_8017A4AC.c:4952. Honest scope: n=1, correlation ablated three ways with a barrier control; no .lreg/.greg dumped, so the order half is attributed but not proven — §164-42's LO_REG over-subscription (two mult in one block, tie broken by allocno number = source-expansion order) is the mechanism to dump for first if this recurs.
§164-30 — THE NARROW-PARAM CONFLICT HAS A THIRD, T0 ESCAPE — CAST AT THE USE; AND sll $aN,$aN,16 IN PLACE DOES NOT PROVE AN s16 DECLARATION. (sharpens §43's triage tell; extends §73's PARAMS row from pointer TYPE to integer WIDTH; bounds §29/§99's "K&R is the fix".) Target shape — a parameter whose only 16-bit use is a truthiness branch:
sll $a0,$a0,0x10 ; bnez $a0,<else> ; nop <- NO `sra`, and $a0 is never read again
§43 states flatly that "the (s16)param_of_s32 cast form CANNOT reproduce this — it extends into fresh v0/v1 temps". That absolute is byte-refuted when the truncated value has ONE use and the arg register is dead after it. With the host TU's own extern void func_8017DF40(s32); (ov_MAIN_012_jr_8017CF3C.c:3695) kept verbatim, the definition
void func_8017DF40(s32 arg0) { if ((s16)arg0 == 0) { …restore… } else { …save… } }
emits the target's sll $a0,$a0,0x10 ; bnez $a0 in place — no temp, no sra. §43's tell is complete only in its FULL conjunctive form (in-place sll AND the raw $aN copied elsewhere first); the raw-copy half is what actually forces K&R, because it is the raw arg outliving its own extension that pins the extension into a fresh temp.
LAW: for a narrow-INTEGER by-value param, cast-at-use is a legitimate §73 T0 escape, not a pointer-only one. §73's PARAMS row is byte-neutral for pointers by construction (all 32-bit); for integers it is byte-neutral only when the truncation's placement is unconstrained — one use, dying immediately, no live range across a jal. Try it BEFORE the §43/§99 K&R rewrite: it needs no //@EDIT, no canon-sig edit and no sibling coordination (the four md_* sibling TUs here carry no prototype at all, so the same body banks unchanged in both decl environments).
Byte evidence (func_8017DF40, ov_MAIN_012, 187 ins, reach 5). Both forms measured: s16 arg0 MATCHes standalone under match_one but cc1 rejects it in the real TU (conflicting types for 'func_8017DF40'); s32 + (s16) cast MATCHes 187/187 and adds zero diagnostics to the TU (whole-TU oracle — warning set byte-identical to the unmodified-TU baseline). Banked whole-binary ×5 (commit:1652); the shipped ov_MAIN_012_jr_8017CF3C.o still disassembles to sll a0,a0,0x10 ; bnez a0.
DIAGNOSTIC TELL: sll $aN,$aN,16 with no following sra and no later read of $aN ⇒ the source truncated to 16 bits at exactly one use site, and the bytes cannot distinguish s16 p from s32 p + (s16)p — so take whatever width the fleet already declares. Add the sra, a raw $aN copy that outlives the extension, or a use after a jal, and you are back in §43/§99 K&R territory.
(SHARPENS — sharpens §162a3, §163b; evidence: byte-probed; from func_8017EB44)
§164-31 — §162a3's discriminator is not discriminating: switch ((s16)x) with cases 1..10 and `s16 sel = (s16)x - 1; switch (sel)
AMENDS §162a3 — THE sll/sra BETWEEN THE BIAS SUBTRACT AND THE sltiu IS A WIDTH TELL, NOT A SOURCE-DECREMENT TELL. (refutes §162a3's C-side half; agrees with §163b, which had the mechanism but never retracted §162a3.)
Target shape:
addiu $a1,$a1,-1 ; sll $a1,$a1,16 ; sra $a1,$a1,16 ; sltiu $v0,$a1,0xA
THE LAW. expand_end_case folds index_expr = MINUS_EXPR(index_type, expr, minval) in the switch operand's OWN type, then widens to SImode for the range test. When that type is short — whether it came from an s16 local, an s16 parameter (§163b), or a cast switch ((s16)x) — the widening is the sll 16 ; sra 16 pair, and it appears whenever minval != 0. It says NOTHING about where the -1 was written. Both C spellings are byte-identical:
switch ((s16)arg1) { case 1 … case 10 } }
s16 sel = (s16)arg1 - 1; switch (sel) { case 0 … case 9 } } same .s, byte for byte
Byte evidence: func_8017EB44 (ov_SC01_005, 134 ins, banked commit:1652, src/ov_SC01_005/ov_SC01_005_jr_8017EB44.c) matches with the cases-1..10 spelling §162a3 forbids; a mechanical rewrite to §162a3's cases-0..9 form diffs to nothing on the pinned cc1 (only the .file line). §162a3's own cited neighbour func_8017F2D4 shares this jtbl shape, and the wave-2 func_8017ED5C bank used the OTHER spelling — two banks, opposite spellings, same asm.
DIAGNOSTIC TELL. Read the sll/sra as "the switch operand is a short" and stop. Do NOT let it pick your case numbering — pick that from the jtbl EDGES (§161a/§162a1/§162a2) and its LENGTH (§163c). §162a3's second tell survives untouched: a decremented value living in a callee-saved reg and re-read long after dispatch is a real source variable, because a compiler bias temp dies at the tablejump.
(SHARPENS — sharpens §48-A1, §48-A4, §50-B, §163c, docs/gcc-2.7.2-map/regalloc.md:283 (RC-11 companion), cookbook L2516 (allocno-priority ref-boost); evidence: byte-probed; from func_8017EB44)
§164-32 — Writing a switch arm's body a SECOND time under an extra case N: label is a zero-byte allocno-density dial: it adds on
A DUPLICATE SWITCH ARM IS A ZERO-BYTE ALLOCNO-DENSITY DIAL — THE PURE-C FORM OF THE RC-11 REF-BOOST. (sharpens §48-A1/A4 — which own the duplicate-and-let-cross_jump-refund pattern only for if/else arms — and cookbook L2516 / regalloc.md:283, which own the floor_log2 ref-step dial only in __asm__ form. Composes with §163c.)
Target shape: a bound/limit computed in bb0 and read once per switch arm, coming out in the WRONG register — a pure REGALLOC-PERM where the long-lived bb0 value and N short per-arm temps are swapped, nothing else off.
THE LAW. global.c:594 allocno_compare prices an allocno floor_log2(refs)*refs/live_length. A bb0 limit read once per arm loses to the arms' own temps by a hair. Writing ONE arm's generic body a SECOND time under an explicit case N: whose jtbl entry already resolves to the default block adds exactly one REG_N_REFS to that limit; when the count crosses a power of two (7→8 ⇒ floor_log2 2→3) its priority roughly doubles (0.318 → 0.545), it allocates FIRST, and the arm temps fall through to the next free reg. The duplicate costs zero bytes: jump_optimize(…, JUMP_CROSS_JUMP, …) runs after reload (pass order, §45-B), the two blocks are register-identical, and the §50-B minimum=1 fall-through path merges them — the surviving block simply carries both labels, so the jtbl entry still resolves.
Byte evidence (func_8017EB44, ov_SC01_005, 134 ins, banked commit:1652): the duplicated case 1: body emits $L14: and $L22: glued to ONE block, 107 cc1-insns — identical count to the un-duplicated source. Deleting it (base19.c, one-hunk diff) gives a clean 19-instruction permutation: lbu $t0 / mult $t0,$v0 / mflo $t0 with the arm temp in $v1, versus lbu $v1 / mult $v1,$v0 / mflo $v1 with the arm temp in $a0 when the duplicate is present. The equivalent RC-11 spelling (__asm__ ("" :: "r"(lim)); in the default arm, v73.c) compiles to an instruction-for-instruction identical stream — same dial, two spellings. Prefer the duplicate-arm form: plain C, no extension, so it survives the §40 family remap and pycparser/permuter, where the asm dummy does not.
THE DOSE BOUND (measured, and NOT what the crack note said). The refund does not stop at one duplicate. On this function, 2, 3, 4, 5, 6 and 7 total copies of the generic body ALL merge to the same 107 insns / 5 distinct jtbl targets. At EIGHT copies the merge fails completely — 172 insns, 10 distinct targets, nothing merged. And it is NOT allocation drift: the 7-copy and 8-copy builds have byte-identical prologues and dispatches (lbu $3 / lbu $2 / mult $3,$2 / mflo $3) and the eight un-merged blocks are byte-identical TO EACH OTHER. The cliff is inside jump.c's cross-jump loop and is not yet root-caused. Practical rule: use one duplicate, verify the instruction count, and never assume a copy count is free without recompiling.
DIAGNOSTIC TELL. A pure REGALLOC-PERM swapping one long-lived bb0 value against N short per-arm temps, where cc1 -da's ;; N regs to allocate: line in t.i.greg puts the long-lived allocno LAST and its density is within ~5% of the temps'. Read that order line — it is the whole diagnosis.
(SHARPENS — sharpens docs/gcc-2.7.2-map/regalloc.md:248 + :283 (RC-11 #APP caveat), §46 ("DENSITY DUMMY PLACEMENT vs maspsx"), §5a; evidence: byte-probed; from func_8017EB44)
**§164-33 — The RC-11 zero-byte density asm is razor-thin on placement: in bb0 between the mflo and the branch its volatil flag **
RC-11 CAVEAT, SECOND HALF — A DENSITY DUMMY IS A DELAY-SLOT FENCE IN cc1 ITSELF, NOT JUST IN maspsx. (sharpens regalloc.md:248/:283 and §46, which scope the #APP caveat to the ASPSX-2.56 slot-hop; and §5a, which covers a volatile asm only against find_cross_jump.)
THE LAW (read out of the pass, not inferred). reorg.c:675 stop_search_p halts fill_simple_delay_slots' backward scan on GET_CODE (PATTERN (insn)) == ASM_INPUT || asm_noperands (PATTERN (insn)) >= 0 — ANY asm insn, volatile or not. So a zero-byte density dummy parked between a candidate insn and the branch that should swallow it does not cost bytes; it costs the SLOT, and the candidate is emitted ahead of the compare instead.
Byte evidence (func_8017EB44, ov_SC01_005, banked): the dial as __asm__ ("" :: "r"(lim)); in the DEFAULT arm ahead of its if is byte-clean — instruction-for-instruction identical to the duplicate-arm form, 107 cc1-insns, MATCH. The same asm moved into bb0 after ret = 0; still costs 0 instructions but re-orders: move $6,$0 moves ABOVE the sltu and the dispatch beq takes sll $2,$5,2 in its slot instead — the target's beq ; move $6,$0 is gone. The crack note's other reported placements (the three non-default arms, and inside the default arm's inner block) each cost +1 instruction.
DIAGNOSTIC TELL. You added a zero-byte density/ref dummy, the register identity you wanted arrived, and a delay slot two to four instructions away went empty or changed occupant. Move the dummy out of the branch's own block — anchor it at a local def inside an arm, never in a block that a branch is going to scan backward through.
(SHARPENS — sharpens §46-L4(c) (L3344-3351) — 'the biv increment is LAST in the body — loop.c inserts the reduced giv's addiu immediately before the biv increment, so i++ at the bottom puts addiu into the loop-back dela; evidence: byte-probed; from func_8017EB70)
§164-34 — TWO SOURCE-LEVEL BIVS EMIT THEIR INCREMENTS IN LUID ORDER, AND THE REDUCED GIV RIDES ITS OWN BIV — so the pointer bump's SOURCE PLACEMENT is the loop-tail order dial. (sharpens §46-L4(c), which states the giv-adjacency half for ONE biv and routes it to the delay slot; sharpens §158's one-line comma-increment aside, which covers order WITHIN a comma only; and BOUNDS the §31 triage table, which files loop.md L6 "giv-add order" under INTRINSIC → permuter.)
Target shape — a counted table walk with a counter biv, a pointer biv, and a rider giv, three addius at the tail:
.L: ...
addiu $s4,$s4,0x1 <- i (biv)
addiu $s2,$s2,0x10C <- the +0xE giv, riding p
slti $v0,$s4,0x60
bnez $v0,.L
addiu $s0,$s0,0x10C <- p (biv), in the loop-back delay slot
THE LAW. strength_reduce inserts each reduced giv's addiu immediately BEFORE its own biv's increment insn (emit_iv_add_mult(..., tv->insn) — this half is loop.md L6 and is genuinely intrinsic). The two BIVS' increments, however, are ordinary insns and emit in INSN_LUID = source order. So when the counter and the pointer are both bumped in source, moving p += K across i++ transposes the pair and drags the rider giv with it:
for (i = 0; i < 0x60; i++) { ...body...; p += 0x10C; } -> [ $s2++, $s4++ ]
for (i = 0; i < 0x60; i++, p += 0x10C) { ...body... } -> [ $s4++, $s2++ ] <- target
Byte evidence. func_8017EB70 (ov_SC03_107 / jr_801789AC, 127 ins, banked at src/ov_SC03_107/ov_SC03_107_jr_801789AC.c:6518): the body-tail spelling left exactly 2 mismatched instructions — an adjacent transposition, registers already correct, count already correct, nothing else in the function moved; the for-increment comma spelling took it to MATCH in one edit. NEGATIVE CONTROL, same TU, independently checked: the banked+matched func_801822E4 (:8014) walks the same D_801202A0 table at the same 0x10C stride using the body-tail spelling. Both spellings live in matched code — this is a per-function READ, never a default, and never a style rule.
THE DIAGNOSTIC TELL. Residual == an adjacent transposition of two addiu rX,rX,K at the loop tail, same registers, same count ⇒ read the target's tail order and place the pointer bump to match: body tail if the pointer's addiu comes FIRST, for-increment comma if the counter's does. Do NOT route it to the permuter as an L6 "giv-add order" intrinsic (§31's current advice), and do NOT reach for the §47/§158 allocno sliders or §49's LUID dial — those split priority TIES; this is an emission-order fact upstream of both, and it is one character of source. cc1 -dL's .loop dump names the givs if you need to confirm which register is the rider.
Symptom lines for the index: "two loop-tail increments transposed" · "an addiu rX,rX,K pair in the wrong order at the loop back-edge" · "close=2, adjacent, same registers, at a counted table walk".
Honesty note: n=1 (func_8017EB70). The giv-adjacency half is source-cited (§46-L4(c) / loop.md L6); the two-biv LUID-order half is the byte-probed part, and the third increment ($s0 in the delay slot) was constant across both arms rather than separately ablated.
*(SHARPENS — sharpens §162e2 (L11162-11178) — 'PREHEADER ORDER IS BODY ORDER', with the func_8017CBC8 exemplar: 'Writing the sp-relative store address as an explicit pointer local — u8 bp = sp18; — makes that address th; evidence: byte-probed; from func_8017EB70)
§164-35 — A BARE &local PASSED AS A CALL ARGUMENT IS NOT A MOVABLE AT ALL; NAMING IT IS THE EXISTENCE LEVER, NOT AN ORDER LEVER — AND IT IS PER-ADDRESS. (sharpens §162e1, whose existence law is scoped to SYMBOL-based arrays and explicitly excludes "a base already in a register", leaving the raw &local case undetermined; and sharpens §162e2 / §162q1#1, which state this exact u8 *bp = sp18; edit for func_8017CBC8 as an ORDER lever only — "the local exists only to order the preheader".)
Target shape — a loop preheader holding an sp-relative address in a callee-saved register:
<preheader> addiu $s6,$sp,0x20
<body> addu $a0,$s6,$zero ; jal func_8012B0B4
THE LAW. &buf written inline at the call site is expanded straight into the argument register: there is no pseudo, scan_loop has no insn to record, and the hoist never happens. Assigning it first — bp = (unsigned int *)&buf; — forces the pseudo, scan_loop records it (loop.c:769-800), move_movables hoists it (:1708), global-alloc gives it a callee-saved reg; §162e2's order law then only decides WHERE in the preheader it lands. Existence and order are two separate consequences of the same edit, and §162e2's exemplar exercised only the second. It is per-ADDRESS, not per-function: in this same loop body &in and &out stay unnamed and stay inline (addiu $a0,$sp,0x10 / addiu $a1,$sp,0x18), because that is what the target shows.
Byte evidence. func_8017EB70 (ov_SC03_107 / jr_801789AC, 127 ins, banked; source comment at src/ov_SC03_107/ov_SC03_107_jr_801789AC.c:6475-6482). Unnamed: 124 ins, $s0–$s5, frame 0x44, no addiu $s6,$sp,0x20. Named bp: 127 ins, $s0–$s6, frame 0x48, MATCH. Exactly one of the three stack addresses was named.
THE DIAGNOSTIC TELL. Draft is exactly one callee-saved register short of the target, frame 4 bytes smaller, exactly −3 ins (the missing sw $sN / lw $sN / preheader addiu), and every subsequent sw $sN,K($sp) offset shifted −4. That entire cascade is ONE un-hoisted address — it is not a regalloc problem, so do not open §47 / §158 / §136 on it. Then route by what is un-hoisted: an sp-relative address passed as a call argument ⇒ name it (this section); a symbol base ⇒ §162e1's uniform-index lever; an address your draft DID hoist that the target recomputes inline ⇒ §148-A (raise the loop's RTL insn count). cc1 -dL's <file>.i.loop settles which by naming every movable, moved / not desirable.
⚠️ Bound, and it is untested. The negative half is READ off the target, not ablated — naming &in/&out was never compiled. §148-A's threshold -= 3 per already-moved movable means an extra named address may simply fail the hoist test and cost nothing. Do not assume naming a second stack address is destructive; measure it.
Symptom lines for the index: "one callee-saved register short + frame 4 smaller + −3 ins" · "every sw $sN offset shifted by 4" · "an addiu $sN,$sp,K preheader hoist my draft never emits".
(SHARPENS — sharpens §31 lever 2 (L2919-2920: "reorg.c forbids asm in a delay slot, so an asm-copy that must fall in one lands a slot early"), §17 zero-reg-copy (L3058-3060: "an asm volatile copy cannot [land in a; evidence: byte-probed; from func_8017ED5C)
§164-36 — AN ASM AT THE HEAD OF A BRANCH TARGET HIDES THE WHOLE BLOCK FROM fill_simple_delay_slots. (sharpens §31-lever-2 and §17's zero-reg-copy note, which state only the ELIGIBILITY half — "an __asm__ cannot itself sit in a delay slot".)
reorg.c:675-701 stop_search_p returns 1 on ASM_INPUT or any asm_noperands(...) >= 0 insn. The forward scan for a delay-slot filler therefore stops at the first asm in the target block — the cost is not "this lever can't go in the slot", it is "nothing behind this lever can either". A zero-byte lever parked at the top of a branch target robs the preceding branch of its natural filler.
Byte evidence (func_8017ED5C, ov_SC01_005, banked commit:1652): with §163f's self-set asm in the block that DEFINES the base, beq $2,$0,$L5 ; addu $2,$4,$5 — the addu takes the slot, 147 ins, MATCH. Move the identical asm to the first statement of the if (t < lim) body: the addu lands below #APP/#NO_APP inside the target block, the branch takes an unrelated sll $2,$16,16, and the function grows to 148. Nothing else changed.
PLACEMENT RULE. Put a qty_const launder (§163f/§153), an allocno ref-boost (§148-C) or a range-extender (§158) in the block that defines the value. Never at the top of a block whose first insn the target steals into a delay slot.
DIAGNOSTIC TELL. +1 instruction and a delay slot holding a plausible-but-wrong insn, appearing the moment you moved a zero-byte asm across a branch. Read the #APP marker's position relative to the branch target label before blaming sched1 or the allocator.
(SHARPENS — sharpens §162a3 (L11059-11064), §163b (L11781-11791), §162a1/§162a2 (L11040-11057), §161a (L10968), §163c (L11793-11799); evidence: byte-probed; from func_8017ED5C)
§164-37 — REWRITE (P30 S48 vet). The sll 16 ; sra 16 between the subtract and the sltiu says the switch INDEX IS A SHORT. It says NOTHING about who owns the subtract. (supersedes §162a3's discriminator; unifies it with §163b, which is the same law.)
c-typeck.c:6589 (c_expand_start_case) calls get_unwidened and strips the integer promotion back off a short switch expression, so index_type stays HImode. stmt.c:4982-4986 then builds
convert (nominal_type, fold (build (MINUS_EXPR, index_type, index_expr, minval)))
— a HImode subtract re-widened to SImode. The re-widening is emitted after the subtract by construction, whether or not minval is 0.
Two BANKED functions, the same four instructions, opposite C:
func_8017ED5C addu $4,$4,-1 ; sll 16 ; sra 16 ; sltu $2,$4,10 s16 sel = param_1 - 1; switch (sel) { case 0 … case 9 }
minval 0 — the subtract is the SOURCE's
func_80189540 addiu a0,a0,-1 ; sll 16 ; sra 16 ; sltiu 0xF s32 f(s16 arg0) { switch (arg0) { case 1 … case 15 } }
minval 1 — the subtract is EXPAND's bias
So the old text — "expand_end_case never puts a HImode sign-extension between its own bias subtract and the range test" — is FALSE, and "read it as s16 idx = a0-1; switch(idx), not switch(a0) cases 1..10" is a coin flip, not a tell.
THE WIDTH HALF IS THE LAW, and it is worth exactly ±2 instructions. Byte-probed in both minval cases on the pinned cc1: func_8017ED5C s16 sel→s32 sel = 104→102; func_80189540 s16 arg0→s32 arg0 = 551→549. Same delta, same cause, opposite minval. This is §163b's oracle; the two entries are one section.
THE DISCRIMINATOR IS THE TABLE, NOT THE DISPATCH. Take ncases from the sltiu bound (never the dlabel span — §8a-pad L394, §131), then read the entry ADDRESSES (§162a1, §163c). If the arms you must write start at slot 0 while the natural domain value there is 1, the -1 is the source's; if the table is biased, it is expand's. §162a3's secondary tell survives and is the only dispatch-local evidence: a compiler bias temp is dead the moment the table is indexed, so a decremented value living in a callee-saved register and re-read long after dispatch (beq $s0,$v0 at 8017F4C8/8017F4D0 in func_8017F2D4) is the source's variable.
Status: PROMOTED, with the law replaced. The C form is banked twice — func_8017ED5C 147/147 and func_8017F2D4 279/279, both commit:1652, src/ov_SC01_005/ov_SC01_005_jr_8017ED5C.c. Drop the "UNPROVEN / bank it before promoting" flag; do NOT carry the shape⇒source-subtract inference forward.
(SHARPENS — sharpens §36 (func_8013A530) bullet "CONDITION-OPERAND ORDER = LOAD PLACEMENT", L2496, §10 Residual A / Fix A1 (RTL operand order = source order; no swap_commutative_operands), L856-864, §88b + §88c (slti lite; evidence: byte-probed; from func_8017F83C)
§164-38 — THE TWO-ARM COMPARE ORACLE: identical slt + identical load ADDRESS order ⇒ the arms are < and >, not two <s. (sharpens §36's "CONDITION-OPERAND ORDER = LOAD PLACEMENT" bullet, which states the canonical-slt collision but only as a forward, one-block scheduling lever.)
Target shape — func_8017F83C (ov_SC03_108, 231 ins, banked), 8017F948..8017F964:
lh $v0,0xa($s0) ; lh $v1,0xfe($s0) ; slt $v0,$v0,$v1 <- arm A
lh $v1,0xa($s0) ; lh $v0,0xfe($s0) ; slt $v0,$v0,$v1 <- arm B, slt word IDENTICAL (0x0043102a)
THE LAW. gcc-2.7.2 expands a comparison's operands LEFT TO RIGHT (RTL operand order = source order, §10-A) and canonicalises GT to slt dst, op1, op0. So the FIRST address loaded in an arm is that arm's FIRST source operand. If both arms load the same address first AND emit the same slt, the second arm is a > b — the operands did not move, the canonicalisation did. Respelling arm B as b < a (what m2c/Ghidra emits, and the obvious guess) reverses the LOAD ORDER: byte-measured 2-ins SCHEDULE-REORDER (.run/wave3/func_8017F83C/v2_gt_swap.c).
READ THE ADDRESSES, NOT THE REGISTERS. The load-ADDRESS order is the expansion fact and carries the information. The dest-register transposition is regalloc-mediated and this same function proves it is not free — the correct </> source gave BOTH arms the same assignment and needed §163g's launder to land the target's. Use the register swap as corroboration only; never as the primary tell.
DIAGNOSTIC TELL. Two arms of one if/else, byte-identical slt, same two addresses, same address order, dests transposed ⇒ write a < b / a > b. If the ADDRESS order flips between the arms, the SOURCE flipped the operands (b < a) — that is the other reading, and it is the one §36 exploits as a lever.
Scope, honestly: one function, two arms. sched1 does move loads (§36), so the address-order half is only a clean oracle in a straight-line arm with no intervening mem-unit hazard.
(SHARPENS — sharpens §162o1 (swapped in EVERY arm = decl ORDER; in ONE arm = decl SCOPE / §136-1 split), L11645-11673, §136-1 rule 1 ("same $v0/$v1 pair swapped in ONE arm only, siblings byte-correct" -> split per arm), L; evidence: byte-probed; from func_8017F83C)
§164-39 — WHEN THE TRANSPOSED PAIR IS THE COMPARE'S OWN OPERANDS, IT IS NOT A DECLARATION PROBLEM (bounds §162o1's ONE-arm branch and §136-1 rule 1).
Target shape: §163f's two arms, both byte-correct except that in ONE arm the two lh destinations are transposed. Residual 3 ins, everything else clean.
§162o1 routes "pair swapped in ONE arm, siblings byte-correct" to §136-1 rule 1 (split the local per arm) and "swapped in EVERY arm" to declaration ORDER. Neither exit exists when the pair is the comparison's own operands — there is no user local to split or reorder. Byte-measured NULL on func_8017F83C, every variant preserved under .run/wave3/func_8017F83C/ (mk.py/mk2.py/mk3.py): block temp for op0 (v_tmp_a), for op1 (v2_tmp_b), both (v2_twotmp), function-scope temps (v2_funcscope), a temp in the OTHER arm (v3_elsetmp), swapped decl order (v3_declswap). Also NULL: ternary into the flag (v_tern), ternary tested directly (v_terndirect, +4 ins — gcc stops sharing the flag pseudo), !(a >= b) (v_ge_neg), (s32) casts on both operands (v3_unsig), inverting the outer test (flips bgez→bltz, 4 off).
WHAT MOVES IT — a zero-byte in-out fence on operand 0, placed BETWEEN the two loads:
s32 ta = *(s16 *)(p + 0xa);
__asm__ __volatile__("" : "=r"(ta) : "0"(ta));
cond = ta < *(s16 *)(p + 0xfe);
Note the placement is not §158's — §158 puts an input-only extender AFTER the rival's last use; here the rival is not yet born. The construct is §153's / §34's fence, which SETS the pseudo.
MECHANISM: OPEN — do not repeat the "live-range" label. No -dl/-dg R/L arithmetic was run (§137 and §158 both set that bar), and the construct chosen is documented elsewhere as a cse-class emptier (§153: the volatile asm is never entered into cse's table and it SETS _m) and as the in-place SET that aborts optimize_reg_copy_1's forward-substitution scan (§162j1, local-alloc.c:732 reg_set_p). A plain temp for op0 — same live range, no SET — is NULL, which points away from a pure live-range story. The discriminating probe was never run: the true §158 extender __asm__ __volatile__("" :: "r"(ta)); (lengthens the range, does NOT set the pseudo) appears in none of the variant files. Run it before anyone writes a mechanism into this section.
Cost note. register s32 __asm__("$2")/("$3") also reaches MATCH here, but per §37 / §48-B4 rung 3 a pin fails dedup_propagate.compiles_standalone and forfeits the family (this exemplar is ×4). The fence ships, and it is already an in-TU idiom — sibling func_8017F2E8, src/ov_SC03_108/ov_SC03_108_jr_8017BEBC.c:3823.
DIAGNOSTIC TELL. The only residual is which of a compare's two inputs got the dest register, in ONE arm, with the slt itself byte-identical. Skip the §162o1 / §136-1 declaration sweep entirely — six spellings of it are already measured dead — and go to the fence on operand 0.
Evidence: func_8017F83C, ov_SC03_108, 231 ins, MATCH (match_one; banked at src/ov_SC03_108/ov_SC03_108_jr_8017F83C.c:2759). n=1.
(SHARPENS — sharpens §82-1 (the inlined-helper signature, func_8017C730), §82-2, §6098 table row (family_remap LENGTH-DRIFT on a static inline helper), §9496 table row (dedup: hand-author the macro with the helper inlined; evidence: byte-probed; from func_8017F9AC)
Addendum (P31 S64 t5j-t5m, func_8017E5C0): §164-39's disposition — "six spellings of it are already measured dead — skip the declaration sweep entirely and go to the fence on operand 0" — is too broad, and the one spelling its NULL list never contained is the one that fires. §164-39 probed block/function-scope temps for op0/op1/both, a temp in the other arm, swapped decl order, two ternary forms, !(a >= b), (s32) casts and an inverted outer test; it never probed the transposed relational spelling (b >= a ⇄ a <= b). On func_8017E5C0 that transposition alone closes the identical residual class, in a single-arm two-load range test rather than §164-39's if/else pair.
The one-variable A/B (§266-grade; the only pair worth citing). Same s16 widths, same decl order lim-then-y, only the compare's operand order changed: v4 if (lim + 0x40 >= y && lim - 0x80 <= y) → near, closeness 6, REGALLOC-PERM/$v1>$a0>$v1, map {"$v1":"$a0","$a0":"$v1"}, kinds {"reg": 6}; v3 if (y <= lim + 0x40 && y >= lim - 0x80) → match, 110/110, residual []. Six phrasings (v2, v4, v6–v9) plateau at exactly 6 with that same sig — the §137 "invariance under permutation is information" signature, here pointing at the compare rather than away from the source.
Why the address-order tell is blind on this shape. The two lhs sit at their own declaration statements, so the load ADDRESS order never moves (mine and target both lh …,0x8A($s0) then lh …,0xA($s0)); only the compare's dest registers transpose. §164-38's law (RTL operand order = source order, GT canonicalised to slt dst,op1,op0) is doing the work in the register direction, which §164-38 sees only as "regalloc-mediated … corroboration only, never the primary tell". That reading rule stands — but it is not a claim the register direction is unsteerable, and here it is the entire lever. Mechanism UNREAD: no -dl/-dg was run, so per §137's own bar do not write a live-range or first-fit label into this; what is byte-proven is source order → colour.
Bound — reject the width claim. The transcript's closing "s16 vs s32 width" note is not supported: v5 (s32 temps, decl order y-before-lim, same straight-order compare) also MATCHed, so width was never isolated and is in tension with the same transcript's read-order claim. Cite v3-vs-v4 and nothing else.
Practical routing. On any REGALLOC-PERM whose swapped pair is a compare's two inputs, sweep the relational transposition first — one character, zero bytes, ~20 s — before §164-39's volatile fence, before §176-B's aliases, and before any pin. This is the third counter-example to §137's "source-level levers are a dead end for REGALLOC-PERM" (with §176-B's t5b–t5d addendum and §211's t5-t4 addendum), and it re-opens for </>/<=/>= the door §10-Residual-A/Fix-A1 already documents for commutative |/&/+.
§164-40 — A REPEATED IN-PLACE BLOCK IS A static inline, AND THE MACRO SPELLING IS NOT EQUIVALENT. (sharpens §82-1, which proves 'this was inlined' and stops there; §82-1's duplicated-addiu $aN,$sp,K-across-a-jal tell cannot fire on a 0-jal function, so this is the second, disjoint route in.)
Target shape — N byte-identical multi-instruction bodies, ZERO jal between them, each opening on its own operand set:
lui $a0,%hi(D_A) ; lw $a0,%lo(D_A)($a0) <- expansion 1's globals
…60 identical instructions…
lui $a0,%hi(D_B) ; lw $a0,%lo(D_B)($a0) <- expansion 2's globals
…the same 60 instructions…
THE LAW. Three source spellings all read as 'inlined' and gcc-2.7.2 treats them as three different programs:
| spelling | result |
|---|---|
out-of-line static |
collapses to N real jals — 199 ins (SIZE-MISMATCH) |
function-like #define macro |
frames the SAME bytes but 1537 (+19) — LENGTH-DRIFT |
static inline |
MATCH (1518) |
The frame does not discriminate inline from macro — only the instruction count does. Never 'simplify' a matched static inline helper into a macro, and never hoist it out of line.
BYTE EVIDENCE. func_8017DC1C (ov_SC07_006, 1,518 ins, 25 expansions) — do-not-re-buy table in .run/giants/s21_8017DC1C_report.md §4, all three rows measured on the MATCH base. Second instance: func_8017F9AC (same TU, 275 ins, 4 expansions) first-gate MATCH re-using the identical static inline morph_lerp at src/ov_SC07_006/ov_SC07_006_jr_8017BEBC.c:4018.
DIAGNOSTIC TELL. N byte-identical bodies, no jal between them ⇒ inline helper. Then read the delta: LENGTH-DRIFT +N spread evenly across the bodies ⇒ you wrote a macro; SIZE-MISMATCH with N jals ⇒ you dropped inline. Count the bodies before anything else — everything else in the function is per-expansion arithmetic (§164b, §164e).
(SHARPENS — sharpens §161b (aliasing a parameter into a local costs a second callee-saved register), §162n2 (same direction, unconditional alias), §162n1 (conditionally-assigned alias kills a spurious giv), the ⚠ reconcil; evidence: byte-probed; from func_8017F9AC)
§164-41 — A POINTER PARAMETER THAT THE LOOP WALKS MUST BE ALIASED INTO A LOCAL. The third face of the alias axis. (completes the §161b / §162n1 / §162n2 triangle, whose stated discriminator — 'conditionally set or not' — has no case for a pointer that is incremented.)
Target shape — a bare copy of each walked pointer in the loop head, sitting in the two delay slots:
lw $t2,0x10($a0) <- the count
addu $t1,$a2,$zero <- load-delay: copy of param `b`
beqz $t2,<exit>
addu $a3,$a1,$zero <- branch-delay: copy of param `a`
THE LAW. §161b/§162n2: an unconditional alias read at straight-line sites costs a callee-saved register — delete it. §162n1: a conditional alias sets cant_derive and kills a spurious giv — keep it. Third case: if the aliased pointer is ++-ed by the loop, the alias is MANDATORY. Walking the parameter itself makes the incoming pseudo and the loop biv the same pseudo, and combine folds the copy into the first load — the preheader addus disappear and the whole loop head re-schedules.
/* target */ /* what deletes the copies */
pb = b; pa = a; for (; i; i--) { … b->vx …; b++; a++; }
for (; i; i--) { … pb++; pa++; }
BYTE EVIDENCE. func_8017DC1C do-not-re-buy table: drop the pb/pa locals, increment the params directly → 1493 ins, 1454 mismatched (LENGTH-DRIFT −25) off a 1518 MATCH base. Second instance, byte-visible: func_8017F9AC .L8017FA14 — lw $t2,0x10($a0) / addu $t1,$a2,$zero / beqz $t2 / addu $a3,$a1,$zero, four times over.
⚠️ Do not quote the DC1C report's '2 moves × 25 = 50 instructions' — its own measured row is −25, i.e. one instruction per block. The direction and the mismatch count are byte-proven; that arithmetic is not.
DIAGNOSTIC TELL. Your loop head is missing a bare addu $tN,$aM,$zero per walked pointer and the count load has grown a nop ⇒ you are walking the parameters, alias them. Distinguish from §161b by asking one question about the aliased pointer: is it incremented? Read at straight-line sites ⇒ §161b, delete it. Assigned in an if ⇒ §162n1, keep it. p++-ed ⇒ this section, add it.
(SHARPENS — sharpens §36 — the func_8013A530 bullet "PIN → RELOAD-RETRY POISON (RC-5 ch.2 extends to retry_global_alloc)" (matching-cookbook.md L2495), docs/gcc-2.7.2-map/regalloc.md §G, "RC-5 channel 2 EXTENDED" (L242-; evidence: byte-probed; from func_8017FDF8)
§164-42 — With N>1 mult in one basic block, every product pseudo prefers the one-register class LO_REG (mips.md `mulsi3_internal
§NNNa — TWO MULTIPLIES IN ONE BLOCK OVER-SUBSCRIBE $lo, AND THE LOSER'S RE-PLACEMENT ROTATES THE WHOLE BLOCK'S REGISTER MAP. (sharpens §36's "PIN → RELOAD-RETRY POISON" bullet and regalloc.md RC-5 ch.2, which see the same Need 1 reg of class LO_REG → retry_global_alloc path but attribute it to a register __asm__ pin, and whose tell — "audit pins, not liveness" — sends you the wrong way when there are no pins.)
Target shape — an inlined/unrolled body with ≥2 mult between the same pair of labels:
mult $2,$7 ; ... ; mflo $t1 <- one product keeps LO
mult $2,$7 ; ... ; mflo $t0
mult $3,$7 ; ... ; mflo $v1 <- one was evicted and re-placed
THE LAW. mips.md:848 mulsi3_internal constrains its destination "=l", so every product pseudo carries reg_preferred_class == LO_REG — a ONE-register class. With N>1 products live in a block at most one can hold it. The products tie exactly in global.c:594 allocno_compare (same refs, same live length), so the comparator falls through to its documented last resort — /* sort by allocno, so that the results of qsort leave nothing to chance */ return *v1 - *v2; — and allocno numbers are handed out in ascending PSEUDO number (global.c:384-397), i.e. source-expansion order. Reload then prints, in -dg:
;; Need 1 reg of class LO_REG (for insn NN).
Spilling reg 65. <- 65 == LO_REGNUM (mips.h:1237)
Register 119 now in 3. <- retry_global_alloc's re-placement
retry_global_alloc (global.c:1203 → find_reg(..., retrying=1)) re-places the evicted product, and where it lands is the entire diff: one spelling puts it on a callee-saved $sN and every loop-carried pseudo slides one register down; another puts it on $v1 and they slide back. No pin is involved in either.
BYTE EVIDENCE (func_8017FDF8, ov_SC07_006, 317 ins, banked; re-measured independently at vet time). Fully-inlined channel expression vs the matching spelling: same 317 ins, same opcode stream, close=104, classifier REGALLOC-LOCAL. Register histograms mine/target are the SAME MULTISET shifted by one — {t1:35,t2:35,t4:20,t3:15,t5:14,t6:10} vs {t2:35,t3:35,t5:20,t4:15,t6:14,t1:10,t7:2}. The two .gregs differ only in the LO lines: Need … LO_REG (for insn 75) / Spilling reg 15 versus (for insn 61) / Spilling reg 24.
DIAGNOSTIC TELL. Instruction count exact, opcode stream exact, classifier REGALLOC-LOCAL, and every register in the hot block is exactly one off the target's in the same direction, with a short-lived product sitting on a callee-saved $sN. Count the mults in the block; if ≥2, grep the -dg dump for Spilling reg 65. The lever is expression granularity (§NNNb), not pins — §162j1's "pins provably cannot help" for a copy problem holds for this priority problem too.
BOUNDS (measured; both correct the crack agent's own write-up).
- "
retry_global_allocre-places it on the LAST free register" is FALSE. In the MATCHING spelling the retried allocno lands in$v1(.greg: Register 119 now in 3). Retry-modefind_regis ordinary first-fit over whatever is free; the register it picks is data, not a rule. - Winning the
$lotie is necessary, not sufficient. Thedr,dgspelling emits a.gregidentical to the matching one in every LO line (insn 61/Spilling reg 24/Spilling reg 65) and is stillclose=35, withmflo $t0where the target hasmflo $t1. At least two independent dials live in this class — do not stop when the LO line matches.
(SHARPENS — sharpens §48-A (A1 sink-init-into-arms, A2 local-alloc $s0 occupant, A3 block-scoped per-case temps), §47 (live-length slider), §37 (allocno-priority ref-boost), §50-A (priority encoding), §148-C (zero-byte al; evidence: byte-probed; from func_8017FDF8)
§164-43 — How many named temps one expression is split into is a byte-neutral global.c allocno-pricing dial — global-alloc price
§NNNb — HOW MANY NAMED TEMPS ONE EXPRESSION IS SPLIT INTO IS A global.c PRICING DIAL, AND IT IS NOT MONOTONE. (sharpens §48-A, whose thesis "any C edit that changes a pseudo's refs or live-length while leaving the emitted insns identical is a free register dial" names five dials — none of them is expression granularity; and bounds §162d1's closing "Statement-vs-expression is not the dial; REG_N_SETS is", which holds for sched1's S2 birthing boost and NOT for global-alloc pricing.)
THE LAW. flow_analysis runs BEFORE sched1 (toplev.c:2983 vs :3033), and sched1 REWRITES the lifetime data global-alloc prices on (sched.c:4712 alloc, :3107/:3861 accumulate, :4915-4946 write back into reg_live_length). So global-alloc ranks allocnos on sched1's order, while the order you read in the .s is sched2's (toplev.c:3117, post-reload). Splitting one expression into named temps changes the pre-reload insn stream — hence the priced live lengths, hence allocno_compare's order — with zero change to the final instruction stream. No declarations beyond the temps, no asm, no pins.
IT IS NOT MONOTONE — hoist the RIGHT subexpressions, not more of them. Ladder on func_8017FDF8 (ov_SC07_006, a 5×-expanded 16-entry CLUT lerp; all rungs 317/317 ins, all re-measured at vet time):
| spelling of the three channel results | close |
|---|---|
fully inlined hi | (r+dr)&M | (g+dg)&M | (b+db)&M |
104 |
hoist the raw products pr,pg,pb |
104 |
hoist nr only |
104 |
hoist nr,ng |
114 |
hoist the unmasked sums sr,sg |
114 |
hoist the deltas dr,dg (db inline) |
35 |
hoist all three deltas dr,dg,db |
115 — and classifier STRENGTH, a structural break |
hoist all three MASKED SUMS nr,ng,nb |
MATCH |
Two rungs are WORSE than not splitting at all, and the all-three-deltas rung leaves the regalloc class entirely. There is no gradient to follow: sweep the partition.
DIAGNOSTIC TELL. A large REGALLOC-LOCAL close on a body with repeated identical per-lane arithmetic, opcode stream exact. Sweep the PARTITION of the expression — the prefixes/subsets of {lane results} as named temps — before reaching for asm sliders, pins or the permuter: it is ~8 compiles and needs no dump read, so it belongs ahead of §47's live-length slider and §148-C/§37's ref-boost. Hoist at the granularity the TARGET shows — here the target holds each channel's masked result in its own register, and that is exactly the rung that matched.
(SHARPENS — sharpens §5a (L211, L224-229), docs/SETUP.md §5.6 (gcc-papermario is 2.8.1, cite tools/reference/gcc-2.7.2 for accuracy); evidence: byte-probed; from func_8017FFD0)
§164-44 — ⚠ CORRECTS §5a's MECHANISM: gcc-2.7.2 has NO volatile-asm veto in find_cross_jump. The barrier works only by making the two RTL streams UNEQUAL — so the SAME barrier in BOTH twin blocks is a silent no-op.
§5a says "find_cross_jump sets lose = 1 (bails) on ANY volatile asm node (ASM_INPUT/MEM_VOLATILE_P)". That clause is real — in gcc 2.8.1. tools/reference/gcc-papermario/jump.c carries it ("Don't allow old-style asm or volatile extended asms to be accepted for cross jumping purposes" → GET_CODE (p1) == ASM_INPUT || … MEM_VOLATILE_P … → lose = 1), and §5a was researched off that tree (§0's own provenance line). Vanilla 2.7.2 — the source of our pinned cc1 (docs/SETUP.md §5.6) — does not have it. diff of the two find_cross_jump bodies: those 11 lines are the only functional difference. In 2.7.2 an __asm__ __volatile__("") breaks the walk only because it is an ASM_INPUT where the other stream holds something else (jump.c:2412 insn code / 2469 pattern code) — and two ASM_INPUTs are compared by strcmp on their text (rtx_renumbered_equal_p, jump.c:4033, case 's').
Byte evidence (func_8017FFD0, ov_SC03_108, both barriers at the in-window position of §163f-1):
- identical
__asm__ __volatile__("")in BOTH twins → 189 ins, −7 — the merge still fires. 2.8.1 would have vetoed; ours compares the twoASM_INPUTs equal and keeps walking. - the same two barriers with different text,
""vs"# x"→ MATCH 196, still zero bytes emitted.
⇒ Barrier ONE side only — or, if you need one in each block for other reasons, give them different asm text.
THE DIAGNOSTIC TELL. You barriered both duplicated blocks "for symmetry" and the length drift did not move at all ⇒ they are comparing equal. Delete one, or change its string.
⚠ Process rule this earns. Any jump.c/RTL mechanism cited from gcc-papermario must be re-read in tools/reference/gcc-2.7.2/ before it is banked as law. SETUP.md §5.6 already warns this (the 2.8.1 &&0 biv-elim divergence, Phase 23); §5a is the second instance, and it sat in the cookbook as a mechanism for ~25 phases without changing any verdict — only the explanation was wrong.
(SHARPENS — sharpens §136b (L9049) — "the cached Ghidra seed was an entirely different body" (func_8017BEBC), §136c (L9088) — search order: engine_core.h twin → same-TU sibling → the .s → the Ghidra seed LAST; evidence: byte-probed; from func_8017FFD0)
§164-45 — .run/ghidra_c/<fn>.c is keyed by ADDRESS, and overlay slots are address-aliased — for an overlay function the cached G
Append to §136c (provenance, not codegen). Why the Ghidra seed is the last rung: .run/ghidra_c/<fn>.c is keyed by ADDRESS, and the Ghidra program holds exactly one body per VRAM address, while overlay slots are address-aliased — 2,499 of the 7,100 distinct nonmatching function addresses in the 0x8017–0x801A overlay range live in ≥2 overlays (35%). Where those overlays hold different code at one address, the seed is right for at most one of them and is silently wrong for every other. Byte-proven a third time: .run/ghidra_c/func_8017FFD0.c is ov_SC01_009's function (lh $v0,0x70($s1) → D_801ED4B4/D_801ED4E4, asm/ov_SC01_009/nonmatchings/ov_SC01_009_jr_8017E590/func_8017FFD0.s:8-17), not ov_SC03_108's (guard on +0xa, switch on +0x34). THE 10-SECOND CHECK: before using a seed for an overlay function, match its FIRST branch — the struct offset it tests and the first global it names — against the target .s in your overlay's directory. Mismatch ⇒ discard the seed for that address in every overlay, not just yours.
(SHARPENS — sharpens §21 (the ⚠ CANDIDATE lbu+signed-compare bullet, docs/matching-cookbook.md:1984-1991), §161a / §162a1-a2 "check BOTH edges" (:10980, :11040-11058) — the section the note MIScites, §3-T4 branch polari; evidence: byte-probed; from func_80180128)
§164-46 — A COMPARE THE ENCLOSING else ALREADY DECIDES IS STILL EMITTED: gcc-2.7.2's range knowledge is EXPRESSION-LOCAL, never cross-block. This byte-probes the ⚠-unverified §21 bltz bullet FROM THE OTHER SIDE. Target shape — ONE global lw, TWO lui edge constants, TWO slts:
lw v1,0(v1) <- the window global, loaded ONCE
lui v0,0xffe0 <- edge A (-0x200000)
lw a0,8(s0) <- the coordinate
addu v0,v1,v0 ; slt v0,a0,v0 ; bnez v0,.Lelse
lui v0,0xffe8 <- DELAY SLOT: edge B (-0x180000) ** THE TELL **
...
.Lelse: addu v0,v1,v0 ; slt v0,v0,a0 ; bnez v0,.Lend <- edge B compare
THE LAW. In if (x >= LO) {…} else { if (HI >= x) {…} } with HI > LO, the inner test is true on every path that reaches it — and gcc-2.7.2 emits it anyway. There is no cross-block value-range propagation, so a range-dead compare in an else arm survives to bytes. Transcribe the window verbatim; never "clean up" the second edge. §21's ⚠-unverified lbu bullet (:1984) states the complement: inside ONE expression a chained || DOES canonicalize and gcc drops the provably-dead bltz. Together they are one rule — gcc-2.7.2 folds a range-dead compare within an expression and never across a statement boundary — and this is the byte-proof of the half §21 could only assert.
Byte evidence (A/B run on the pinned triple by the S48 skeptic pass; func_80180128, ov_SC06_008, banked at 77 ins):
- baseline as banked = 77 ins;
- delete ONLY the inner
if→ 73 ins and a 20-line diff, not a clean −4:addu v0,v1,v0/slt v0,a0,v0becomeaddu v1,v1,v0/slt a0,a0,v1, the outer branch's delay slot collapses tonop, and every branch target shifts. A near-miss of this shape does not read as "4 instructions short" — it reads as a regalloc wall, which is exactly how a family gets written off. - polarity:
x <= HIis byte-identical toHI >= x; the strictx < HIcosts 2 ins (slt v0,a0,v0+beqzwhere the target hasslt v0,v0,a0+bnez). Because the test is dead at runtime, semantics cannot pick the spelling — §3-T4 ("branch polarity is READ OFF THE TARGET OPCODE") is the sole oracle for a dead compare. - 8 byte-instances:
func_80180128+ its 6family_hseqsiblings (ov_SC06_010/018/022/024/032/033, 6/6 banked,.run/jr48/w3prop_func_80180128.log), plus the independently-matchedfunc_8017F2E8(src/ov_SC06_008/ov_SC06_008_jr_8017C294.c:3943) whose edges arehi-8/hi+8s16 locals — same dead-inner-compare shape from a different constant provenance, so this is not an artifact of the global.
THE DIAGNOSTIC TELL. ONE lw of a global (or one local) feeding two addus with two different lui/immediate edges and two slts, the second guarding the else arm. The cheap confirmation is the delay slot of the outer branch: with the inner if present it carries the second edge's lui; delete the if and it is a nop. Read that slot before writing a line.
(SHARPENS — sharpens §3-T4 (L90-103) — branch polarity + "put the target's fall-through block in the if, the branched-to block in the else", §32.2 (L2426) — "branch polarity is READ OFF THE TARGET OPCODE", docs/cookbo; evidence: byte-probed; from func_801814D8)
§164-47 — AN if/else if/else CHAIN CANNOT PUT ITS FIRST-TESTED BODY LAST; INVERT THE OUTER TEST AND NEST. (sharpens §3-T4 / §32.2, which state only the TWO-arm form — "put the branched-to block in the else". The string "else if" appears nowhere else in this file.) Target shape — three bodies, and the one the FIRST branch jumps to is emitted LAST, falling into the epilogue with no j:
lh $v0,0x70($s0) ; move $v1,$v0 ; andi $v0,$v0,0x8000
bnez $v0,.LX <- first test branches FORWARD, past both other bodies
li $v0,3 <- .LX's OWN first insn, stolen into the delay slot
andi $v0,$v1,0x800 ; beqz $v0,.LZ ; li $v0,5
[Y] … jal … ; j .Lepi ; nop
.LZ: li $v0,1 ; j .Lepi ; sh $v0,0x2($s0) <- its `j` steals its OWN store
.LX: lw $v1,0x64($s0) … jal … ; sh $v0,0x18($v1) <- falls through, no `j`
.Lepi:
THE LAW. gcc-2.7.2 expands if(C){P}else{Q} as test C; branch-if-!C → Lq; P; j end; Lq: Q;, so in a FLAT if(A){X} else if(B){Y} else {Z} the source order IS the layout order and only the last-written body can fall into the epilogue. If the target lays the FIRST-TESTED body last, no polarity flip inside a flat chain reaches it — §3-T4's lever is exhausted. Make that body the OUTERMOST else and nest the other two in the then-arm: if (!A) { if (B) Y else Z } else { X }.
THE COST IS EXACTLY 2 INSTRUCTIONS AND IS COUNTABLE OFF THE TARGET BEFORE YOU COMPILE. Every non-final body pays j epilogue + a delay slot; the final body pays nothing; a body ending in a jal pays j+nop (its slot is already spent on its own arg/store); a body ending in a plain store pays j+<that store, hoisted>. Here: flat = X(j+nop) + Y(j+nop) = 4 filler; nested = Y(j+nop) + Z(j+sh) = 2, X free ⇒ the naive spelling reads as LENGTH-DRIFT +2 with everything else byte-identical.
Byte evidence: func_801814D8 / ov_SC02_026, banked MATCH — src/ov_SC02_026/ov_SC02_026_jr_8017C180.c:4699-4714, built object build/src/ov_SC02_026/ov_SC02_026_jr_8017C180.o 0x5434-0x54ac. The +2 was measured by the crack agent on the flat spelling and reproduces exactly from the filler arithmetic above.
DIAGNOSTIC TELL. Count the j <epilogue> sites and find the body that has none — that body is the outermost else, and the condition the FIRST branch tests is its condition NEGATED. Second confirmation: the first branch's delay slot holds an insn belonging to the block it branches TO (li $v0,3 here, for that arm's *(s16*)(a0+2) = 3) — a forward bnez whose slot serves the far side is the fingerprint of "my target is the last block". Do not read "heaviest arm" — the criterion is purely which body carries no j.
(SHARPENS — sharpens §17 (Register-allocation ORDER: call-crossing $s0/$s1 swap → FORCE it with register pins, L1384), §162k1 (QImode-vs-SImode LOCAL WIDTH IS LOAD-BEARING, L11421), §136 type-form rule 10 (L8896: unexplai; evidence: byte-probed; from func_801818F0)
§164-48 — A LOCAL'S DECLARED WIDTH IS A CALLEE-SAVED ORDER LEVER, NOT ONLY A MASK/EXTEND LEVER. (extends §162k1 from LENGTH to ALLOCATION; supplies §17's missing pin-free lever for the call-crossing $s0/$s1 swap.) Target shape (func_801818F0, ov_SC03_093, 165 ins, banked src/ov_SC03_093/ov_SC03_093_jr_8017D898.c:4671):
jal rand ; nop
bgez $v0,.L58 ; addu $s0,$v0,$zero <- the temp is born in ONE reg
addiu $s0,$v0,0xFFF
.L58: sra $s0,$s0,12 ; sll $s0,$s0,12
jal rand ; subu $s0,$v0,$s0 <- x never copied out of $v0
...
sll $a1,$s0,16 ; sra $a1,$a1,16 <- the PROMOTION at the use
THE LAW. §17 reads a call-crossing $s0/$s1 swap as a global.c allocno_compare ranking problem and prescribes a register __asm__ pin. Before pinning, check the loser's DECLARED WIDTH: if the target stores that value narrow and re-promotes it with sll 16 ; sra 16 at a use, the original local is a short, and declaring it s16 re-ranks the two allocnos and hands over the whole grant. It is the same knob §162k1 measures on the andi count, acting one pass later.
BYTE EVIDENCE (one character, controlled, in one compilation). s32 t = rand() % 0x1000; → 166 ins, 130 mismatched, param→$s0 / t→$s1, plus a move $v1,$v0 the target lacks. s16 t; → 165 ins, 0 mismatched, t→$s0 / param→$s1, and the call site's sll/sra pair emits for free. Banked; R22 green.
THE DIAGNOSTIC TELL. A near-total $s0/$s1 permutation (>100 mismatched) at LENGTH-DRIFT +1, on a function whose target shows sll $aN,$sX,16 ; sra $aN,$aN,16 feeding a call argument or a narrow store. That promotion pair is a declaration, not an accident — §163b reads it for a switch PARAMETER; read it for the LOCAL too. Cheaper than every alternative for this class: it needs no register __asm__ (so unlike §17's pin it does not fail dedup_propagate.compiles_standalone, §37) and no zero-byte asm (§47/§37 ref-boost).
Mechanism — HYPOTHESIS, not source-verified (§136g), stated so it can be refuted: the HImode destination changes the temp chain's tying at local-alloc and its refs/live_length at global.c:594. No .lreg/.greg was dumped and no priority was computed — the width→grant coupling is the measurement, the RTL story is the guess. Scope, honestly: n=1. The draft's own framing keyed this to expand_divmod / x % 2^k; nothing isolates the divmod — no probe swapped the modulo for another narrow-stored expression — so read % as scenery and the width as the variable.
(SHARPENS — sharpens §17 (pins are THE lever for the call-crossing $s0/$s1 swap, L1384), §72 (a pin is a PREFERENCE, not a reservation, L5726), §80 (the pin's hidden cost is an unconditional qty_phys_sugg, L6291), §36 fun; evidence: byte-probed; from func_801818F0)
§164-49 — A PIN THAT FIXES THE REGISTER AND LEAVES A LOAD-SHAPED RESIDUAL IS THE CAUSE, NOT THE CURE. (bounds §17's 'try the PINS' and adds a FIFTH channel to RC-5, whose four are all allocation channels; the disposition-inverse of §36's func_8013A530.)
The seduction. On func_801818F0 (ov_SC03_093) a wrong callee-saved grant looked like textbook §17: two values live across a call, allocno_compare handing $s0 to the wrong one. register s32 t __asm__("$16") did exactly what §17 promises — reproduced the grant AND the temp-chain tie, 166→165 ins, 130→19 mismatched. The register map was then EXACT. The remaining 19 contained an 11-instruction block in which sched1 had hoisted an unrelated lw $v0,0x20($s1) six slots up.
THE LAW. RC-5 lists four pin side-effect channels — init copy, bad_spill_regs poisoning, pass-0 availability shift, range blocking — and every one is regalloc. There is a fifth: the pinned variable is a hard reg in the RTL from expansion onward, i.e. before sched1 runs, so it also perturbs SCHEDULING, and the perturbation can surface on an insn that has nothing to do with the pinned value. §17 buys the register and can sell you a schedule. Here no source form reached it: 12 permutations (statement order, temp aliasing, pointer local, asm barrier, volatile) all stuck at exactly 11 — the flat plateau that means you are not steering the pass that decides.
THE DIAGNOSTIC TELL. Pinned draft within ~20 of MATCH · register map exact · residual is one LOAD displaced several slots with everything around it in place · every source edit returns the same number. Run §78/§80's attribution primitive on the pinned draft: cc1 -fno-schedule-insns. Here it reproduced the target's assignment and order but for one insn ⇒ sched1 owned it. When it collapses under -fno-schedule-insns, do not hunt a scheduling form — delete the pin and buy the same register from a pin-free allocation lever (declared width §163f, live-length slider §47, ref-boost §37, scope/reuse §76/§162b1, RC-12 opaque copy §136d-1). Dropping the pin for s16 t took 19 → 0. Bonus: the pin-free route also keeps the family (a register __asm__ fails dedup_propagate.compiles_standalone, §37/§162p3).
Contrast with §36 (func_8013A530) — same symptom, opposite disposition. There the sched1 load-hoist was owned by CONDITION-OPERAND ORDER and fell to a >-vs-< flip with the pins still in. Decide which you have with the plateau: a residual that MOVES under source permutation is §36's; one that does not move at all is this.
Honest scope (n=1, and the ablation is confounded): the cure changed the pin AND the type in one edit. register s16 t __asm__("$16") was never compiled, so 'the pin caused the hoist' is inferred from the -fno-schedule-insns probe plus the unpinned s16 draft's clean schedule — not isolated. Run that one probe before promoting the mechanism; the PRESCRIPTION (probe with -fno-schedule-insns before believing a pinned near-miss) stands on its own.
(SHARPENS — sharpens sched.md §1.7 + §S12 (2026-07-28 correction: hard-reg dests ARE boosted; the gate is reg_n_sets == 1), regalloc.md §7 + §F ('Pins do NOT kill the S2 boost … Check the SET COUNT, not the pin'), §37 /; evidence: byte-probed; from func_80181A30)
§164-50 — WHICH HARD REGISTER YOU PIN DECIDES WHETHER THE BIRTHING BOOST SURVIVES: an ARGUMENT-register pin is a free S2 kill. (sharpens sched.md §1.7/§S12 and regalloc.md §7/§F — the 2026-07-28 'pins do NOT kill the boost, check the SET COUNT' correction — and §37's 'single-set pins, INCL. hard-reg, DO boost'. Do NOT restate this as 'a pin kills the boost'; that phrasing is the retired error.)
Target shape — a single-set load that must NOT sink, in a block where the target also holds a constant in the next register up:
sb $3,3($2)
lw $3,28($18)
lw $4,0($2) <- must stay HERE
...
or $3,$3,$5 <- and the constant must be in $a1, not $a0
THE LAW (the new half). birthing_insn_p (sched.c:2469) accepts any SET (REG, …) — hard regs included — and gates only on reg_n_sets[i] == 1 (:2490). For a HARD reg that count is a property of the FUNCTION, not of your variable: flow.c mark_set_1 (~:2047) increments reg_n_sets for regno < FIRST_PSEUDO_REGISTER, and every call site emits (set (reg:SI 4) …) for its first argument. Therefore
- pin to a
$sNthat only your variable touches ->reg_n_sets == 1-> boost SURVIVES (§37's reading); - pin to
$4/$5/$6/$7in a function with >= 1 call ->reg_n_sets= (#arg setups + 1) -> boost DEAD, zero bytes.
So when the residual is 'a single-set load sank past its rivals AND it took the wrong register', one argument-register pin pays both.
Byte evidence (func_80181A30, ov_SC03_117, 148 ins, MATCH; 5 calls, so reg_n_sets[$4] >= 5). -dS traces in .run/wave3/func_80181A30/: unpinned, insn 303 is (7f000001) and launches at T-12 (o.i.sched:312) — it sinks below or/sw 4($v0) and frees $a0 for the 0xE1000200 constant; the target has the same insn at plain (2) winning the T-18 tie only on potential_hazard, memory over ALU (e.i.sched:319-320). Measured alternatives on one base: §30#3 post-use re-tie alone (var_n/var_o) -> correct ORDER, $a0/$a1 swapped, 5 mismatches; ONE temp shared across both addPrim halves -> also kills the boost but becomes a 2-death global allocno (local-alloc.c:472, §76/§136-1) and loses $v1 in the FIRST half, 7 mismatches; pin + re-tie (var_p) -> MATCH but redundant; pin alone (var_r) -> MATCH. The pin propagated to all 4 h_seq siblings (src/ov_SC04_011/…:6518, src/ov_SC06_016/…:5078, src/ov_SC07_002/…:4971), so §162p's 'a register __asm__ pin forfeits the family' cost does not bind on the family_sweep --hseq path.
DIAGNOSTIC TELL. (7f000001) on a load in a -dS ready list, plus a call-clobbered register holding a constant that the target holds one register higher. Before writing any pin, COUNT how many times the function sets the register you are about to pin — that number, not the pin, decides the boost.
Honest scope: no -dS was taken of the pinned build, so the pin -> reg_n_sets > 1 -> no-boost link is read from flow.c plus the re-tie A/B rather than traced; the re-tie control sits after the last use at end-of-block, so it acts as a boost-kill and not as an internal sched barrier.
(SHARPENS — sharpens §147-B (as corrected in the '⚠️ §147 CORRECTED BY THE BYTES' block), §162i1, §162k1, §83c, §163e; evidence: byte-probed; from func_80181B00)
§164-51 — gcc-2.7.2 can reserve stack area BEYOND the sum of declared locals (here +0x10 on 0x28 declared), so sizeof-arithmetic u
§16Xy — THE (u16)-ON-A-HImode-VALUE PAIR COSTS 8 BYTES OF INVISIBLE FRAME PER SITE: the andi shape and the phantom vars are ONE event. (sharpens §162i1's frame arithmetic, §147-B's orphan-slot ablation, and §162k1's andi-count law — none of the three currently connects to the other two.)
Target shape. A narrow (HI/QI) value loaded from MEMORY, branched on, then handed to an SImode use with the OPPOSITE signedness:
lh $v0,TBL($at)
beq $v0,$0,.L
addu $a0,$v0,$zero <- the copy
andi $a0,$a0,0xFFFF <- the re-widen
THE LAW. s16 s = TBL[i]; if (s != 0) f((u16)s, 0); emits that copy+andi pair AND reserves exactly 8 bytes of vars that no instruction ever references — per site, additive. The two are inseparable: s & 0xFFFF emits the andi with no copy and no frame; s32 s and u16 s emit neither. ⇒ If the target has N addu $aN,$vN,$zero ; andi $aN,$aN,0xFFFF pairs, its vars sits 8×N ABOVE the sum of its declared locals.
Mechanism (byte-probed, and it is §147-B's — NOT assign_stack_temp). cc1 -dc -dl -dg on the reproducer: combine leaves (set (reg/v:HI 73) (subreg:HI (reg:SI 75) 0)) then (set (reg:SI a0) (zero_extend:SI (reg/v:HI 73))). .greg reports 2 regs to allocate: 73 76 with 76 conflicts: EMPTY and no disposition (73 in 4 75 in 2) — pseudo 76 appears in no insn at all, so reg_renumber<0 and alter_reg hands it an 8-byte slot that emits nothing.
BYTE EVIDENCE.
- Isolated A/B (4 lines, one call, zero aggregates):
(u16)s→.frame $sp,32 # vars= 8;s & 0xFFFF→.frame $sp,24 # vars= 0(diff is frame-only + the droppedmove);s32 s→ 0;u16 s→ 0;(s16)on au16→ 0; value from a CALL RETURN (already SImode) instead of a narrow memory load → 0. QImode is identical ((u8)on ans8→ 8). Two sites →vars= 16— linear. func_80181B00/ov_SC01_005, banked atsrc/ov_SC01_005/ov_SC01_005_jr_8017ED5C.c:3296(320 ins, twin atov_SC01_006/..._jr_8017ED5C.c:3303): declared localss32 m[8]; u16 sv[4]= 0x28; target.frame $sp,88,$31 # vars= 56, regs= 4/0, args= 16. The 0x10 gap is its twofunc_8002D4C8((u16)snd, 0)sites — delete both blocks andvarsdrops to exactly 40; delete one and it drops to 48. Bytes 0x38..0x47 carry zero$spreferences.
DIAGNOSTIC TELL. Before concluding a dead local exists: count the andi $x,$y,0xFFFF/0xFF sites that are PRECEDED BY A REGISTER COPY, multiply by 8, and add that to your declared-local sum. Only the remainder is a pad. Getting it backwards is a full-function $sp-immediate cascade that reads like a frame-layout wall: the func_80181B00 crack hand-summed to 0x48, inferred a 16-byte dead local, and shipped s32 pad[4] on top of a vars that was ALREADY correct — 0x58 → 0x68, 17 diffs, 16 of them $sp immediates. §162i1's "read vars= off your own draft" is the step that catches it; never derive the pad from sizeof arithmetic.
(SHARPENS — sharpens §48-B THE EBB RULE ("cse resets its hash table at a label with >1 predecessor") and its func_8013F350 corollary ("a pointer-to-global survives only if every use is at offset 0. With p[k], k!=0, cse's ; evidence: byte-probed; from func_80182044)
§164-52 — A POINTER-TO-GLOBAL LOCAL LIVES OR DIES BY THE JUMP-REF COUNT OF THE LABEL IN FRONT OF IT; THE OFFSET IS IRRELEVANT. (bounds §48-B: its corollary "a pointer-to-global survives only if every use is at offset 0 ... for offset uses you need a struct" is FALSE, and its reset criterion "a label with >1 predecessor" is the wrong dial. Supplies the missing precondition to §42-2's cache-vs-recompute oracle and to §32#1/§35's "no cross-bb CSE" shorthand — cse spans basic blocks freely; it is PATHS it cannot span.)
Target shape — a symbol address in the ENTRY block, in a callee-saved reg, serving several NONZERO offsets:
la $s1,D_80126B58 <- entry block, ahead of every branch
... lw $v0,0x10($s1) ... lw $v0,0x18($s1) ... lw $v0,0x34($s1)
LAW. At -O2 cse walks a PATH, not a block: -fcse-follow-jumps carries its hash table through conditional
branches and through every label with ONE jump reference (fall-through predecessors do not count). Only a label
with >=2 jump references starts a fresh table. Inside one path cse knows p == SYMBOL_REF, so fold_rtx
rewrites every p[k] to the absolute lw $v0,SYM+4k; the la loses its last user and is deleted, taking its
callee-saved register and 8 bytes of frame with it. Past a >=2-jump-ref label the equivalence is gone and the
la + k($sN) form survives for EVERY downstream use, at any offset, with no struct needed.
Byte evidence (pinned cc1, -O2 -G0 -mips1 -mcpu=3000; minimal pair, ONE statement apart):
int *p = &D_80126B58;
if (c == 7) goto join;
if (c == 8) goto join; /* delete this line and the pointer local evaporates */
sink(0);
join: ... p[4] ... p[6] ... p[13] ...
2 gotos (join has 2 jump refs) -> la $17,D_80126B58 ; lw $3,16($17) ; lw $3,24($17) ; lw $3,52($17) frame 32
1 goto (join has 1 jump ref) -> lw $3,D_80126B58+16 ; +24 ; +52 no la, no $17, frame 24
Same verdict with the uses grouped after the join, and with init+uses in one block (folds). In-tree:
func_80182044 (ov_SC02_041, 142 ins, banked at src/ov_SC02_041/ov_SC02_041_jr_8017BEBC.c:5441) — its
4-branch entry guard makes $L2 a multi-jump-ref label, and that is the ONLY reason s32 *base = &D_80126B58;
survives to serve 0x10/0x18/0x34 off $s1.
TELL. Target holds a global base in $sN with nonzero offsets: before writing the pointer local, find the
join label between the init point and the first use and COUNT the branches INTO it. Two or more -> the local
works as written. Fewer -> it will silently evaporate into lw $v0,SYM+k and you are grinding the wrong lever
(restructure the guard chain into a real join, or take §48-C1's struct-typed-global route). Converse tell: a
target that re-materialises %hi/%lo at every use is telling you those uses share ONE cse path — spell them as
bare globals, and expect two same-path reads of one global to collapse to a single lw on their own (no temp
local: func_80182044's D_8011F730 == 2 / == 1 pair emits one lw $v1).
(SHARPENS — sharpens §162i1 (THE DEAD-LOCAL FRAME PAD HAS AN 8-BYTE FLOOR — its DIAGNOSTIC TELL is literally this computation), §147-A (the three-stratum frame law + the 30-second measurement recipe, 'run this BEFORE draf; evidence: byte-probed; from func_80183324)
§164-53 — CORRECTION TO THE TELL: the SAVE AREA is 8-aligned BEFORE you subtract it. (the tell as written mis-fires on an ODD save count.)
§162i1 says: compute target_frame − args − 4×regs, 'Δ is always a multiple of 8', and 'if Δ is 4, you do not have a dead local at all'. compute_frame_size (tools/reference/gcc-2.7.2/config/mips/mips.c:4443) actually computes
total = MIPS_STACK_ALIGN(vars) + MIPS_STACK_ALIGN(args) + MIPS_STACK_ALIGN(4 × n_gp_saves)
MIPS_STACK_ALIGN(x) = (x + 7) & ~7 (config/mips/mips.h:2061)
— the save area is rounded to 8 on its own. With an ODD number of saved GP registers the naive subtraction over-reads by exactly 4: the invariant breaks and the escape hatch fires on a draft that is already right.
BYTE EVIDENCE — func_80183324 (ov_SC01_077, 324 ins, banked). Target frame 0x30, args 0x10, regs= 3/0.
- naive:
0x30 − 0x10 − 0xC = 0x14— not a multiple of 8, and §162i1 routes you to §83c/§147-B. - true:
0x30 − 0x10 − ROUND8(0xC)=0x10⇒ vars = 0x10 ⇒s32 pad[4]. cc1 then prints.frame $sp,48,$31 # vars= 16, regs= 3/0, args= 16with the saves at0x20/0x24/0x28, exactly the target (.run/wave3/func_80183324/raw.s:22-33). One line of C closed 20 of the draft's 24 mismatches.
TELL (replaces §162i1's). vars = target_frame − ROUND8(args) − ROUND8(4 × regs), then pad[vars/4] (≥2 elements, §162i1's floor). A Δ of 4 is evidence you mis-rounded the save area, before it is evidence of a spill slot — re-do the arithmetic with the rounding before going to §83c/§147-B/§162i2.
(SHARPENS — sharpens §55a bullet "Switch TREE vs jump table — CASE_VALUES_THRESHOLD is 5" (L4110-4115), §163c "case_values_threshold IS 5" (L11793), §162a1/§162a2/§162a3 (table EDGES — minval/maxval), §163b (switch-index ; evidence: byte-probed; from func_80184938)
§164-54 — THE DISPATCH TOPOLOGY ORACLE: an slti says switch, a bare bne staircase says if/else-if. (sharpens §55a's "Switch TREE vs jump table" bullet and §163c — both give the TABLE-vs-TREE half of the trichotomy and neither names the third branch; and §55a's "Ghidra's if-chain for a switch is a decompiler artifact" points a reader the WRONG way when the target really is an if-chain.)
Target shape — both sides of the threshold inside ONE banked function (func_80184938, ov_SC03_093, 406 ins):
; outer, 6 dense cases -> TABLE
80184944 sltiu $v0,$a1,0x6 ; sll $v0,$a1,2 ; lui $at,%hi(jtbl_801CAF74) ; lw ; jr $v0
; inner (case 5), 4 dense cases -> BARE STAIRCASE, no ordering test anywhere
80184BF8 lh $v1,0xFC($s2) <- discriminant loaded ONCE, lives in $v1
80184BFC addiu $v0,$zero,0x1
80184C00 bne $v1,$v0,.L80184CD0
80184C04 addiu $v0,$zero,0x2 <- next compare constant in the DELAY SLOT
80184CD0 bne $v1,$v0,.L80184DBC ; 3
80184DBC bne $v1,$v0,.L80184E8C ; 4
80184E8C bne $v1,$v0,.L80184F70 <- last miss falls to the epilogue
THE LAW. Below case_values_threshold (=5, §55a/§163c) the SOURCE CONSTRUCT — not the case count — picks the compare topology:
switchwith 4 dense cases →expand_end_case→balance_case_nodes/emit_case_nodes→ a MEDIAN-SPLIT TREE: at least onesltiordering test among thebeqs (li $v0,2 ; beq $v1,$v0,.. ; slti $v0,$v1,3 ; beqz ..).t = expr; if (t==A) … else if (t==B) …→expand_start_cond→ N sequentialbnes in ARM ORDER, one load oft, each next compare constant materialised in the previous branch's delay slot. No ordering test is ever emitted.
READ IT BACKWARDS — THE TELL. For any dispatch of ≥4 arms on one value: any slti/sltu ordering test in the compare region ⇒ write a switch. A pure bne staircase with zero ordering tests ⇒ write a cached local + if/else-if in the arms' own order. It is a two-second read and it was the ONLY change between DIFF and MATCH here.
COROLLARY — KILL THE FALSE TELL. "The discriminant is loaded once and stays in one register across every compare" is not evidence of a switch; a cached local (t = *(s16*)(a0+0xFC);) produces the identical single load. Only the topology discriminates. §55a's Ghidra warning is true of decompiler output and must not be carried over to the target's own compare topology.
Byte evidence. func_80184938, ov_SC03_093, banked whole-binary (commit:1652, R22 213/213). Draft #1 spelled case 5 as a nested switch (*(s16*)(a0+0xFC)) → 412 ins vs 406, 213 mismatched, LENGTH-DRIFT, emitting exactly the median-split tree above. Draft #2 changed nothing but the construct (src/ov_SC03_093/ov_SC03_093_jr_801825B8.c:4647-4711) → MATCH in one iteration. The outer 6-case switch in the same function is untouched and still emits jtbl_801CAF74, so the threshold AND both sub-threshold constructs are byte-witnessed in one banked function.
⚠ Bounds — three, all load-bearing.
- Scope to ≥4 arms. At 2-3 arms
emit_case_nodesdegenerates toward bare equality tests, so aswitchand an if-chain can both emit abnestaircase. The oracle only bites once the tree would need a median. - The threshold is NOT the mechanism on this axis.
case_values_thresholddecides table-vs-tree insideexpand_end_case; switch-vs-if/else is a statement-construct fact upstream of it. (The originating wave note claimed the threshold "is what puts the boundary between these two behaviours" — that sentence is a misattribution; the observable law is unaffected.) - One exemplar for the new half. The tree half is corroborated by §55a (
func_801387B8) and §163c (func_80181BE4); the linear-chain half and the topology tell rest onfunc_80184938alone.
Symptom line: "a jr/switch function 4-8 instructions LONG, with the drift starting at the first compare of a SUB-dispatch" — check the sub-dispatch's construct before touching anything else.
(SHARPENS — sharpens §3-T4 (L90-102), §21 shared join-block placement (L1994-2004), §8 / L1893-1904 duplicate-the-call-into-both-arms, §50-B / §5a cross-jump floor; evidence: byte-probed; from func_80184944)
§164-55 — A return-TERMINATING ARM PLACED AFTER THE FALL-THROUGH ARM COSTS ONE j; THE LEVER IS ARM ORDER, NOT THE GUARD-CLAUSE SPELLING. (sharpens §3-T4, which states this only for a 2-arm if/else whose join is the EPILOGUE and prices it as a duplicated tail; and bounds §21's join-placement entry, which covers a join reached from two arms, not a third arm interposed in front of it.)
Target shape. A 3-way test chain that converges on an INTERIOR common tail, where one arm bails out. Read the target for where the j <epilogue> ; sh <errcode> pair sits:
.L801849C0: bnez $v0, .L801849D0 ; addiu $v0,$zero,0x15 <- test
.L801849C8: j .L80184A94 ; sh $v0,0x2($s0) <- error block INLINE, right after its own test
.L801849D0: …continuation…
.L80184A44: lw $a0,0x20($s0) … sh $v1,0x12($a0) ; addu $a0,$s0,$zero <- merged tail
.L80184A60: lui $a1,… ; jal func_8012B178 <- join, entered by FALL-THROUGH
THE LAW. gcc-2.7.2 has no basic-block reordering pass (bb-reorder is gcc-3.x); RTL block order is the order expand_stmt emitted them, i.e. source statement order. Therefore an arm that ends in return and is written after the arm that falls into the join is emitted between that arm and the join, and the falling-through arm must grow a j over it. Hoist the terminating arm above the fall-through arm and the j disappears. The spelling is irrelevant — a leading if (bad) { err; return; } guard and } else if (bad) { err; return; } else { … } compile to the same bytes. §3-T4's 'write error cases as early returns' is the right advice for the wrong stated reason: what it buys is ORDER, and any construct that delivers the order buys it too.
Byte evidence — func_80184944 (ov_SC06_018, 89 ins, target asm/ov_SC06_018/nonmatchings/ov_SC06_018_jr_8017C24C/func_80184944.s; draft .run/backlog_drafts/func_80184944.c). Three variants through the pinned triple, tools/match_one.py:
| second range-check spelling | ins | verdict |
|---|---|---|
leading guard if ((u32)(v-0x24000) <= 0xA0000) { err; return; } then continuation |
89 | MATCH |
} else if (cont) { Y } else { err; return; } (error arm LAST) |
90 | DIFF, 48 mismatched, LENGTH-DRIFT |
} else if ((u32)(v-0x24000) <= 0xA0000) { err; return; } else { Y } (error arm FIRST) |
89 | MATCH — cc1 .s byte-identical to the guard form modulo label numbers |
In the cc1 .s of the 90-ins form the mechanism is literal: $L6: j $L1 ; sh $2,2($16) (the error block) is emitted between the merged field-0x12 tail $L11 and the common continuation $L5, so $L11 ends j $L5 ; sh $3,18($4). In the 89-ins forms the same tail ends sh $3,18($4) ; move $4,$16 and falls straight into the join. That one j is the entire delta.
⚠ BOUND — the terminating arm must stay at the SAME nesting level as the arm it rejects. Hoisting it above the whole if/else as if (v <= 0xC4000 && (u32)(v-0x24000) <= 0xA0000) { err; return; } restores the count (89) but re-orders the two range tests and flips the first branch: 15 mismatched, class BRANCH-POLARITY (bnez where the target has beqz). Move the arm WITHIN its chain; do not lift it out of it.
THE DIAGNOSTIC TELL. match_one reports LENGTH-DRIFT +1 and the extra insn is a j <join> at the END of the arm that feeds the join, with a 2-instruction j <epilogue> ; sh/sw <errcode> block sitting immediately AFTER it. In the target that same 2-instruction block sits immediately after the test that selects it. Diff the two positions and move the terminating arm up in the source — do not touch polarity, casts, or scheduling first.
⚠ Provenance (R37). func_80184944 is not banked — INCLUDE_ASM at src/ov_SC06_018/ov_SC06_018_jr_8017C24C.c:6948, backlog row 63 ('match_one MATCH but gate rejected — declaration/TU plumbing'). The three-way A/B above is relocation-masked match_one, re-run at vetting time; the law rests on the cc1 .s block order, which is directly readable.
Cross-link, opposite sign: §136g-1 has an early-return 0 guard causing a block relocation (jump.c:1806's if (foo) bar; else break; swap), fixed by wrapping the body in an if. Neither entry is universal — read the target's block order and match it.
(SHARPENS — sharpens §21 (the func_801775E0 CANDIDATE bullet, cookbook L1985-1993: "lbu value with SIGNED compares → write each range bound as a SEPARATE if (…) goto, never a chained ||"), docs/cookbook-index.md L; evidence: byte-probed; from func_80185EF8)
Addendum (P31 S63 t5a, func_8017E2CC): §164-55's price — an arm written after the arm that feeds the join costs one j — holds with no return anywhere and with the join an ordinary interior store; and when the late arm is a single constant assignment, putting it first buys more than the j. Written as the TRUE arm it stops being a block at all: it is the lone REG SET that jump.c's if-then-else → conditional-overwrite collapse (§136d-2, §164-74) hoists above the branch, where dbr sinks it into that branch's own delay slot, and the call-bearing arm then falls straight through into the merge with zero control flow of its own. Byte evidence — func_8017E2CC (ov_SC01_084, 26 ins, target asm/ov_SC01_084/nonmatchings/ov_SC01_084_jr_8017CA80/func_8017E2CC.s), three single-axis rounds on the pinned triple: if (f()==0) { g(); h(); x = *(u8*)(p+0x214)+1; } else { x = 3; } → 27 ins, closeness 8, LENGTH-DRIFT +1, residual[0] = target 24020003 addiu $v0,$zero,0x3 where the draft has nop, plus a spurious j <merge> ahead of the constant; hoisting x = 3; above the if (the Ghidra seed's own shape) → 28 ins, closeness 22, +2 as sw $s1,20($sp) / li $s1,3 — the constant's live range now spans f(), so it is a global allocno and takes a callee-saved register (§76 / §88b's live-range clause, index L31 "copy-assigned-in-an-arm goes global allocno → callee-saved + 2 prologue insns"); flipping polarity to if (f()!=0) { x = 3; } else { g(); h(); x = *(u8*)(p+0x214)+1; } → MATCH 26/26 (banked body shape at .run/t5a/sonnet/func_8017E2CC.c). THE TELL. A trivial li/addiu rD,$zero,K sitting in a bnez/beqz's own delay slot with a side-effecting block falling through into a single shared store ⇒ the constant was the source's TRUE arm and the side-effecting block the else. (§164-57 states the same readback for a two-constant MEM select; here one arm is call-bearing and computed, so §164-73/§164-74's "write the store twice" is the wrong lever — §165-21's bound applies.) ⚠ §3-T4 / §32.2 read this backwards: "put the target's fall-through block in the if" names the call-bearing block, which is exactly the +1 spelling — the polarity and the fall-through you can see are post-collapse and carry no source-order information (same trap as §165-23 and §167-27, both of which invert §3-T4 for their own reasons). Do not "fix" the +1 by hoisting the constant: that is the conditional-overwrite spelling and it pays §76's callee-saved pair instead. Finally, this is the in-tree counter-instance §195-F BOUND 3 asks for — its isolated if (f()==1) r=0; else r=g(); probe found both arm orders byte-identical, but with a MEM consumer and a multi-call arm the order is worth a whole j.
§164-56 — THE PASS IS range_test, NOT fold_range_test; IT IS PAIRWISE ON ONE OPERAND, AND IT FOLDS IN THE OPERAND'S OWN MODE. (promotes §21's UNVERIFIED func_801775E0 bullet to byte-proven, and corrects its name, its scope and its mode.)
Target shape — N comparisons on one operand, each keeping its own signed test:
lh $v1,… slti $v0,$v1,0xF2 … slti $v0,$v1,-0x101 … slti $a0,$v1,-0x4AA <- no mask anywhere
Yours, from hit = x >= 0xF2 || x < -0x101 || z >= -0x106 || z < -0x4AA; — 150 mismatched:
lhu $v0,… ; addiu $v0,$v0,0x101 ; andi $v0,$v0,0xffff ; sltiu $v0,$v0,0x1F3
addiu , 0x4AA ; andi ,0xffff ; sltiu ,0x3A4
THE LAW. fold_truthop (fold-const.c:2745-2756) hands any PAIR of comparison-class terms whose right operands are INTEGER_CST and whose left operands are operand_equal_p to range_test (fold-const.c:2519). It rewrites VAR < LO || VAR > HI — and VAR >= LO && VAR <= HI, VAR == K || VAR == K+1, VAR != K && VAR != K+1 — into (unsigned TYPEOF(VAR))(VAR - LO) <rel> (HI-LO). utype = unsigned_type (TREE_TYPE (var)) at fold-const.c:2613-2619 is the whole story about the mask: the subtract and compare happen in the OPERAND'S OWN MODE, so a short operand emits andi 0xffff + sltiu and an int operand emits neither. It is a fold, so its only barrier is a statement boundary — split the chain into separate if/gotos and every comparison keeps its own signed slt*. It is also pairwise: a 4-term chain over two variables folds to exactly TWO range tests, not one.
TWO CORRECTIONS TO §21, from the pinned tree.
- The name.
fold_range_testis the gcc-2.8/egcs name and does not exist intools/reference/gcc-2.7.2— grep returns nothing. Ours is the staticrange_test. (The banked source comment atsrc/ov_SC03_014/ov_SC03_014_jr_801848E4.c:2960inherited the wrong name; read it asrange_test.) BRANCH_COSTis 1 here, so §21's second hazard is dead.fold_truthop's "it wins to evaluate the RHS unconditionally on machines with expensive branches" arm is guardedresult == 0 && BRANCH_COST >= 2(fold-const.c:2762);mips.h:2935gives 2 only for R4000/R6000 and our cc1 runs-mcpu=3000(tools/permuter/compile.sh:13). On BFM, the range fold is the ONLY thing an||/&&chain buys you — nothing else about short-circuit spelling is observable.
Byte evidence: func_80185EF8 (ov_SC03_014, 304 ins, MATCH, banked commit:1671, checkpoint commit:1674 R22 213/213); fix at src/ov_SC03_014/ov_SC03_014_jr_801848E4.c:2960-2968. Arithmetic checksum on the reported A/B, recomputed from range_test: x < -0x101 || x > 0xF1 → (u16)(x+0x101) > 0x1F2, inverted at the branch = sltiu …,0x1F3; z < -0x4AA || z > -0x107 → (u16)(z+0x4AA) > 0x3A3 → sltiu …,0x3A4. Both immediates match the measured ones exactly — they are not guessable.
DIAGNOSTIC TELL. Count andi $x,0xffff sitting between an addiu and a sltiu. Target has N slt/slti on one operand and NO mask; yours has ⌈N/2⌉ addiu+andi 0xffff+sltiu triples. That mask is range_test's unsigned conversion of a HImode operand and nothing else puts it there. The fix is structural (separate statements) — casts, __asm__ launders and barriers all fail, because gcc re-derives the range across them (§21).
(SHARPENS — sharpens §136d-2 ("gcc-2.7.2 jump.c COLLAPSES an if-then-else into a conditional overwrite", func_80184494, L9105-9113 — states the transform and gives the two-separate-CALLS escape), §46-L3 ("a store merg; evidence: byte-probed; from func_80185EF8)
§164-57 — A CONSTANT-STORE ?: FEEDS §136d-2's jump.c COLLAPSE; TWO SEPARATE STORES CANNOT, BECAUSE A MEM IS NOT A REG. (bounds §136d-2 to REG destinations and gives the store case a cheaper escape than its two-separate-CALLS lever.)
Target shape — one shared store, TRUE value in the delay slot:
bnez $v0,.L ; ori $v0,$zero,6 … sh $v0,0x2($s1) <- one `sh`, reached both ways
Yours, from *(u16 *)(p + 2) = (rand() & 1) ? 6 : 8;:
beqz $v0,.L ; li $v1,8 <- FALSE value, wrong register, wrong polarity
…plus the next call's move $a0,$s1 hoisted above the store.
THE LAW. *(T*)p = c ? A : B; never stores in the arms. store_expr offers the MEM to expand_expr as original_target, but expr.c:5771-5791 reuses it only when GET_MODE (original_target) == mode — the ?: has type int, so HImode MEM ≠ SImode mode → temp = gen_reg_rtx (mode) and both arms store_expr into a pseudo (expr.c:5987-6009), with one sh after the join. That pseudo pair is exactly the input to §136d-2's transform at jump.c:728-760, whose gate is GET_CODE (temp1 = SET_DEST (temp4)) == REG; it rewrites the pair to t = B; if (c) t = A; and deletes the jump around the set (jump.c:821), leaving the FALSE constant immediately before the branch where dbr sinks it into the delay slot.
Write the two stores explicitly and the destinations are MEMs, jump.c:731 fails, the arms survive to post-reload cross_jump, which merges the identical sh tail, and jump.c's invert-over-j then flips beqz→bnez:
if (rand() & 1) { *(u16 *)(p + 2) = 6; } else { *(u16 *)(p + 2) = 8; }
So §136d-2's escape has a sibling: a call is not a simple SET, and neither is a MEM. Use the two-calls form when the selected value feeds a call; use two stores when it feeds memory.
Byte evidence: func_80185EF8 (ov_SC03_014, 304 ins, MATCH, banked commit:1671), banked shape at src/ov_SC03_014/ov_SC03_014_jr_801848E4.c:3029-3033. Pass order matters and is checkable: jump1 runs at toplev.c:2827, before cse1 at :2865, so this reshaping is upstream of everything else you might blame.
DIAGNOSTIC TELL. Compare the branch's delay slot against the two constants. Target: the TRUE arm's constant, on the polarity that reaches the FALSE arm by fall-through. Yours: the FALSE constant, opposite polarity, other scratch register. That triple — polarity + which constant + which register, all flipped together — is the ternary's signature. Do not chase it with §2-T4's polarity invert or a register __asm__ pin; all three are symptoms of one source construct. (Distinct from the index's $v1-vs-$v0 entry → §76, which is the same select landing in a LOCAL and is levered by variable reuse, not by block structure.)
(SHARPENS — sharpens §45 Lever A (merged accumulator variables break a whole-function permutation; global.c:594 allocno_compare; "one hard reg spanning disjoint regions can only come from one reused source variable"), §; evidence: byte-probed; from func_80186C4C)
**§164-58 — When a 2-register callee-saved perm is caused by an over-ranked short-lived pseudo, MERGE it into a same-class variable **
§16Xa — THE MERGE CAN DEMOTE, NOT ONLY PROMOTE — and the step in floor_log2(R) means ONE ref can decide a callee-saved grant. Target shape: a clean 2-register swap where the target holds the PARAMETER copy in $s0 and a call-result in $s1, and yours mirrors it (move s1,a0 … beqz s0 where the target has move s0,a0 … beqz s1), everything else byte-identical.
THE LAW. global.c:594 allocno_compare ranks by floor_log2(R)·R/L. Merging a short high-density allocno into a longer-lived one moves BOTH terms: once floor_log2(R) has saturated (R ≥ 16), extra refs are free and the added live length strictly LOWERS priority. §45-A and §156-1 merge to RAISE priority (their merges multiply loop-weighted refs); the direction is arithmetic, not doctrine — evaluate it, never assume it. Corollary, and the sharper half: because the multiplier is floor_log2, an edit that changes R by ONE across a power of two (16→15) moves priority as much as a 60-insn range change. Any edit that changes a variable's reference count is a register-allocation edit, including a load-CSE hoist you made for instruction count.
BYTE EVIDENCE (func_80186C4C, ov_SC03_014 / ov_SC03_015, 273 ins; pinned triple, -dl -dg allocno dumps; param copy = R30/L172 → pri .698):
| variant | r allocno |
pri | outcome |
|---|---|---|---|
separate result var, two r->0xCC reloads (v1.c) |
R16 / L73 | .877 | r→$s0, param→$s1 — 112 mismatched |
merged table+result (vA.c) |
R20 / L132 | .606 | param→$s0 — allocation correct |
separate vars + r->0xCC hoisted (vD.c) |
R15 / L72 | .625 | param→$s0 — MATCH 273/273 |
merged + hoisted (vC.c, banked) |
R19 / L131 | .580 | MATCH 273/273 |
⚠ The banked header credits the MERGE; that is refuted as a necessity. Two separate locals match, independent of declaration order and init placement (3 variants). The edit that crosses the line in the shipped body is the load hoist's −1 ref.
DIAGNOSTIC TELL. A clean callee-saved swap between a parameter copy and a call-result whose ref count sits just above a power of two (16, 32). Dump -dl and evaluate floor_log2(R)·R/L for the two contenders BEFORE editing (§137's two-compile method): you will usually find you can cross the step by deleting one reference (hoist a reload, drop a dead re-read) instead of merging variables or pinning.
(SHARPENS — sharpens §2-T2 (source statement order drives instruction scheduling), §25 "The genuine scheduler — rank_for_schedule" (PRIORITY → CLASS vs last_scheduled_insn → LUID = source order; "gcc fills an address-gen→; evidence: byte-probed; from func_80186C4C)
§164-59 — -O2 schedules the statement FOLLOWING a multiply into the gap between that multiply's mult and its mflo; with two mu
§16Xb — THE mult→mflo WINDOW IS A SOURCE-POSITION ORACLE. Target shape — an ENTIRE unrelated statement parked between a multiply and its mflo:
mult v0,v1
lui v1,0xffff ; lw v0,72(s1) ; ori v1,v1,0x4000 ; addu v0,v0,v1 ; sw v0,72(s1) <- the whole `r->0x48 -= 0xC000` statement
lw v0,12(s1)
mflo t0
THE LAW. gcc-2.7.2 emits mult and mflo as separate insns and sched1 fills the window between them with the next LO-independent insns in LUID (= source) order (§25's rank_for_schedule: PRIORITY, then CLASS vs last_scheduled_insn, then LUID). The window is multi-cycle, so it swallows a whole statement, not one filler insn. Consequence: with two multiplies in one block, WHICH window holds the filler is a literal read-back of that statement's position between them — a source-shape oracle, not a tie-break to permute.
BYTE EVIDENCE (func_80186C4C, ov_SC03_014, 273 ins). Matching body: the r->0x48 statement follows the r->0xC statement in source and lands in the SECOND multiply's window (stream idx 191-198 above). Moving it up between the r->0x4 and r->0xC RMWs puts the same 5 insns in the FIRST multiply's window: 18 mismatched at 273 ins (.run/wave3/func_80186C4C/vC.c vs the same file with the statement raised). The first window in the matching body holds only lw v0,4(s1) — part of the multiply's OWN statement — so read the rule as "next independent insns in source order", not "next statement".
DIAGNOSTIC TELL. A LENGTH-DRIFT 0 residual whose diff block opens on a mult and closes on its mflo, with the same instructions present on both sides in a different order ⇒ do not touch registers, scheduling pins or the permuter: move the statement that follows the multiplying statement. With N multiplies you get N gaps and N source positions, and they read one-to-one.
(SHARPENS — sharpens §21-family bullet (line ~1883): "force a memory-operand RELOAD … Hoisting the field into one local (int t = *(p+K); …; use t;) instead keeps it in a reg → a single lw, misses" (evidence func_80168; evidence: byte-probed; from func_80186C4C)
§164-60 — Two literal *(s16 *)(*(s32 *)(r + 0xCC) + K) stores re-load the pointer because the intervening store is may-alias and
§16Xc — A LOAD-CSE HOIST IS ALSO A REF-COUNT EDIT (the §21 hoist, read through global.c). The §21 bullet's two arms are about instruction COUNT: hoist *(p+K) into a local ⇒ one lw; leave it literal with an intervening store ⇒ two. Both arms also move allocno_compare. The hoisted local absorbs the second reference, so the POINTER variable loses one ref — and because the rank multiplier is floor_log2(R), losing the ref that carries R across a power of two moves the grant as hard as a 60-insn live-range change.
BYTE EVIDENCE (func_80186C4C, ov_SC03_014): the matching body loads r->0xCC once into $v1 and stores both halfwords off it. Restoring the two literal derefs is +2 instructions AND takes r from R15/L72 (pri .625) to R16/L73 (pri .877) — past the parameter copy's .698 — so r seizes $s0, the parameter takes $s1, and the residual reads 62 mismatched, not 2 (.run/wave3/func_80186C4C/vC.c vs the same file with c inlined). An agent triaging that 62 as "whole-function REGALLOC-PERM" would never look at a load hoist.
DIAGNOSTIC TELL. You made a ±2 ins CSE edit and the diff moved by 30+ with a callee-saved pair swapped ⇒ you changed a ref count across a floor_log2 step, not the schedule. Same shape as §162b1: one source edit, two passes, and the second pass owns the symptom you can see.
(SHARPENS — sharpens §161c (LOOSE-PROTOTYPE ENGINE HELPERS: jal + addu $a0,$s0,$zero in the delay slot ⇒ it really takes an argument), §32-1 / line 2425 (no cross-bb CSE / copy propagation in gcc-2.7.2), §162f1 (a NON; evidence: byte-probed; from func_80186C4C)
§164-61 — The arg-copy delay-slot tell reads a callee's arity only for calls OUTSIDE the parameter's own basic block; a call in th
§16Xd — THE ARG-COPY ARITY TELL IS BASIC-BLOCK SCOPED (bounds §161c). §161c reads jal f ; addu $a0,$s0,$zero as proof that a loose-prototyped helper really takes an argument. The converse does not hold, and one function shows both halves:
0 addiu sp,sp,-40 ; sw s0,24(sp) ; move s0,a0 ; …
8 bgtz v0,0x38
9 sw v0,28(s0)
10 jal func_8012C218 <- entry block: NO arg copy, delay slot is a real `nop`
11 nop
…
14 jal func_8012CBCC <- branch-target block
15 move a0,s0 <- the copy §161c reads
THE LAW. gcc-2.7.2 has no cross-BB copy propagation (§32-1). Inside the block that defines the parameter, the incoming $a0 still holds it, so passing it costs NOTHING and emits nothing; from any later block the value lives in $sN and the copy must be emitted. A nop delay slot on a call in the parameter's own block is therefore compatible with BOTH arities and carries no information.
BYTE EVIDENCE (func_80186C4C, ov_SC03_014, 273 ins). Spelling the entry-block call ((void (*)(void))func_8012C218)(); and spelling it func_8012C218((void *)a0); compile to identical bytes (273/273, 0 mismatched). The crack agent read the nop as proof of a zero-argument callee and bought the §17a-1 cast idiom to dodge the TU's void * prototype; it bought nothing.
DIAGNOSTIC TELL. Before reading arity out of a delay slot, ask which block the jal is in. Only calls whose block is NOT the parameter's definition block are evidence. When the only witness is an entry-block call, the arity is UNDETERMINED from bytes — take it from another call site, a sibling overlay, or the TU's existing decl, and write the prototype-conforming call (never a cast that asserts an arity you cannot prove).
(SHARPENS — sharpens §36 KEEPALIVE KILLS THE DYING-HARD-REG SUGGESTION (L2498) — shows only the single-input form __asm__("" :: "r"(fc)); and never warns the placement can be lost, §34 ZERO-BYTE ASM TOOLKIT (L2460-2461); evidence: byte-probed; from func_801874E4)
§164-62 — A §36 keepalive is not a scheduling barrier: written after the consuming insn but naming only the dying source, sched1 p
§16x — A §36 KEEPALIVE IS NOT A BARRIER: ANCHOR IT BELOW THE CONSUMING INSN OR IT DOES NOTHING. (sharpens §36, §80-R7; bounded by §34's toolkit; adds the missing 4th branch to §162j1's triage)
Target shape — a one-register diff where the target computes into a fresh caller-saved reg and yours reuses the dying operand's own register as the dest:
mine: andi $a0,$a0,0x1 ; beqz $a0,… <- `flags` pinned to $a0, dies here
target: andi $v0,$a0,0x1 ; beqz $v0,…
LAW. local-alloc.c:1813 records the hard reg in qty_phys_sugg for the result quantity whenever a hard-reg operand feeds an insn setting a pseudo (§80: unconditional, no death guard), and find_free_reg (local-alloc.c:2150) tries suggested regs first — but the grant only lands if that hard reg is free across the quantity's range, i.e. only if it DIES in that insn. §36's cure (keep it alive) is right; §36's form is not portable. A gcc-2.7.2 asm written without volatile and with only inputs is a plain non-volatile ASM_OPERANDS — stmt.c:1502 sets MEM_VOLATILE_P from the volatile keyword alone, there is no implicit-volatile-on-zero-outputs in 2.7.2 — and sched.c:1957 takes the clobber-everything barrier path only for ASM_INPUT / UNSPEC_VOLATILE / volatile ASM_OPERANDS. (A bare asm("") with no colons is ASM_INPUT, stmt.c:1340, and IS a barrier — that is why §47/§153's barrier language does not carry over.) So the keepalive is an ordinary schedulable insn carrying exactly the deps of its named operands = §34's "input-only dummy floats to v's def". Written after the consuming insn but naming only the dying source, it schedules ABOVE it and the REG_DEAD note stays on the consuming insn. Name a value whose def is at or below the consuming insn — normally that insn's own result (§34's "multi-input dummy anchors at the LATEST def").
f1 = flags & 1;
__asm__("" :: "r"(flags), "r"(f1)); /* the SECOND operand is the whole anchor */
if (f1 != 0) { … }
BYTE EVIDENCE — func_801874E4 (ov_SC06_018, 94 ins). One-line A/B, drafts + full -da dumps preserved at .run/wave3/func_801874E4/try/{D,E}.c and try/rtl{D,E}/; diff D.c E.c is exactly one line.
asm("" :: "r"(flags))→t.semits#APP/#NO_APPbeforeandi $4,$4,0x0001;.lreginsn chain 49→47 withREG_DEAD (reg/v:SI 4 a0)on the andi;t.i.lreg:107 ';; Register 80 in 4.'asm("" :: "r"(flags), "r"(f1))→#APP/#NO_APPafterandi $2,$4,0x0001; chain 47→49 withREG_DEAD (reg/v:SI 4 a0)on the asm;t.i.lreg:107 ';; Register 80 in 2.'= the target.
SCOPE. The anchor operand is needed only when the dying source's own def sits ABOVE the consuming insn. Same function, second site: func_8001C924(…, D_801B53A4[mode]); __asm__("" :: "r"(mode)); matches single-input (and the sll's result is an internal address temp with no C name anyway). Do not read the rule as "always two operands"; read it as "anchor on something defined at or below".
DIAGNOSTIC TELL — and the 4th branch §162j1 is missing. A one-register diff where the target's dest is a fresh $v0/$v1 and yours is the operand's own register, with the pseudo NUMBER identical in every -da dump and only its COLOUR differing. That is neither §162j1 (which needs the pseudo number to change between .sched and .lreg) nor §25's combine_regs tie — §162j1's "never changed at all ⇒ pins" branch misroutes it. Confirm in two greps: ';; Register N in H.' for the dest pseudo in .lreg (it IS allocated there — this is local-alloc, not global-alloc), and REG_DEAD (reg … <hardreg>) on the consuming insn. Free second tell once you have written a keepalive: grep -n '#APP' t.s — if the #APP block sits on the wrong side of the insn you wrote it after, nothing is anchoring it there and the lever is inert.
(SHARPENS — sharpens §67 (L5315-5403) — the arg-copy PLACEMENT lever; same symptom line 'mirrored sw $sN / move $sN,$aX prologue pairs', and its rule 1 'Do not pin the laundered variable to a hard register', §136-16 (; evidence: byte-probed; from func_801874E4)
§164-63 — With TWO hard-reg-pinned entry values, the prologue sw $sN/move $sN,$aX pair order is invariant to C declaration ord
§16x — TWO HARD-REG-PINNED ENTRY VALUES: THE PROLOGUE PAIR ORDER IS SET BY AN INTERPOSED ZERO-BYTE asm, AND BY NOTHING ELSE YOU WOULD TRY FIRST. (sharpens §67 rule 1, §136-16, §36's "pins wreck the prologue save-birthing order"; bounds §52's decl-position rule)
Target shape:
target: sw $s1,0x24($sp) ; addu $s1,$a0,$zero ; sw $s0,0x20($sp) ; addu $s0,$a1,$zero
mine: sw $16,32($sp) ; move $16,$5 ; sw $17,36($sp) ; move $17,$4
§67 diagnoses this pair order correctly as a copy-PLACEMENT problem and hands you an unpinned launder; §67 rule 1, §136-16 and §36 all then say do not pin the incoming parameter. When the pins are load-bearing that advice is a dead end — here de-pinning flags alone takes the draft from 0 to 60+ mismatches — and this is the version of the lever that survives pins.
LAW. With both values held in register … __asm__("$N") locals, the sw/move pair order is invariant to (a) declaration order, (b) which hard reg each pin names, (c) which parameter each takes, and (d) reference count, in the entry block or function-wide. The one dial that moves it is a zero-byte non-volatile input asm naming ONE of the two, occupying a slot between their two SETs. C89 forbids a statement between two declarations, so split the later pin from its initializer:
register s32 obj __asm__("$17") = a0;
register s32 p1 __asm__("$16"); /* no initializer */
… /* rest of the decl block */
__asm__("" :: "r"(obj)); /* <- the entire lever */
p1 = a1;
BYTE EVIDENCE — func_801874E4 (ov_SC06_018, 94 ins, 4/94 → MATCH). Every row is a one-edit A/B off .run/wave3/func_801874E4/try/E.c, re-measured through the pinned cc1:
| variant | edit | prologue |
|---|---|---|
E.c |
baseline, both pins initialized at decl | $16(a1) pair first |
F.c |
the two declarations reversed | byte-identical to E |
G_diag.c |
pins swapped $16↔$17 |
same variable's pair still first |
| param-swap probe | obj=a1, p1=a0, body unchanged |
$16(p1) pair still first |
+4 refs on p1 fn-wide / +3 refs on p1 inside the entry block |
order unchanged, both times | |
H2.c minus the asm line |
decl/init split alone | reverts to $16 first |
H2.c |
decl/init split + __asm__("" :: "r"(obj)); |
$17(a0) pair first = target |
Final draft assembles to the target's 94-word stream with the only differences being j/jal targets and %hi/%lo immediates (13 relocated words).
⚠ DO NOT SIZE THIS WITH REF-COUNT ARITHMETIC. The producing agent's stated reason — "the LESS-referenced of {obj,p1} always schedules first when both are hard-reg pins" — is byte-refuted by rows 5-6 above: taking p1 from 1 to 4 entry-block mentions (and separately +4 function-wide) does not move the pair. This is a sched1 dependency/LUID effect (the asm cannot be scheduled above obj's set and takes the slot between the two sets), not an allocno_compare density story. Reach for §158a/§148-C arithmetic only when the register LETTERS are wrong, not their ORDER.
DIAGNOSTIC TELL. §67's tell — mirrored sw $sN/move $sN,$aX prologue pairs — with the register letters already correct, both values pinned, and the instruction count exact. Wrong letters instead means allocation (§162m/§158a), not this. Before spending the asm, de-pin one value as a control: if the residual barely moves, the pins were not load-bearing and §67's unpinned launder is the cheaper, propagation-safe route (a register __asm__ pin fails dedup_propagate.compiles_standalone, §37 — this draft banks ×1 and forfeits its family).
(SHARPENS — sharpens §48-A2 (the local-alloc $s0 OCCUPANT — a call-crossing block-local temp enters regs_used_so_far and pushes the arg0 copy from $s0 to $s1), §76 bullet 2 (global.c:668-671 re-marks locally-placed pseu; evidence: byte-probed; from func_80188B84)
§164-64 — A LOCAL-ALLOC'd EXPRESSION TEMP CAN CLAIM $a0 AND EVICT PARAMETER 1 FROM ITS INCOMING REGISTER. (SHARPENS §48-A2 and §76-2: both state that a local-alloc placement removes a hard reg from the global pool, but only for a NAMED call-crossing temp taking $s0 and pushing the arg0 COPY to $s1. Neither covers an argument register, an ANONYMOUS expression temp as the occupant, or the prologue copy it manufactures.)
Target shape — the incoming arg register is used in place, and the only copy is the one the target actually has:
target : <no prologue move> ; arg0 stays in $a0, the arg1 copy is `addu $t0,$a1,$zero`
mine : move $t1,$a0 ; + every downstream arg-register assignment permuted
THE LAW. n = A * B; as ONE expression gives the first operand an anonymous single-block pseudo. local-alloc runs first and first-fits it ($v0, $v1, then $a0 — mips.h defines no REG_ALLOC_ORDER). global.c:668-671 then re-marks every locally-placed pseudo as a hard register for the global conflict scan, so parameter 1's allocno conflicts with $4; prune_preferences (global.c:849-862) ANDs hard_reg_conflicts out of hard_reg_copy_preferences, stripping the $4 preference that set_preference recorded off the prologue's (set pseudo (reg 4)) — so find_reg cannot grant it and the copy survives as a real move. Write the product IN PLACE — n = A; n = n * B; — and the first load lands directly in n's own global allocno: no anonymous local exists, $a0 is never claimed, the parameter keeps its incoming register, and the whole prologue falls into place.
(Citation care: set_preference, global.c:1535-1619, has NO conflict test — it records the preference unconditionally. The conflict bites in prune_preferences. Do not cite :1535 for this.)
Byte evidence. func_80188B84 (166 ins, banked whole-binary in ov_SC04_018 + ov_SC04_019). Single-expression form vs the banked two-statement form (n = D_80115159[(s16)arg2*2]; n = n * D_80115158[(s16)arg2*2];): 50 mismatched → clean prologue, one source edit, nothing else changed.
THE DIAGNOSTIC TELL. A surviving move $tN,$a0 (or any incoming-argument copy) near the top of the function, plus an argument-register permutation downstream, while the instruction SHAPE is already exact. Do not reach for pins. Find the multi-operand expression nearest the top of the function and split its FIRST operand into the destination variable. Confirm before editing: cc1 -da, read the ;; N conflicts: hard-reg tail of .greg (§76) and see which hard reg the occupant took — $s0 is §48-A2's form, $a0-$a3 is this one. (Scope: one function. The mechanism is source-cited and shared with §48-A2/§76; the anonymous-temp TRIGGER is n=1.)
(SHARPENS — sharpens §34 ref-boost bullet (L2516: __asm__("" :: "r"(v)) at block top → +1 flow-ref, crosses the floor_log2 step, v wins the reg over a short block temp), §148-C (the zero-byte ALLOCNO-PRIORITY slider — m; evidence: byte-probed; from func_80188B84)
§164-65 — BUY +2 ALLOCNO REFS BY NAMING A VALUE YOU WERE GOING TO CONSUME INLINE. (SHARPENS §34's ref-boost, §148-C, §158a and §156-1. All four already say that crossing a floor_log2 step wins the low register; their levers are an empty asm, a do{}while(0) wrapper, or MERGING two real roles. This is the third and cheapest form, and it costs no construct at all.)
if (f(c) & 0x20) { … } /* 0 refs bought */
n = f(c); if (n & 0x20) { … } /* +2 refs on an EXISTING local, zero bytes */
THE LAW. global.c:594 pri = floor_log2(refs)·refs·size / live_length. A def+use pair of an already-declared variable is +2 refs for ~+2 live insns, so it is the cheapest way to cross a floor_log2 step from ordinary C. Measured on func_80188B84: n at 6 refs / 47 insns = 0.26 lost the first free low register to the per-case j allocnos (0.33–0.6); reusing n as case 14's second probe temp took it to 8 refs, floor_log2 2→3, pri ≈ 0.51 — n allocates first, takes $v1, and j falls to $a1/$a0 exactly as the target. Precondition: the variable must be DEAD at the reuse site (otherwise you have §156-1's merge, with its call-crossing → callee-saved consequence).
Byte evidence. func_80188B84 (166 ins, banked whole-binary in ov_SC04_018 + ov_SC04_019), n = func_800291B4(c); if (n & 0x20) … — this closed the last 23-instruction residual.
THE DIAGNOSTIC TELL. Identical to §158a's: a long-lived value and a short-lived per-block temp in swapped registers, densities within ~0.05. Before minting a do{}while(0) (§158a) or an empty asm (§148-C/§34), scan the rest of the function for a site where a FRESH temp holds a call result — give that site the long-lived variable instead. Size it first (§158 steps 1-2, cc1 -dl).
⚠ Honesty. The edit was found by the permuter, not by reading; the ref arithmetic is post-hoc and uses the pre-edit live_length. The byte outcome is gated; the causal story is reconstructed.
(SHARPENS — sharpens §36 (VOLATILE-FRAME-PARITY bullet, L2490), §147-B corrected (L10120-10124), §162i1 (L11367-11389), §21/T6 forced-reload bullet (L1816-1831); evidence: byte-probed; from func_8018A974)
§164-66 — A VOLATILE UNSIGNED SUB-WORD READ FEEDING A C BITFIELD ASSIGNMENT STRANDS AN ORPHAN PSEUDO: +8 BYTES OF FRAME THAT NOTHING REFERENCES. (sharpens §36's VOLATILE-FRAME-PARITY, which records the SAME orphan-slot class at the OPPOSITE polarity, and supplies §162i1's missing negative-Δ arm.)
Target shape. Every opcode and register right, regs= right, and every $sp immediate off by ONE constant, with your frame 8 bytes LARGER than the target's — and grep '(sp)' on your .s shows the extra 8 bytes are never addressed.
THE LAW. Under the pinned cc1 -O2 -G0 -mips1 -mcpu=3000, two or more reads of a volatile UNSIGNED sub-word lvalue (u8/u16) assigned into a C bitfield member leave an unallocated allocno — .greg prints ;; 1 regs to allocate: N for a pseudo that appears in no insn — and alter_reg hands it an 8-byte stack slot that emits nothing. §21/T6's volatile u16 *bidx = &D_800B9A02; CSE-defeat lever + the PSY-Q addPrim bitfield idiom (pk->addr = …; otp->addr = …;) is exactly that pair, so every OT-insert body that uses both carries a hidden +8.
Every neighbour is INERT — this is far narrower than "volatile costs frame" (all measured on the pinned cc1; probes at .run/vet_8018A974/):
| variant | vars= |
|---|---|
volatile u16×2 → bitfield (A2) · volatile u8×2 → bitfield (T2) · volatile GLOBAL, no pointer local (O2) · two different objects (P2) |
+8 |
one volatile read (A1) · one read cached in a local, two inserts (N2) |
0 |
volatile s16×2 → bitfield (U2) · volatile u32×2 → bitfield (Q2) |
0 |
volatile u16×2 → ordinary s16/u8 members (J2,K2,R2) · in arithmetic (E2,F2,G2,H2,I2) |
0 |
the same RMW hand-written instead of as a bitfield (S2) · non-volatile ×2 (B2,M2) |
0 |
non-volatile u16 * + __asm__("":::"memory") between the reads (C2) — still emits 2 lhu |
0 |
⇒ zero_extend is the discriminator. The volatile MEM blocks combine from folding (zero_extend (mem/v:HI)) into the lhu; the value survives as (subreg:SI (reg:HI 81) 0) (A2.i.lreg insn 39) and the SImode extension pseudo is stranded. The s16 and u32 spellings never build it.
THE LEVER, and it is free. The memory-clobber form of §21's reload lever pays no tax. When your frame is +8 and you are holding the reload open with volatile, swap to u16 *p + a non-volatile __asm__("":::"memory") at each reload site before touching pins, scoping or spelling.
DIAGNOSTIC TELL. Frame exactly 8 over, regs= exact, extra 8 bytes never addressed. This is §162i1 run backwards — its "Δ a multiple of 8 ⇒ declare s32 pad[Δ/4]" is the target-larger arm only. (func_8018A974, ov_SC06_018, 99 ins, NEAR 16: the excess is 14 of the 16 mismatched lines.)
Do NOT cite func_80185944 as corroboration — its banked header (src/ov_SC03_119/ov_SC03_119_jr_8017FB84.c:4634) attributes its 8-byte temp to "the two symbol+register memory references (both D_800A651C)", and its volatile reads feed an index multiply, not a bitfield insert.
(SHARPENS — sharpens §52-2 (L3929-3932, cites 'loop.c:3824 worthwhile test' with no arithmetic), §148-A (L10148, the move_movables threshold at loop.c:1631 — a DIFFERENT threshold), §158a/§162m (L11537, the allocno-priori; evidence: byte-probed; from func_8018E8A0)
§164-67 — THE giv "NOT WORTH WHILE" TEST IS ARITHMETIC YOU CAN COMPUTE — AND A LONE ADDRESS-GIV IS NEVER REDUCED. (sharpens §52-2, which cites loop.c:3824 without the numbers; this is §148-A's counterpart for strength reduction rather than invariant motion — two different thresholds, do not mix them)
reduce iff lifetime x threshold x benefit >= insn_count (loop.c:3823)
threshold = (loop_has_call ? 1 : 2) * (3 + n_non_fixed_regs) (loop.c:3241; ~31 with a call)
benefit = 2n - add_cost * bl->biv_count (loop.c:3804)
add_cost = rtx_cost(PLUS,SImode) = COSTS_N_INSNS(1) = 2 (loop.c:307, mips.h:2764-2781)
n = giv RECORDS in the group; combine_givs sums BOTH benefit and lifetime (loop.c:5517/5521)
insn_count= the whole loop's real insns, printed by `cc1 -dL`
THE UNIVERSAL COROLLARY (no loop-specific input needed). n=1 ⇒ benefit 0 ⇒ 0 < insn_count always ⇒ a single address-giv off a biv is NEVER strength-reduced — it stays k($biv). Corollary of the corollary: a read-modify-write of one address contributes TWO records (find_mem_givs recurses over every MEM in the pattern, loop.c:4188-4212, so the load MEM and the store MEM each call record_giv), so *p += x can clear a floor that a lone load cannot. This is the dial that decides whether an offset keeps its own register or collapses to a displacement.
BYTE EVIDENCE. func_8018E8A0 (ov_SC02_011). .run/wave3/func_8018E8A0/dump/t.i.loop:65 — giv of insn 93 not worth while, 0 vs 142, against that dump's own Loop from 77 to 432: 142 real insns; sw/v6_pq8/t.i.loop:67,69 — 0 vs 143 twice. Three independent n=1/benefit-0 refusals, dumped.
⚠ DO NOT MEMORISE A RECORD COUNT. Because lifetime is the SUM over the combined group and insn_count is the whole loop's, the n=2 / n>=3 boundary MOVES per loop. The wave-3 note that produced this section asserted "n=2 never reduces (124 < 143), n>=3 always" — that is one loop's numbers with lifetime=2 back-solved from the formula, it was never dumped, and it is simply false for a long loop with short-lived givs. Evaluate the product; only the n=1 floor is a constant.
DIAGNOSTIC TELL. An offset you expected in its own register stayed k($biv), or vice versa, and the IV COUNT is the diff. Do not reshape the C first: run cc1 -dL, which prints both sides of the comparison verbatim (giv of insn N not worth while, X vs Y) and names the insn.
Symptom lines for the index: "one induction register too few" · "the offset did not get its own register" · "not worth while in the loop dump" · "how many accesses do I need to force a giv".
(SHARPENS — sharpens §153 (THE ADDRESS-REMATERIALISATION LAUNDER, L10463), docs/gcc-2.7.2-map/cse_expr.md §2 (cross-call ADDRESS caching, hoist-vs-remat), cookbook-index.md:18 ("the address hoisted into a callee-saved reg; evidence: byte-probed; from func_8018FF98)
§164-68 — A BLOCK-MOVE DESTINATION IS A CSE REFERENCE: the memset pointer must not be cse-equal to the struct-assign destination. (sharpens §153, whose trigger is "an address ARGUMENT used twice in one block" and whose symptom line does not fire here; and it inverts gcc-2.7.2-map/cse_expr.md §2's placement rule.)
Target shape — a clear-then-seed pair on one global, the address materialised TWICE:
lui $a0,%hi(SYM) ; addiu $a0,$a0,%lo(SYM)
jal func_800233CC ; li $a1,0x80
lui $a1,%hi(SRC) ; addiu $a1,$a1,%lo(SRC)
lui $a0,%hi(SYM) ; addiu $a0,$a0,%lo(SYM) <- RE-MATERIALISED after the call
lwl $v0,0x3($a1) ; lwr $v0,0x0($a1) ; nop ; swl $v0,0x3($a0) ; swr $v0,0x0($a0)
THE LAW. f(SYM, n); SYM[0] = SRC[0]; gives the symbol ONE pseudo (the block move's destination address is forced through copy_to_mode_reg), live across the call. cse never kills a pseudo at a call — invalidate_for_call (cse.c:1725-1760) iterates regno < FIRST_PSEUDO_REGISTER and its table sweep continues on any REGNO >= FIRST_PSEUDO_REGISTER. local-alloc.c:1080's remat path is gated reg_n_refs == 2 && reg_basic_block < 0 and cannot fire on a single-block 3-ref pseudo, so global.c hands it a callee-saved register. The target's double la is what you get when the two references are NOT the same rtx: the call's copy exists only in hard $a0, which the call DOES invalidate. ⇒ the second reference does not have to be an argument. Any cse reference in the same block — a struct assignment, an array store, a second call — is enough.
CURE — launder the CALL's pointer, BEFORE the call, §153's re-tie form:
mp = (u8 *)SYM; __asm__ __volatile__("" : "=r"(mp) : "0"(mp)); f(mp, 0x80);
SYM[0] = SRC[0];
Which side you launder is load-bearing, and it is the OPPOSITE of cse_expr.md §2. That entry (two call SITES, a stack local) kills the class with an output-only asm placed after the first call, because its value must be dead. Here the laundered value IS the argument, so it must stay live: re-tie form, before the call. Laundering the later reference is a no-op — byte-probed.
BYTE EVIDENCE (.run/wave3/func_8018FF98/, pinned cpp|cc1):
probe1.cp_a(no launder) →la $16,D_801D59F0 ; move $4,$16,swl/swr … ($16).p_b(launder on the DESTINATION, after the call) → identical defect.probe2.cp_c/p_e→ the target shape (la $4,SYMat the call, freshlaper block move).- Full function:
d2.txtidx 26-47 is the defect against the target (mine=384 / target=385,sig=LENGTH-DRIFT/-1); the banked launder formsrc/ov_SC06_018/ov_SC06_018_jr_8018FF98.c:3232-3235gates MATCH 385/385, whole-binary green.
DIAGNOSTIC TELL — and a correction to §153's index line. §153 indexes this class as "an extra sw $sN in the prologue". That does not fire when the register is already in the target's save set — here regs= matches exactly and the drift is −1, because the hoist is one instruction cheaper than the target's two las (which is why gcc takes it). Key on the pair instead: la $sN,SYM ; move $aN,$sN feeding a call whose target has a bare la $aN,SYM, with the post-call memory ops based on $sN where the target re-materialises %hi/%lo.
(SHARPENS — sharpens §88a (repeated CALL-shaped blocks are left UNMERGED; call-free tails are merged for you, L6680), §31 Lever 4 (cross-jump the duplicated tail, L3270), §48-A1 / §48-A4 (sink init / consumer call into th; evidence: byte-probed; from func_8018FF98)
§164-69 — THE CROSS-JUMP TELL NEEDS A CALL-FREE GUARD: a tail the compiler WILL merge must be duplicated in source, never hand-factored. (guards §162g's forward-tell; the prescription itself is §88a + §31-Lever-4 — this entry supplies the discriminator §162g's tell table is missing, a positive tell for "the source duplicated", and the pass that actually decides it.)
The guard. §162g reads: forward j, >=2 sources, target inside the last-emitted arm's body ⇒ a merged tail; write it ONCE at that arm with gotos from the earlier arms. func_8018FF98 cases 4/5 satisfy that tell exactly — and the prescription loses. Look inside the merged block for a jal first:
call-BEARING merged block -> cross_jump REFUSED it -> the target's `j`s were SOURCE gotos -> hand-factor once at the last arm (§162g, func_8017F2D4)
call-FREE merged block -> cross_jump MADE it -> the source DUPLICATED -> write it longhand in every arm (§88a, this entry)
Target shape (build/ov_SC06_018/ov_SC06_018.elf @ 0x801902E8-0x801903DC; .L8019038C contains no jal):
case4: … lui $v1,0x2000 ; or $v0,$v0,$v1 ; lw $v1,0x20($s3) ; sw $v0,0x58($s3) … j .L8019038C
case5: … lui $v1,0x2000 ; or $v0,$v0,$v1 ; lw $v1,0x20($s3) ; sw $v0,0x58($s3) … (falls through)
.L8019038C: li $v0,8 ; sb $v0,0x75($s3) ; … ; lhu $v0,0x2C($v1) ; ori $v0,0x80 ; sh $v0,0x2C($v1)
lw $v1,0x20($s3) ; addiu $v0,$s3,0xE8 ; sw $v0,0x80($v1)
THE LAW. sched1 runs BEFORE reload and its scope is the basic block; jump2's cross_jump runs AFTER (pass order per §45-B). Duplicated, arm+tail is ONE block at sched1 time, so the tail's lw 0x20($s3) is hoisted INTO the arm's or-chain and recycles the $v1 the chain's lui constants just vacated; the identical suffix is merged back for free afterwards. Hand-factored, the tail is its own block, the load is pinned in the arm at its source position, and it takes a different register. ⇒ duplication here is not the byte-count trick of §48-A1/A4/§50-B — it is a SCHEDULING-SCOPE lever, and no pin substitutes for it.
BYTE EVIDENCE. src/ov_SC06_018/ov_SC06_018_jr_8018FF98.c:3299-3331 (both arms longhand, no shared local, no goto into a join) gates MATCH 385/385, banked, whole-binary green. Seven join spellings measured and all lost (.run/wave3/func_8018FF98/vA,v1,v2,v3,p1,p2.c): obj moved to four source positions, retyped s32→u16*, pinned register s32 obj __asm__("$3") (the pin only moves the constant temp to $a0), and a w temp interposed. Reported best 6 mismatched.
DIAGNOSTIC TELL — read the INTERLEAVE, not the register. "The merged block uses a register the arms loaded" does not discriminate: a goto-join needs a shared local for the same value, so its arms load into a register the join reads too. The discriminating signature is that the arm's copy of the tail's load sits in the MIDDLE of the arm's own unrelated computation, reusing a register that computation has just finished with — a load the source placed in the arm ahead of a goto sits at its source position and has no reason to recycle that register. Corroborate with a second read: here the merged block re-loads the same base for its second use (lw $v1,0x20($s3) again at 0x801903C4), which a shared obj local would have made redundant.
⚠ Bound (the control was not run). Every measured join variant carried an obj local, so goto-vs-duplicate and local-vs-no-local moved together. A join whose shared block re-derives the base itself was never compiled. The mechanism predicts it loses too (the tail is still its own sched1 block) — unmeasured; R14 applies to the mechanism half, not to the prescription.
(SHARPENS — sharpens §37 "the /s-DEP LATTICE (load+store dual)" (names the drop clause at sched.c:820, but only for the store↔LOAD edge), §135-2 / §30-1 / §30a (how to set or clear /s from C: ARRAY_REF and COMPONENT_REF g; evidence: byte-probed; from func_80191320)
§164-70 — THE /s ALIAS PAIR IS A STORE↔STORE LEVER TOO, AND IT IS THE ESCAPE HATCH §135-14 SAYS DOES NOT EXIST. (Bounds §135-4; extends §37's lattice from output_dependence's siblings to output_dependence itself.) Target shape — two adjacent stores, one $sp-relative and one through a pointer, that the target emits in the OPPOSITE order to your source with everything else byte-identical:
target: sw $a0,0x34($sp) ; addiu $v0,$v0,1 ; sw $v0,0x1C($v1)
mine: addiu $v0,$v0,1 ; sw $v0,0x1C($v1) ; sw $a0,0x34($sp)
THE LAW. output_dependence (sched.c:866-882) carries the SAME drop clause as true_dependence/anti_dependence: the write-after-write edge is dropped only when one MEM is (MEM_IN_STRUCT_P && rtx_addr_varies_p && mode != QImode) and the other is (!MEM_IN_STRUCT_P && !rtx_addr_varies_p). A frame-slot store and a pointer-based store in one block are exactly that shape — but only if you spell BOTH halves. With the edge present, priority() (depth over LOG_LINKS) pins the pair to source order; drop it and the frame store floats to the top of the block. §135-14's "$sp-based and register-based MEMs CONFLICT" is memrefs_conflict_p only — the /s clause sits above it and you control it from C.
BYTE EVIDENCE. func_80191320 (ov_SC06_018, 470 ins, banked). Three variants, identical but for the two spellings (.run/wave3/func_80191320/s1.c, s2.c, s3.c), rebuilt and re-verified against the banked object:
counter `*(s32*)(owner+0x1C)` + colour `*(u32*)((u8*)col+4)` -> 3 mismatched
counter `((s32*)owner)[7]` + colour `col[1]` -> 3 mismatched
counter `((s32*)owner)[7]` + colour `*(u32*)((u8*)col+4)` -> 0 MATCH
ARRAY_REF grants /s to the varying pointer store; the cast-over-PLUS clears it on the fixed frame store (§30-1). Each half alone is worth ZERO. Banked at ov_SC06_018_jr_8019059C.c:3318 and :3320.
NEGATIVE CONTROL (this is the part that matters). All 120 orderings of the five preheader statements were compiled and scored (.run/wave3/func_80191320/perm.py, _perm/p0..p119); best = 3. §135-4's "when a store lands too late, move it earlier in SOURCE" cannot reach a store pair that still carries a dependence edge — source order is the lever only for disambiguable stores. Break the edge first.
THE DIAGNOSTIC TELL. Two adjacent stores swapped in the schedule, one $sp-relative and one register-based, everything else byte-identical, and no statement permutation moves them. That last clause is the discriminator: a permutation-responsive store pair is §135-4; a permutation-immune one is this. Per §162q1, grant /s at the named site only.
(SHARPENS — sharpens §163e (gives only the OPPOSITE prescription: "a bare s32 sz can never land on 0x30 and must be written s32 sz[1]"), §162i1 (layout_type collapses a ONE-element array to the element's mode — whic; evidence: byte-probed; from func_80191320)
§164-71 — TO REACH A 4-MOD-8 FRAME OFFSET, USE THE SECOND WORD OF A ≥2-ELEMENT ARRAY. (Completes §163e, which only teaches the 8-ALIGNED direction, and repairs its s32 sz[1] recipe against §162i1.) Target shape: a 4-byte value the target stores at an $sp offset that is 4 mod 8 (sw $a0,0x34($sp) with SVECTORs filling 0x10..0x2F).
THE LAW. Every BLKmode local is 8-aligned (§163e), and every address-taken SCALAR is slotted lazily at its first &, i.e. after every aggregate in the block (§82-2). A plain u32 col; therefore cannot land at 0x34 — it is pushed past even a LATER-declared aggregate. The only construction that puts a word at a 4-mod-8 offset is a ≥2-element array (u32 col[2] → 0x30..0x37) addressed at its second word. Two elements, not one: §162i1's layout_type collapse turns a one-element array into the element's mode, which is 4-aligned and register-eligible — so §163e's s32 sz[1] is the wrong spelling of its own rule.
BYTE EVIDENCE. func_80191320 (ov_SC06_018, 470 ins, banked; ov_SC06_018_jr_8019059C.c:3305). u32 col[2] + *(u32*)((u8*)col+4) ⇒ sw $a0,0x34($sp), MATCH. Rebuilt with u32 col; + *(Blk4*)&col, everything else identical ⇒ the word moves to 0x38 (sw a0,56(sp), lwl v0,59(sp), lwr v0,56(sp)), 3 mismatched, same frame size.
THE DIAGNOSTIC TELL. An $sp store whose offset is 4 mod 8 and which your draft keeps emitting 4 or 8 bytes high. ⚠️ Coupling: the array-vs-cast spelling of that access is simultaneously the §163g /s lever. Fix the DECLARATION to place the slot, then respell the ACCESS to set the alias flag — changing the declaration to fix aliasing moves the slot back.
(SHARPENS — sharpens §162i1 (the dead-local frame pad has an 8-byte floor; only a BLKmode local reserves anything — stated purely over DECLARATIONS), §163e (slot ORDER; also declaration-only), §32-5, §21, §42-3, §135-6 (t; evidence: byte-probed; from func_80191320)
§164-72 — THE FRAME HAS NON-DECLARED OCCUPANTS: VERIFY THE PAD, NEVER COMPUTE IT. (Amends §162i1/§163e, which reason only over declarations.)
THE LAW. vars= can exceed the sum of the declared locals. An expression form — not a declaration — can claim a slot, so a pad[N] sized by adding up the decls will be a whole 8-byte slot wrong, and the error moves whenever you edit an unrelated statement.
BYTE EVIDENCE. func_80191320 (ov_SC06_018, 470 ins, banked). Declared locals: 4×8-byte SVECTOR + u32 col[2] + s32 pad1[2] = 48 bytes. Emitted: .frame $sp,88,$31 # vars= 56 — 8 bytes with no declaration behind them. Changing ONLY the case-1 memory re-read to a named local (tRR = *(s16*)(e+0x18); if (tRR < 0x500) …), same declaration set, emits .frame $sp,80,$31 # vars= 48 — the gap closes exactly. (The re-read form is also 1 insn LONGER, 470 vs 469 — §160d's asymmetry, and the reason the re-read spelling is the one that matches.) Mechanism honestly unresolved: an expression stack temp and a pair of 4-byte reload slots both fit the 8 bytes; only the delta is measured.
THE DIAGNOSTIC TELL. Your pad arithmetic is right and the frame is still 8 bytes off, or an edit that changed no declaration moved every $sp displacement. Re-grep '\.frame' after every statement-level edit; §163e's "verify, never reason about declaration order" now extends to verifying the SIZE, not just the ORDER.
(SHARPENS — sharpens cookbook-index L26 → §76 ("a two-constant if/else or ?: result lands in $v1 where the target reuses the condition's $v0 → fold the condition into a NAMED local and overwrite that SAME variable wit; evidence: byte-probed; from func_80192768)
§164-73 — A TWO-CONSTANT SELECT WHOSE DESTINATION IS MEMORY WANTS TWO STORES IN THE ARMS, NOT ONE NAMED LOCAL. (sharpens cookbook-index L26 / §76, which sends this exact symptom the other way; the cross-jump half is already §50-B / §162h.)
Target shape — a two-constant select stored to a field, immediately followed by an UNRELATED constant materialised into the SAME register, with no move between them:
.Larm0: addiu $v0,$zero,-0x1F4 ; j .Ljoin
.Larm1: addiu $v0,$zero,-0x3E8 <- falls through
.Ljoin: sh $v0,0x76($s0)
addiu $v0,$zero,0x2 <- the SAME $v0, held below the sh by anti-dependency
sh $v0,0x5E($s0)
THE LAW. Index L26's prescription — "fold the condition into a NAMED local and overwrite that SAME variable with the two constants" — is for a select consumed in a register. When the select's only consumer is a store, every spelling that NAMES the value keeps it live across the join, so the next constant conflicts with it and takes $v1. Written as two stores the pseudo never crosses the join, the next constant serially reuses $v0, and that anti-dependency is the only thing holding addiu $v0,$zero,0x2 BELOW sh $v0,0x76. The duplicated 1-instruction sh tail is free because one arm falls through into it — find_cross_jump (insn, JUMP_LABEL (insn), 1, …), jump.c:1978, the §50-B/§162h minimum=1 path.
Byte evidence — 4-spelling ladder, all 6 instructions (func_80192768, ov_SC06_018, 254 ins, banked at src/ov_SC06_018/ov_SC06_018_jr_80191C50.c:3225-3229; drafts in .run/wave3/func_80192768/):
| spelling | file | result |
|---|---|---|
?: inline into the store |
v_base.c, vC1.c |
5 mismatched |
?: into a shared local, then one store |
vB1.c |
5 mismatched |
| if/else into a shared local, then one store | vB3.c |
5 mismatched |
if/else with TWO sh stores |
final | MATCH |
Second instance, same TU: func_80192B60's −1000/−500 select at +0x76 (banked, :3402-3406) — a temp is live from before the +0xF4 load, conflicts with that load's allocno, is pushed to $v1, and lets sched2 hoist lhu 0x5C above sh 0x76 (+4 wrong ins).
DIAGNOSTIC TELL — the discriminator is the select's CONSUMER. Target materialises a second, unrelated constant into the same register the select's store just used, no move between ⇒ the values are serial, not simultaneous ⇒ sink the store into both arms. Target instead holds the select's value in $v1 while the condition sits in $v0 ⇒ you are in §76/L26's case ⇒ reuse one named s32 local. Do not run L26's lever on a memory-destination select; it stalls at a constant residual and reads as unsteerable.
(SHARPENS — sharpens §136d-2 (jump.c COLLAPSES an if-then-else into a conditional overwrite: if (c) t=A; else t=B; → t=B; if(c) t=A;, hoisting one arm's constant above the beqz; lever = TWO SEPARATE CALLS, not a t; evidence: byte-probed; from func_80192B60)
§164-74 — A TWO-ARMED CONSTANT STORE: WRITE THE STORE TWICE. THE POISON IS A FRESH ALLOCNO, NOT 'a temp'. (sharpens §136d-2 from a CALL consumer to a STORE consumer, and prices the failure; re-states §78's fresh-vs-reused rule at a shape §78 never covered.)
Target shape — a select of two literals into one field, the constant riding the branch delay slot and sharing the flag's register:
lw $v0,0xF4($v1)
beqz $v0,.L2
addiu $v0,$zero,-1000 <- constant IN the slot, in the FLAG's register
…
.L2: addiu $v0,$zero,-500
.L3: sh $v0,0x76($s0) <- ONE store, cross-jumped DOWN out of both arms
THE LAW. Write both arms as complete stores:
if (*(s32 *)(*(s32 *)(p + 0x64) + 0xF4) != 0) { *(s16 *)(iv + 0x76) = -1000; }
else { *(s16 *)(iv + 0x76) = -500; }
A store is not a simple SET of a pseudo, so §136d-2's jump.c if-then-else → conditional-overwrite collapse cannot fire; jump2's cross_jump then merges the identical sh tails downward (§50-B's floor is met — the tail is the sh plus the fall-through), which is what puts the constant in the delay slot AND lets it share $v0 with the flag it was tested from. This is §136d-2's 'two separate calls' lever with a store as the consumer — the commoner case, and the one §136d-2 does not name.
WHAT THE TEMP COSTS, AND WHAT IT DOESN'T. With s32 t = -1000; if (!flag) t = -500; (and with the if/else and ternary forms) the collapse hoists one arm's addiu ABOVE the beqz, so t is live simultaneously with the flag in $v0, is pushed to $v1, and that frees sched2 to hoist a later lhu 0x5C above the sh 0x76 — four wrong instructions out of one register. But:
⚠ 'A temp poisons the allocno' is FALSE as stated.
s32 t = *(s32 *)(*(s32 *)(p + 0x64) + 0xF4); if (t == 0) t = -500; else t = -1000;also MATCHes — the temp is the flag's own allocno, so nothing new is born and nothing conflicts. This is §78's third block ('a fresh short-lived local gets the allocation catastrophically wrong; reusing an already-busy variable gets both right') at a shape §78 never covered. Spend the flag's allocno or write no variable at all; never manufacture a third one.
BYTE EVIDENCE — func_80192B60 (ov_SC06_018, 257 ins, banked; src/ov_SC06_018/ov_SC06_018_jr_80191C50.c:3402-3406). Five spellings, one match_one run each (.run/wave3/func_80192B60/variants3.py, v_T_*.c): T_dup (both stores) MATCH; T_reuse (temp = the flag load) MATCH; T_ter (ternary), T_s16 (s16 t), and the plain two-statement temp all lose 4 ins to the lhu 0x5C/sh 0x76 transposition; T_pin (register s32 t __asm__("$2")) reached 2 mismatched and never closed — the pin fixes the register and leaves the schedule, which is the tell that the register was the cause and the schedule the symptom.
THE DIAGNOSTIC TELL. A branch whose delay slot holds one arm's literal, plus a single store below the join, plus a later load in the same block transposed above that store ⇒ you wrote a select through a variable. Duplicate the store into both arms. Inverse (§136d-2's tell, still valid): one arm's %hi/%lo or addiu appearing BEFORE the branch and no j-over-arm ⇒ the collapse already fired.
Scope: one function, five spellings, two independent MATCHes. The conflict → $v1 → sched2-hoist chain is reconstructed from the diffs, not gdb-probed; the -da .lreg/.sched pair on v_T_dup vs v_T_s16 is the cheap confirmation and has not been run.
(SHARPENS — sharpens §162c (L11025) — THE | CHAIN, TWO SEPARATE RULES / 'The constant MOVES', §78 (L6174, evidence L6196-6197) — fold never leaves a literal first in an | chain; all seven parenthesisations REASSOCIA; evidence: single-instance; from func_8017DD28)
§164-75 — FOLD RE-ASSOCIATES A CONSTANT OUT OF AN INLINED +/-; ONLY A STATEMENT BOUNDARY PINS IT. (extends §162c / §78 from the | chain to the PLUS/MINUS axis, and promotes §17's one-clause "explicit temps for any reassociation" tip to a law with a tell.)
Target shape — the -K rides the SUBTRAHEND's register, and the sum is formed afterwards:
addiu $v1, $a0, -0x60 <- n - 0x60, in n's own register
…
lhu $v0, 0x0($a1) <- q->f0
addu $v0, $v0, $v1
sh $v0, 0x8($s3)
THE LAW. Written inline, q->f0 + (n - K) lets fold lift K out of the inner MINUS and re-attach it on the other side of the sum; the addiu …,-K then reads the LOAD's register, not n's. Hoisting the subtract to its own statement — d = n - K; … q->f0 + d; — puts a tree boundary in fold's way and the addiu stays on n. Same phenomenon §78 measured on | chains (all seven parenthesisations reassociate) and §162c wrote as "the constant MOVES", one operator over.
Byte evidence. func_8017DD28, ov_MAIN_012, 124 ins, banked whole-binary in commit:1629 — src/ov_MAIN_012/ov_MAIN_012_jr_8017CF3C.c:4330-4331 (d = n - 0x60; then q->f0 + d). The inline spelling emitted addiu $v0,$v0,-0x60 where the target has addiu $v1,$a0,-0x60. Target shape read off the still-unbanked family twin asm/md_SC03_076/nonmatchings/md_SC03_076/func_801F1520.s:47,52-55.
THE DIAGNOSTIC TELL. An addiu $rD,$rS,-K whose $rS is the WRONG operand of a nearby addu — same instruction count, one register wrong, no length drift. Do not reach for pins: hoist the x ± K sub-expression into its own local. Inversely — target puts the constant on the LOAD's register and your draft puts it on the variable's — inline the sub-expression.
⚠ Scope, honestly. One function, one direction, measured at an intermediate stage (40 mismatched) with the draft's other two levers still unfixed; no probe artifacts survive (this draft was recovered from a transcript, .run/jr48/wave2_lost.json). The observation "addiu $v0,$v0,-0x60 on the lhu result" does not discriminate (A − K) + B from (A + B) − K — both are length-neutral and both land on $v0. Determine which tree fold built before citing a mechanism, and measure it together with §163z's independent func_80192B60 claim ("fold's PLUS/MINUS re-association is source-form invariant — only a statement boundary breaks it"), which is the same law from a second agent in the same harvest.
(SHARPENS — sharpens §48-B4 rung 1 — 'write the copy IN THE LOOP BODY as a loop-invariant and let move_movables carry it to the preheader' (L11712), §48-B4 rung 3 — 'Try RC-12 first' (L11712), §136d-1 — RC-12, the $0-ad; evidence: single-instance; from func_8017DEFC)
§164-76 — A HARD-REG OPERAND IS NEVER LOOP-INVARIANT IN A CALL-BEARING LOOP: RC-12 IS DISQUALIFIED FROM §48-B4 RUNG 1. (bounds §48-B4 and §136d-1.) Target shape — a plain copy of a value, emitted in the PREHEADER after an earlier address hoist:
<preheader> lui $s5,%hi(SYM) ; addiu $s5,$s5,%lo(SYM) ; addu $s4,$a0,$zero
<body> …uses $s4 as the loop bound…
THE LAW. invariant_p's case REG: (loop.c:2751-2754) is if (loop_has_call && REGNO (x) < FIRST_PSEUDO_REGISTER && call_used_regs[REGNO (x)]) return 0;, and MIPS sets call_used_regs[0] = 1 (config/mips/mips.h:1203-1210). invariant_p recurses into a PLUS's operands (loop.c:2795-2800), so count = n + zr (§136d-1's $0-add) is not a movable inside any loop that contains a jal — it stays in the body and costs a live register there. §48-B4's ladder puts RC-12 at rung 3 (straight-line tail, no boundary) and never says it cannot serve rung 1; it cannot. The rung-1 form is the naked count = n;, written in the body BEFORE any conditional jump (so maybe_never is still 0 — §162e3) and BEFORE the value it must follow in the preheader (§162e2).
Scope — the headline is conditional, not absolute. With no call in the loop the same case REG: falls through to n_times_set[0] == 0 and returns 1, so the $0-add IS invariant and WILL hoist. State the bound as call-bearing loop, never as 'never'.
GENERALISES past $0. The guard keys on call_used_regs, not on register 0, so any register __asm__ pin to a call-clobbered hard reg poisons the movable the same way — the reflexive 'pin it' fallback is exactly the wrong move when the diff is a copy that should have been hoisted.
THE DIAGNOSTIC TELL. The target has a plain addu $sD,$sS,$zero in the PREHEADER that no pre-loop statement of yours can produce (a pre-loop copy lands before the guard branch, not after it), and your opaque-copy or pinned spelling sits inside the loop instead. Strip the + zr / strip the pin, write the bare copy in the body, and read cc1 -dL's <file>.i.loop to confirm it is listed moved.
Evidence scope (honest): source-derived from the pinned tree and consistent with the banked func_8017DEFC (ov_SC01_005, 124 ins, MATCH; .run/wave2/func_8017DEFC.c:133-137), but the disqualified variant was not preserved with a quoted instruction count. The loop_has_call bound and the pin corollary are read out of loop.c:2751, not measured.
(SHARPENS — sharpens §150-B (Same address + same name ≠ same body — stated for FUNCTIONS and for the backlog ledger's keying), §159 (THE DECLARATION AXIS: 'the HEADER is usually the liar, not the target'), §58b (a MATCHin; evidence: single-instance; from func_80183324)
**§164-77 — A per-overlay data symbol's type must be re-derived from THIS target's load width; another overlay's declaration of the **
§150-B (data analogue) — A PER-OVERLAY DATA SYMBOL HAS NO FLEET CONSENSUS TO CONSULT. (sharpens §150-B, which proves this for FUNCTIONS; bounds the plurality data-type oracle of §41/L2437 and §159's 'conform to the fleet' direction.)
THE LAW. §150-B's 'same address + same name ≠ same body' holds for DATA, and harder: 134 overlays load at the same VRAM, so a D_8018xxxx/D_801Axxxx address holds an unrelated object in every overlay. Another overlay's extern for that name — including a fleet PLURALITY of them — is a declaration of a different object and carries zero authority. Derive the type from THIS target's access width and stride, per §58b.
BYTE EVIDENCE. D_8018AD60 is declared three incompatible ways across the tree:
extern u16 D_8018AD60[]; src/ov_SC06_000/…_jr_8015C32C.c:3644 (+7 more ov_SC06_000 TUs)
extern void (*D_8018AD60[])(); src/ov_SC03_089/…_jr_80140608.c:1828 ; src/ov_SC03_029/…_jr_80178D40.c:2146
extern s32 D_8018AD60[]; src/ov_SC01_077/ov_SC01_077_jr_80183324.c:3103 <- banked, 324 ins
The plurality is u16[] and it is wrong here: func_80183324 does an lw at D_8018AD60[idx], so ov_SC01_077's object is 4-byte-strided. Copying the fleet spelling would have changed the load width.
TELL. Before adopting a data extern from a sibling overlay — or letting a family remap carry the exemplar's types onto a sibling — check the address: shared/engine space carries over, overlay space does not. For an overlay-private address, re-read the width off the sibling's OWN .s (lw/lhu/lbu, and the index scale) and re-derive. The failure is silent at match_one on the exemplar and only shows up as a wrong access width in the sibling.
(SHARPENS — sharpens §48-B (THE EBB RULE, L3468-3480: "cse resets its hash table at a label with >1 predecessor … anything you need to survive cse must have its def and its uses in different extended basic blocks" — insta; evidence: single-instance; from func_80185EF8)
§164-78 — A -1 RESIDUAL WHOSE MISSING INSTRUCTION IS A REDUNDANT CONSTANT RELOAD CAN BE AN UPSTREAM ?:, NOT A MISSING STATEMENT. (§48-B's EBB rule read as a SYMPTOM rather than a lever, and extended from copies/addresses to constants. Mechanism NOT isolated — see scope.)
Symptom: your draft is exactly one instruction short, and the missing instruction is a re-materialisation of a constant the code already holds — li $x,0xE inside a nested if, where the same li $x,0xE ran a few blocks above. The reflex reading ("I dropped a statement") is wrong when the statement is already in your source: cse deleted it.
THE OBSERVATION (measured). In func_80185EF8 the single A/B that fixed §163h also moved this. With *(u16 *)(p+2) = c ? 6 : 8; the redundant anim = 0xE; in the following nested if was deleted — 303 ins. With the two-store if/else it survived — 304 ins, MATCH. The duplicate is real and load-bearing in the banked source: anim = 0xE; at src/ov_SC03_014/ov_SC03_014_jr_801848E4.c:3028 and again at :3036, with *(u16 *)(a0+0x34) = 0; duplicated alongside it.
THE CANDIDATE MECHANISM (§48-B on a constant). jump1 (toplev.c:2827) collapses the ternary before cse1 (toplev.c:2865) runs, so the two spellings present cse with different block structure and different reach for the anim == 0xE equivalence — the same rule §48-B states for a reg-reg copy and a held address, applied to a constant. This attribution is INFERENCE. The note quotes an instruction-count delta, not a diff isolating the li, and the ternary→if/else edit changed §163h's polarity, register and delay slot in the same step. Re-deriving it from jump.c does not close it either: both forms still leave a 2-predecessor label between the two writes (jump.c:821 deletes the jump-around, not the branch target). Cite this as a place to look, never as a proven pass behaviour.
Byte evidence: func_80185EF8 (ov_SC03_014, 304 ins, MATCH, banked commit:1671, checkpoint commit:1674 R22 213/213). Scope: one function, one A/B, two effects not separated.
DIAGNOSTIC TELL. LENGTH-DRIFT -1 and the missing insn is a constant load whose value is already live at that point. Before adding the statement (it is probably already there), walk UPSTREAM and find the nearest construct that could have removed a basic-block boundary between the two writes — a ?:, a collapsed if/else (§136d-2), a merged tail (§46-L3). Re-writing the statement will not help while cse can still see across; you have to restore the boundary.
(SHARPENS — sharpens §135-14 (L8916, memrefs_conflict_p, $sp-vs-register axis), §135-13 (L8911, load cannot hoist above stores through a different register base ⇒ source order is the only lever), §78 (L6183, 'a nop in the; evidence: single-instance; from func_8018E8A0)
§164-79 — TWO BASE REGISTERS ARE AN ALIAS BARRIER, SO A SURPLUS nop IN THE TARGET IS EVIDENCE OF A TWO-POINTER SOURCE. (extends §135-14 from the $sp-vs-register axis to register-vs-register, and adds a SECOND cause for §78's target-nop rule)
Target shape — interleaved narrow accesses to one record where the load stubbornly does NOT rise:
sw $v0,0($s4)
lh $v0,2($s6) <- different base; edge kept; the load-delay `nop` survives
THE LAW. memrefs_conflict_p can disambiguate two MEMs off the SAME base register at different constant offsets ((plus r c1) vs (plus r c2)) and cannot disambiguate two unrelated base registers, so true_dependence (sched.c:817-838) keeps the dependence and the scheduler will not lift the load. Consequence that bites in practice: collapsing two pointers into one (§135-17/§145a) does more than change the IV count — it hands the scheduler a provable disjointness it did not previously have, and instructions you were not trying to move start moving. Read the IV lever and the scheduling lever together.
BYTE EVIDENCE. func_8018E8A0 (ov_SC02_011, NEAR 138): the single-pointer spelling puts 0xCE and 0xD4 off one register and lands at 210 insns; the two-pointer spelling holds 211 = the target's count (.run/wave3/func_8018E8A0/dump/ vs wk/). Honest limit: the count A/B is measured, the "two nops recovered" accounting is NOT — pre-maspsx the delta is one insn (177 vs 178), and that one is the §34 giv-init move. Treat the direction as established and the magnitude as unmeasured.
DIAGNOSTIC TELL AND ITS DISCRIMINATOR. You are one or two insns SHORT inside a loop, with a load sitting in a delay slot that the target leaves as a real nop, and in your draft the two addresses involved reach one struct through ONE register. Two causes now exist for that nop — check them in this order: (1) §78 — the register the target wanted was still live, so it could not fill the slot; look at the liveness first, it is free to check. (2) If the register was free, the cause is ALIASING and the lever is a second pointer (§163f1).
Symptom lines for the index: "my load hoisted into a delay slot and the target's did not" · "one insn short in a loop with a target nop" · "collapsing to one pointer moved instructions I did not touch" · "two base registers into one struct".
(SHARPENS — sharpens §162j1 (DEFEATING local-alloc's optimize_reg_copy_1 WITH AN IN-PLACE SHIFT, L11392), §162d1 (THE ANONYMOUS TEMP IS A SINGLE-SET PSEUDO, L11098 — the inverse direction), §55a (birthing_insn_p governs s; evidence: single-instance; from func_8018FF98)
§164-80 — A SELF-REFERENTIAL COMPOUND EXPRESSION BORROWS A SCRATCH; TWO COMPOUND ASSIGNMENTS DO NOT. And the schedule is a SECOND, independent lever. (sharpens §162j's "the lever is in-place-ness, not a pin" — but not §162j1's mechanism: optimize_reg_copy_1 requires a reg-reg COPY whose SRC does not die, and there is none in this shape. The scratch is born at expand, before local-alloc runs. Companion in the other direction: §162d1.)
Target shape — a packed <hi,lo> word rebuilt in place:
target: and $s5,$s5,$v1 ; or $s5,$s5,$v0 <- both in place on t's register
yours: and $v1,$s5,$v1 ; or $s5,$v1,$v0 <- the AND landed in a scratch
LAW 1 — registers. t = (t & M) | x; evaluates the inner AND into an anonymous temp pseudo, and that temp takes the scratch. t &= M; t |= x; are two assignments whose destination IS t, so both expand in place on t's pseudo. Same instruction count, different register identity. A pin is not the lever (§162j's standing result), and neither is operand order.
LAW 2 — schedule (independent; do not assume Law 1 covers it). In-place-ness alone does not fix a pair that follows a call. t &= 0xFFFF0000; carries no data dependence on the preceding ratan2, so sched1 hoists it above the mult/mflo. Naming the call's result first — r3 = ratan2(dy,-dz); t &= 0xFFFF0000; t |= r3 & 0xFFFF; — puts a statement boundary between them and the AND stays put (§55a's birthing_insn_p placement dial; L2491's INSN_LUID tie-break). Per-site, never file-wide: in the same function the FIRST mask/or pair must NOT get the temp — its andi is supposed to hoist into the load-delay slot after lh $s1,0xDC($s3).
BYTE EVIDENCE. src/ov_SC06_018/ov_SC06_018_jr_8018FF98.c:3279-3289, banked MATCH 385/385, whole-binary green. ⚠ n=1, winner-only: the losing spellings' bytes are quoted in prose and no probe survived in .run/wave3/func_8018FF98/ (every preserved variant differs in the case-4/5 lever, not this one). Re-measure before generalising Law 1 beyond and/or.
DIAGNOSTIC TELL. A two-instruction bitfield rebuild (and/or, andi/ori, sll/or) where the TARGET writes the same register in both slots and yours writes a scratch in the first ⇒ split the expression into compound assignments. If the registers then agree but the pair has MOVED relative to a neighbouring mult/mflo or jal, that is Law 2 — give the call's result a name and re-gate.
(SHARPENS — sharpens §135-17 (extra induction register ⇒ do NOT write the second pointer; record_giv prepends, combine_givs takes the head), §52-2 ("No derived-base variable" — let loop.c mint a combined DEST_ADDR giv of ; evidence: single-instance; from func_80191320)
§164-81 — TWO SAME-STRIDE REGISTERS IN A LOOP ARE ONE SOURCE POINTER. The READING half of §135-17 / §52-2 / §145a, which all state only the authoring half. Target shape — a 0x10-stride table walk:
addiu $a2,$a3,0x6 <- preheader: the ONLY derived register
lh $v0,0x8($a2) / lbu 0x0($a3) / swl 0x3($a3) ; swr 0x0($a3)
...
addiu $a3,$a3,0x10 <- one bump, one stride
THE LAW. find_mem_givs (loop.c:4198-4200) refuses a DEST_ADDR giv when mult_val == 1 && add_val == 0, so the offset-0 accesses can never leave the biv. Everything at a non-zero offset combines onto ONE giv, anchored at the LAST address-giv in source order (§145a). One source pointer therefore always presents as biv + 1 giv = two registers at the same stride — never one. Corollary for block moves: the MIPS unaligned-move pattern movsi_usw (config/mips/mips.md, the insv/extv expanders) is a single insn with a single MEM; the swl base+3 / swr base+0 pair is the assembler macro. An 8-byte align-1 struct assignment (§160a) therefore contributes exactly TWO addresses to the giv census (p+8, p+0xC), not four.
BYTE EVIDENCE. func_80191320 (ov_SC06_018, 470 ins, banked src/ov_SC06_018/ov_SC06_018_jr_8019059C.c:3283; loop at :3323-3346). Target walks D_801D57F0 with $a3 and $a2 = $a3+6. Written as two source pointers p and q = p+6 (both += 0x10) the draft carried FOUR induction registers (addiu t1,a3,6 / addiu t0,a3,2 / addiu a2,a3,5 all visible in .run/wave3/func_80191320/d1.txt, 404 mismatched). Written as ONE u8 *p with every access spelled p[k] / *(T*)(p+k), the loop body goes byte-identical: p+0 stays on the biv ($a3, carrying p[0] and the Blk4 store), and p+1,2,4,5,6,8,0xC,0xE all combine at p[6] — the last address-giv in source order — giving $a2 with MEM offsets -5..+8. (Iteration-trail measurement, not an isolated A/B; the mechanism cites are source-read.)
THE DIAGNOSTIC TELL. Count the registers the loop bumps by the same stride and take their pairwise base differences. All differences < the stride ⇒ ONE source pointer. Do not read addiu $aM,$aN,K as a second walked pointer; §135-17's cure applies to what YOU wrote, not to what the target shows. (§162n1 remains the off-diagonal: a bare copy addu $aM,$sN,$zero plus a literal MEM offset — no stride bump — is the conditionally-assigned pointer, not a giv.)
(SHARPENS — sharpens §46-L2 ('To make a reg-reg COPY survive, split its def and uses across extended basic blocks'; 'a source-level fp = q; ALWAYS dies'; worked example is literally if (arg1->a.w != 0) { fp = arg1->a.w; evidence: single-instance; from func_80192B60`)
§164-82 — A RE-READ IS NOT REDUNDANT, AND IT NEEDS NO EBB BOUNDARY. This BOUNDS §46-L2 / §162p rung 3. (§162p's ladder says a straight-line copy costs you an RC-12 $0-add or a pin. There is a free fourth rung: give the copy's SOURCE an extra use.)
Target shape — a pointer null-test whose delay slot holds a copy into a callee-saved register:
lw $v0,0x64($s1)
lw $v0,0xCC($v0)
beqz $v0,.L
addu $s0,$v0,$zero <- the "redundant" re-read, in the slot
THE LAW. v = LOAD; if (v != 0) {…} gives the load its own destination — one lw $s0, no copy. if (LOAD != 0) { v = LOAD; … } gives the test's load a separate pseudo; cse commons the second load down to a reg-reg copy, and the copy survives because the compare is an intervening use of its source: local-alloc's optimize_reg_copy_1 needs the SRC to NOT die in the copy (§162j) and cse's delete-by-rewriting-the-previous-dest path needs the previous insn's dest to be free (§163d) — the beqz denies both. dbr then parks the copy in the branch's delay slot. The two spellings differ by exactly one instruction, and in this target that instruction is real.
⚠ THIS BOUNDS §46-L2 AND §162p. §46-L2 says a source-level copy 'ALWAYS dies' outside an EBB split and prescribes the loop/guard construction; §162p turns that into a ladder whose straight-line rung 3 spends an RC-12 opaque copy or a register __asm__ pin — and a pin forfeits the family (§37, compiles_standalone). Here there is no loop, no >1-predecessor label, no pin and no $0-add: the arm is plain fall-through from the beqz. Add the rung: rung 0 — if the value is a pointer you are about to null-test, test the memory expression and re-read it inside the arm. Free, family-safe, and it is what the 1998 source did.
BYTE EVIDENCE — func_80192B60 (ov_SC06_018, 257 ins, banked). Both spellings appear in the ONE function and each is required at its own site:
src/ov_SC06_018/ov_SC06_018_jr_80191C50.c:3463-3464— test-then-re-read:if (*(s32 *)(*(s32 *)(p + 0x64) + 0xCC) != 0) { iv = *(s32 *)(*(s32 *)(p + 0x64) + 0xCC); … }:3398-3399— load-then-test:iv = *(s32 *)(p + 0xCC); … if (iv != 0) { … }
Collapsing the first site to the second spelling costs 27 → 67 mismatched and 257 → 256 ins in one edit — that single copy was the entire length drift.
THE DIAGNOSTIC TELL. A LENGTH-DRIFT −1 sitting next to a pointer null-test, with the target's beqz delay slot holding addu $sD,$v0,$zero ⇒ you hoisted a load the original re-read. Fix the spelling before suspecting anything structural — this presents as a whole-tail shift, not as one missing insn. Inverse: if the target's lw writes the callee-saved register directly, the source assigned first and tested the local; do not add a re-read.
Scope, honestly: ONE measured A/B. The two sites are not matched controls — :3463 is a double indirection reached by fall-through, :3398 a single indirection with a store between load and test — and the optimize_reg_copy_1/cse account above is reconstructed from §162j/§163d, not probed. The cheap confirmation nobody has run: cc1 -da on both spellings and diff .cse against .lreg for the copy.
§164z — REFUTED CLAIMS: do NOT re-derive these
A skeptic judged each of these UNSOUND — over-generalised from one instance, contradicted by a byte-proven section, or the stated mechanism does not follow from the evidence. They are recorded so a future agent does not spend a wave rediscovering them.
func_8017DD28— This is §162f1's mechanism read forward — the return TYPE declares $v0 liveness at the end, and making $v0 busy through the last store is 'the same mechanism' keeping reorg/sched2 off that slot. Why refuted: §162f1's mechanism isreorg'send_of_function_needs, seeded from the return rtx, which makesfill_simple_delay_slotsrefuse a $v0-SETTING insn in a BRANCH delay slot bound for the epilogue. func_8017DD28 already returnss32 *, so that $v0 liveness was present in every failing variant and did not stop the hoist — which is a direct self-refutation of 'same mechanism read forward'. The two difunc_8018A974— gcc-2.7.2 -O2 charges an unavoidable 8-byte anonymous compiler temp whenever a global is forced to reload 2+ times by an explicit mechanism (a volatile deref OR a non-volatile asm(:::"memory") clo Why refuted: The PHENOMENON is real — I reproduced it (A2:volatile u16 *bidx; pk->addr=*bidx; ((PTag*)pk)->addr=*bidx;→.frame $sp,32 # vars= 8, and the 8 bytes are never addressed). But the stated law is byte-REFUTED as written. I ran 19 controlled variants through the pinned triple (cc1 -O2 -G0 -mips1 -mcpu=3000, files in .run/vet_8018A974/): TWO volatile u16 reads in arithmetic (E2/I2), through a pofunc_8018A974— func_8018A974's target must get its threelhureloads from reload-driven rematerialization of a single CSE'd non-volatile load, because every explicit forcing mechanism pays the +8. Why refuted: Self-flagged by the agent as inference ('not byte-proven'), and its motivating premise is false: probeC2shows a non-volatile pointer +__asm__("":::"memory")produces the twolhureloads withvars=0, so at least one explicit forcing mechanism does NOT pay the tax. The 'reload rematerializes a CSE'd load under pressure' story is also mechanically shaky — reload rematerializes from a REG_Efunc_8017D1E0— A value live across a call, split from its own copy by cse, NEEDS BOTH ends pinned (register s32 tmp __asm__("$4"); register s32 t __asm__("$17");); the double pin is the only thing that reproduces Why refuted: BYTE-REFUTED by me, not merely doubted. I rebuilt the banked body standalone (pinned triple: cpp / cc1-2.7.2 -O2 / maspsx 2.56 / as; masked_diff vs the banked, whole-binary-gated 78-ins body) and A/B'd the pin decl only. Results — banked double pin: 78 ins, baseline. RC-12 with ZERO pins (register s32 zr __asm__("$0"); … t = tmp + zr;): 78 ins, diffs-vs-banked = 0, byte-identical MATCH. So tfunc_8017D1E0— cse's forward copy-propagation eliminates a trivialt = tmp;whenever tmp is provably unchanged at t's later use, REGARDLESS OF SOURCE POSITION — no way of splitting the C into named temporaries can Why refuted: The universal over source position is refuted by four byte-proven sections that make position the DIAL. §48-B: 'anything you need to survive cse must have its def and its uses in different extended basic blocks' — cse resets its hash table at a label with >1 predecessor. §156 is the same shape as this very target and states the opposite conclusion: 'A second pseudo copy (sub = pct) survives csefunc_801814D8— Statement grouping relative to a call is a THIRD register-pressure lever (beyond §161b's aliasing and §76/§136's scope): splitting the field stores away from the following call made the pointer's live Why refuted: The stated mechanism does not follow from the function.func_8012B2CC(a0)is the LAST statement of that arm and the function returns immediately after — nothing is used past it, so the pointerdstcannot be live across that call under ANY ordering of statements placed BEFORE it. Moving pre-call statements around changes a pseudo's death point, not whether it survives a CALL_INSN that has no sufunc_8017EB44— An accumulator that must be zeroed after an entry-block temp dies must BE that temp: only an anti-dependence (one C variable serving as both the index and the result accumulator) can scheduleret = 0 *Why refuted:* Refuted by the banked function itself.src/ov_SC01_005/ov_SC01_005_jr_8017EB44.c(committedcommit:1652) declaress32 ret;as its own variable, never merged with the index, writes a plainret = 0;, and carries NOregister asmpin and no asm of any kind — yet it emits exactly the disputed instruction. I compiled it on the pinned cc1:beq $2,$0,$L22 / move $6,$0—ret = 0` IS in the dfunc_8017EB44— Two loads both pinned to hard registers are emitted in ascending hard-register order and no C perturbation can reorder them; rule of thumb, do not pin both operands of a mult when the target loads the Why refuted: Self-retracted by the successor agent ("TRUE but MOOT — it is an artifact of pinning both mult operands. Unpinned, the load order is correct and always was … Don't chase it"), and the retraction is right. I compiled the banked pin-free draft: it emitslbu $3,D_80115159($6)BEFORElbu $2,D_80115158($6)— i.e. DESCENDING hard-reg order, $v1 then $v0, which is the very ordering the note says gccfunc_8017EB44— The duplicate-arm dial is not generalisable to N copies: exactly one duplicate is the working dose, because with seven duplicates the extra allocnos perturb the allocation so the copies stop being reg Why refuted: Both halves fail a byte check. (1) The threshold is wrong. I built the dose curve on the pinned cc1 by progressively giving cases 2,3,4,5,7,8 real bodies: 2, 3, 4, 5, 6 and 7 total copies of the generic block ALL compile to 107 cc1-insns with 5 distinct jtbl targets — identical to the one-duplicate build. Only at 8 copies does it break (172 insns, 10 distinct targets). The agent measured only thefunc_801874E4— Two 'last-use, value-dies-here' ops compile IN PLACE (the dying source register becomes its own dest) instead of into a fresh $v0, and this is a local-alloc/reload preference with no source-level leve Why refuted: The PHENOMENON is real and reproduced, but both halves of the claim fail. (1) Mechanism is not new: §36 already names it verbatim ('the temp grabbed $a1 via qty_phys_sugg, X dying in that insn; suggested qtys allocate first') and §80 supplies the citation (local-alloc.c:1795, unconditional, no death guard) plus the R7 cure ('a zero-byte asm ref that keeps the pinned value LIVE PAST the temp, sfunc_801874E4— The in-place reuse is GLOBAL-alloc's dying-hardreg suggestion (global.c's qty_phys_sugg), a different pass from §162j1's local-alloc optimize_reg_copy_1, and the two are told apart by dump stage: the Why refuted: Byte-refuted twice over, by the agent's own artifacts. (a) FILE:qty_phys_suggappears 0 times in tools/reference/gcc-2.7.2/global.c and 11 times in local-alloc.c (declared :107, set :1813/:1834, read by find_free_reg :2150). It is a local-alloc quantity structure; §80 cites it correctly, this note does not. (b) DUMP READING: in the agent's own dumps the andi's dest pseudo is coloured at .lreg,func_801874E4— The prologue pair order under two hard-reg pins is driven by usage/reference count inside the entry block: the LESS-referenced of the two always schedules first; natural unpinned allocation has the op Why refuted: The note's controls (swap the pin numbers; reverse the declarations) are real and I reproduced both, but neither of them tests reference count — they only eliminate two rival explanations. I ran the direct test the note is missing, twice: adding 4 extrap1references function-wide, and adding 3 extrap1references INSIDE the entry block (taking p1 from 1 to 4 entry-block mentions against obj'sfunc_80191320— A tail duplicated longhand into N arms needs N SEPARATE per-arm locals rather than one shared function-scope temp, because a shared multi-block pseudo becomes a global allocno that lands on $v1 after Why refuted: The PRESCRIPTION is right and is already §76's law verbatim — per-arm declarations make N one-death local pseudos, a shared one is a multi-death global allocno. The stated MECHANISM does not follow from the evidence. I ran the isolated A/B (banked body, t34a/t34b/t34c merged into a single function-scopeu16 t34s, nothing else changed): the result is **8 mismatched at 470 instructions — identicalfunc_8017DF40— The choice between the assembler-macro addressing form (lui $at,%hi(SYM); addu $at,$at,$idx; lbu %lo(SYM)($at)) and a preheader-hoisted base register (la+addu+ zero-displacement load) for an Why refuted: THE OBSERVATION IS REAL — I byte-confirmed it in the shipped object. func_8017DF40's four copy loops split exactly as described: loop 2 (u16[64], scale 2) and loop 5 (align-1 4-byte struct[24], scale 4) hoistlui/addiubase pairs into $a1/$a2 and $t1/$t2 in the preheader then load at 0(reg); loops 3 and 4 (u8[], scale 1) emitlui at,0 ; addu at,at,v0 ; lbu a0,0(at)per access inside the loop.func_8017F83C— Comparison-arm REGALLOC-PERM is a LIVE-RANGE problem, not a source-shape problem, and rewriting the expression provably cannot fix it. Why refuted: Three problems. (1) The notes REFUTE themselves:b > ain arm1 "gets the REGISTERS right" — that IS an expression rewrite moving the registers to the target's; it merely cost the load order. So the true statement is §158's trade-off ("buys the ORDER or the REGISTERS but never both"), not an impossibility. "Provably cannot fix" is an over-claim on its own data. (2) The sound residue is already owfunc_8017D7C0— Compound assignment is not byte-equivalent to its expanded form because theop=form PINS the memory lvalue as the accumulator so the constant cannot migrate onto the variable term. Why refuted: The OBSERVATION (a += J - Kanda = a - K + Jare not byte-equivalent) is true and byte-probed. The stated MECHANISM does not follow from the agent's own evidence and is refuted by the compiler source; it must not be banked in this form. (1) THE CONSTANT DOES MIGRATE IN THE MATCHING FORM — and that is the whole point. The sourcea += J - 0x200puts K beside the jitter term; the emitted asm tfunc_80181B00— The extra 0x10 is 'stack temps' that gcc-2.7.2 had ALREADY reserved beyond the declared locals — i.e. a baseline the naive sum under-predicts. Why refuted: The mechanism is mis-attributed and the framing is misleading in a way that would cost a future agent time. (1) 'Stack temps' points atassign_stack_temp/§83c's spill area; it is neither..gregon the isolated reproducer shows an ORPHAN PSEUDO (76 conflicts:empty, no disposition, present in no insn) getting analter_regslot — §147-B's already-corrected combine-orphan story. A future agenfunc_80181A30— Moving the tag-word read to AFTER thesbis what makes its destination a single-set pseudo, and that is why the birthing boost fires there. Why refuted: The stated causal chain does not follow from the agent's own evidence.qtis a plain, single-set local in BOTH probe files (o.c and e.c; neither carries a pin — I checked), soreg_n_setsis IDENTICAL across the A/B and cannot be the variable that changed. The traces confirm this directly: the SAME insn number 303 is(7f000001)in o.i.sched:312 and plain(2)in e.i.sched:319. The real discrfunc_80182044— The struct type places the two 8-byte locals at sp+0x10 and sp+0x18 because the first-declared local takes the lower stack offset. Why refuted: Over-generalises one observation into a rule that a byte-proven section explicitly forbids. §163e establishes that slot assignment falls out of pseudo NUMBERING, not source order, and its practical rule is to VERIFY withgrep '.frame'plus the sp-relative store offsets rather than reason from declaration order — the agent did neither, it just noted that the offsets came out in declaration orderfunc_80182044— func_80182044 also sits behind INCLUDE_ASM in ov_SC04_002 and ov_SC03_124, making those two remap candidates for the banked body. Why refuted: Byte-refuted by direct measurement, and it is the one claim in these notes that would cost a future agent real time. The banked ov_SC02_041 body is 142 instructions.asm/ov_SC04_002/nonmatchings/ov_SC04_002_jr_8017BEBC/func_80182044.sis 0x13C (79 ins) and openslw $a1,0x20($s0) / lh $a0,0xFE($s0) / jal func_80182618 / jal rand— a different function entirely. `asm/ov_SC03_124/nonmatchings/ov_func_80186C4C— Pinning the parameter copy withregister s32 a0 __asm__("$16"); a0 = arg0;fixes the registers but COSTS an instruction (275 vs 273), because the parameter survives as a second live pseudo and copy- Why refuted: The QUALITATIVE half reproduces; the QUANTITATIVE claim does not. I rebuilt the pin on the final body (scratchpad/vE.c:void func_80186C4C(s32 arg0) { register s32 a0 __asm__("$16"); … a0 = arg0; }): 273 instructions, exactly ONE diff — idx 15, minenopwhere the target hasmove a0,s0in thejal func_8012CBCCdelay slot. Same length, not +1. The agent's "275 vs 273" compares a pinned buildfunc_80186C4C— The early-outjal func_8012C218has anopdelay slot and nomove $a0,$s0in its block while every other call sets $a0; gcc always emits the arg copy when the callee takes one and dbr would have Why refuted: BYTE-REFUTED. I compiled the banked body with the early-out call spelled the natural way —func_8012C218((void *)a0);against the TU's ownextern void func_8012C218(void *a0);— and it is IDENTICAL to the cast-idiom version: 273 instructions, 0 mismatched (scratchpad/vH.c). Both arities emit the same bytes, so the delay-slotnopcarries NO arity information at this site and the inference canfunc_8017CA18— For this store/RMW ordering residual, a memory-clobber asm barrier (andvolatile, and register pins) applied to the RMW side is a no-op — none of them changes the schedule. Why refuted: Contradicted by the pinned source and by a byte-proven cookbook section.sched_analyze_2,tools/reference/gcc-2.7.2/sched.c:1943-1970: forASM_OPERANDSwithMEM_VOLATILE_P(i.e.asm volatile/ a memory clobber) gcc adds a dependence on every reg_last_use and reg_last_set, setsreg_pending_sets_all, and callsflush_pending_lists (insn)— which at:1634-1652makes the barrier anti-defunc_801823E8— DISCRIMINATOR (offered as a new cookbook rule): when the target's 'true' arm is 3 stores that fall straight into the epilogue and the 'false' arm is a single call, try the GUARD-CLAUSE spelling first Why refuted: Two independent problems, either one disqualifying.
- The A/B does not isolate the variable it names. The agent compared
if (bit) {writes} else {call}againstif (!bit) {call; return;} writes;. Those differ in TWO ways at once: arm order AND early-return-vs-else. §3-T4 predicts the arm order alone accounts for it, so the untested third cell — `if ((v0 & 0x2000) == 0) { call(); } else { write
func_80184944— The second range-check MUST be written as an early-return GUARD CLAUSE (if (bad){err;return;}+ continuation at the same nesting level) rather than as theelsearm of anif/else if/elsechain; t Why refuted: The A/B the agent ran is real but it moved TWO variables at once (spelling AND arm order) and credited the wrong one. I re-ran the pinned triple (tools/match_one.py + tools/permuter/compile.sh) on three variants of the drafted body: (A) as-drafted guard clause -> MATCH (89 ins); (B)} else if (cont) { Y } else { err; return; }(error arm LAST) -> DIFF, mine=90, 48 mismatched, class LENGTH-DRIFT;func_80184944— gcc-2.7.2 physically relocated the final else's error-store block to the very end of the function next to the epilogue instead of inline right after the test. Why refuted: No relocation occurred. In the 90-ins cc1.sthe block order is exactly SOURCE order — X's merged tail ($L11), then the final else ($L6), then the common continuation ($L5) — which is where the source put the final else. gcc-2.7.2 has no block-reordering pass; describing plain source-order emission as the compiler 'physically relocating' a block will send the next agent hunting for a pass that dfunc_80184944— The else-if-else version additionally failed to cross-jump-merge the two*(u16*)(...)+=rfield-0x12 update tails from the two branches, inserting a spurious extraj; this mirrors/extends the cross Why refuted: Byte-refuted. The merge succeeded in BOTH variants: the high arm'sj $L11(90-ins form) /j $L10(89-ins form) into the else-arm's field-0x12 update block is present in each, and the merged block is the same 6-7 instructions in each. Nothing about cross-jumping differs between the two spellings. The arithmetic never supported the claim either: losing a merge of a ~7-instruction tail costs ~+7,func_80180128— The cross-TU grep of the angle idiom confirms the FIELD WIDTH of the +0x12 angle field (i.e. that it is s16), because the idiom is attested "identically" across ~15 matched TUs. Why refuted: Refuted by byte probe and by the corpus itself. (1) I recompiled the banked draft with the+=lvalue respelled*(u16*)instead of*(s16*)— the object is BYTE-IDENTICAL (77 ins, zero diff). The byte gate is therefore BLIND to the signedness of that field here (the value is only 16-bit-added and stored back withsh, so nolh/lhudistinction survives), which makes the claim unfalsifiable
§165 — S48 WAVE-4 HARVEST (P30, 2026-08-12): banked the same day the wave landed
19 note-sets, 131 distinct laws claimed, one independent skeptic each, vetted against a cookbook that ALREADY contained §162/§163/§164 from this same campaign:
| verdict | count | evidence | count | |
|---|---|---|---|---|
| NEW | 8 | byte-probed | 61 | |
| SHARPENS | 39 | single-instance | 43 | |
| COVERED | 64 | asserted | 27 | |
| UNSOUND | 20 |
COVERED+UNSOUND is now 64%, up from §164's 57% — the duplicate rate RISES as the knowledge base grows, which is the argument for vetting every wave rather than every few. Harvest immediately: a wave launched before its predecessor's harvest lands re-derives laws already on disk.
THE PASS CORRECTED ITS OWN PREDECESSOR. §165-01 BOUNDS §163a — banked hours earlier in this same
session. §163a says "block scope is a conflict SOLVENT", byte-proven on a DATA symbol; §165-01 shows
it does NOT reach an ARITY conflict, because there the two decls are COMPATIBLE (no conflicting types diagnostic exists for scope to downgrade) and the failure is call-vs-composite in
convert_arguments (c-typeck.c:1623). The diagnostic word picks the lever: conflicting types
⇒ §163a's solvent is live; too many arguments ⇒ no declaration spelling at any scope helps, cast
the call site (§17a-1/§161c).
(NEW; evidence: byte-probed; from func_8017C294)
**§165-01 — B2. Diagnostic: cc1 -dl reports an orphan as "Register N used 2 times ... ST_REGS or none" (a dead pseudo's **
(NEW; evidence: byte-probed; from func_8017C294)
§165-03 — ; ST_REGS or none IN -dl IS THE ONE-GREP FINGERPRINT OF AN ORPHAN PSEUDO — AND IT IS WHY THE ORPHAN TAKES A STACK SLOT INSTEAD OF A FREE REGISTER. (supplies the mechanism §16Xy, §164-66 and §36 all leave open, and replaces their two-dump .greg join with a single grep.)
Target shape. vars= larger than the sum of your declared locals by a multiple of 8, and grep '(sp)' on your .s shows the excess is never addressed.
THE DUMP LINE. cc1 -dl writes dump_flow_info at the head of .lreg (toplev.c:3062 → flow.c:2876), one line per pseudo with reg_n_refs != 0. An orphan prints as
Register 102 used 2 times across 2 insns in block 0; dies in 0 places; ST_REGS or none.
and appears in no insn in -dc's combine dump or in .lreg's own RTL.
WHY ST_REGS, AND WHY IT MATTERS. The class name is not decoration. regclass.c:931-949 walks class = ALL_REGS-1 downto 1 and merges equal-cost classes through reg_class_subunion, which (regclass.c:229-256) keeps the largest-numbered class CONTAINED IN the union — not the smallest superset, that is reg_class_superunion. On the MIPS class list (mips.h:1375-1385: … MD_REGS, ST_REGS, ALL_REGS) ST_REGS is class 7, one below ALL_REGS, so an all-zero cost vector converges there; alt stays NO_REGS because the p->cost[class] < p->mem_cost test is 0 < 0. ST_REGS contains one FP status register, so find_reg (global.c:918, using reg_preferred_class) has nowhere to put an SImode value, and global.c:568 never retries an allocno whose alternate class is NO_REGS. reg_renumber stays −1 and alter_reg (reload1.c:2309, gated on reg_n_refs[i] > 0) hands it an 8-byte slot. That is the answer to the question the three existing orphan entries do not ask: a conflict-free dead pseudo does not get a free register because its preferred class cannot hold its mode and it has no alternate.
BYTE EVIDENCE (.run/wave4/func_8017C294/dmp/, pinned cc1 -O2 -G0 -mips1 -mcpu=3000 -dl -dg -dc):
t.i.lreg— exactly 16; ST_REGS or nonelines (grep -c).t.i.greg— each of those pseudos has an emptyconflicts:list (;; 102 conflicts:) and the file contains zero;; Register N in H.dispositions.grep ' 102' t.i.combine t.i.lreg→ nothing: present in no insn in either dump.t.s:22—.frame $sp,312,$31 # vars= 256, regs= 10/0, args= 16, and 112 bytes of declared locals + 8×16 orphans + 8×2 real spills = 256, exact.
FRAME ARITHMETIC — and do NOT memorise the constant. vars = Σ(declared locals) + 8×orphans + 8×(real spills). The note this came from wrote it as 112 + 8×(1 + orphans + 1); the 112 is this function's decl total and the two 1s are its two spills. Generalise or it will be wrong on the next function (§164-72's warning, with a named cause).
DIAGNOSTIC TELL. Frame over by a multiple of 8 with no $sp reference to the excess ⇒ cc1 -dl and grep -c 'ST_REGS or none'. That count × 8 is your orphan budget, in one grep, before any .greg cross-referencing. §162i1 as repaired by §164-53 (vars = target_frame − ROUND8(args) − ROUND8(4×regs)) tells you the number you OWE; this tells you the number you HAVE, and the difference is what you have to build with §165-02's recipe.
⚠ One open detail, stated so it can be closed cheaply: the dump says used 2 times for a pseudo present in no printed insn, so reg_n_refs is STALE by the time alter_reg reads it — combine's (use (reg)) plant (combine.c:10835-10847, §36) is counted by flow and the insn is gone by .lreg. Which pass deletes it has not been traced. It does not affect the tell.
(NEW; evidence: byte-probed; from func_8017F2D4)
§165-02 — A BACKWARD CONDITIONAL EDGE INTO AN EARLIER ARM'S BODY CAN BE COMPILER-MADE. (Bounds §162g's direction tell to unconditional js.)
§162g: "Compiler tail-merge is ALWAYS a forward j into a LATER block. A backward j into the middle of an earlier sibling arm's body therefore cannot be cross-jumping — it is a goto that was in the source." The survivorship half is read off do_cross_jump/jump_chain and is sound for the N-js-to-one-label chain. The tell is not sound for conditional branches.
Byte evidence (the direct12 A/B of §165-02, func_8017F2D4, pinned triple). With cases 1/2 and 3 spelling the same global increment identically, cross_jump deleted case 3's tail — the LATER copy — kept case 1/2's at its original site (0x60-0x88, one lhu/sh pair on D_8011511A left in the whole arm region where the baseline has two), and the edge that reaches the survivor is
bc: 1462ffe9 bne v1,v0,64 <func_8017F2D4+0x64> <- BACKWARD, into an earlier arm's body
with no goto anywhere in the source. Mechanism: the deleted range's label keeps its references, and jump.c then tensions the conditional through the leftover label onto the surviving block — the find_cross_jump (insn, JUMP_LABEL (insn), **1**, …) path at jump.c:1978 that §162g's own Scope bullet already flags as keeping the EARLIER copy, here reaching a sibling ARM rather than a loop back-edge.
THE RULE. Apply §162g's direction tell only to unconditional js, and only after the §164-69 call-free guard. A backward beq/bne into an earlier arm proves nothing about the source — check whether the two arms' tails are RTL-identical first (§165-02).
(NEW; evidence: byte-probed; from func_8017F2D4)
§165-03 — A DEAD EQUALITY RE-TEST NEEDS THE ZERO-BYTE TIE-TO-SELF; A DEAD RANGE TEST DOES NOT. (the equality half of §164-46, and the one cse use the §21/§34/§17/§5a barrier catalogue never names — those use the same asm for store scheduling, reg_n_sets, known-zero-bits and RTL inequality.)
Target shape — three tests on one value, the third already decided by the first two:
.L8017F450: beq $s0,$v1,.L8017F468 <- $v1 = 3
addiu $v0,$zero,0x5
bne $s0,$v0,.L8017F49C <- $v0 = 5
nop
bne $s0,$v1,.L8017F478 <- $s0 is PROVABLY 5 here. The test survives anyway.
nop
THE LAW. cse carries equality knowledge across a conditional branch. record_jump_cond (tools/reference/gcc-2.7.2/cse.c:5944-5990) inserts a real equivalence — the comment at :5782-5785 states the case outright: "if we are following the taken case of if (i == 2) we can add the fact that i and 2 are now equivalent" — and it does so only for code == EQ, on whichever side the branch selects (cse_basic_block calls record_jump_equiv at :8448 taken and :7511 fall-through). So a later if (t != K2) nested inside an if (t == K1) block folds to a constant: the branch is deleted and jump.c retargets the enclosing conditional straight at the far arm. To keep the target's dead test, break the equivalence with the zero-byte in-place re-tie, inside the block and before the dead test:
if (t == 5) {
__asm__ ("" : "=r"(t) : "0"(t)); /* 0 bytes; t is now SET by an ASM_OPERANDS, not by the EQ class */
if (t != 3) goto sel_CD4;
…
}
This is the exact complement of §164-46. RANGE knowledge is expression-local and a range-dead slt survives with no help at all; EQUALITY knowledge lives in the cse hash table across the branch and a dead beq/bne does not survive without the barrier. Do not read §164-46's headline as covering equalities.
BYTE EVIDENCE (one-hunk A/B on the pinned triple, func_8017F2D4, ov_SC01_005, 279 ins). Baseline /home/musashi/bfm-decomp/.run/wave4/func_8017F2D4/func_8017F2D4.c (sha1 9fd76d10…) → MATCH 279. Delete only the __asm__ line → mine=279, target=279, 3 mismatched, class BRANCH-POLARITY / beq!=bne: gcc folds t != 3 and emits beq $s0,$v0(3) / li $v0,5 / beq $s0,$v0(5) → sel_CD4 / j, coalescing the constant 3 into $v0; the target keeps beq $s0,$v1(3) / bne $s0,$v0(5) / bne $s0,$v1(3) with 3 and 5 in two live registers (asm/ov_SC01_005/nonmatchings/ov_SC01_005_jr_8017ED5C/func_8017F2D4.s, 8017F450-8017F464).
THE DIAGNOSTIC TELL. Count-neutral BRANCH-POLARITY where YOUR output has a plain j and the target has one more conditional branch, and the two branch constants share ONE register in yours but occupy TWO in the target. The deleted compare is paid for by the j, so there is no LENGTH-DRIFT to point at it and match_one files it as a polarity bug — §3-T4 is the wrong door. Look for a re-test of a value that an enclosing == already pinned.
⚠ Bounds. (a) The barrier is needed only in the block entered by the equality; two sibling if (t == K) tests at the same level are separate PATHS and neither folds the other (§164-52). (b) It buys nothing against a range-dead compare — that is §164-46, and adding an asm there is a §3-perturbation trap. (c) Non-volatile is enough; it must be a SET, not a barrier.
(NEW; evidence: byte-probed; from func_8017FE38)
Addendum (P31 S65 t5s+t5t, func_8017EB30): the EQ-channel dial has an asm-free form — respell the re-tested OPERAND, not the operator. ⚠ UNPROVEN (no whole-binary gate pass; see the evidence caveat).
Second instance of the exact target shape, register for register. asm/ov_SC01_004/nonmatchings/ov_SC01_004_jr_8017E5B8/func_8017EB30.s:104-108 — 8017ECAC beq $s0,$v1,.L8017ECC4 / 8017ECB0 addiu $v0,$zero,0x5 / 8017ECB4 bne $s0,$v0,.L8017ECF8 / 8017ECBC bne $s0,$v1,.L8017ECD4 ($v1=3 loaded at 8017EC98) — the same 279-instruction template as §165-03's func_8017F2D4 (ov_SC01_005) in the adjacent overlay, same constants, same $s0/$v1/$v0. Source shape if (j == 3 || j == 5) { if (j == 3) … }; the third branch is provably dead and the retail binary emits it anyway. match_one files the naive draft as BRANCH-POLARITY / beq!=bne at nins-exact 279 — §165-03's tell, verbatim; do not go to §3-T4.
THE ADDED DIAL (unproven, but zero-asm). §165-03 prescribes only __asm__ ("" : "=r"(t) : "0"(t)), and its bound (c) frames the requirement as "it must be a SET" of that same pseudo. Two pure-C spellings reportedly do the same job with no #APP at all: re-derive the value at the inner test (if ((s16)param_2 == 3) instead of if (j == 3)), or test a fresh local (s16 j2 = param_2; if (j2 == 3)). Both took the draft from closeness 3 → 0. This widens §165-03's bound (c) — a different quantity suffices, a second SET of the original is not required — and it is the EQ-channel counterpart of §197-B's "make the two compares reference DIFFERENT quantities", which BOUND 4 there explicitly declines to claim for the code == EQ arm. It also adds a fourth lever to §308 BOUND 3's list (mask / launder / re-tie / respell), i.e. a fourth way to accidentally buy back a branch §308 wants deleted.
TWO CONTROLS, AND THEY ARE THE VALUABLE PART. (i) Changing the operator while keeping the variable — if (!(j - 3)) — is INERT: still near, closeness 3, identical residual. The axis is operand identity, not expression form; do not waste rounds on !=/==/subtract respellings. (ii) Materialising the constant into a local instead (s32 three = 3; if (j == three)) blew the function to closeness 242 — a new local perturbs the frame and the whole allocation (§167-06/§195-M territory). Reach for re-derivation or a narrow copy; never for a constant temp.
MECHANISM — the transcript's attribution is REJECTED; cite §165-03's. The agent named jump.c's thread_jumps matching identical comparison RTX. §165-03 already settles this channel from the source with a one-hunk A/B: cse's record_jump_cond (tools/reference/gcc-2.7.2/cse.c:5944-5990, the code == EQ arm at :5951) merges the operands of a taken == into one class, and fold_rtx then proves the re-test. Deleting only an asm SET — which leaves both C comparisons syntactically identical — restores the fold, which a purely syntactic jump-threading story cannot explain. One datum is genuinely open and nobody has dumped RTL for it: why (s16)param_2 == 3 does not re-hit cse's table entry for the already-computed extension. Treat that as the thing to -da if this recurs; do not bank thread_jumps.
⚠ EVIDENCE CAVEAT — this addendum is NOT byte-proven, and the instrument was broken while it was measured. The draft never passed the whole-binary SHA1 gate (G3/P9 remains the sole arbiter). Worse, both the before (3) and after (0) numbers came from masked_diff while its mask-from-one-side asymmetry was live: a j on MY side with an R_MIPS_26 reloc returned a 0 mask and swallowed whatever the target held, so j-vs-bne at one index scored 0 (honest pre-fix residual was 4, not 3). That defect was found by this very agent and is now fixed in tools/masked_diff.py:126-146 (the fix comment credits "a t5s drafting agent on func_8017EB30") — an §301/§195 instrument addendum in its own right. The only independent check on the "MATCH" is the agent's ad-hoc verify.py (both objdump streams reconstructed, HI16/LO16/R_MIPS_26 symbols compared, internal j targets computed by hand; its 2 "BAD" rows were the lui/lw %hi/%lo(jtbl_8018EDA4) pair, hand-cross-checked against asm/ov_SC01_004/data/tail19.data.s and confirmed a formatting artifact — all 10 table entries matched). Re-measure both variants under the fixed comparer and gate them before promoting any of this to law.
§165-04 — THE andi IMMEDIATE IN AN andi M ; srl k PAIR IS A VERBATIM TRANSCRIPTION OF THE SOURCE MASK — INCLUDING THE BITS THE SHIFT THROWS AWAY. (NEW. The file's only adjacent law is the disjoint-bits x + CONST -> ori fold (L1789-1796), a different pass and a different axis; §162k1 counts andis, it never reads their immediates.)
Target shape:
andi $v0,$a0,0x3C0
srl $v0,$v0,6
THE LAW. gcc-2.7.2 has two passes that would rewrite this and neither runs: force_to_mode's AND arm (combine.c:5808-5818) narrows an AND's constant to mask & INTVAL — the bits its consumer actually needs, i.e. 0x3FF -> 0x3C0 — and simplify_shift_const's (shift (logical)) arm (combine.c:8072-8088) moves the AND to the OUTSIDE of the shift, giving srl 6 ; andi 0xF. Both live inside try_combine, which reverts wholesale unless the merged pattern recognises; a 2-insn-in / 2-insn-out rewrite through find_split_point (combine.c:1821) buys no instruction here, so expand's form survives byte-for-byte and the mask you write is the mask you get. Same reasoning for andi M ; sll k.
BYTE EVIDENCE — one-character A/B, everything else identical (.run/wave4/func_8017FE38/): (x & 0x3FF) >> 6 (v_V4/t.o) -> 30a203ff andi $v0,$a1,0x3ff; (x & 0x3C0) >> 6 (v_V5, v_V6) -> 30a203c0 andi $v0,$a1,0x3c0 = the target byte (func_8017FE38.s:186); (x >> 6) & 0xF (v_V7) -> the moved-out form, srl then andi 0xf. Three spellings, three codegens, from one arithmetic identity.
CONSEQUENCE FOR PSY-Q. BFM's tpage code is not the stock getTPage macro, which spells 0x3ff. Read the mask off the target and transcribe it; do not "correct" it to the SDK header.
THE DIAGNOSTIC TELL. Your andi immediate differs from the target's while the surrounding shift/or chain is byte-identical ⇒ you normalised a mask the compiler never normalises. Copy the immediate literally. Read backwards: an andi whose low bits are entirely discarded by the following srl is a fingerprint of the ORIGINAL author's mask, not of a compiler simplification — treat it as source text.
(NEW; evidence: byte-probed; from func_8018308C)
§165-05 — ⚠ CORRECTS §50-B AND §162g's FLOOR BOUND: A ONE-INSTRUCTION TAIL REACHED BY TWO js DOES MERGE WHEN THE MERGE FEEDS A JUMP-AROUND-JUMP INVERT. (refutes §50-B's "two js with a 1-instruction common tail will NOT merge" (L3590) and §162g's bound "a 1-instruction tail reached by two js never merges" (L11289); completes §164-09's minimum accounting, which lists the matching-insn decrement and nothing else.)
THE LAW. find_cross_jump (tools/reference/gcc-2.7.2/jump.c:2371) reaches minimum <= 0 (the merge test, 2532) by three routes, not one:
-
--minimumper matching non-USE/CLOBBERinsn (2524-2528) — the only one §50-B / §164-09 count; -
a CODE_LABEL on the E1 side:
if (GET_CODE (i1) == CODE_LABEL) { --minimum; break; }(2403-2409) — "we will get to this code by jumping, those jumps will be tensioned…"; -
at the MISMATCH, the jump-around-jump discount (
2513-2519):/* If cross-jumping here will feed a jump-around-jump optimization, this jump won't cost extra, so reduce the minimum. */ if (GET_CODE (i1) == JUMP_INSN && JUMP_LABEL (i1) && prev_real_insn (JUMP_LABEL (i1)) == e1) --minimum;
So the floor for two js to one label is minimum = 2 MINUS up to two discounts — one matching instruction is enough in the common case. Discount 3 fires exactly when the earlier j is the last insn of an if-THEN arm: i1 is that arm's guarding conditional branch and JUMP_LABEL (i1) is the else label, which sits immediately after e1. Every <store>; j <join> written as an if-then arm therefore merges on a one-instruction tail — and the discount is named for the very transform that then fires, jump.c:1737's invert-a-cond-jump-that-jumps-over-an-uncond-jump.
BYTE EVIDENCE. func_8018308C (ov_SC01_077, 166 ins, banked commit:1678). Common tail = the single insn addu $s1,$zero,$zero; two j .L80183120 reach it; cc1 emits exactly one copy — in both A/B spellings of §164-83 (grep -c 'move\t$17,$0' = 1 in each .s, 139 instruction lines each). Under §50-B / §162g's stated floor neither should have merged. Trace: i1 = the arm's conditional branch, JUMP_LABEL (i1) = the else label, prev_real_insn of it = e1 ⇒ discount 3 ⇒ minimum 2 → 1 (one matching insn) → 0 ⇒ merge.
THE DIAGNOSTIC TELL. Before pricing a §48-A1/A4 duplicate-into-both-arms edit off §50-B's "tails must be ≥ 2 instructions" rule, ask whether the duplicated block ends an if-then arm whose else label follows its j. If it does, the tail merges at length 1 and the refund arrives. §50-B's 290-insn counterexample and §162g's bound remain valid measurements of tails that lacked that structure — keep them as a conditional, not as a floor, and settle any borderline case by reading the two streams' mismatch insn rather than by counting matching instructions.
(NEW — corrects §50-B / §162g; evidence: byte-probed (cc1 .s, one copy in both A/B builds) + source-read jump.c:2403-2409, 2513-2519, 2532; from func_8018308C)
(NEW; evidence: byte-probed; from func_80183560)
**§165-06 — "(p)++ IS NOT p += 1 TO THE PRE-RA SCHEDULER." On a memory lvalue, postincrement expands through an explicit
(NEW — evidence: byte-probed, 7 one-token spellings + expand-RTL, independently reproduced at vet time; from func_80183560)
§164-XX — (*p)++ IS NOT *p += 1. THE INCREMENT OPERATOR ON A MEMORY LVALUE BUYS AN EXTRA reg→reg INSN AT EXPAND, AND THAT IS A WHOLE-FUNCTION REGALLOC/SCHEDULE DIAL. (new axis. Closest prior art is §136g rule 15, which splits an RMW into v = *p + 1; … *p = v; to hoist the LOAD — this is the same dial read from the STORE side, and it names the operator that buys it for one token.)
Target shape — instruction count exact, instruction MULTISET exact, but a whole register file shifted by one:
MINE TARGET
li v0,16 lhu v1,2(s0) <- the RMW load hoisted ABOVE the unrelated store
sw v0,28(s0) li v0,16
lhu v0,2(s0) sw v0,28(s0)
…
div zero,a0,v0 div zero,a1,v0 <- every quotient one register up
addu v1,v1,a0 addu a0,a0,a1
sh v1,236(s0) addu v0,v0,a2 <- target batches 3 addu THEN 3 sh; mine interleaves
THE LAW. expand_increment (expr.c:8482) has an in-place fast path that queues (set MEM (plus MEM 1)) as ONE insn — and on MIPS it is unreachable for a memory lvalue. It gates on insn_operand_predicate[icode][0](op0, mode) (:8635); every MIPS add expander's operand 0 is register_operand (mips.md:365) and addhi3/addqi3 do not exist at all. So gcc falls through to copy_to_reg (op0) (:8648) → expand_binop (…, target = op0) (:8657) → if (op1 != op0) emit_move_insn (op0, op1) (:8660-8661). A compound assignment never enters that function: build_modify_expr (c-typeck.c:3858-3862) lowers *p += 1 to a plain MODIFY_EXPR and store_expr writes the add's result straight into the MEM.
The discriminator is one RTL insn — read it in cc1 -dr:
*p += 1 (set (reg:SI 155) (plus (subreg:SI (reg:HI 154)) 1))
(set (mem:HI …) (subreg:HI (reg:SI 155))) <- store from a SUBREG
(*p)++ (set (reg:SI 155) (plus (subreg:SI (reg:HI 154)) 1))
(set (reg:HI 156) (subreg:HI (reg:SI 155))) <- THE EXTRA INSN
(set (mem:HI …) (reg:HI 156)) <- store from a BARE PSEUDO
The copy coalesces away by final, so the emitted COUNT is identical. What changed is the depth sched1 priced and the pseudo count local-alloc saw.
BYTE EVIDENCE. func_80183560 (ov_SC02_041, 132 ins, banked src/ov_SC02_041/ov_SC02_041_jr_8017BEBC.c:5981). Seven one-token edits on the banked body, all 135-line objects:
| spelling | store's SET_SRC | diffs |
|---|---|---|
(*p)++ |
bare reg:HI |
0 — MATCH |
++(*p) |
bare reg:HI |
0 — MATCH |
s16 t = *p + 1; *p = t; |
bare reg:HI |
0 — MATCH |
*p += 1 |
subreg:HI |
25 |
*p = *p + 1 |
subreg:HI |
25 |
s16 t = *p; *p = t + 1; |
subreg:HI |
25 |
s32 t = *p + 1; *p = t; |
subreg:HI |
25 |
Two corrections to the obvious readings. (1) It is not post-vs-pre — ++(*p) matches too (preincrement reaches the same shape via the (!post && !single_insn) branch at :8590, expand_assignment (…, want_value = 1, …)). It is INCREMENT-OPERATOR vs COMPOUND-ASSIGNMENT. (2) The manual-temp form works only at the lvalue's own width: s16 t matches, s32 t does not, because only a HImode temp puts a bare pseudo in the store.
DIAGNOSTIC TELL. Count exact, multiset exact, but (a) a run of identical operations sits one register up/down as a block ($a0/$a1/$a2 vs $a1/$a2/$a3 across N identical divisions), and (b) your tail interleaves op/store where the target batches all ops then all stores. Audit every increment/decrement spelling in the function before pins, allocno sliders or the permuter — it is one token and it took this function 25 → MATCH.
Scope, honestly. The +1-insn expand fact is width-independent (minimal probe: (*p)++ = 5 expand insns vs *p += 1 = 4 at QI, HI and SI). The 25-diff CASCADE is n=1 and is amplified by this function's shape: maspsx --expand-div means each div is still ONE insn at cc1 time, so cc1 -S emits zero labels — the whole function is a single basic block and a +1 dependence depth propagates through sched1 and local-alloc end to end. In a function with real branches, expect a local effect.
Symptom lines for the index: "same multiset, whole register file shifted by one" · "target batches ops then stores, mine interleaves" · "an RMW load that will not hoist above an unrelated store".
(NEW; evidence: byte-probed; from func_8018E8A0)
§165-07 — A THIRD, DEAD COUNTER IS A POSITIONING DIAL FOR A REDUCED GIV'S UPDATE: THE GIV RIDES THE BIV YOU HANG IT ON, AND A BIV WHOSE ONLY SURVIVING USE IS ITS OWN INCREMENT COSTS ZERO INSTRUCTIONS. (extends §46-L4(c) and §164-34, which give the adjacency law and the two-REAL-biv LUID transposition dial but leave the giv's slot pinned to whichever real biv the C derived it from.)
Target shape — three loop-tail increments with the giv's update BETWEEN two real bivs:
addu $s5,$s5,1 <- i (counter biv; feeds the slt, so it cannot move)
addu $s4,$s4,12 <- the reduced giv
…
bne $v0,$zero,.L
addu $s6,$s6,12 <- p (pointer biv, in the loop-back delay slot)
THE LAW (two halves, both from the pinned source). (a) strength_reduce emits each reduced giv's update with emit_iv_add_mult (…, tv->insn) — loop.c:3868, not :3866 — which ends in emit_insn_before (loop.c:5560): the update lands immediately BEFORE the increment of the biv it derives from and can never follow it (§46-L4(c) / loop.md L6). (b) The bivs' own increments emit in INSN_LUID = source order (§164-34). Together, the giv's slot in the tail is entirely determined by which biv it is written in terms of. So when the target's giv update sits between two increments and neither real biv can be moved, mint a third counter purely to carry it:
for (i = 0, k = 0; i < n; i++, k++, p += 3) /* giv written as q = zb + k * S */
After reduction k's only remaining reference is its own increment. flow.c's propagate_block computes liveness optimistically from an empty live-in set and skips mark_used_regs on any insn insn_dead_p (flow.c:1705-1772) already calls dead — so a self-referential increment with no other use never becomes live and is turned into a NOTE_INSN_DELETED on the final pass (flow.c:1490-1494). The counter is free.
⚠ MECHANISM CORRECTION — do not credit loop.c. loop.c's own biv-increment deletion loop is #if 0'd out (loop.c:4069-4080), with the comment "deleting them will invalidate the regno_last_uid info, so keeping them around is more convenient … it is dead anyways." Nor is it delete_dead_from_cse, which runs only after the FIRST cse, before loop_optimize (toplev.c:2867 vs :2895) and is never called again. It is flow, at toplev.c:2983.
BYTE EVIDENCE. func_8018E8A0 (ov_SC02_011, MATCH 211/211). Giv hung on i ⇒ tail [$s4+=12][i++][$s6+=12], and no scheduling lever moves it (i++ feeds the slt, so its priority always dominates). Giv hung on k ⇒ .run/wave4/func_8018E8A0/wk/w3/t.s:297-306 emits exactly three tail insns — addu $21,$21,1 / addu $20,$20,12 / bne $2,$0,$L5 / addu $22,$22,12 — no k increment anywhere, the rider between the two real bivs, and reorg filling the loop-back delay slot with the pointer bump.
THE DIAGNOSTIC TELL. Loop-tail increments in the wrong order, registers and count already correct, and §164-34's transposition dial cannot reach the target because the rider is on the wrong biv. Count the target's tail addius and split them: a biv's register is read elsewhere in the body; a rider's register is only ever a MEM base. If the rider must sit where no real biv's increment can go, add a counter for it — cc1 -dL names it (Insn N: giv reg R src reg B).
Cost note: the extra counter is one more pseudo through cse and loop. Re-check the preheader hoist order (§162e2) and the allocno density ranking (§158a/§162m) before shipping — neither was separately ablated here.
(NEW; evidence: single-instance; from func_80185B44)
§165-08 — Corollary — "Symbol-vs-symbol IS disambiguated (the lhu does sit above the sw D_801EAC80), so only the reg
§16Z row 1 (fold into the §16Z table above; do not bank separately) — A LOAD OF ONE GLOBAL FLOATS FREELY OVER A STORE TO A DIFFERENT GLOBAL. Two distinct SYMBOL_REF addresses are provably disjoint: memrefs_conflict_p reaches sched.c:775 (if (CONSTANT_P (y))) and returns rtx_equal_for_memref_p (x, y) && ... = 0 at :776-778 — the function's own header comment states it at :605 ("static variables with different addresses cannot conflict"). This is the OPPOSITE pairing to §30 #1, which is about a register-addressed bare-deref load stuck under a fixed-symbol store. Consequence for reading a diff: if your lui/lw of a D_ symbol is not moving, the blocker is never another D_ symbol's store — look for the register-based store (§16Z row 4) or for a call (flush_pending_lists: no memory op ever crosses a call, sched.md §64). (evidence: single-instance target reading in func_80185B44 case 0, plus a decisive source citation in the pinned tools/reference/gcc-2.7.2/sched.c)
(SHARPENS — sharpens §147-B corrected (L10117-10121, combine-orphaned sign-extension intermediates + the per-construct ablation table), §16Xy (L12922, the (u16)-on-HImode pair costs 8 bytes per site); evidence: byte-probed; from func_8017C294)
§165-09 — B1. THE ORPHAN RECIPE: +1 orphan slot = one MEMORY-loaded short with a surviving HImode use; a register-sour
(SHARPENS — sharpens §147-B as corrected (L10117-10121, the per-construct ablation table with no rule behind it), §16Xy (L12922), §164-66 (L13281); evidence: byte-probed; from func_8017C294)
§165-02 — THE ORPHAN-SLOT RECIPE: A NARROW VALUE MUST COME FROM MEMORY TO COST FRAME. This is the rule behind §147-B's ablation table, §16Xy's (u16) pair and §164-66's volatile bitfield — all three are one law.
THE LAW. An orphan slot costs exactly 8 bytes and needs a narrow (HI/QI) value LOADED FROM MEMORY whose narrow use survives combine. Memory is the load-bearing half: combine can only fold the widening into an lh/lhu when there IS a load to fold it into, so a register-sourced short — a parameter, a call return, an arithmetic result — can never orphan, no matter how you cast or re-tie it.
Read the three existing entries through it and they collapse into one table:
| construct | orphans | why |
|---|---|---|
MIN(mem_short, mem_short) chain (§147-B) |
4 | two memory loads × two surviving narrow uses |
MAX chain (§147-B) |
2 | cse1 elides two conversions per max pass |
f((u16)s, …) on s16 s = TBL[i] (§16Xy) |
1/site | copy + andi keeps the narrow pseudo |
volatile u16 ×2 → C bitfield (§164-66) |
1 | volatile MEM blocks the zero_extend fold |
| clamp / base-fold, value already SImode (§147-B) | 0 | no lh |
| value from a CALL RETURN (§164-66's table) | 0 | not from memory |
Wording correction to carry forward: the pseudo that gets the slot is the SImode extension, stranded when combine cannot fold it (§164-66's (subreg:SI (reg:HI 81) 0)). The surviving narrow use is what prevents the fold; it is not itself the orphan.
BYTE EVIDENCE. func_8017C294 (ov_SC01_077, 246 ins): the target carries 16 orphan slots, the draft 14, and every candidate the crack agent probed for a 15th/16th confirms the recipe from the negative side — duplicate loads of one short (cse merges), short-to-short alias chains (cse coalesces), per-group tail accumulators, and 128 retyping combos over {x,y,xw,yh,bx,bz,min,max,t} all buy zero, because none of them adds a memory load with a surviving narrow use. Independent corroboration: §164-66's isolated 4-line A/B table on the same cc1.
DIAGNOSTIC TELL. You need N more (or fewer) bytes of vars and you are hunting a dead local. Count memory-loaded narrow values with a surviving narrow use, not declarations. To ADD 8 bytes, add a memory short read with a narrow consumer; to REMOVE 8, bind the value to an s32 first — a register-sourced short is inert and no amount of retyping will make it pay.
(SHARPENS — sharpens §20, §164-44, §5a, §160d; evidence: byte-probed; from func_8017F2D4)
§165-10 — THE §20 POINTER-TO-GLOBAL SPELLING IS A CROSS-JUMP IDENTITY DIAL: two arms that touch the SAME global must not be spelled the same way. (sharpens §20's scalar-global-RMW bullet (L1907-1918), which prices the choice only as one-base-reg vs two independent %lo folds; and §164-44, which says the §5a barrier works by making the two RTL streams UNEQUAL — this is the pure-C way to do that. Composes with §88a/§162h/§164-69.)
Target shape — the same global incremented in two arms of one switch, in two DIFFERENT forms:
case 1/2 @8017F338: lui $v1,%hi(D) ; addiu $v1,$v1,%lo(D) ; lhu $v0,0($v1) ; addiu $v0,1 ; sh $v0,0($v1)
case 3 @8017F3A4: lui $v0,%hi(D) ; lhu $v0,%lo(D)($v0) ; nop ; addiu $v0,1 ; lui $at,%hi(D) ; sh $v0,%lo(D)($at)
THE LAW. T *p = &D; *p = *p + 1; and D++ are not merely two address materialisations (§20) — they are two RTL streams, and find_cross_jump compares streams. Two switch arms whose tails are <increment D>; f(K,0); return 0; merge iff you spell the increment the same way in both. Spell them alike and gcc eats one arm's whole tail; spell them as the target does — pointer form in one arm, bare global in the other — and both survive. Read the form off EACH arm separately: a global RMW is not a per-function style choice. (The bare-global form is also +1 instruction on its own — an extra lui $at and a load-delay nop against the pointer form's shared base — so budget the two effects separately.)
BYTE EVIDENCE — func_8017F2D4, ov_SC01_005, 279 ins; one-hunk A/Bs off the MATCH baseline /home/musashi/bfm-decomp/.run/wave4/func_8017F2D4/func_8017F2D4.c (sha1 9fd76d10…), pinned triple via tools/match_one.py --asm-subdir asm/ov_SC01_005/nonmatchings/ov_SC01_005_jr_8017ED5C:
- baseline (pointer form in cases 1/2 and 8,
D_8011511A++in case 3) → MATCH 279; - case 1/2 rewritten to
D_8011511A++→ 270 ins, −9, 224 mismatched, LENGTH-DRIFT.objdump -drzshows only onelhu 0(...)/sh 0(...)pair onD_8011511Aleft in the arm region (baseline has two) and a backwardbne $v1,$v0,0x64from case 3 into case 1/2's surviving tail; - case 3 rewritten to the pointer form → 269 ins, −10, 199 mismatched. Both directions, one function, one global.
THE DIAGNOSTIC TELL. LENGTH-DRIFT −N on a jr-switch where N ≈ one whole arm tail, and the target shows the same symbol reached through a base register in one arm and through %lo(sym)($at) in another. Count the %hi(sym) materialisations in the target per arm before writing either arm. The inverse tell: if the target already merged the arms (one tail, N js into it), spell them identically and let cross_jump do it (§164-69).
(SHARPENS — sharpens §17a-1, §161c, §136 rule 12 (L8905), §135-9; evidence: byte-probed; from func_8017F2D4)
§165-11 — THE s16-RETURN TWIN OF §136 RULE 12: the residual is sll 16 ; sra 16, and it is COUNT-NEUTRAL. (sharpens §136 type-form rule 12 (L8905), which states the u16/andi 0xffff fingerprint only; the cure — call through a cast, §17a-1/§161c/tools/cast_call_sites.py — is unchanged.)
Target shape — a helper whose visible prototype returns s16, consumed as a plain s32:
/* 8017F588 */ jal func_8014168C
/* 8017F58C */ addu $s0,$v0,$zero <- RAW $v0, no extension
THE LAW. With an s16 return in scope, s32 r = f(1); makes the assignment carry the conversion and emits sll $v0,$v0,16 ; sra $sN,$v0,16 in place of the copy. ((s32 (*)(s32))f)(1) folds to the same direct jal (codegen-neutral, §161c) and keeps the raw $v0. Signed twin of rule 12: same cause, same cure, different fingerprint.
BYTE EVIDENCE (func_8017F2D4 case 7, pinned triple): baseline with the cast → MATCH 279; dropping only the cast, against the draft's own extern s16 func_8014168C(s16 a0); → mine=279, target=279, 3 mismatched, class STRENGTH/sll!=addu, idx 174-176 (sll v0,v0,0x10 ; sra s0,v0,0x10 ; bnez s0 vs addu $s0,$v0,$zero ; sll $v0,$s0,16 ; bnez $v0).
THE DIAGNOSTIC TELL. sll 16 ; sra 16 immediately after a jal in YOUR output where the target has addu $sN,$v0,$zero — and the counts are equal, because the extension displaces the copy rather than adding to it. Rule 12's andi 0xffff tell cannot fire on a signed callee; check the declared return type of every callee whose result you widen.
(SHARPENS — sharpens §16 (shared-ret0 goto), §164-55, §3-T4, cookbook-index L26; evidence: byte-probed; from func_8017F2D4)
§165-12 — break AND return 0 ARE BYTE-DISTINCT INSIDE A SWITCH ARM, AND THE DIFF IS COUNT-NEUTRAL. (sharpens §16's shared-ret0-goto bullet, which covers two if tests routed to one trailing return 0, and §164-55, which prices ARM ORDER; neither says the spelling INSIDE one arm moves bytes at equal length.)
Target shape — one arm reaches the switch's own fall-out instead of materialising its zero:
/* 8017F57C */ j .L8017F710 <- this arm `break`s
...
.L8017F710: addu $v0,$zero,$zero <- expand_end_case's fall-out, shared with the out-of-range default
.L8017F714: lw $ra,0x1c($sp) … <- the epilogue every `return` jumps to
THE LAW. break as an arm's last statement jumps to the expand_end_case fall-out label, which already holds the default's $v0 = 0; return 0; materialises its own zero and jumps to the epilogue label one instruction later. Both spellings cost 2 instructions, so the count does not move — what moves is what is live into the tail and how the arm's own body schedules against it.
BYTE EVIDENCE (func_8017F2D4, case 8, pinned triple): baseline break; → MATCH 279. That one break; → return 0; → mine=279, target=279, 6 mismatched, class OPCODE-MIXED: the arm's D_8011511A increment loses its shared base register (lui $a0 ; addiu $a0 ; lhu $v1,0($a0) where the target has lui $v1 ; addiu $v1 ; lhu $v0,0($v1)) and the move $v0,zero lands in the load-delay slot the target leaves as nop.
THE DIAGNOSTIC TELL. Equal counts, a handful of mismatches concentrated in ONE arm's tail, and a j in the target that lands on a bare addu $vN,$zero,$zero which the out-of-range default also reaches. That shared zero is the break target — never write return 0 into an arm whose j lands there. Inversely, an arm whose j SKIPS that instruction and lands on the epilogue returned its own value. Read the two labels apart before writing either arm; they are one instruction and two very different C spellings.
(SHARPENS — sharpens §93, §88e, §20 (stale .o); evidence: byte-probed; from func_8017F2D4)
§165-13 — match_one PRINTS A STAGE'S STDERR ONLY ON A NONZERO rc, SO WARNINGS ARE INVISIBLE — "the gate was quiet" is not evidence. (the rc==0 complement of §93, which covers the FAILING-stage case.)
tools/match_one.py:84-90 is, for each of cpp / cc1 / maspsx / as:
if p.returncode: print('CC1 FAIL\n' + p.stderr.decode()[-1800:]); sys.exit(1)
— stderr is dropped when the stage succeeds. A MATCH therefore tells you nothing about diagnostics: an implicit declaration, a block-scope extern whose type conflict is only a warning (§55a), or a -Wreturn-type on the body all pass silently and then surface as TU noise — or, if the decl order flips, as a cc1 exit 33 — once the body is spliced. This is a §52b-gap contributor, not a cosmetic one.
THE CHECK, ~30 seconds. Run the §93 recipe on a passing build and assert stderr is zero bytes, not that rc is 0:
mipsel-linux-gnu-cpp … > t.i 2> cpp.err ; echo "cpp rc=$? err=$(stat -c%s cpp.err)"
cc1 -O2 … < t.i > t.s 2> cc1.err ; echo "cc1 rc=$? err=$(stat -c%s cc1.err)"
Zero-byte cc1.err is the only evidence that a clean gate means a clean compile. Do this before reporting a draft as bank-ready.
(SHARPENS — sharpens §164-41 (L12718-12741) — proves the copies must EXIST, says nothing about their ORDER relative to the count read, §55a / L2491 (INSN_LUID tie-break, named but never instantiated at; evidence: byte-probed; from func_8017F9AC)
§165-14 — L3 — i = o->n; must be read BEFORE the two alias copies, so the count load's delay slot is filled by the `pb
(SHARPENS — sharpens §164-41 (L12718, which proves the alias copies must EXIST and stops there) with the statement-ORDER half; first source-cited instance of the sched.c INSN_LUID tie-break §55a/L2491 names; evidence: byte-probed; from func_8017F9AC / func_8017DC1C)
§NNN — THE ALIAS COPIES ARE NOT ENOUGH: THE COUNT MUST BE READ BEFORE THEM, AND for (i = o->n; …) IS THE WRONG PLACE. (a separate +11 off the same MATCH base as §164-41's -25 — two independent rows, two independent mistakes.)
Target shape — the loop head with BOTH delay slots carrying an alias copy, and the j that enters the head left as a real nop:
j .L8017FA14
nop <- reorg did NOT get to steal a copy into this
.L8017FA14: lw $t2,0x10($a0) <- the count addu $t1,$a2,$zero <- load-delay: pb = b beqz $t2, addu $a3,$a1,$zero <- branch-delay: pa = a
THE LAW. Write i = o->n; FIRST, then pb = b; pa = a;. All three insns are mutually independent, so rank_for_schedule ties on INSN_PRIORITY and on all three dependence classes and falls through to its documented stable-sort fallback — /* If insns are equally good, sort by INSN_LUID (original insn order) … */ return INSN_LUID (tmp) - INSN_LUID (tmp2); (sched.c:2425-2428, source-checked in tools/reference/gcc-2.7.2/). Source order IS the schedule here. Put the copies first and the count load takes a nop of its own and reorg steals the pb copy into the j delay slot the target leaves empty.
for (i = o->n; i != 0; i--) IS THE SAME MISTAKE — this is the trap. The for-init is emitted where the for statement sits, i.e. below the copies, so it carries the same LUID position and produces byte-identical wrong output. The count read must be its OWN statement, above the copies.
BYTE EVIDENCE. func_8017DC1C (ov_SC07_006, 1,518-ins MATCH base, 25 expansions) — do-not-re-buy table .run/giants/s21_8017DC1C_report.md §4, two adjacent rows: i = o->n; after pb = b; pa = a; → 1529 ins, 1452 mismatched (+11); for (i = o->n; …) → 1529 ins, 1452 mismatched — identical. Second instance, byte-visible: func_8017F9AC at .L8017FA14 and three more, each entered by a j whose delay slot is a real nop (8017F9F4).
⚠ The +11 is NOT one insn per site across 25 sites, so the narration 'a nop per block plus a stolen copy' does not add up to what was measured — two passes partly cancel and the per-site arithmetic was never derived. Direction and magnitude are byte-proven; the per-site story is not. (Same report, same caution as §164-41's ⚠ over its '2 moves × 25 = 50'.)
DIAGNOSTIC TELL. You already have §164-41's alias copies and the count load STILL grew a nop, and the j entering your loop head has swallowed one of the copies where the target leaves a real nop ⇒ you wrote the count read after the copies, or folded it into the for. Hoist it above them as its own statement. Do not reach for a scheduling fence or a pin — this is LUID order and it is free.
(SHARPENS — sharpens §75c (L5997), §161c (L10999), §163a (L11762), §51g (L3769, the cc1 compatibility oracle table L3845-3852); evidence: byte-probed; from func_8017FDF8)
§165-15 — BLOCK SCOPE IS NOT A SOLVENT FOR AN ARITY CONFLICT: A () DECL DEFUSES A VISIBLE PROTOTYPE AT NO SCOPE. (BOUNDS §163a, whose "block scope is a conflict SOLVENT" is byte-proven on a DATA symbol only; the file-scope arm restates §75c/§161c — the new content is the block-scope arm and the reason the solvent has nothing to dissolve.)
The situation. The host TU already declares the callee with the WRONG arity at file scope, and your body calls it with more arguments:
src/ov_SC07_006/ov_SC07_006_jr_8017BEBC.c:609 extern void func_800599B8(u16 *);
your draft (x5) func_800599B8(D_8018DF60, &D_801F60A0[0]);
THE LAW. Neither placement of a no-prototype declaration rescues the call:
| what you add | result |
|---|---|
extern void f(); beside the host's file-scope prototype |
CC1 FAIL, too many arguments to function 'f' x5 |
extern void f(); at block scope inside the caller, host line untouched |
CC1 FAIL, the identical 5 errors |
| host prototype kept verbatim + §17a-1 call-site cast | MATCH 317 |
host line 609 replaced with extern void f(); |
MATCH 317 — but it costs a host edit |
WHY — and why §163a does not reach it. The two declarations are COMPATIBLE: §51g's cc1-validated oracle has void X(s16); then void X(); -> ACCEPTED, and the stderr proves it — five too many arguments, zero conflicting types. So there is no redeclaration diagnostic for block scope to downgrade to a warning. The failure is not decl-vs-decl at all; it is call-vs-composite. convert_arguments (tools/reference/gcc-2.7.2/c-typeck.c:1623) errors only on type == void_type_node with arguments remaining — i.e. only when the callee's type at the call STILL carries a prototype TYPE_ARG_TYPES. A () decl has TYPE_ARG_TYPES == NULL and can never trigger it alone; it triggers because C89 3.1.2.6 makes the later declaration's type the composite, and the trigger condition is that a prior declaration is VISIBLE, not that it is in the same scope. Block scope inherits void f(u16 *) exactly as file scope does.
PREFER THE CAST, NOT THE HOST REWRITE. Both bank at 317, but keeping the host's prototype text byte-for-byte and casting at the call leaves the integration surface empty (§52b/§161c) — no //@EDIT, no sibling coordination, and the family remap carries unchanged:
extern void func_800599B8(u16 *); /* the host's exact text */
((void (*)(u8 *, u16 *))func_800599B8)(D_8018DF60, &D_801F60A0[0]);
DIAGNOSTIC TELL — one word in the diagnostic picks the lever.
conflicting types for 'X'⇒ decl-vs-decl ⇒ §163a's block-scope solvent is live (move BOTH the typedef and the extern into the block).too many arguments to function 'X'⇒ call-vs-composite ⇒ no declaration spelling at any scope helps. Cast the call site (§17a-1/§161c), or replace the host prototype.
Byte evidence: func_8017FDF8 (ov_SC07_006, 317 ins, banked). Probes .run/wave4/func_8017FDF8/probes/A_filescope.c:44-45 (file-scope ()), B_blockscope.c:44,106 (block-scope ()), C_cast.c = shipped func_8017FDF8.c:86 (MATCH 317), E_widen_decl.c (host rewrite, MATCH 317). Scope: one callee, one host TU; the negative arms are compile-outcome probes. The file-scope arm reproduces §75c's func_8012F14C result independently — two functions, same law.
(SHARPENS — sharpens §162c, §78, §164-16, §164-24; evidence: byte-probed; from func_8017FE38)
§165-16 — THE |-CHAIN MIRROR SELECTS ONE TERM, AND FOR getTPage IT IS THE STOCK MACRO ORDER THAT SELECTS THE RIGHT ONE. (sharpens §162c, which states the mirror for a 2-term | and ends asking for exactly this sweep; BOUNDS §78's headline as §164-24 paraphrases it — 'all seven | parenthesisations COLLAPSE to one codegen'. They all reassociate; they do not all land the constant on the same term.)
Target shape — a 5-term tpage chain where the folded constant rides ONE of the variable terms:
lhu $a0,0x104($a1) ; x
lhu $a1,0x106($a1) ; y
addiu $a0,$a0,0x140
andi $v1,$a1,0x100 ; the y-lo term
srl $v1,$v1,4
andi $v0,$a0,0x3C0 ; the x term
srl $v0,$v0,6
ori $v0,$v0,0x80 <- the constant rides the X term
THE LAW. With tp/abr compile-time constants the PsyQ macro folds to 0x80 | A | B | C over three variable terms. fold's associate:/split_tree path (§164-16) then migrates 0x80 to exactly ONE of them, decided by the written tree — not by a tendency, and not uniformly. Write the macro VERBATIM in its canonical left-assoc order — constant first — and only then does the ori land on the x term.
BYTE EVIDENCE — 7 spellings, one compilation each (.run/wave4/func_8017FE38/vary.py, builds in v_V3…v_VA; target asm/ov_SC07_001/nonmatchings/ov_SC07_001_jr_8017BEBC/func_8017FE38.s:184-191):
| spelling | ori 0x80 lands on |
|---|---|
0x80 | A | B | C (V4, canonical) |
the x term — TARGET |
A | B | 0x80 | C (V5) · (B|0x80) | C | A (V9) |
a y term |
A | C | (B|0x80) (V3) · A | ((B|0x80)) | C (V6) · A | C | B | 0x80 (V8) · A | (0x80|B) | C (VA) |
the y&0x100 term |
THE DIAGNOSTIC TELL. Your |-chain block is byte-exact except that the single ori $rX,$rX,K hangs off the WRONG andi/srl pair. Do not sweep parenthesisations at random and do not hand-fold: transcribe the SDK macro in source order first — it is the spelling the original used, and it is the only one of seven that puts the constant on the third term.
(SHARPENS — sharpens §164-48, §162k1, §163f, §164-49; evidence: byte-probed; from func_8017FE38)
§165-17 — THE DECLARED-WIDTH ALLOCATION LEVER REACHES LOCAL-ALLOC AND THE ARGUMENT REGISTERS, WITH NO CALL IN THE FUNCTION. (sharpens §164-48, which proves width->allocation only at the callee-saved / global.c:594 level on a call-crossing $s0/$s1 swap, and whose mechanism note explicitly leaves the local-alloc half as an unverified hypothesis; extends §162k1 from the andi COUNT to allocation, at HImode.)
THE LAW. Declaring the destination of a truncating assignment u16 rather than s32 reorders the block quantities in local-alloc and transposes two CALLER-saved registers — here x/y move $a1/$a0 -> $a0/$a1, which is what frees $a1 for the incoming parameter. func_8017FE38 contains no call at all, so §164-48's call-crossing framing is scenery: the knob is the width, the scope is whichever allocator owns the pseudo.
Byte evidence. func_8017FE38 (ov_SC07_001, 239 ins): 44 -> 32 mismatched on the declared width of x/y alone, inside a controlled type x order matrix (~200 builds, .run/wave4/func_8017FE38/f_*, o_*, q_* — five destination types crossed with operand orders).
THE DIAGNOSTIC TELL. Two CALLER-saved registers transposed across a whole block, with an incoming parameter evicted from its own argument register, on values that arrive via lhu. Sweep the destination widths before any register __asm__ pin — §164-49's rule applies here too, and the width route keeps the family (§37/§162p3).
Scope: n=1 function; no .lreg/.greg dumped. The measurement is the law; the RTL story is still §164-48's open hypothesis — refute it, do not cite it.
(SHARPENS — sharpens §164-16 (L12191, honest scope: 'one function, two sites, ten spellings... Not replicated on a second function'), §164-24/§16x (L12381, closing imperative 'When you use the variable; evidence: byte-probed; from func_80180C40)
§165-18 — THE FOLD-MISPLACED CONSTANT CAN COST LENGTH, NOT ONLY A REGISTER — AND A PAIR OF FRESH TEMPS IS ENOUGH TO BUY IT BACK. (replicates §164-16 (L12191) on a second function and a second overlay; bounds §164-24's closing imperative 'when you use the variable-K form, SPEND AN EXISTING VARIABLE' (L12381); adds a third residual surface to §164-75 (L13479). Mechanism is unchanged and is fold, NOT combine — see the ⚠ at the end.)
Target shape — three terms: a loaded field, a masked call result, and a literal.
target: jal rand ; … ; lhu $v1,0x12($v0) ; addiu $v1,$v1,-0x200 ; andi $v0,$v0,0x3FF ; addu ; sh
yours : jal rand ; … ; andi 0x3FF ; <constant materialised standalone> ; addu ; lhu … ; addu ; sh <- literal rode the CALL term, +7 ins
THE LAW — unchanged, cite §164-16/§164-24 for it. fold's associate: block (fold-const.c:3685) splits arg0 first (:3703), arg1 second (:3759), runs ONCE, and rebuilds VAR op (ARG1 op CON) (:3735-3737): the literal always lands on the term you did NOT write it beside. split_tree (:882-950) is opaque to a leaf VAR_DECL, which is why a statement boundary — and at this level only a statement boundary — is the lever. What is new here is the residual's SHAPE and the cost of the fix.
BYTE EVIDENCE — func_80180C40 (ov_SC02_041, 165 ins, MATCH; INCLUDE_ASM site src/ov_SC02_041/ov_SC02_041_jr_8017BEBC.c:4911; drafts preserved in .run/wave4/func_80180C40/). Five spellings of X - 0x200 + (rand() & 0x3FF), where X = *(u16 *)(*(s32 *)(*(s32 *)(e + 0x64) + 0x20) + 0x12):
| spelling | file | result | why (pinned source) |
|---|---|---|---|
s32 r = rand(); … X - 0x200 + (r & 0x3FF) |
v3.c |
7-ins residual | arg0 splits, :3703 |
s32 r = rand() & 0x3FF; … X - 0x200 + r |
v4.c |
same 7 | naming the call temp is inert |
(rand() & 0x3FF) + (X - 0x200) |
v7.c |
same 7 | arg1 splits, :3759 |
X + (rand() & 0x3FF) - 0x200 |
v6.c |
worse, 166 ins | neither operand splittable ⇒ K applies to the SUM (§164-24 row 2) |
s32 r = rand(); s32 b = X - 0x200; *(u16 *)(*(s32 *)(e + 0x20) + 0x12) = b + (r & 0x3FF); |
v5.c = final |
MATCH 165/165 | b is a VAR_DECL leaf ⇒ split_tree refuses |
Both temps are load-bearing, for the two reasons §164-16 already names: r alone leaves the fold intact (its v_X8); b alone hoists the whole lhu chain above the jal (preexpand_calls, expr.c:8672 — no pass moves a load back across a call).
⚠ BOUNDS §164-24 — its fresh-temp warning is allocation pressure, not a property of the form. §164-24 measured t = a - K; a = t + J on func_8017D7C0 — the neighbouring function in this same TU, same -0x200 literal — as 'gets the tree right and still fails on allocation', and closed with spend an already-live variable. Here the fresh pair r/b matched with no pins, no pad, frame 0x20, $s0/$s1/$s2 natural. The discriminator is the destination: §164-24's site is an in-place s16 RMW (rot.vx += …) whose accumulator is already live; this site STORES TO A DIFFERENT OBJECT than the one it loads, so the hoisted temp adds no conflict. When the store target differs from the loaded field, try the fresh pair first; reach for §164-24's reused variable only on an RMW. (Not ablated — no variant here isolates fresh-vs-reused at fixed tree shape.)
DIAGNOSTIC TELL — the same fold, now three surfaces. (i) §164-75: addiu -K on the WRONG operand of a nearby addu, same instruction count, no length drift. (ii) §164-16: your addiu K sits under the jal's return value where the target's sits under the lhu. (iii) NEW — the literal never appears as an addiu at all: it is materialised as a standalone constant plus a separate addu, and the residual carries LENGTH. All three mean one thing: hoist the field ± K sub-expression into its own statement, keep the call in its own statement above it, and stop sweeping parenthesisations — §164-16's ten spellings and this function's five both say the sweep is wasted budget.
⚠ UNVERIFIED SUB-OBSERVATION — do not cite as law. The drafter recorded surface (iii)'s standalone constant as li $a0,0xFE00 — an unsigned 16-bit materialisation of -0x200. Neither pinned path produces that: force_to_mode's PLUS arm (combine.c:5855-5879) requires exact_log2(-smask) >= 0 (an alignment mask) and never fires for a 0xFFFF halfword mask, and its binop fallthrough clears bits outside MASK only if (code == IOR || code == XOR) (:5931-5934), explicitly not PLUS; gen_lowpart_common (emit-rtl.c, CONST_INT arm) PRESERVES -512 as a negative HImode value because its high bits already equal the sign bit. §1841 warns that objdump prints ori $r,$zero,K (34xx) and addiu $r,$zero,-K (24xx) identically as li. Re-read the opcode before this goes in the tell; the length drift is the reliable half.
(SHARPENS §164-16, §164-24, §164-75; evidence: byte-probed, five spellings + MATCH; from func_80180C40.)
(SHARPENS — sharpens §162d1, §48-A2, §76, §156-1; evidence: byte-probed; from func_80181E18)
§165-19 — THE BIRTHING BOOST IS A REGISTER LEVER TOO: the call-1 result that sinks into call-2's delay slot INHERITS the callee-saved register that call-2's ARGUMENT SETUP just killed. (sharpens §162d1, which states the same reg_n_sets == 1 dial as a pure SCHEDULING lever and whose tell — "the insn in the second jal's delay slot is that call's own argument CONSTANT" — cannot fire when the arg setup is a register copy; and bounds §162d1's "reach for a fresh local first", which is silent on RE-ASSIGNING that local.)
Target shape — a call pair whose second call passes a held address that nothing uses afterwards:
lui/addiu $s0, %hi/%lo(D_800D3918) <- the address, cse'd into a callee-saved reg
jal func_8012B70C <- call 1 ($a0 = $s0)
sh $v0, 0x14($sp)
addiu $a0, $sp, 0x10 <- call 2's arg setup …
addu $a1, $s0, $zero <- … the LAST use of $s0
jal func_8012B70C <- call 2
addu $s0, $v0, $zero <- call 1's result, IN THE SLOT, REUSING $s0
sll $s0,$s0,1 ; subu $s0,$s0,$v0 ; andi $s0,$s0,0xFFF
THE LAW. birthing_insn_p (tools/reference/gcc-2.7.2/sched.c:2468-2495) raises an insn to max_priority only when reg_n_sets[dest] == 1 (:2489), via adjust_priority (:2506-2549), so call-1's result copy sinks below call-2's argument setup only if the C variable holding it is assigned exactly once. §162d1 stops at the schedule. The consequence worth the entry is the ALLOCATION: once the copy sinks past the last use of the address register, the two live ranges stop overlapping and global-alloc hands the sunk copy that very register. Write t = f(a); t = (t*2 - g(b)) & M; — two sets — and the copy is emitted eagerly under call 1, overlaps the address, and buys a second callee-saved register.
BYTE EVIDENCE (func_80181E18, ov_SC02_041 / jr_8017BEBC, 121 ins, banked; drafts in .run/wave4/func_80181E18/v/, counter-probes re-run at vet time against a reconstructed target):
| spelling of call-1's result | sets | close |
|---|---|---|
t1 = f(); t1 = (t1*2 - g()) & 0xFFF; (vG.c) |
2 | 7 — move $s2,$v0 directly under call 1 |
same + s16 *p alias for the address (vI.c) |
2 | 7 |
same + register s32 t1 __asm__("$16") (vH.c) |
2 | 8 — the pin goes the WRONG way (§72) |
t1 = f(); … (t1*2 - g()) & 0xFFF … (vJ.c, banked) |
1 | MATCH — addu $s0,$v0,$zero in the slot |
both results named single-set locals (P5) |
1 | MATCH |
expression result named too, then stored (P6) |
1 | MATCH |
call-2's result named AND re-assigned (P7) |
1 | MATCH |
⚠ The banked header's own explanation is refuted — do not repeat it. It reads "don't name the result at all; let the store consume it." Naming is inert: P5/P6/P7 name every value (P7 even re-assigns call-2's result) and all three MATCH. The only variable whose set count matters is the one holding call 1's result.
THE DIAGNOSTIC TELL. A call pair; an argument register loaded from a callee-saved register that has no use after the setup; and your draft holding call-1's result in a different callee-saved register with the copy sitting directly under call 1, where the target has it in the second jal's delay slot in the SAME register as the dying argument. Count the assignments to the C variable holding call-1's result before you touch scope, pins or the permuter — it is a one-statement edit. (Scope: one function; the reg_n_sets half is corroborated by §162d1 and sched.md S2, the register-inheritance half is n=1 but source-explained.)
(SHARPENS — sharpens §136-13, §136-14, §164-79, §164-25; evidence: byte-probed; from func_80181E18)
§165-20 — THE STATEMENT THAT CONSUMES A CALL'S RETURN VALUE IS WHERE $v0 DIES: every constant materialised ABOVE it is denied $v0. (sharpens §136-13, whose tell already names "constants landing in $v0 instead of $v1" but whose lever is a LOAD hoisted above the stores under a varying-address blocker; and it is the free, source-order form of what §164-25 buys with a $2 pin.)
Target shape — a call-pair result and three constants stored through one base:
andi $s0,$s0,0xFFF <- the expression finishes; $v0 (call 2's return) dies HERE
addiu $v0,$zero,0x1 ; sh $v0,0x34($s1)
addiu $v0,$zero,0x1E ; sw $v0,0x1C($s1)
addiu $v0,$zero,0x10 ; sw $s0,0xE4($s1)
j … ; sw $v0,0xE8($s1)
THE LAW. A constant is materialised where its statement sits. Write the store that consumes the call expression BELOW the constant stores and the sll/subu/andi tail that reads $v0 is expanded below them too — $v0 is still live, and every constant takes $v1. Move that one store ABOVE them and the constants reuse $v0 serially, exactly as the target has it. Move the consuming STORE, not the expression (the calls are already above either way).
And you cannot read emitted store order as source order. Source order 0xE4, 0x34, 0x1C, 0xE8 emits as 0x34, 0x1C, 0xE4, 0xE8: memrefs_conflict_p (tools/reference/gcc-2.7.2/sched.c:614) matches the two MEMs' common base (rtx_equal_for_memref_p (x0,y0)), recurses onto the constants and returns no-conflict when the byte ranges are disjoint (:752-758), so same-base/distinct-offset stores reorder freely. That is §136-14's $sp clause on a general register base, and the complement of §164-79's two-different-bases barrier. Do not chase the emitted order.
BYTE EVIDENCE (func_80181E18, ov_SC02_041, 121 ins, banked; probes re-run at vet time):
position of *(s32 *)(a0+0xE4) = (t1*2 - g()) & 0xFFF; among the block's 4 stores |
close |
|---|---|
3rd — below sh 0x34 and sw 0x1C (P1) |
8 |
2nd — below sh 0x34 only (P8) |
5 |
1st (vJ.c, banked) |
MATCH |
The two dials are orthogonal and must be closed together. With a two-set t1 (§NNN, the birthing-boost entry) the count is 7 whether the store is 1st (P4) or 3rd (vG); with a single-set t1 and the store 3rd it is 8 — worse than fixing neither. There is no gradient across the pair, only within this one (§164-43's warning, second instance).
THE DIAGNOSTIC TELL. Small li/addiu constants in $v1 where the target has $v0, inside a block that also stores a call result, opcode stream otherwise exact. Count how many stores sit between the call pair and the store that consumes it, and lift that store one position at a time — each position is worth real instructions.
(SHARPENS — sharpens §164-73, §164-74, §136d-2, §164-57; evidence: byte-probed; from func_80181E18)
§165-21 — TWO COMPUTED ARMS WANT ONE SHARED TRAILING STORE. §164-73/§164-74's "write the store twice" is scoped to CONSTANT arms. (bounds §164-73 ("A TWO-CONSTANT SELECT WHOSE DESTINATION IS MEMORY WANTS TWO STORES IN THE ARMS") and §164-74 ("A TWO-ARMED CONSTANT STORE: WRITE THE STORE TWICE") — both are stated for two literals, and running either prescription on computed arms costs +2 instructions.)
Target shape — the delay slot holds ARITHMETIC, not a literal, and exactly one store sits below the join:
bnez $v0,.L1
addu $v0,$s2,$s0 <- the TRUE arm's value, in the slot
subu $v0,$s2,$s0 <- the FALSE arm, falling through
.L1: j .Ljoin
sw $v0,0xE4($s1) <- ONE store
THE LAW. When the two arms are single-insn expressions over the SAME pair of live registers (not two literals), write the plain two-armed select into a local and store it ONCE:
if (rand() & 1) { v = base + r; } else { v = base - r; }
*(s32 *)(a0 + 0xE4) = v;
The arms are then one insn each, dbr takes the true arm into the bnez's slot, and the shared store rides the join's j. §164-74's lever works by denying jump.c's if-then-else→conditional-overwrite collapse a REG destination so that cross_jump merges two identical sh tails — with computed arms there is real arithmetic above each store, the merge is not the 1-insn minimum path, and you pay for it.
BYTE EVIDENCE (func_80181E18, ov_SC02_041, 121 ins, banked; probes re-run at vet time):
| spelling | result |
|---|---|
if/else into v, ONE trailing store (banked) |
MATCH, 121 ins |
store duplicated into both arms (Q2, §164-73/§164-74's prescription) |
123 ins, 70 mismatched |
v = base + r; if (!(rand() & 1)) v = base - r; (Q1, §136d-2's canonical form) |
123 ins, 107 mismatched |
⚠ Q1 is CONFOUNDED and proves nothing about jump.c: that spelling also moves the rand() call below the first arm's arithmetic, so the two streams differ for a reason unrelated to the collapse. Read it as "do not reach for the conditional-overwrite spelling when a CALL is the condition", not as a jump.c result.
THE DIAGNOSTIC TELL — read the delay slot. A literal in the branch's delay slot plus one store below the join ⇒ §164-74, duplicate the store. An addu/subu/addiu rD,rS,rT in the slot plus one store below the join ⇒ this entry, one shared store. (Scope: one function, three spellings.)
(SHARPENS — sharpens §164-56 (L13070-13086) — fold_truthop → range_test, the OTHER path of the same function; its closing sentence 'On BFM, the range fold is the ONLY thing an ||/&& chain buys ; evidence: byte-probed; from func_80182784)
§165-22 — fold_truthop HAS A SECOND PATH: TWO BIT-MASK TESTS ON ONE WORD MERGE INTO A SINGLE andi + COMPARE. The barrier is splitting the if, NOT naming the word. (sharpens §164-56, which reads the range_test path of the SAME function and closes with the now-refuted "On BFM, the range fold is the ONLY thing an ||/&& chain buys you — nothing else about short-circuit spelling is observable"; and §21's func_801775E0 bullet, which prescribes the split for the range case only.)
Target shape — one word, two independent bit tests, ONE load (cse commons it):
lw $v1,0xE0($s0)
andi $v0,$v1,0x1 ; beqz $v0,.Lskip ; andi $v0,$v1,0x2 ; beqz $v0,.Lskip
Yours, from if ((f & 1) && (f & 2)) { … }:
lw $v0,0xE0($s0) ; li $v1,0x3 ; andi $v0,$v0,0x3 ; bne $v0,$v1,.Lskip <- ONE test, mask 3, compare 3
THE LAW (read out of the pinned fold-const.c, not inferred). When the range_test hand-off at :2747-2775 fails — which it always does for two DIFFERENT masks, because operand_equal_p (ll_arg, rl_arg) is false for f&1 vs f&2 — and the BRANCH_COST >= 2 "evaluate the RHS unconditionally" bail at :2786-2790 does not fire (BRANCH_COST is 1 at -mcpu=3000, §164-56), control falls into the field-merge path at :2794-3027. decode_field_reference (:2392) peels each f & K to (inner f, mask K); :2821 demands both sides decode to the same inner; wanted_code is EQ_EXPR for && and NE_EXPR for || (:2838); a != 0 test with the wrong code is rewritten as == mask at :2839-2853 only if integer_pow2p (mask); and the tail at :3019-3027 emits (inner & (ll_mask|rl_mask)) wanted_code (l_const|r_const). Hence (f&1) && (f&2) ⇒ (f & 3) == 3. §164-56 reads BRANCH_COST==1 as the reason short-circuit spelling is unobservable; it is the reason this path FIRES.
Bounds, every one byte-probed on the pinned cc1:
&&requires BOTH masks to be powers of two (the!=0→==maskinversion).(f&3) && (f&4)does not merge — bothandi/beqsurvive.||requires nothing —wanted_codeis alreadyNE_EXPR:(f&3) || (f&4)⇒andi 7 / beq, noliat all (3 ins vs 5). Double-negated&&likewise:!(f&1) && !(f&2)⇒andi 3 / bne $2,$0.- NOT pairwise — unlike §164-56's
range_test. The merged node is itself comparison-class, so it re-enters:(f&1)&&(f&2)&&(f&4)collapses all the way toandi 7 / li 7 / bne. - Same word only.
(*(int*)(p+0xE0)&1) && (*(int*)(p+0xE4)&2)fails:2821and stays two tests. - A named temp is NOT a barrier.
int t = *(int*)(p+0xE0); if ((t&1) && (t&2))merges identically. Read §164-56's "its only barrier is a statement boundary" as a boundary on the&&NODE. The lever is nestedifs; cse still commons the repeatedlwinto the one load the target has. - Masks above
0xFFFFstill merge, asli $v1,MASK / and / bne— 5 ins, not 4.
BYTE EVIDENCE. func_80182784 (ov_SC02_041, 124 ins, match_one MATCH on iteration 2, .run/wave4/func_80182784/func_80182784.c; ⚠ §52b CANDIDATE — not byte-gated): the && gate on *(s32*)(a0+0xE0) scored 52 mismatched, three nested ifs scored 0. Isolated 11-spelling A/B on the pinned cc1 (-quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float): && ⇒ lw ; li $3,3 ; andi $2,$2,3 ; bne $2,$3 (the li fills the load-delay slot); nested ⇒ lw ; #nop ; andi $2,$3,1 ; beq ; andi $2,$3,2 ; beq. The isolated delta is −1 instruction, not the −2 the crack note reports — that draft's second instruction came from elsewhere; do not use −2 as the tell.
THE DIAGNOSTIC TELL. One andi in your output whose immediate is the bitwise OR of two of the target's andi immediates, preceded by an li of that same value and terminated by bne $x,$y where the target has beqz (for ||: the OR'd mask and no li). match_one classes it BRANCH-POLARITY or LENGTH-DRIFT −1 and the index routes you to §3-T4 — do not invert anything; add the mask immediates first. Then split the && into nested ifs. Do not hoist the word into a temp and do not reach for an __asm__ launder: this is a fold, and per §21 gcc re-derives it across barriers.
(SHARPENS — sharpens §164-56 (L13070-13086) and §21's func_801775E0 CANDIDATE bullet (L1985-1993); evidence: byte-probed; from func_80182784)
(SHARPENS — sharpens §162g — CROSS-JUMP DIRECTION: the surviving copy is always the LATER one (L11263), §3-T4 — Branch polarity: invert the source condition (L90), §32.2 — branch polarity is READ OFF T; evidence: byte-probed; from func_8018308C)
§165-23 — WHEN ONE ZERO-STORE SERVES AN OUTER AND AN INNER ARM, THE SURVIVOR'S POSITION IS THE SOURCE'S ARM ORDER — AND §3-T4 READS IT BACKWARDS. (the nested-arm corollary of §162g, whose tell table covers only N sibling arms feeding one label; BOUNDS §3-T4 / §32.2, whose "put the branched-to block in the else" gives the LOSING spelling here.)
Target shape — exactly ONE copy of a <store>; j <join> pair, sitting between an inner conditional branch and the block that branch selects, with an EARLIER conditional branch jumping forward into it (asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_80182E7C/func_8018308C.s):
.L801830CC: beqz $a0, .L801830F8 <- outer test, FORWARD into the survivor
nop
jal func_8012E544
...
beq $v1,$v0,.L80183100 <- inner test
nop
.L801830F8: j .L80183120 <- THE SURVIVOR (one instruction + its `j`)
addu $s1,$zero,$zero
.L80183100: lh $v1,0xE($a0) ... <- the block the inner test selects
THE LAW. Two source arms that write the SAME value and fall to the SAME join emit two identical <store>; j <join> blocks. Post-reload cross_jump keeps the LATER copy and redirects the earlier j into it (§162g: jump_chain push-front, jump.c:218-227; do_cross_jump deletes the stream before insn, jump.c:2537), and jump.c's invert-a-cond-jump-that-jumps-over-an-uncond-jump (jump.c:1737, invert_jump (insn, JUMP_LABEL (reallabelprev))) then folds the leftover cond-branch ; j pair into one inverted branch. So the survivor's POSITION names the LAST-EMITTED arm that contained it — and gcc-2.7.2 has no block-reordering pass (§164-55), so that is the last arm in SOURCE order.
-
survivor inside the inner arm, before the block the inner test selects ⇒ the outer copy was emitted FIRST ⇒ write the zero as the THEN of an inverted outer test:
if (cond == 0) { flag = 0; } else { p = f(); if (p) { if (*(u16*)(p+2) != 2) { flag = 0; } else { …compute… } } } -
survivor at the tail, after the inner compute block ⇒ the outer copy was LAST ⇒ it is the outer
else.
⚠ THIS INVERTS §3-T4 / §32.2. Their rule — "put the target's fall-through block in the if, the branched-to block in the else" — reads beqz $a0,.L801830F8 as "the zero-store is branched-to ⇒ make it the else", which is the losing spelling. The polarity you can see is post-invert: jump.c manufactured it from a bne the source never asked for. When a conditional branch's target is a cross-jump SURVIVOR, its opcode carries no source-order information — read the survivor's position instead.
BYTE EVIDENCE. func_8018308C (ov_SC01_077, 166 ins, banked commit:1678, src/ov_SC01_077/ov_SC01_077_jr_80182E7C.c:3203). A/B re-run at vetting time through the pinned triple, cc1 .s diffed directly (only the two spellings differ):
| source spelling | outer branch | inner branch | survivor sits | result |
|---|---|---|---|---|
if (cond == 0) { flag = 0; } else { … } |
beq $4,$0,$L33 |
beq $3,$2,$L6 |
between the inner test and the compute block | 139 insns — MATCH |
if (cond != 0) { … } else { flag = 0; } |
beq $4,$0,$L3 |
bne $3,$2,$L3 |
after the compute block, at the tail | 139 insns — 11 mismatched |
Read the second row carefully: the OUTER branch opcode is beq in BOTH. The outer polarity is not the discriminator — the INNER branch plus the block slide is.
THE DIAGNOSTIC TELL. match_one reports a clean instruction-count TIE with residual_class = BRANCH-POLARITY, and the diff shows one conditional branch flipped beq↔bne together with a whole block changing sides of it. A tie plus a sliding block is a block-ORDER diagnosis, not a polarity one. Do not invert the branch whose opcode differs (§3-T4) and do not reach for a pin: count the copies of the shared block — ONE in the target where your source has TWO arms writing the same value — and move the earlier one by re-ordering the OUTER arms.
⚠ Precondition. The tell only reads when the merge actually fired. TWO live copies in the target mean cross_jump refused (call-bearing suffix — §88a / §162h), and §164-69's call-free guard applies instead: hand-factor once at the last arm rather than duplicating.
(SHARPENS §162g, §3-T4, §32.2; evidence: byte-probed (A/B re-run at vetting, cc1 .s compared); from func_8018308C)
(SHARPENS — sharpens §8 duplicate-the-call-into-BOTH-arms bullet (L1893-1904, func_80159BE4), §31 Lever 4 — cross-jump the duplicated tail (L3269-3276, func_80163EC8), §164-69 — the cross-jump tell nee; evidence: byte-probed; from func_80183D68)
§165-24 — THE SHARED jal's OWN DELAY SLOT IS A COPY-COUNT ORACLE: a nop in the call's slot plus the SAME addu $aN,$sN,$zero in EVERY predecessor's slot means the SOURCE wrote the call >=2 times. (sharpens §8's duplicate-the-call-into-both-arms bullet (L1893-1904) and §31-Lever-4 (L3269-3276), which give the TARGET shape but never the residual signature; BOUNDS §164-69, whose discriminator table sends a call-bearing merged block to "source gotos"; the count law that permits this one is §162h.)
Target shape (func_80183D68, ov_SC03_118 / ov_SC03_119, 255 ins, MATCH; the two overlays' .s are byte-identical):
80184048 bne $v0,$v1,.L80184074
8018404C addu $a0,$s1,$zero <- arg copy 1
80184050 jal func_8018416C
80184054 addu $a0,$s1,$zero
80184058 j .L80184080
8018405C addu $a0,$s1,$zero <- arg copy 2
...
8018406C beqz $v0,.L80184148
80184070 addu $a0,$s1,$zero <- arg copy 3
.L80184074: lw $v1,0xCC($s1) ; addiu $v0,$zero,0x88 ; sb $v0,0x27($v1)
.L80184080: jal func_801841C0
80184084 nop <- the CALL's own slot is empty
THE LAW. One source-level func_801841C0(a0) written after the join leaves the move immediately before the jal in the same block, and fill_simple_delay_slots' backward scan (reorg.c:2907) takes it into the CALL's own slot — every predecessor branch then keeps a nop. Write the call in >=2 places and post-reload cross_jump merges the copies (the [move][jal] suffix clears §50-B's floor on the minimum=1 fall-through path, jump.c:1978); the surviving block is now entered by >=2 edges, so own_thread_p is false and dbr copies the block's head insn into each predecessor's delay slot and redirects the jump past it — add_to_delay_list (copy_rtx (next_trial), …) + reorg_redirect_jump (trial, new_label) (reorg.c:3128-3130, simplejump path; copy_rtx (trial) at :3430 / :3578 for a non-owned thread). dbr is an inverse cross-jump for exactly one insn, and that insn is your argument move.
⚠ Mechanism correction to the originating note (recorded so nobody re-derives it): the note read this as "cross_jump merges the tail but the merge point lands ABOVE the per-arm argument setups." find_cross_jump keeps the deepest match (last1, jump.c:2528, tested at :2532), so an identical move sitting immediately above an identical jal is included in the merge. The replication is not cross_jump stopping short — it is reorg.c un-merging one insn afterwards.
BYTE EVIDENCE — five spellings one edit apart, generated and scored by .run/wave4/func_80183D68/gen3.py into v3/d1..d5 + w4/:
call written longhand in all 3 arms -> MATCH 255/255
goto-join, call written 2x -> MATCH 255/255
goto-join, call written 3x -> MATCH 255/255
single shared call after the join (2 forms) -> 4 mismatched, DELAY-SLOT/4 profile=schedule
Banked C: src/ov_SC03_118/ov_SC03_118_jr_8017FB84.c @ the func_80183D68 site (and the literal copy in ov_SC03_119).
THE DOSE IS >=2, NOT "one per arm." §88a / §164-69's "write it longhand in EVERY arm" is sufficient, not necessary — two copies plus a goto scored identically here. Choose the spelling that leaves the rest of the block's registers alone, then re-measure.
⚠ BOUNDS §164-69. Its table row "call-BEARING merged block ⇒ cross_jump REFUSED it ⇒ the target's js were source gotos ⇒ hand-factor once at the last arm" is refuted here: .L80184080 holds nothing but a jal, and cross_jump made it — through §162h's minimum=1 fall-through path, exactly as §8's func_80159BE4 did. Decide with §162h's COUNT test (walk back from both jumps and count matching insns, excluding the jumps: >=2 when both sides are js, 1 when one side falls through), never from the presence of a call.
THE DIAGNOSTIC TELL. A DELAY-SLOT/N profile=schedule residual in which your jal <shared callee> carries addu $aN,$sN,$zero in its own slot while the target has a nop there and the same move duplicated in the delay slot of every branch that reaches the call. Do not permute statements, do not pin a register: count the source copies of the call and raise them to >=2.
(SHARPENS — sharpens §37 (L2515, the /s-DEP LATTICE), §136-13 (L8913), §16Xy (L12297), §162q (L11719); evidence: byte-probed; from func_80185B44)
§165-25 — Base half B — gcc will NOT hoist a SYMBOL-addressed load above a REGISTER-based store; "alias oracle refuses s
§16Z — SHARPENS (sharpens §37 "the /s-DEP LATTICE", §136-13, §136-14, §16Xy, §162q, gcc-2.7.2-map/sched.md §64)
The ADDRESS-CLASS TABLE: which load/store pairs even REACH the /s clause (P30 S48 wave 4, func_80185B44, ov_SC03_014)
Target shape. One case arm that touches two globals and the parameter struct, with all three access classes in one block:
lhu $v1,%lo(D_80190490)($at) <- FIXED (symbol)
sw $zero,%lo(D_801EAC80)($at) <- FIXED (a DIFFERENT symbol) -> the lhu floats ABOVE it
sh $v0,0xFE($s0) <- VARYING (register base) -> the lhu CANNOT float above it
THE LAW. §37 / §16Xy / §162q name the /s drop clause but never say which pairs get as far as it.
memrefs_conflict_p (sched.c:614) is the FIRST conjunct of all three dependence predicates and it
decides on ADDRESS CLASS alone, before /s is read at all. Its own header comment (:603-605) names
the only two disambiguations it knows. The complete table for the classes we actually emit:
| store address | load address | memrefs_conflict_p |
/s escape if it conflicts |
|---|---|---|---|
| symbol A | symbol B (B≠A) | 0 — :775-778, two distinct SYMBOL_REFs |
not needed |
$sp+K |
symbol | 0 — :646-650, CONSTANT_P(y) |
not needed |
| reg+K | same reg+J (J≠K) | 0 — :686-694 then the size test :755-760 |
not needed |
| reg+K | symbol | 1 — :753 falls to :780; a SYMBOL_REF vs a REG is unprovable |
arm 1 (:834-836) — grant /s to the STORE |
| reg+K | different reg+J | 1 — :780 |
none — both sides varying, neither arm can fire (§136-13) |
$sp+K |
reg+J | 1 — :668 |
(untested) |
And a load is never blocked by another load: read_dependence (sched.c:807-813) returns nonzero only
when BOTH mems are volatile.
Row 4 is the cell this function lives in, and it is NOT the §136-13 cell. §136-13's "the only lever
is source order" is right when load and store are BOTH varying — each arm of the clause needs one
non-varying side. A symbol-addressed load is FIXED, so arm 1 is available: make the blocking
register-based store /s (((struct{s16 h;}*)a0)->h = v; — COMPONENT_REF, unconditional /s,
expr.c:4888), keep the global read a bare D_xxx[0], and the edge is simply absent.
Bound: the store must not be QImode (GET_MODE (mem) != QImode, :835) — an intervening sb can
never be escaped this way.
⚠ Provenance (R14). The table is source-confirmed against the pinned tools/reference/gcc-2.7.2/sched.c.
The row-4 store-side /s grant is DERIVED, not gated — it is the mirror of §37's banked store-side
grant (func_80176D94), so try it before accepting a source-order-only verdict, and record the
refutation here if it fails.
BYTE EVIDENCE. func_80185B44 (ov_SC03_014, 237 ins, banked whole-binary; drafts in
.run/wave4/func_80185B44/v1.c vs func_80185B44.c). Case 2, row 4, one edit:
*(s32*)(a0+0x1C) = 0x1E; D_801EAC80 += 1; -> li ; sb ; sw 0x1C ; lui ; lw ; NOP ; addiu ; lui ; sw = 238 ins
c = D_801EAC80; *(s32*)(a0+0x1C) = 0x1E; D_801EAC80 = c + 1; -> sb ; lui ; lw ; li 0x1E ; sw 0x1C ; addiu ; lui ; sw = 237 ins, MATCH
The global's lw cannot cross sw 0x1C($s0), so it has to be WRITTEN above it; the field store then
fills its load-delay slot and the nop dies. Case 0 shows rows 1 and 4 side by side in one block: the
target's lhu %lo(D_80190490) sits above sw $zero,%lo(D_801EAC80) (row 1, no edge) but had to be
written as t = D_80190490[0]; above *(s16*)(a0+0xFE) = 1; (row 4) to reach that position — and the
value then lands in $v1 instead of $v0.
THE DIAGNOSTIC TELL. A lui+lw/lhu of a D_ symbol pinned BELOW a sw/sh through a pointer
register, with a nop in its load-delay slot, while in the same block that same load floats freely
over stores to OTHER D_ symbols. The mixed behaviour is the signature: it is an address-class fact,
not a scheduler tie. Do not reach for a barrier, a register __asm__ pin, or the permuter. Move the read
above the register-based store in SOURCE (byte-proven), or grant /s to that store (derived, untested).
(SHARPENS — sharpens §37 (the /s-DEP LATTICE, which names the drop clause but not the address classes that reach it), §136-13 (bounds its "only lever is source order" to the varying×varying cell), §136-14 ($sp×reg is one row of a larger table), §16Xy, §162q; evidence: byte-probed (row 4 + the 238->237 A/B), source-confirmed (all rows, pinned sched.c); from func_80185B44)
(SHARPENS — sharpens §153 THE ADDRESS-REMATERIALISATION LAUNDER (L10463-L10500), §164-35 (L12603-L12615, naming as the EXISTENCE lever — loop/sp-relative only), §162e1/§162e2 (L11142-L11180, symbol bas; evidence: byte-probed; from func_801874C0)
§165-26 — A SYMBOL BASE PASSED TO N>=2 CALLS IN ONE BLOCK: NOT DECLARING IT IS THE REGISTER LEVER. The pseudo is born at the FIRST argument setup, and that birth index is what allocno_compare reads. (sharpens §153, which establishes that >=2 inline &SYM arguments in one cse block collapse to ONE callee-saved pseudo but answers only WHETHER it exists, never WHICH $sN it gets; and completes §164-35 / §162e1 / §162e2, whose 'naming it is the lever' results are all LOOP-hoist cases. §162o1 / §79 / §158 cover declaration ORDER among declared locals — this is declaration EXISTENCE in straight-line code.)
Target shape — one table base handed to the same callee three times in one switch arm, with a small constant living one register BELOW it:
li $s1,1
lui $s2,%hi(D_80190570) … addiu $s2,$s2,%lo(D_80190570)
move $a0,$s0 ; move $a1,$s2 ; jal func_8012D5E4 ; beq $v0,$s1,…
THE LAW. Write the symbol INLINE at every call site. Each (s32)D_SYM argument expands to its own pseudo; cse.c make_regs_eqv makes the FIRST one the canonical register of the class (L5631) and the rest are replaced by it, so the single surviving pseudo is born at the first argument setup. Hoisting the same value into s32 base = (s32)D_SYM; births it at the initializer instead — a LOWER pseudo number and a LONGER live range, and global.c allocno_compare reads both (floor_log2(refs)·refs·size/live_length, exact ties by allocno number = pseudo number ≈ first use, §79/§158). The earlier-born base is allocated first and takes the LOWER hard register, pushing the neighbouring constant up one. The lever is not having a declaration at all — there is no statement to reorder and no asm to place.
Byte evidence. func_801874C0 (ov_SC03_014, 241 ins, NEAR 9; ov_SC03_015/jr_801848E4 is byte-identical, so a crack templates x2 with no symbol remap). Named s32 base: $s1=base, $s2=const 1 — A/B preserved at .run/wave4/func_801874C0/s2_*.c / s3_*.c. Inline at all three func_8012D5E4(a0, (s32)D_80190570, …) sites: $s2=base, $s1=const 1 = the target. 18 of the 27 mismatches removed by that one deletion.
DIAGNOSTIC TELL. A $sN/$sN+1 pair swapped between a SYMBOL base and a small constant, in a block that hands that symbol to >=2 calls, with the rest of the arm byte-exact. Do NOT open §162o1's declaration-ORDER sweep (there is only one candidate local to order) and do NOT reach for §47/§158's allocno sliders — delete the declaration first: one line, zero bytes, zero risk.
Honest scope: n=1. The A/B is a mismatch-count delta across the prologue and the arm, not a register-isolated diff, and removing the declaration moves BOTH the pseudo number and the live length — the allocno_compare priority half and the tie-break half were never separated.
(SHARPENS — sharpens §30 (L2387 parenthetical: "CSE's fixed-scalar-store invalidation only kills non-/s entries"), §31 catalog row L2414 → docs/gcc-2.7.2-map/cse_expr.md §4b, §30a-1 (cast-over-PLUS /s ; evidence: byte-probed; from func_80189340)
§165-27 — THE CSE STORE-FLUSH IS DISCRIMINATED BY THE LOAD'S DECLARATION, NOT THE STORE'S SPELLING; $sp SLOTS ARE FIRST-CLASS PARTICIPANTS. (SHARPENS §30's L2387 parenthetical — that line describes a FIXED-address store, but the rule that decides survivorship in practice is the VARYING-address path; promotes docs/gcc-2.7.2-map/cse_expr.md §4b row 1 from a table to a lever and corrects its "fixed-SYMBOL scalar loads SURVIVE" to "any non-varying MEM, $sp+const frame slots included"; BOUNDS §30a-1.)
Target asm shape. Two address-taken locals in adjacent frame slots, read inside ONE basic block across the same run of pointer stores — and the target holds one in a register while re-loading the other at every store:
lw $3,152($sp) <- `s32 rgb` : loaded ONCE per iteration
sw $0,-22($16) ; sw $0,-6($16)
sw $3,-30($16) ; sw $3,-14($16) <- both colour stores off the ONE load
sb $2,-27($16)
lhu $2,144($sp) ; #nop ; sh $2,-26($16)
lhu $2,144($sp) ; #nop ; sh $2,-18($16) <- `u16 xy[4]` : RELOADED for every `sh`
THE LAW. cse's memory kill for a store at a VARYING address is note_mem_written (cse.c:7539) feeding invalidate_memory (cse.c:1701), and it is a TWO-LEVEL predicate — one test on the store, one on each cached load:
- store side (
cse.c:7571-7575):if (! ((MEM_IN_STRUCT_P (written) || GET_CODE (XEXP (written, 0)) == PLUS) && GET_MODE (written) != QImode)) writes_ptr->all = 1;then unconditionallywrites_ptr->nonscalar = 1; - load side (
cse.c:1713-1717): an entry dies iffp->in_memory && (all || (nonscalar && p->in_struct) || cse_rtx_addr_varies_p (p->exp)).
So an ordinary p->f = v / p[k] = v / *(T *)(p + k) = v store sets nonscalar only, and nonscalar kills exactly the cached loads that carry /s. The dial is the LOAD's declaration. u16 xy[4] makes every read an ARRAY_REF ⇒ /s ⇒ elt->in_struct (cse.c:1951, :7368) ⇒ dead at every store. s32 rgb is a plain scalar VAR_DECL ⇒ no /s, and $sp+const is non-varying ⇒ it survives the whole run. That is how one basic block holds both a CSE'd and a non-CSE'd frame read.
BYTE EVIDENCE (func_80189340, ov_SC02_000, 260 ins, banked at src/ov_SC02_000/ov_SC02_000_jr_8018173C.c:3939). Single-axis A/B on the banked body, touching ONLY the 8 xy[] read sites in the j body, pinned triple, isolated compile:
->x0 = xy[0]; (ARRAY_REF, /s) -> 8 x `lhu`, 220 ins [the MATCH]
->x0 = *(u16 *)((s32)xy + 0); (cast-over-PLUS) -> 4 x `lhu`, 216 ins
Exactly −4 instructions, all of them loads. rgb is loaded ONCE in BOTH (lw ...,152($sp) count = 1 either way), so the two slots — 8 bytes apart, both address-taken, both $sp-relative — split purely on their declaration. The intervening store between the two surviving lhu $2,144($sp) is a single sh $2,-26($16): HImode, /s, varying ⇒ nonscalar alone ⇒ exactly the arm that kills the /s entry and spares the scalar.
⚠ THE TRAP (this campaign walked into it). writes->all — the TOTAL memory-table flush — is set by only two things: a QImode store through a pointer, and a store whose address RTL is a bare REG. *(T *)(p + k) = v still has a top-level PLUS address, so §30a-1's cast-over-PLUS /s-denial is INERT against cse.c:7571. Respelling all 30 prim accesses of func_80189340 from anonymous-struct members to *(T *)(pp + k) produced byte-identical assembly. §30a-1 governs expr.c/sched.c; it does not reach the CSE store flush. Note the corollary in the shape above: the two QImode sb stores (->len, ->code) DO set all=1 and partition the block into CSE regions — the colour stores survive only because they sit between them.
THE DIAGNOSTIC TELL. Your draft emits N loads from one $sp slot where the target emits one (or the reverse), and a different $sp slot in the same block behaves the opposite way. That second clause is the discriminator — it rules out a label/call flush (§1, which takes everything) and rules out scheduling (sched cannot delete a load). Do not touch the stores and do not permute statements: change the surviving side's DECLARATION — array→scalar to make it survive, scalar→T v[2] to make it reload (§162i1: one element collapses to the element's mode). Prerequisite: the blocking stores must be non-QImode and not bare-REG-addressed, or all=1 flushes both and there is no lever.
⚠ Coupling — three passes on one word. The same declaration also places the frame slot (§163e/§164-71) and the same /s bit drives all three sched.c dependence predicates (§37, §164-70, §16Xy). Changing an array to a scalar for CSE moves the schedule and can move the frame; re-verify all three before reading the new residual.
(SHARPENS — sharpens §55a (Switch TREE vs jump table — CASE_VALUES_THRESHOLD is 5; func_801387B8), §135-1 (UNSIGNED switch index ⇒ pure equality chain), L5722 (a jump-table grep is not a switch detecto; evidence: byte-probed; from func_8018CC9C)
§165-28 — CASE_VALUES_THRESHOLD IS 5 BECAUSE casesi IS GUARDED, NOT BECAUSE IT IS ABSENT — and the guard is evaluated at cc1-RUN time. (sharpens §55a's "Switch TREE vs jump table" bullet, which banked the number 5 with no citation; corrects the mechanism a P30 S48 wave note stated as "no casesi on this MIPS config".)
Target shape — a 4-arm switch that emits a comparison tree where a jtbl grep expects a table:
lhu $v1,0x34($s0) ; addiu $v0,$zero,0x1
beq $v1,$v0,<case 1> <- root tests the MEDIAN, not case 0
slti $v0,$v1,0x2 <- the lone median split, in the delay slot
beqz $v0,<hi half> ; beqz $v1,<case 0> ; j <default>
<hi half>: addiu $v0,$zero,2 ; beq → case 2 ; addiu $v0,$zero,3 ; beq → case 3 ; j <default>
THE LAW (read out of the pinned source, not inferred). stmt.c:4806-4815 selects the threshold as #ifdef HAVE_casesi → (HAVE_casesi ? 4 : 5), falling back to a literal 5 only when the macro is undefined. mips.md:5946 DOES define a casesi expander, so the #ifdef arm is the one taken — but its condition string is "TARGET_EMBEDDED_PIC" (mips.md:5964; the comment at :5944 says so outright: *"Switches are implemented by tablejump' when not using -membedded-pic"*). genflags therefore emits #define HAVE_casesi (TARGET_EMBEDDED_PIC)and the ternary is a **runtime** test inside cc1. We never pass-membedded-pic⇒ 0 ⇒ threshold **5** ⇒count < CASE_VALUES_THRESHOLD (stmt.c:4818) routes 4 cases to emit_case_nodes' balanced tree. Two mistakes this forecloses: **do not read "threshold 4" off the mere presence of casesiinmips.md**, and **do not read "no casesi" off the threshold being 5.** (The next :4818disjunct bites independently —range > 10 * count` forces a tree at any count.)
Byte evidence (controlled, one file, pinned triple -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker). Cases {0,1,2,3} → beq $3,1 ; slt $2,$3,2 ; beq $2,$0 ; beq $3,$0 ; beq $3,2 ; beq $3,3, no .rdata. The identical switch plus a 5th case → sltu $2,$3,5 ; lw $2,$L17($2) ; j $2 + a 5-entry .rdata .word table. The 4-case output is the byte-exact fingerprint of func_8018CC9C @8018CCAC-8018CCEC (ov_SC02_011, 133 ins, banked, R22 green).
DIAGNOSTIC TELL. The dispatch's FIRST beq tests the median case value and is followed by exactly one slti of (median+1) ⇒ emit_case_nodes, ncases ≤ 4 — write a switch. A source if / else if chain tests 0,1,2,3 in source order and never emits the lone slti. Ghidra floors both as an if-chain (§55a).
(SHARPENS — sharpens §55a, §135-1; evidence: byte-probed; from func_8018CC9C)
(SHARPENS — sharpens §135-1 (UNSIGNED switch index ⇒ pure equality chain, no range test), §164-37 / §164-31 (the sll 16 ; sra 16 between bias subtract and sltiu is a WIDTH tell), §163b; evidence: byte-probed; from func_8018CC9C)
§165-29 — A u16 SWITCH OPERAND IS A SIGNED SWITCH: promotion runs before expand_end_case, so lhu in the dispatch plus an slti range split is not a contradiction. (sharpens §135-1, which contrasts only *(s32*) vs *(u32*); its tell inverts wrong on a halfword operand.)
Target shape — the two halves that look mutually exclusive and are not:
lhu $v1,0x34($s0) <- UNSIGNED narrow load feeding the dispatch
...
slti $v0,$v1,0x2 <- SIGNED median split (§135-1 says this means "signed index")
THE LAW. §135-1's mechanism is node_has_low_bound: emit_case_nodes drops the case-0 leaf's bound test only when 0 == TYPE_MIN (index_type). index_type is TREE_TYPE (index_expr) after the default argument promotions, and every unsigned short value fits in int, so a u16 operand promotes to signed int, TYPE_MIN is INT_MIN, 0 != INT_MIN, and the bound test is emitted. Only a type still unsigned after promotion — unsigned int/u32 — reaches §135-1's no-range-test form. The load opcode is chosen by the C lvalue's own type and carries no information about the switch's signedness; the slti does.
Byte evidence (three spellings of the same 4-arm switch over {0,1,2,3}, one file, pinned triple):
switch (*(u16 *)p) -> lhu $3 ; beq $3,1 ; slt $2,$3,2 ; beq $2,$0 ; beq $3,$0 ; beq $3,2 ; beq $3,3
switch (*(s16 *)p) -> lh $3 ; beq $3,1 ; slt $2,$3,2 ; ... (same tree, WRONG load)
switch (*(u32 *)p) -> lw $4 ; beq $4,1 ; beq $4,$0 ; beq $4,2 ; beq $4,3 (NO slt/sltu at all)
func_8018CC9C (ov_SC02_011, 133 ins, banked whole-binary, R22 green) matches with switch (*(u16 *)(param_1 + 0x34)) — the lhu and the slti bought by one spelling.
DIAGNOSTIC TELL. Narrow unsigned load (lhu/lbu) feeding the dispatch and an slti median split ⇒ write the operand *(u16 *)/*(u8 *). *(s16 *) gives you the same tree with lh (one wrong opcode, no length drift — easy to misread as a register problem); *(u32 *) deletes the slti outright (§135-1). Narrow-unsigned is the only spelling that buys both halves. Read §164-37 before touching the width when the dispatch ALSO carries a bias subtract — there the sll 16 ; sra 16 pair is the width tell and this one does not apply (minval == 0 ⇒ no subtract, no promotion pair).
(SHARPENS — sharpens §135-1, §164-37; evidence: byte-probed; from func_8018CC9C)
(SHARPENS — sharpens §56b (the exemplar's externs MUST be fleet-canonical; audit by high-count consensus), §30 #2 / L2389 (widen a DEFINE macro's extern void → s32; byte-neutral when the caller dis; evidence: byte-probed; from func_8018CC9C)
§165-30 — THE FLEET-CANONICAL void CAN BE AN UNDER-SPECIFICATION: §56b's consensus-alignment is byte-neutral only when the return is DISCARDED, and narrowing a used return is exit 33, not a warning. (BOUNDS §56b; the missing precondition on its "return-type diffs are byte-neutral" clause. The fix is §30 #2's widening move, run on the DEFINE macro instead of the caller.)
The trap. §56b tells you to audit a banked exemplar's callee externs against the fleet and rewrite each to the high-count consensus, because "Return-type and pointer↔int param diffs are byte-neutral (gcc-2.7.2 warns, doesn't error; ... a discarded/(cast)-assigned return emits identically)". Run its own command on func_80143BDC:
$ grep -rh 'extern.*\bfunc_80143BDC\b' src/ | sort | uniq -c | sort -rn
88 extern void func_80143BDC(u16 *a0);
4 extern s32 func_80143BDC(u16 *a0);
The consensus is void by 22:1 — and applying it deletes all four banked members of the func_8018CC9C family, which read the return (jal func_80143BDC ; beqz $v0, asm/ov_SC02_011/.../func_8018CC9C.s @8018CDA8-CDB0).
THE LAW. §56b's neutrality holds only under the clause it states as a justification and never as a precondition: the return must be discarded at every call site in the TU being rewritten. Narrowing a decl whose return is consumed is not a diagnostic, it is a hard stop — c-typeck.c:1060 / c-convert.c:74 error ("void value not ignored as it ought to be"). So the audit is asymmetric: widening void→s32 is free, narrowing s32→void is only free where nothing reads $v0. A void in the fleet consensus is evidence of nothing except that 88 callers discarded the value — it is a floor on the real signature, not the signature.
Byte evidence (pinned triple, three controlled probes):
extern void f(u16*); int e; e = f(p);→void value not ignored as it ought to be, cc1 EXIT 33. Not a warning.extern int f(u16*); void f(u16*a0){}in ONE TU →conflicting types for 'f'+ the same error, EXIT 33. (This is why the divergence is legal today and only today:DEFINE_func_80143BDC()is expanded inov_SC02_011_jr_80140608.c, a different TU from the caller inov_SC02_011_jr_8017AE2C.c. Move the macro into the caller's TU and it exit-33s.)- The divergence is removable, byte-neutrally.
DEFINE_func_80143BDC's body ends in the tail callfunc_8012C51C(&sp, 0);, so$v0already carries that callee's return by ABI happenstance — which is what the four callers actually read. Compiling the macro body asvoid ... func_8012C51C(&sp,0); }versuss32 ... return func_8012C51C(&sp,0); }produces assembly identical apart from the.filedirective. Widening the macro (§30 #2, byte-neutral for all 88 discarding callers) is the permanent fix; the 4extern s32sites then agree with canonical and §56b's audit becomes safe again.
DIAGNOSTIC TELL. Before running §56b's consensus rewrite on a callee, grep the exemplar for a use of that call's value (= func_Y(, if (func_Y(, return func_Y(). If there is one and the consensus is void, stop — the consensus is under-specified. Fix the definition (widen the DEFINE_func_* macro, re-verify check-all), never the caller. Generalises to any auto-reconciler (sig_unify, cast_call_sites, reconcile_tu, normalize_self_decls, family_sweep --fix-def-sig): a return-type normalizer must be one-directional — widen only.
(SHARPENS — sharpens §56b, §30 #2, §57; evidence: byte-probed; from func_8018CC9C)
(SHARPENS — sharpens §164-14, §164-67, §145a, §135-17; evidence: byte-probed; from func_8018E8A0)
§165-31 — simplify_giv_expr REFUSES invariant-REG + CONST_INT, SO THE DEPENDENT MEMs ARE NOT GIVS AT ALL — NOT 'GIVS RULED NOT WORTH WHILE'. (CORRECTS §164-14's mechanism; its prescription stands. Bounds §164-67, whose benefit dial is inert here.)
Target shape — ONE induction register serving displacements that straddle zero:
addiu $s4,$s3,0xD4
lw -4($s4) ; lw 0($s4) ; lh -2($s4) ; lh 2($s4)
THE LAW. loop.c:5091-5109, the PLUS case's "Both invariant" arm: when both addends simplify to invariants (CONST_INT or USE), simplify_giv_expr returns tem, which is left 0 unless CONSTANT_P (arg0) && GET_CODE (arg1) == CONST_INT (:5102). CONSTANT_P (rtl.h:237-240) admits LABEL_REF / SYMBOL_REF / CONST_INT / CONST_DOUBLE / CONST / HIGH — never a REG. Three consequences:
- An
add_valcan never be(plus (reg) (const_int)).symbol + constis fine (plus_constant,:5104); an invariant pseudo plus a constant is not. - For a DEST_REG giv
qwhoseadd_valisUSE (invariant pseudo), every dependent addressq + kre-associates through thecase PLUSarm (:5116-5122) into an innerUSE(zb) + const→ 0 → the whole address returns 0. general_induction_vartherefore returns benefit 0, andfind_mem_givsgatesrecord_givonbenefit > 0(loop.c:4195-4211) ⇒ no giv record is ever created. The literal offsets survive as MEM displacements offq's single reduced register.
⚠ CORRECTION TO §164-14. §164-14 explains the same observation via express_from's GET_CODE (g1->add_val) == CONST_INT gate (loop.c:5426) plus a "not worth while" refusal (§164-67). Both are true statements about those functions and neither is what happens here: the q[j] MEMs never reach record_giv, so they are not in bl->giv, combine_givs never sees them, and loop.c:3823's arithmetic is never evaluated on them. This matters: §164-67's benefit dial (add records to clear the n=1 floor) is inert against this class — no amount of n or lifetime will reduce these addresses, and nothing will stop them being reduced once the base carries a CONST_INT add_val.
RECIPE (both directions). One register carrying literal offsets on both signs ⇒ base the group on a giv with an invariant-PSEUDO add_val: zb = base + K; invariant, q = zb + k * S; in the loop, every access written q[j] / ((s16*)q)[j]. All displacements merged onto one anchor instead ⇒ base them on a pointer biv so the add_vals are CONST_INTs and §145a's last-recorded-anchor rule takes over.
BYTE EVIDENCE. func_8018E8A0 (ov_SC02_011, 211 ins, MATCH 211/211, .run/wave4/func_8018E8A0/func_8018E8A0.c). Pointer-biv spelling: cc1 -dL prints giv at 288/294/297/303/306/313 combined with giv at 320, one register addu $19,$20,214, 0xCE reached as -8($19) — 210 ins, wrong. Invariant-pseudo spelling: $s6=obj+0xCC and $s4=obj+0xD4 with the target's exact −4/−2/0/+2 displacements — 211 ins. Contrast dump wk/a1/gccdump.loop:39 for what a CONST_INT add_val looks like when it is recorded: Insn 285: giv reg 142 src reg 78 benefit 8 … mult 12 add 212.
DIAGNOSTIC TELL. One induction register where the target has two, or target displacements that straddle zero off a single register. Read cc1 -dL: if the offending addresses are absent from the giv list entirely — not "not worth while", not "combined with" — you are here, and the dial is the base's add_val TYPE, not its benefit.
Symptom lines for the index: "negative and positive offsets off one induction register" · "the giv dump does not mention my addresses at all" · "combine_givs merged offsets the target keeps separate".
(SHARPENS — sharpens §164-14, §34, §70, §162e2; evidence: byte-probed; from func_8018E8A0)
§165-32 — THE 2-INSN GIV INIT §164-14 SAYS YOU MUST PAY IS AVOIDABLE: ASSIGN THE INVARIANT BASE INSIDE THE LOOP, AND LICM PUTS IT IN THE SAME BLOCK AS THE GIV INIT, WHERE IT COALESCES. (pays off §164-14's "⚠ COST — PAY IT KNOWINGLY"; the OPPOSITE direction from §34's giv-init fence, and orthogonal to §70's base-register law — read all three before choosing.)
Target shape — a one-instruction preheader giv init off a non-constant base:
<preheader> addiu $s7,$sp,0x58
addiu $s4,$s3,0xD4 <- ONE insn, not `addiu $v1,…` + `move $s4,$v1`
THE LAW. emit_iv_add_mult computes bl->initial_value * mult + add_val into the giv register; with a pseudo add_val and a biv starting at 0 the whole computation collapses to emit_move_insn (new_reg, zb) (loop.c:5555-5557), inserted immediately before loop_start (emit_insn_before, :5560). Whether that copy costs a byte is decided by where zb's own def sits:
zb = base + K;written before the loop ⇒ theaddiuis in the ENTRY block, the copy is in the PREHEADER, and combine never spans basic blocks (flow.c:2087, §31's cross-BB combine law) ⇒ both survive:addiu $v1,$s3,212 ; move $s4,$v1.zb = base + K;written inside the loop body ⇒scan_looprecords it as a movable (loop.c:769-800),move_movableshoists it into the preheader (:1708), and the two insns are now in ONE block ⇒ they coalesce to a singleaddiu $s4,$s3,0xD4. Its slot among the other hoists follows §162e2 (preheader order = body order), landing after the LICM'daddiu $s7,$sp,0x58exactly as the target has it.
BYTE EVIDENCE. func_8018E8A0 (ov_SC02_011). Base-before-loop: .run/wave4/func_8018E8A0/base.txt idx 39-41 — addiu $v1,$s3,212 / addiu $s7,$sp,88 / move $s4,$v1 against the target's addiu $s7,$sp,0x58 / addiu $s4,$s3,0xD4 (138 mismatched, class SHIFT-DRIFT/−1). Base-inside-loop (func_8018E8A0.c:148, first statement of the body): MATCH 211/211.
THE DIAGNOSTIC TELL. LENGTH-DRIFT +1 with a preheader addiu rT,rBase,K ; move rGiv,rT pair where the target has one addiu, and the extra insn's dest is a scratch register nothing else in the function touches. Do not reach for §34's fence — that one ADDS the move. Move the invariant's assignment INTO the loop body.
Honesty note: n=1, two measured arms. The coalescing MECHANISM (cross-BB combine) is read out of the pinned source and is consistent with both emissions, but was not separately ablated — e.g. by forcing the two into one block without LICM.
(SHARPENS — sharpens §162j1 (L11392), §135-13 (L8912-8916), §164-82 (L13600), §164-80 (L13565); evidence: byte-probed; from func_8018EDB8)
§165-33 — DEFEAT optimize_reg_copy_1 BY MOVING THE SURVIVING USE ABOVE THE COPY: the escape for the case §162j1's in-place-SET lever structurally cannot reach. (SHARPENS §162j1 — supplies its second lever, corrects the disqualifying gate, and bounds it with a PORT fact. Also the register-side half of §135-13, which already prescribes this exact source edit and already owns half this tell, but as a pure SCHEDULING lever with no register story; and the lever-side reading of the copy_1 gate §164-82 quotes only to explain a copy SURVIVING.)
Target shape — one pointer living in TWO registers, a load on one and a store block plus the call on the other:
target: lw $v1,0x20($s0) <- the load keeps the ORIGINAL register
move $a0,$s0
sh $v0,0x5C($a0) ; sh $zero,0x60($a0) ; lhu … ; jal func_8018D870
mine: move $a0,$s0
sh … 0x5C($a0) ; sh … 0x60($a0)
lw $v1,0x20($a0) <- rewritten to the COPY's register, and 2 insns late
THE LAW. Spell the copy the TU's own way (register s32 q __asm__("$4"); q = iv; — the idiom func_8018D870 itself uses at :5028) and the pin is necessary and not sufficient. Leave the surviving use of iv BELOW the copy and optimize_reg_copy_1 fires exactly as §162j1 describes, rewriting the load's base to $a0. Hoist that use ABOVE the copy in source order and the pass is disqualified at its CALL SITE — not merely out-scanned:
s32 sv;
iv = *(s32 *)(p + 0x64);
sv = *(s32 *)(iv + 0x20); /* the surviving use, hoisted */
q = iv; /* iv's last insn is now the copy itself */
*(u16 *)(q + 0x5C) = 1; …
Three facts in the pinned tree, and they stack:
local-alloc.c:1002-1007— the call is guarded by! find_reg_note (insn, REG_DEAD, SET_SRC (set)). Once the hoisted use is SRC's LAST, flow puts SRC's death note on the copy andoptimize_reg_copy_1is never invoked. This, not the scan, is what the hoist buys.local-alloc.c:721—for (p = NEXT_INSN (insn); …), strictly forward. So even when SRC keeps other later uses (and the pass still runs on those), the hoisted use is unreachable either way. The hoist is safe in both regimes.local-alloc.c:1009-1015— theelse iffallbackoptimize_reg_copy_2requires both regs>= FIRST_PSEUDO_REGISTER(:870: "it is assumed that DEST and SRC are pseudos"). Aregister __asm__("$4")DEST fails that test. So the pin, which §162j1 correctly says cannot exempt the copy from copy_1, DOES disqualify copy_2 — and once you make SRC die at the copy, the copy is untouchable by both optimisers. Neither §162j1 nor §164-82 states this half.
BOUND ON §162j1 — its lever cannot exist here, and the reason is the PORT, not the shape. §162j1's cure is "make the use insn also SET src" (local-alloc.c:732 reg_set_p). The surviving use here is a lw, and config/mips/mips.h:2173-2177 leaves HAVE_POST_INCREMENT, HAVE_POST_DECREMENT and HAVE_PRE_INCREMENT all commented out — no MIPS memory reference can modify its own base. Whenever the surviving use is a MEM read, §162j1's lever is unreachable by construction: skip it and hoist. The hoist also needs no do{}while(0) loop-note wedge (§162j1's second, untested suggestion at :723) and buys no extra allocno refs (§55a), so it is the zero-side-effect form for this branch.
BYTE EVIDENCE. func_8018EDB8 (ov_SC06_018, 170 ins) — banked MATCH, src/ov_SC06_018/ov_SC06_018_jr_80187AEC.c:6025-6039, committed commit:1679, propagated to its 3 zero-crack siblings in commit:1697. With the 0x20 load left below q = iv: 172 ins plus the register diff. ⚠ Winner-only, n=1. .run/wave4/func_8018EDB8/ preserves exactly one file, so the 172-insn loser is quoted from the session and is not reproducible from disk; and the hoist was only ever measured against the pinned baseline — hoist-without-pin was never gated. Same honesty class as §164-80.
⚠ THE +2 IS NOT AN ALIASING EFFECT — do not repeat that story. See the companion refutation: the substitution moved the load ONTO the stores' base, which is the DISAMBIGUABLE case (§164-79, §135-14), not a barrier. The account that survives the source is a register dependence — after the rewrite the load's address reads $a0, a hard register SET by the copy, so it can no longer be scheduled above the copy, and the copy sits immediately in front of the store block. Effect confirmed, mechanism relabelled. Consistent with §162j1's own pass-order note (the substitution is invisible in .sched, present in .lreg): whatever reschedules is sched2, after reload, on hard registers.
DIAGNOSTIC TELL. A LENGTH-DRIFT +2 whose drifting insns are load-delay nops, coupled to a one-register diff in which the target's load reads a callee-saved $sN and yours reads the argument register a nearby move $aN,$sN wrote. Register diff and length diff have ONE cause — do not triage them separately. §162j1's triage stays valid for identifying the pass (operand pseudo changes between .sched and .lreg); this adds the second exit from that branch: if the target reads the copy's SOURCE at a MEM use, hoist that use above the copy in source; if at an arithmetic use, §162j1's in-place SET is cheaper. §135-13's nop half of the tell is prior art — the discriminator that makes it THIS section is the accompanying register swap.
Symptom lines for the index: "my load reads the copy's register and the target reads the original" · "+2 load-delay nops beside a register __asm__ pinned pointer" · "one pointer, two registers: stores on one, a load on the other" · "the pin fixed nothing".
(SHARPENS — sharpens §147-C (L10068-10071, the byte-refuted negative verdict), §163e (L11813, slot order = pseudo NUMBER order, reload1.c:658), §136-6 / §79 (L8925 / L6240, declaration-order frame orac; evidence: single-instance; from func_8017C294)
§165-34 — A. §147-C ("inner-block declaration does NOT delay slot allocation — BYTE-REFUTED") is true only for BLKmode/a
(SHARPENS — sharpens §147-C (L10068-10071, "Inner-block declaration does NOT delay slot allocation — BYTE-REFUTED") and §163e (L11813, slot order falls out of pseudo NUMBERING); evidence: single-instance; from func_8017C294)
§165-01 — §147-C's "BLOCK SCOPE IS INERT" HOLDS FOR STACK-ALLOCATED LOCALS AND FAILS FOR REGISTER-CANDIDATE PSEUDOS. THE DIAL IS THE DECL'S EXPAND POSITION RELATIVE TO STATEMENTS, NOT THE BRACES.
Target shape. Every declared local at the right $sp offset, regs= exact — and ONE spilled value's displacement low by a multiple of 8, immovable by any reordering of the declaration list.
THE LAW. expand_decl (stmt.c:3316) runs once per declaration in parse order — stmt.c:2958: *"The variables are declared one by one, by calls to expand_decl'"* — and it has two arms. A **non-BLKmode, non-addressable, non-volatile** local takes the register arm (stmt.c:3357-3390) and is minted right there with gen_reg_rtx; if it later spills, reload1.c:658walksalter_reg (i,-1)in ascending pseudo NUMBER (§163e), so its slot position is decided by **where in the expansion its declaration sat**. An **addressable or BLKmode** local takes theassign_stack_temp` arm of the same function and is slotted into stratum 1 at declaration time, where block nesting genuinely does not show — that is the case §147-C measured, and for that case §147-C stands.
C89 gives exactly one way to move a declaration below a statement: an inner block. So { …stmts…; { T *base = …; … } } mints base after every pseudo those statements minted — including §147-B's combine-orphans — and its spill slot moves up 8 bytes per intervening pseudo. The braces are the spelling, not the mechanism: crossing statements that mint no pseudos buys nothing.
⚠ §147-C's stated reason is not in the pinned source. "expand_function_start walks the whole BLOCK tree" — expand_function_start (function.c:4993) never touches local declarations at all. Whatever that probe measured, it was not that.
BYTE EVIDENCE. func_8017C294 (ov_SC01_077), pointer-walk body: moving the walk pointer's declaration into the loop's own block moved its reload slot 0x88 → 0x98 (+2 slots) with every declared-local offset unchanged.
DIAGNOSTIC TELL. Declared-local offsets all exact, one SPILLED value low by a multiple of 8, and permuting the declaration list does nothing (⇒ it is not §136-6/§163e's stratum). Move that declaration DOWN past statements and re-grep '\.frame'.
Honest scope: ONE measurement, on a body that was later abandoned for an index-loop rewrite; the "+2 = the head's 2 orphans" attribution is inferred, not dumped. The scope split itself is read from the source and is safe; the magnitude is not a law yet.
(SHARPENS — sharpens §148-C (L10228, the zero-byte ALLOCNO-PRIORITY slider, "emits nothing"), §153 (L10463, the address-rematerialisation launder, "Zero bytes."), §17/§21 in-place re-tie (L1784, L2917); evidence: single-instance; from func_8017C294)
§165-35 — C. A ZERO-EMISSION RE-TIE CAN CREATE AN ORPHAN. __asm__ __volatile__("" : "=r"(t) : "0"(t)) on a memory-load
(SHARPENS — BOUNDS §148-C (L10228), §153 (L10463), §17/§21's in-place re-tie (L1784), §34's zero-byte toolkit (L2459), §47/§49 and §164-02 — every one of which certifies this instrument as "emits nothing"/"zero bytes"; evidence: single-instance; from func_8017C294)
§165-04 — THE "ZERO-BYTE" ASM DIALS ARE NOT FREE ON A MEMORY-LOADED NARROW VALUE: an in-place re-tie on an s16 buys +8 BYTES OF FRAME PER SITE.
s16 t = p[2]; __asm__ __volatile__("" : "=r"(t) : "0"(t)); /* 0 instructions, +8 bytes of vars */
s32 t = p[2]; __asm__ __volatile__("" : "=r"(t) : "0"(t)); /* 0 instructions, 0 bytes */
THE LAW. The "=r"/"0" pair forces the value through an SImode register operand, which stops combine folding the widening into the lh; the stranded SImode extension pseudo is then §165-02's orphan and alter_reg gives it 8 bytes (§165-03). On a register-sourced short the asm is inert — same precondition as the recipe: no load, nothing to fold, no orphan. Every existing entry in the zero-emission family (§148-C's priority slider, §153's launder, §17/§21's barrier, §34's toolkit, §47/§49's sliders, §164-02's qty_const launder) is certified "zero bytes" against SImode values only; the certification does not carry to a narrow local.
Used deliberately this is a frame dial nobody has recorded — the counterpart to §148-C's ref-count slider, on the other axis, and the only known way to buy an orphan without adding a source expression. Used accidentally it is a footgun: reach for §148-C on an s16 and every $sp displacement in the function moves at once.
BYTE EVIDENCE. func_8017C294 (ov_SC01_077) pointer-walk body: two re-tie sites on memory-loaded s16s took vars= 240 → 256; the same asm on register-sourced shorts left vars unchanged.
DIAGNOSTIC TELL. You placed a zero-byte dial and the diff count exploded, all of it $sp immediates. grep '\.frame' before and after every asm dial you put on a narrow local — this is §164-72's "re-grep after every statement-level edit" with a named cause.
Honest scope: ONE A/B pair, on a body later abandoned; no instruction-count control was quoted, and the 4-line isolated reproducer that §16Xy and §164-66 both carry was never run for this spelling. The cheap confirmation, ~2 minutes: a 4-line function with one memory short and one call, compiled with and without the re-tie, grep '\.frame', then the same pair with s32. Do that before citing this as law.
(SHARPENS — sharpens §160g, §71, §12, §19; evidence: single-instance; from func_8017F2D4)
§165-36 — WAVE STEP 0, PART 2: GLOB .run/ FOR THIS FUNCTION'S OWN PRIOR DRAFT, AND RE-RESOLVE ITS TU FROM THE CURRENT asm/ TREE. (extends §160g/§71's "grep the callee set for an already-matched SIBLING" from src/ to the wave scratch, and adds the staleness guard §161c's process note is missing. PROCESS, not a compiler law.)
Two rules, both earned on func_8017F2D4:
- A function flagged "draft lost" often is not.
find .run -name '*func_8017F2D4*'returns FIVE byte-identical copies of the verified MATCH —.run/wave1/func_8017F2D4.c,.run/wave3/func_8017F2D4/func_8017F2D4.c,.run/bank48/ov_SC01_005/,.run/bank51/ov_SC01_005/,.run/backlog_drafts/— all sha19fd76d10c5cfa9dc859b0ce51bb7bf78b8df05f3. Re-deriving a 279-instruction jump-table body with a hand-placed cse barrier would have been pure waste. Run the glob before you spend an agent; wave 2's harness defect (commit:1628) lost 21 drafts to a shared output dir, so "lost" in a backlog row means "not where the harness looked", not "gone". - Never trust a prior note's TU path or line numbers. Those notes name
ov_SC01_005_jr_8017C340as the destination TU and cite "TU:4164 splice / prototype at TU:3533". splat has since moved the function: the only two.sfiles repo-wide are under..._jr_8017ED5C, and the live splice issrc/ov_SC01_005/ov_SC01_005_jr_8017ED5C.c:3115(prototype at :2721)..run/jr48/wave1_ready.jsonstill records the old subdir. Re-resolve the splice fromasm/plus a fresh grep ofsrc/every time; carry forward the notes' REASONING, never their coordinates.
⚠ Provenance correction this earns. §164-31 and §164-37 both state that func_8017F2D4 is "banked ... 279/279 ... commit:1652". It is not. git show commit:1652 -- src/ov_SC01_005/ov_SC01_005_jr_8017ED5C.c carries no hunk for it, and the TU still holds INCLUDE_ASM("asm/ov_SC01_005/nonmatchings/ov_SC01_005_jr_8017ED5C", func_8017F2D4); at line 3115. §164-37's "Status: PROMOTED" rests on ONE bank (func_8017ED5C), not two; §162g's "match_one MATCH — gate-blocked on TU plumbing, NOT yet banked" is still the true state.
(SHARPENS — sharpens §161c, §52b, §8c; evidence: single-instance; from func_8017F2D4)
§165-37 — When prior notes cite a destination TU by path and line, re-resolve the splice from the CURRENT asm subdir — s
(folded into §165-07 above, rule 2 — same entry, same lesson.)
(SHARPENS — sharpens §162d1 (L11097-11124) — same two-calls-into-one-consumer shape, OPPOSITE slot polarity, and its DIAGNOSTIC TELL misfires here; evidence: single-instance; from func_8017F9AC)
§165-38 — The blend factor u = f(*(s16*)(p+0xFC)) + (f(*(s16*)(p+0xFE) + K) >> 4) — argument evaluation is left-to-rig
(SHARPENS — sharpens §162d1 (L11097-11124), whose DIAGNOSTIC TELL — 'the insn sitting in the second jal's delay slot is that call's own argument constant' — is stated as the FAILURE fingerprint and INVERTS on this shape; evidence: single-instance; from func_8017F9AC)
§NNN — WHEN CALL 2's ARGUMENT IS DERIVED FROM A LOAD, THE ARGUMENT ARITHMETIC OWNS THE jal SLOT AND CALL 1's RESULT SAVE MOVES UP INTO THE LOAD's DELAY SLOT. (§162d1's law is unchanged and is what matches; only its tell needs bounding.)
Target shape — f(x) + (g(y + K) >> k) written as ONE expression:
lh $a0,0xFC($s2)
jal func_8004787C
nop <- call 1's slot is a REAL nop; nothing independent exists yet
lh $a0,0xFE($s2) <- call 2's arg LOAD, hoisted into call 1's return shadow
addu $s0,$v0,$zero <- call 1's result save, in the LOAD-delay slot
jal func_8004787C
addiu $a0,$a0,0x100 <- call 2's own `+ K`, in the JAL slot
sra $v0,$v0,4
addu $s0,$s0,$v0
THE LAW. §162d1's shape has call 2's argument as an independent CONSTANT (addiu $a1,$zero,0xC), which sched can place ABOVE the jal, leaving the slot free for the result save — which is why §162d1 reads 'arg constant in the slot' as you got it wrong. Invert that reading when call 2's argument is a LOAD plus a constant. The lh must precede its dependent addiu by one slot (load delay) and the addiu must precede the jal, so the +K is the ONLY insn that can fill the jal slot; the result save is displaced one insn up and fills the lh's load-delay slot instead. Keep the pair in ONE expression exactly as §162d1 prescribes — the intermediate stays an anonymous single-set pseudo and the placement falls out.
Reading the BINDING off the stream (free, no compile). The sra sits BETWEEN call 2's jal and the addu $s0,$s0,$v0, so it consumes call 2's $v0 alone ⇒ the source is f(..) + (g(..) >> k), never (f(..) + g(..)) >> k; a shift of the sum would have to follow the addu. And the +K inside the jal slot is call 2's ARGUMENT, not a statement between the calls.
BYTE EVIDENCE. func_8017F9AC (ov_SC07_006, 275 ins, first-gate MATCH, zero iterations) — 8017FABC-8017FADC (K = 0x100) and 8017FBD4-8017FBF4 (K = 0x200): u = func_8004787C(*(s16 *)(p + 0xFC)) + (func_8004787C(*(s16 *)(p + 0xFE) + K) >> 4);. Both lh (not lhu) ⇒ signed s16 reads (§2-T3).
Honest scope: the shape is byte-observed twice in ONE function and the whole-function MATCH confirms the one-expression spelling; the two-statement spelling was never compiled, so the 'arg-load forces the slot' mechanism is derived from the load-delay/branch-delay hazard rules, not from an A/B.
DIAGNOSTIC TELL. Two jals to the same helper feeding one +, call 1's slot a real nop, call 2's slot holding an addiu $aN,$aN,K ⇒ do NOT apply §162d1's 'you put the save above the arg setup' reading; this IS the match. Write it as one expression with the +K inside call 2's argument and let the save land in the preceding load's slot. Reach for §162d1's fresh-single-set-local lever only when call 2's argument is a bare constant.
(SHARPENS — sharpens §164-35, §162e1, §162e2, §42; evidence: single-instance; from func_8017FE38)
§165-39 — NAMING AN INLINE-ASM OPERAND'S ADDRESS MOVES ITS DEF TO THE STATEMENT; INLINE, IT IS BORN AT THE #APP. The loop-free face of §164-35. (sharpens §164-35, whose existence law is scoped to a call argument in a LOOP and whose mechanism is scan_loop/move_movables — neither exists in a call-free, loop-free leaf, so do not carry that mechanism across. Also §162e1/§162e2, same reason.)
Target shape — a p + K whose only consumer is a GTE macro a hundred instructions later, yet it is materialised in the prologue region:
/* 8017FE74 */ addiu $a0,$a1,0xDC <- insn 9; consumed by gte_ldv3 at 0x80180020
...
/* 80180034 */ addiu $v1,$a1,0xE4 <- the other two operands stay adjacent to the asm
/* 80180038 */ addiu $v0,$a1,0xEC
THE LAW. An address written INLINE in an asm operand list has no statement of its own, so expand emits it where the asm is. Assign it to a named local first and the def acquires a statement position and is emitted THERE — the ordinary §42 statement-placement law, reaching a construct you otherwise cannot place. Like §164-35 it is per-ADDRESS: name the one the target hoists, leave the siblings inline.
BYTE EVIDENCE. func_8017FE38 (ov_SC07_001, 239 ins, NEAR 3). sv = p + 0xDC; at the head of the body, then gte_ldv3(sv, p + 0xE4, p + 0xEC) reproduces the target's insn-9 addiu $a0,$a1,0xDC and leaves the other two adjacent to the macro. All three inline: 146 mismatched; the one named: 59 (.run/wave4/func_8017FE38/func_8017FE38.c:174).
⚠ MECHANISM — do not repeat the draft's 'sched1 gets a real live range to price'. No .lreg/.greg was dumped and no priority was computed; statement placement explains the observation without invoking the scheduler. The placement is the measurement, the RTL story is unowned.
THE DIAGNOSTIC TELL. A lone addiu $aN,$aM,K sitting in the prologue region whose only consumer is a GTE/#APP block far below ⇒ the original had a named local. Inversely, an addiu sitting directly on top of its #APP block was written inline in the macro call — leave it there.
(SHARPENS — sharpens §47, §158, §164-36, §164-49; evidence: single-instance; from func_8017FE38)
§165-40 — SPEND §47's "PERTURBATION TRAP" ON PURPOSE: A BARE __asm__ volatile("") BEFORE A DEFINITION IS A RANGE SHORTENER. (sharpens §47, which names this construct only as a hazard — 'a bare asm("") elsewhere is a cse table-flush + sched barrier + a maspsx #APP hop-killer' — and §158, which extends a range; this is the same dial run backwards. Placement per §164-36.)
Target shape — a symbol address materialised in the BODY rather than swept up into the prologue:
/* 8017FE90 */ addiu $v1,$v1,%lo(D_800AF648) <- insn 12, not insn 4
THE LAW. sched1 will hoist an independent lui/addiu symbol-address pair to the top of its basic block, lengthening the pseudo's live range and re-pricing it. A zero-byte volatile asm placed immediately before the defining statement is a hard scheduling fence (L1820), so the pair stays where the source put it; the SHORTER allocno then flips local-alloc's block-quantity choice ($t1 -> $v1 here) and the argument registers cascade behind it (p -> $a1, ot -> $a2). §47/§158 lengthen a range to split a priority tie; this denies a hoist to shorten one.
Byte evidence. func_8017FE38 (ov_SC07_001): one __asm__ volatile (""); before af = D_800AF648; takes 59 -> 44 mismatched (.run/wave4/func_8017FE38/func_8017FE38.c:181-182).
ATTRIBUTION — file law, restated because it is what found this. cc1 -fno-schedule-insns reproduced the TARGET's allocation ⇒ sched1, not sched2 and not reload, owned the residual. That is §78/§80's attribution primitive and §164-49's prescription: run it before believing any allocation story, and never bank it as a new finding.
PLACEMENT. Before the DEFINING statement, in the block that defines the value — never at the head of a block whose first insn the target steals into a delay slot (§164-36). In a GTE-heavy body prefer §47's between-two-volatile-asms slot when you want the +1 live-length WITHOUT the fence; use the bare form only when the fence is the point.
Scope: n=1, and the 59->44 step is a stage in a progression, not a drop-one ablation (§150). Ablate before leaning on the cascade.
(SHARPENS — sharpens §162k1, §162b1, §164-30; evidence: single-instance; from func_8017FE38)
§165-41 — §162k1's andi COUNT MUST EXCLUDE THE MASKED USES: a narrower mask ABSORBS the HImode/QImode extend. (sharpens §162k1, whose tell — 'count the andi $x,$y,0xFF on each lbu-fed value, PER BLOCK; N andis => a QI local used N times in SImode' — has no exception clause and mis-reads this function by two.)
Target shape — ONE andi 0xFFFF in a block that uses the same lhu-loaded value three times:
andi $v1,$a1,0x100 ; srl $v1,$v1,4 <- no extend
andi $v0,$a1,0x200 ; sll $v0,$v0,2 <- no extend
andi $v0,$a1,0xFFFF <- the ONLY extend
THE LAW (read out of the pinned combine.c, not inferred). force_to_mode's AND arm (:5808-5818) rewrites an AND's constant to mask & INTVAL — the bits its consumer actually needs — and then deletes the AND entirely when what is left equals that mask. The implicit & 0xFFFF of a HImode local therefore VANISHES at any use that is itself masked narrower: y & 0x100 emits one andi 0x100 and no andi 0xFFFF. Only a use that needs all 16 bits — a store, an add, a wide compare — shows the extend. Same arm, same consequence, at 0xFF for a u8 local.
Evidence. func_8017FE38 (ov_SC07_001): three SImode uses of one lhu-loaded y, one andi 0xFFFF (asm/ov_SC07_001/nonmatchings/ov_SC07_001_jr_8017BEBC/func_8017FE38.s:184,190,199); type matrix in .run/wave4/func_8017FE38/f_*.
THE DIAGNOSTIC TELL — the repaired oracle. When reading a target back to a declared width, tally only the UNMASKED SImode uses. A single andi 0xFFFF sitting among several andi <small> on ONE loaded value is a HImode local, not an s32 carrying one hand-written mask — the small masks are the same variable, absorbed.
(SHARPENS — sharpens docs/gcc-2.7.2-map/sched.md D3 (:163 MOVE-vs-COPY, :185 triage row, lever (d)), §31 triage table row 'D3 eager-steal' (L2411), §161a corollary (L10989), cookbook L2489 (loop-backed; evidence: single-instance; from func_80180C90)
§165-42 — A JUMP-TABLE ENTRY THAT LANDS ONE INSTRUCTION BELOW EVERY BRANCH TARGET IS THE eager COPY-STEAL, NOT A CASE BODY OF ITS OWN. (the jr-function reading of docs/gcc-2.7.2-map/sched.md D3 (:163, :185), which states the label+4 tell for a BRANCH target against a join label and never for a .rodata word; and the missing inverse of §161a's corollary (L10989), which rules only on a branch target that is NOT a table entry. The cookbook's one in-file instance of the shape — L2489's loop-backedge sll — is a different CFG and is stated as an aside.)
Target shape — one shared arm, reached from a table and from branches, at TWO addresses 4 bytes apart:
jtbl_801D9118[4] = .L80180D44 ; [6] = .L80180D44 <- the TABLE's address
...
sltiu $v0,$v1,0x8
beqz $v0,.L80180D48 <- the BRANCHES' address = table entry + 4
addu $a0,$s0,$zero <- the stolen COPY
...
beqz $v0,.L80180D48
addu $a0,$s0,$zero <- the same insn, stolen again
j .L80180D48
sh $zero,0xE2($s0)
.L80180D44: addu $a0,$s0,$zero <- the ORIGINAL, still there .L80180D48: addiu $v0,$zero,0x1E
THE LAW (read out of the pinned gcc). A switch arm's label always has LABEL_NUSES > 1 — the ADDR_VEC word counts as a use — so own_thread_p returns 0 for it (reorg.c:2151-2170, the insn != label || LABEL_NUSES (insn) != 1 test). With own_thread == 0 the eager filler cannot delete what it takes: it puts copy_rtx (trial) in the slot and sets new_thread = next_active_insn (trial) (reorg.c:3423-3433), then if (new_thread != thread) mints a label at the next insn and retargets the branch — label = get_label_before (new_thread); reorg_redirect_jump (insn, label); (reorg.c:3593-3616). reorg_redirect_jump rewrites JUMPs and never touches the ADDR_VEC, so the table keeps the original address while every branch moves one instruction past it. A table word 4 bytes below every branch target names the SAME arm — do not invent a case-4/6-only statement to explain it. (Owned threads are sched.md D3's other branch: the insn is MOVED, and no second address appears.) This is cc1, not the assembler: the ASPSX slot-hop of L2497 can neither mint a label nor retarget a branch.
BYTE EVIDENCE — the two halves of ONE banked function, from character-identical source. func_80180C90 (ov_SC01_077, 160 ins, banked commit:1678, src/ov_SC01_077/ov_SC01_077_jr_80180B64.c:3116). The body is an if/else whose arms carry the SAME switch written out twice (:3128-3155 and :3166-3193, identical apart from the goto label name — §164-11's k-tables rule), and the two tables are structurally identical.
- Arm A stole; arm B did not. A: table entries 4/6 =
0x80180D44, bothbeqzand thejtarget0x80180D48, andaddu $a0,$s0,$zeroappears THREE times — at0x80180D44and in both slots (asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_80180B64/func_80180C90.s:10,12,62-63,75-76,84-86). - B: table entries 4/6 =
0x80180E5C= the branch target — ONE address — with the fall-throughsll $v0,$v1,2in the first slot andnopin the second (:24,26,139-140,151-152,160-161). - The price is exactly one instruction, and it is not the copy. Dispatch
beqzthrough the shared head is 21 insns in arm A against 20 in arm B: the second steal costs nothing (it replaces anop), and the whole delta is that arm A'ssllwas left at its own address instead of being eaten by the slot. A draft that reproduces arm B's shape in arm A reads as LENGTH-DRIFT −1 with every later branch target shifted — this function's earlier 116-mismatch attempt (.run/backlog_drafts/func_80180C90.c) has exactly that,sll $v0,$v1,2in arm A's slot, its diff re-aligning one slot off from that instruction onward (.run/backlog.jsonl, residual idx 26).
THE DIAGNOSTIC TELL. Before writing C for a jr function, take the SET of addresses the tables name and the SET the branches name. An address in the table but in no branch, sitting 4 bytes below an address that only branches name, is a stolen head insn — one arm, two entry points. §161a's corollary rules the other direction (branch target that is not a table entry ⇒ plain if/else); this is the inverse, and it is not a case.
⚠ Bounds. (1) Why the copies diverge is NOT established. mostly_true_jump (reorg.c:1335-1420) returns 0 for both beqz (target-label rarity 0 on both sides, then the EQ ⇒ not taken rule), so both copies should have tried the fall-through first and only arm A's attempt failed; the remaining suspect is mark_target_live_regs, a per-basic-block cached approximation that falls back to "assume everything is live" when it cannot find the block (reorg.c:2441-2510) — context-sensitive by construction. Practical rule: when the target's two copies of one block disagree in their slots, transcribe each copy separately and stop trying to make them agree. (2) Do NOT read that as "steals are unsteerable" — sched.md D3 lever (d) is byte-proven the other way (polarity/CFG shape moves the steal; statement order does not), and the 116-mismatch draft above did change arm A's fill with a source edit. The claim is only that two copies of ONE source block are not obliged to agree. (3) 4 bytes is the normal case, not an invariant: MIPS fills one slot, but new_thread can advance further past insns redundant with the slot (reorg.c:3441-3447).
(SHARPENS — sharpens docs/gcc-2.7.2-map/sched.md D3 (:163 MOVE-vs-COPY, :185 triage row) and §31's D3 row (L2411); the inverse of §161a's corollary (L10989); composes with §164-11 (L12074). Evidence: single-instance, banked whole-binary; mechanism re-derived from tools/reference/gcc-2.7.2/reorg.c by the vet, not the crack agent; from func_80180C90.)
(SHARPENS — sharpens §88c (L6693-6697) — the mirror-image false positive: equality constants are ALWAYS materialised, so addiu $vX,$zero,1; bne does NOT mean the constant was a variable, §88b / §78 (; evidence: single-instance; from func_80183D68)
§165-43 — A TAKEN EQUALITY BRANCH SEEDS A cse EQUIVALENCE CLASS, so a register-to-register bne can be a comparison against a LITERAL. (the missing complement to §88c, which warns only that a MATERIALISED equality constant is not evidence of a source variable; §162e names record_jump_equiv but only for folding a loop index.)
Target shape. A dispatcher tests the state word against a register-held constant, and inside the guarded arm a later compare reuses that register:
beq $s0,$s2, case1 <- $s2 holds 1; on this edge cse learns $s0 == $s2 == 1
...
case1: jal func_801789AC
bne $v0,$s0, skip <- reads as "compare against the switch variable"; it is `== 1`
THE LAW. record_jump_equiv (tools/reference/gcc-2.7.2/cse.c:5791) calls record_jump_cond (:5839), which merges the two operands of a taken EQ into ONE equivalence class for the remainder of the path. Inside the arm the switch value's register is the constant, so cse's cheapest-form lookup emits the compare against that register and no li is generated at all. Per §164-52 the equivalence survives until a label with >=2 jump references resets the table — it is a path property, not a block property. Corollary, and the reason this is a trap: §88c tells you a materialised constant proves nothing; this tells you the ABSENCE of a materialised constant proves nothing either. On an equality compare, neither operand form carries information about the source.
BYTE EVIDENCE. func_80183D68 (ov_SC03_118 / ov_SC03_119, 255 ins, MATCH). Both guarded compares are plain literals in the banked C — func_801789AC(a0) == 1 and func_8014CB58() == 3 — with the register operands supplied entirely by the dispatcher's own beq $s0,$s2 / addiu $v0,$zero,3; beq $s0,$v0. Draft: .run/wave4/func_80183D68/func_80183D68.c. (One sighting; the register assignment was read from the pre-bank .s and not re-confirmed after banking.)
THE DIAGNOSTIC TELL. An ==/!= compare against a callee-saved register inside an arm guarded by an equality test on that same register. Before writing x == var, ask what the guard proved about that register on this path: if a taken beq put a constant into its class, the source almost certainly says == K. Writing the register-to-register reading gives a source that cannot match and that no permutation recovers.
(SHARPENS — sharpens §135-4 (L8790), §135-13 (L8911), §135-14 (L8916), §164-10 (L12052); evidence: single-instance; from func_80187400)
§165-44 — A STORE'S DELAY-SLOT LANDING SITE IS CHOSEN BY WHICH LOADS PRECEDE IT IN SOURCE: the two-base barrier read in the ANTI direction. (sharpens §164-79, which states the barrier only as a DIAGNOSTIC and only for a load that will not rise; corrects the predicate — for a store moving past a load it is anti_dependence, not true_dependence. Completes the §135-4 / §135-13 pair with its third leg. NOT a new escape: §164-10's "the edge is unconditional, so its direction is whatever you wrote" is this same law on a same-base QImode pair.)
Target shape — a constant store parked in an unrelated load's stall, its base register absent from that load's address chain:
lw $v0,0x20($s0)
lhu $v1,0x12($v0)
li $a0,0x1D
sh $a0,0x5E($s1) <- the store IS the load-delay filler
THE LAW. For a MEM destination, sched_analyze_1 (tools/reference/gcc-2.7.2/sched.c:1735-1759) walks pending_read_insns and calls anti_dependence (pending_mem, dest) (:844-864) — write-after-read, REG_DEP_ANTI. Its /s drop clause needs one MEM /s+varying and the other non-/s+FIXED; two register bases are both varying, so the clause cannot fire, and memrefs_conflict_p (:613) has no arm that disambiguates two unrelated base registers (§164-79). The edge is undroppable, so its direction is the source order (§164-10). What you can spend that on: a constant store carries no data dependence, so it is the cheapest thing in the ready list and lands in the first stall BELOW the last load you wrote above it. Moving the store's source line across a load moves it across that load's delay slot:
*(u16*)(p+0x62) = *(u16*)(*(s32*)(a0+0x20)+0x12) + 0x800;
*(u16*)(p+0x5E) = 0x1D; /* below both a0-loads -> fills the `lhu 0x12` stall */
*(u16*)(p+0x5E) = 0x1D; /* above them -> floats into the earlier `lhu 0x5C` */
*(u16*)(p+0x62) = …; /* shadow, and the `lhu 0x12` slot stays a real nop */
BYTE EVIDENCE. func_80187400 (ov_SC02_027, 249 ins, gate-confirmed; draft .run/wave4/func_80187400/func_80187400.c:83-84). ⚠ Winner-only, n=1 — the losing order is quoted in prose and no probe survives in .run/wave4/func_80187400/. ⚠ Mechanism NOT isolated: §135-4 predicts the same placement from the LUID tie-break alone ("a store written late in source SINKS"), with no aliasing edge required. The discriminator — grant /s to one side (§30a-1) so the edge drops, and see whether the store then floats — was not run. Cite the prescription; do not cite the mechanism.
DIAGNOSTIC TELL. Your build leaves a real nop where the target fills a load-delay slot with an unrelated constant sh/sw, and that store's base register appears nowhere in the load's address chain. Do not reach for a pin or a __asm__ fence — find the store in your source and move it BELOW the load whose slot the target uses. Reciprocal of §135-13 (the load moves; temp it above the stores) and of §135-4 (store↔store; move it earlier). Per §164-70 the lever only works while the edge stands: if either side is /s+varying against a non-/s+FIXED partner, there is no edge and source order buys nothing.
(SHARPENS — sharpens docs/gcc-2.7.2-map/sched.md rule S5 (L72, L111, L114), §151 step 1 (L10372), §164-50 (L12912); evidence: asserted; from func_801874C0)
§165-45 — THE potential_hazard PREFERENCE IS NOT UNCONDITIONAL: it VANISHES in a block that holds exactly ONE memory-unit insn. (bounds gcc-2.7.2-map/sched.md rule S5, §151 step 1 and §164-50's potential_hazard citation — all three state 'memory-unit users first' with no gate.)
THE LAW. potential_hazard (sched.c:1318-1352) computes
ncost = (minb * 0x40 + maxb) * ((unit_n_insns[unit] - 1) * 0x1000 + unit);
and returns 0 unless that beats the running cost. unit_n_insns[unit] is the COUNT of that unit's insns in the CURRENT BASIC BLOCK (prepare_unit, sched.c:1181-1194, called once per insn from priority() at :1495; zeroed per block by clear_units() at :3184) — not a remaining-work counter. memory is the first define_function_unit in mips.md:153, i.e. unit index 0, so a block holding exactly ONE memory insn gives ncost = X * ((1-1)*0x1000 + 0) = 0 — identical to the 0 a unit-less insn gets from insn_unit returning -1 (:1099-1126) — and schedule_select (:2656-2672) falls straight back to ready-list order, i.e. rank_for_schedule's class-then-LUID rules. With TWO or more memory insns in the block the multiplier is >= 0x1000 and the preference is unbeatable at equal priority.
CONSEQUENCE — a triage split. Before you diagnose a store-beats-ALU pick as §151's unreachable hardware-model fact, count the memory-unit insns in that basic block. One ⇒ the residual is an ordinary LUID / statement-order problem and §49's LUID dial and §31 rule 3 apply normally. Two or more ⇒ §151, and the only exits are the ghost-wedge re-tie or a priority change.
⚠ Source-read only; no A/B, and one inference. The memory == unit 0 half is read off declaration order in mips.md, not confirmed against the generated insn-attrtab.c; if the index were nonzero the multiplier degrades to unit and the preference becomes weak rather than absent. Settle it empirically per function: ;; insn N has a greater potential hazard present in the -dS/-dR dump means the rule fired, absent means it did not.
(SHARPENS — sharpens §37, §164-70, §34 (pass order), §45-B; evidence: asserted; from func_8018E8A0)
**§165-46 — Sub-note (iii): the /s asymmetry only exists pre-reload ($fp is non-varying, $sp IS varying in rtx_varies_p), **
§16Xd (HOLD — do not bank until rtlanal.c is pinned) — THE /s DROP CLAUSE IS A PRE-RELOAD INSTRUMENT: A FRAME SLOT IS NON-VARYING AT SCHED1 AND VARYING AT SCHED2. (would scope §37's lattice, §164-70 and §136d-3 #3, all of which lean on a frame/fixed-address MEM being the non-varying half.)
PROPOSED LAW. true_dependence/anti_dependence/output_dependence (sched.c:834-839, :858-863, :876-881) drop an edge only when one MEM is (MEM_IN_STRUCT_P && rtx_addr_varies_p && mode != QImode) and the other is (!MEM_IN_STRUCT_P && !rtx_addr_varies_p). Pre-reload a stack slot is (plus (reg frame_pointer) (const_int K)), which rtx_varies_p exempts ⇒ non-varying ⇒ the clause can fire. Reload eliminates the frame pointer to $sp; if stack_pointer_rtx is NOT in 2.7.2's exemption list the same MEM becomes VARYING at sched2 ⇒ the clause can no longer fire. ⇒ every /s lever in this file is a sched1 lever, and it reaches the final schedule only through sched1's output becoming sched2's LUID order.
⚠ UNVERIFIED, AND THE SOURCE IS MISSING. rtx_varies_p / rtx_addr_varies_p live in rtlanal.c, which is not present in tools/reference/gcc-2.7.2/. The only local copy is gcc-papermario, which is 2.8.1 — §164-46's process rule forbids banking a mechanism from it. Fetch rtlanal.c from the FSF 2.7.2 tarball (SETUP §5.6), read the case REG: arm of rtx_varies_p, and either confirm or delete this section. Until then the practical guidance is unchanged and already banked: these levers steer sched1 (§34 pass order, §45-B).
IF IT HOLDS, THE DIAGNOSTIC TELL. A /s edit that visibly changes the .sched (sched1) dump and is then undone in .sched2 ⇒ this. Confirm with cc1 -dS -dR and compare the two dumps' orders for the pair, not just the final .s.
(SHARPENS — sharpens §136d-1 (RC-12), §72, §147-E correction (L10337), §162j1; evidence: single-instance; from func_8018E8A0)
§165-47 — WHEN A PIN IS THE ONLY LEVER LEFT FOR A THREE-ADDRESS INSN, PIN ALL THREE OPERANDS OR NONE: EACH SINGLETON PIN FAILS IN A DIFFERENT DIRECTION. (sharpens §136d-1's "pinning the dest lets gcc propagate the hard reg forward and delete the copy", which covers the COPY case only, and §72's "a pin is a preference, not a reservation".)
Target shape — a subtract whose three registers are all specific:
lw $v0,0x1C($s3)
sll $v1,$s5,2
subu $a0,$v0,$v1
WHY THE DEFAULT IS WRONG. global_conflicts runs mark_reg_death for the insn's REG_DEAD notes (global.c:719-722) before note_stores (PATTERN (insn), mark_reg_store) (:729), so an output never conflicts with a dying input and d = A − B freely reuses B's register — the same ordering fact §147-E's correction (L10337) uses to explain a vanished self-move.
THE COMPOSITION LAW. Pinning the DEST alone is a trap: gcc propagates the hard reg backward into the producer and emits lw $a0 / sll $v0 / subu $a0,$a0,$v0 — §136d-1's warning, here in its three-address form. Pinning only the operands leaves the dest on $v1. Only register s32 c1 __asm__("$2"); register s32 c2 __asm__("$3"); in an inner block plus register s32 d __asm__("$4"); at function scope reproduces the target triple. A pin is a preference over ONE quantity (§72); a register relationship needs every member of the relationship named.
⚠ PAY THE FAMILY COST KNOWINGLY. A register __asm__ pin fails dedup_propagate.compiles_standalone (§37) — the function banks ×1 and forfeits its h_seq siblings (§162p rung 3). Exhaust §136d-1's $0-add opaque copy, §163f's declared-width re-rank, §47's live-length slider and §162b1's scope/reuse first.
Evidence: func_8018E8A0 (ov_SC02_011, MATCH 211/211) — three pin configurations compared, one function. The intermediate codegen ("combine folds the load into the dest") is reported from the emitted .s, not from a -da dump; treat the mechanism of the dest-only failure as inherited from §136d-1 rather than independently established here.
§165z — REFUTED THIS WAVE: do NOT re-derive
func_8018E8A0— Sub-note (ii):pcp[i]through au32 *variable still came out mem/s for the outer three of a CHAINED assignment — only four separate explicit casted stores reliably give IN_STR Why: The observation is probably real; the attribution to CHAINING does not follow from it and is very likely a confound.pcp[i]is an ARRAY_REF, and the file already states unconditionally (L8778-8780, §164-70, §135-2) that ARRAY_REF sets MEM_IN_STRUCT_P — expr.c rebuilds a variable-index ARRAY_REF as (&arr + isize), whose INDIRECT_REF operand is afunc_8018E8A0— Corollary: this contradicts §162's "register pins provably cannot help" for this defect class — pins DO help, they just have to be applied as a set Why: Misreads the section it claims to refute. §162j1's 'pins provably cannot help' is scoped to ONE mechanism — local-alloc's optimize_reg_copy_1 rewriting a reg-reg COPY's later uses — and its stated reason is specific and checkable: the hard-reg escape at local-alloc.c:712 sits inside #ifdef SMALL_REGISTER_CLASSES, which config/mips/mips.h never defifunc_80183560— SECOND FINDING: §162j's "untested" do{}while(0) wedge IS a real scheduler barrier — gcc-2.7.2 sched_analyze treats NOTE_INSN_LOOP_BEG/END as a FULL scheduling barrier (all regs + m Why: The EFFECT is real and I reproduced it — on the+= 1-spelled draft the wedge gives 25 -> 7 (the agent's 19 -> 7 is off a slightly different temp arrangement; same phenomenon), and the residual it leaves is 7 lines confined to one RMW's local schedule with the whole callee register file already correct. The stated MECHANISM is refuted by a controlfunc_80183560— Thelhus in the target are combine's own force_to_mode rewrite of a sign_extend whose high half is dead — do NOT chase them withu16casts; thes16spelling produces them for Why: The PRESCRIPTION is right and already banked; the stated MECHANISM does not survive the RTL and would misroute the next agent. There is no sign_extend and no combine rewrite:cc1 -dron this very function shows the copy loads expanding as a bare HImode move,(set (reg:HI 154) (mem:HI (plus (reg 72) (const_int 2)))), matched bymovhi_internal2func_80189340— L5 (prescription): the 14 prim stores MUST go through STRUCT types — plain*(s32 *)(pp + 0x04)casts re-loadrgbtwice and kill the match. Why: BYTE-REFUTED, twice. (1) The agent never ran this A/B: its own scratch at .run/wave4/func_80189340/ (v1.c, a.c-j.c) only permutes statement order and adds thec/cdtemps — every variant already spells the prim accesses as struct members, so no cast twin was ever compiled. (2) I ran it. Mechanically respelling all 30((PG4_80189340 *)pp)->f/func_80189340— Stated mechanism: "gcc-2.7.2 alias.c:true_dependence drops the dependence of a varying in-struct store on a fixed non-struct scalar" — i.e. the CSE outcome is produced by true_depe Why: Wrong file and wrong pass, checked against the pinned source at tools/reference/gcc-2.7.2/. (1) There is NOalias.cin gcc-2.7.2 —ls tools/reference/gcc-2.7.2/alias.c→ no such file; alias.c first appears in gcc 2.8/egcs. (2)true_dependenceis defined atsched.c:817and its ONLY callers in the tree aresched.c:1924,loop.c:2779, and `func_80183D68— Source-level INTERLEAVE of two independent store chains is real, not scheduler noise — and the register split is the tell: when asw-of-address lands between twosh-of-field st Why: The first half — that the interleave of independent global stores is load-bearing source order and must be transcribed, not tidied — is already banked twice, in §50-E (with a byte-level mechanism: the assembler'slui $atmerge) and §135-4. Nothing new there. The second half, offered as a reusable 'rule of thumb', does not follow from its evidencefunc_8018EDB8— SECOND-ORDER MECHANISM: once optimize_reg_copy_1 rebases the load onto $a0, "the now-$a0-based load can no longer be scheduled above the $a0-based stores, because gcc must assume t Why: The polarity is inverted, and it contradicts a byte-evidenced banked section from this same campaign. §164-79: "memrefs_conflict_pCAN disambiguate two MEMs off the SAME base register at different constant offsets ((plus r c1)vs(plus r c2)) and CANNOT disambiguate two unrelated base registers, sotrue_dependence(sched.c:817-838) keeps thfunc_80187400— Rider on the MEM_IN_STRUCT_P law: 'the escape only fires pre-reload (sched1), because after reload the global's address becomes lo_sum(reg,symbol) and starts varying.' Why: Refuted from the pinned source; do not carry it into the file. (1) This MIPS port never forms a LO_SUM for a global. GO_IF_LEGITIMATE_ADDRESS (tools/reference/gcc-2.7.2/config/mips/mips.h:2284-2299) accepts a bare CONSTANT_ADDRESS_P outright, and config/mips/mips.md contains NO lo_sum pattern at all (grep: zero hits; LO_SUM appears only in mips.c'sfunc_80187400— Integration rider: 'Block-scope struct V8 / struct Cnt ... must stay block-scope for the dedup_propagate lift', i.e. block scope is what makes the named local types propagatable. Why: Block scope is necessary but NOT sufficient, and the note asserts the wrong invariant — this is the precise trap §30 #1 already documents ('Use an ANONYMOUS struct in the cast — dedup_propagate rejects inline NAMED structs, so anonymous keeps the /s flag AND stays propagatable x134'). Checked against the tool: tools/dedup_propagate.py:712 skips anyfunc_80180C40— MECHANISM:combinefolded the constant into the OTHER operand's single-use pseudo; generalises as 'when a 3-term add mixes a masked call result with a loaded field and a literal, Why: Wrong pass, and the stated gate mispredicts the drafter's own harness. (1) WRONG PASS: this isfold, the tree-level front end, before RTL and pseudos exist. Read out of the pinned tree:PLUS_EXPRfalls through toassociate:(fold-const.c:3685),split_tree(:882-950) decomposes an operand and the node is rebuilt asVAR op (ARG1 op CON)(:3func_80180C40— TELL: ali $a0,0xFE00(ori, unsigned 16-bit) where you expectaddiu -0x200means the constant folded onto the other operand. Why: The raw observation may be real, but neither the stated mechanism nor the transcription survives checking, and I could not compile (read/grep only). Two pinned-source refutations of the obvious narrowing routes:force_to_mode's PLUS arm (combine.c:5855-5879) masks a constant only underexact_log2(-smask) >= 0— an alignment mask like ~7 — so itfunc_80181E18— NEW LAW (as worded): "unname the result, let the store consume it" — don't name the call result at all; making the memory store its sole consumer is what sinks themove t1,$v0co Why: Byte-REFUTED at vet time. I rebuilt the target .s (121 words, verified by re-MATCHing the banked draft) and ran three counter-probes the agent never wrote: P5 = BOTH call results in named single-set locals (t1,t2) with the store first → MATCH; P6 = the finished expression assigned to a named local (res = (t1*2 - f(...)) & 0xFFF;) and onlfunc_80185B44— Lever B — "maspsx fills ajaldelay slot ONLY from the IMMEDIATELY preceding instruction"; therefore the symbol read must be hoisted into an explicit temp placed BEFORE the inter Why: The stated mechanism is wrong on two independently checkable counts, and the mechanism is the only thing in Lever B that isn't already §136-13. (1) maspsx does not fill delay slots at all.tools/maspsx/maspsx/__init__.pyforces.set noreorderper function (:857-859) and only INSERTS nops:res.append("nop # DEBUG: branch/jump")after a branfunc_801874C0— The residual is unreachable by respelling: true_dependence (sched.c:817) -> memrefs_conflict_p PROVES(plus regP 4)and(plus regP 12)disjoint, solw 0xCcarries no dependen Why: The MECHANISM half is byte-correct and already banked verbatim. §164-79: 'memrefs_conflict_p can disambiguate two MEMs off the SAME base register at different constant offsets ((plus r c1) vs (plus r c2)) and cannot disambiguate two unrelated base registers, so true_dependence (sched.c:817-838) keeps the dependence and the scheduler will not lift tfunc_8017FE38— A volatile-asm barrier orders tail insns only at STATEMENT granularity — you cannot splitmem = expr;without perturbing allocation. This is the wall that stopped this function a Why: The negative measurements are real (96 type x order x barrier-position combos, 2-barrier layouts, tied/read-only asm barriers, SHB, pins on $4/$5/$2) but the WALL verdict does not follow, and the file has already retired this exact verdict once. sched.md:102 says of a delay-slot pick: 'Corrects the draft-header verdict "reorg's pick is unsteerablefunc_8017F2D4— gcc-2.7.2 cross-jumping keeps the FIRST copy, while the target keeps the LAST — which is why the tail must be hand-written at the last arm. Why: This is the one causal story §162g explicitly refused to carry: 'jump.c keeps the LAST on every path. The longhand failure at 8017F2D4 is explained by §88a (call-bearing suffix ⇒ no merge ⇒ 14 live copies), not by first-copy survivorship. The 224→12 delta is real; that causal story is not.' The note re-asserts the retired mechanism in new words — tfunc_8017F2D4— Tying the barrier to a FRESH temp instead of to itself costs a realmove. Why: Byte-refuted by me, two ways.{ s32 t2; __asm__ ("" : "=r"(t2) : "0"(t)); t = t2; }→ MATCH 279.{ s32 u; __asm__ ("" : "=r"(u) : "0"(t)); t = u; }→ MATCH 279. The"0"input tie means gcc emits the copy it was going to emit anyway and local-alloc coalesces the pseudo away; the extra name is free here. The self-tie is the right idiom for othefunc_8017F2D4— A HAND-WRITTEN threadif (t == 3) goto sel_C94;is required because the same barrier also blocks thread_jumps. Why: Both halves fail. (1) NECESSITY, byte-refuted: replacingif (t == 3) { goto sel_C94; }with the duplicated bodyif (t == 3) { sel = (u32)&D_801C3C94; goto set_sel; }→ MATCH 279. The 2-instructionlui/addiu+jtail clears §50-B's floor and jump.c merges it for you — this is precisely §164-69's 'a tail the compiler WILL merge must be duplicfunc_8017F2D4—s32 lt = mode < 0xA;must be an explicit local because gcc-2.7.2 has no gcse and the singlesltimust precede the branch both arms need it after. Why: Byte-refuted by me: deleting the local and inlining the compare at both use sites —if ((mode < 0xA) || (D_8011515A == 0x100))andif (!(mode < 0xA))— gives MATCH 279. The C form is byte-INERT here, so it is not a lever and must not be taught as one. The mechanism phrasing is also imprecise: §164-52 byte-establishes that cse spans basic blocks
§166 — THE DESTINATION-TU ORACLE (P30 S48): the seven-attempt bug that was never codegen
§166a — THE SPLAT ASM SUBDIR NAMES THE DESTINATION TU. A PROSE CITATION THAT DISAGREES WITH IT IS WRONG. (NEW. Nothing in §52b/§161c/§163a/§165-01 — the whole decl-conflict family — covers "you are editing the wrong file". They all assume the TU is known.)
asm/<overlay>/nonmatchings/<TU_stem>/<fn>.s ⇒ the INCLUDE_ASM is in src/<overlay>/<TU_stem>.c
The third path component is the TU stem, derived from the split config. It is authoritative.
WHY THIS COSTS WAVES. A grep for the function name also hits callers and prototypes in
OTHER TUs of the same overlay, and those hits read exactly like a destination hit. func_8017F2D4
was cited in five waves of notes as living in ov_SC01_005_jr_8017C340.c — that file holds only
ret = func_8017F2D4(c, ret); and a prototype. The real INCLUDE_ASM is in
ov_SC01_005_jr_8017ED5C.c:3115. A HUMAN OR AGENT following that citation splices into a file with
no INCLUDE_ASM to replace — a no-op that leaves the original bytes and reads as a gate refusal.
⚠ SCOPE CORRECTION (mine, R14 — the causal half was NOT verified before I banked it). The
crack agent presented the wrong-TU citation as the CAUSE of that function's seven gate refusals, and
I relayed it. It is not. corpus.stubs() derives each stub's TU from the actual INCLUDE_ASM
site, and gate_stage splices via corpus — so the harness was always editing the right file;
only the PROSE was wrong. Measured after banking §166a: func_8017F2D4 is still a stub and still
classifies DIFF. It is a has_mid_jr function referencing jtbl_801CC504, so it carries a jump
table the standalone match_one gate cannot see — an extra failure surface, and the real residual
is CAUSE NOT DETERMINED. The ORACLE below stands on its own evidence (the path IS the TU stem,
by construction from the split config); the "this is why seven attempts failed" story does not.
The irony is instructive: this entry was written to stop a tool from printing an unmeasured cause,
and its first draft printed one.
THE COMPOUNDING FAILURE — a guess printed as a finding. gate_stage labelled every such refusal
match_one MATCH but gate rejected (declaration/TU plumbing). That string is not a measurement; the
tool never checked for a declaration conflict. Seven attempts across five waves hunted codegen and
decl conflicts on a body that was byte-correct from the first attempt. Fixed: the message now
states only what is true (the two oracles disagree) and hands over this check first.
THE RULE. For ANY "standalone MATCH / whole-binary DIFF" entry: re-derive the TU path from the
asm subdir before hunting anything. Then prove it in situ — splice into a private copy of the real
TU (one directory deep, with a shared symlink so ../shared/engine_core.h resolves), run the
pinned triple end-to-end, and masked-diff your function out of the WHOLE-TU object.
TWO PROBE GOTCHAS (both paid for in wave-5/6 agent time):
- the wrong
--aspsx-versionyields ~32 spurious mismatches all of theori-vs-addiuli-form shape — that uniformity is the fingerprint of a version mismatch, never a codegen residual. Use--aspsx-version=2.56 --expand-div. - a collateral-drift check must filter to symbols with a real size (
nm -S): the*.NON_MATCHINGaliases are zero-size markers, so a masked diff falls back to the whole.textand reports every one of them as drift purely because your function's bytes changed.
Byte evidence: func_8017F2D4 (ov_SC01_005 + ov_SC01_006, 279 ins). In-situ splice into BOTH real
TUs: 279/279, 0 masked diffs, cpp/cc1 clean; collateral check 71/71 other sized symbols identical.
The same agent corrected the family reach to ×2 (only two .s exist, byte-identical modulo the
overlay name) against a map that claimed ×5.
§167 — S48 WAVE-5/6 HARVEST (P30, 2026-08-12): the saturation point
27 note-sets, 197 distinct laws claimed, one independent skeptic each, vetted against a cookbook already holding §162-§166 from this same campaign:
| verdict | count | evidence | count | |
|---|---|---|---|---|
| NEW | 5 | byte-probed | 92 | |
| SHARPENS | 43 | single-instance | 75 | |
| COVERED | 126 | asserted | 30 | |
| UNSOUND | 23 |
COVERED+UNSOUND: 57% (§164) → 64% (§165) → 76% (here). The duplicate rate rises monotonically as the base grows — the idiom well for THIS class of function is approaching dry. Five genuinely new laws out of 197 claims is the signal to stop mining waves for idioms and spend the tokens on cracks.
THE SKEPTICS RAN THEIR OWN A/Bs THIS TIME — several refutations are byte-measured, not
argued. The best example: a crack agent claimed "the SOURCE STATEMENT BOUNDARY decides whether the
scheduler hoists a far-consumed load". The vetter built that exact spelling and got .text
byte-identical to the inline form (12 mismatched either way) — then swept eight positions and
got five distinct objects, showing the lever is the statement's POSITION (the already-banked
INSN_LUID tie-break), not the boundary. A plausible mechanism, refuted by measurement, with the
true lever named in its place.
(NEW; evidence: byte-probed; from func_8017EEEC)
§167-01 — A JUMP-TABLE ENTRY THAT LANDS IN THE INTERIOR OF ANOTHER ARM'S BODY IS A C CASE FALL-THROUGH — AND match_one CANNOT SEE THE DIFFERENCE. (NEW. §163c reads the entry values only for the jtbl[k] == default label fingerprint; §162a1/§162a2 rule the two EDGES; §165-42 rules an entry at arm_start+4 (the reorg copy-steal) and says outright it is 'not a case'. This is the interior case, it IS a case, and it is the positive half §161a's corollary (L10989) never stated. It also adds a FIFTH row to §87's blindness ladder — §81's row is a duplicated table at the wrong ADDRESS; this is the right table at the right address with the WRONG WORDS.)
Target shape (func_8017EEEC, ov_SC06_011, 108 ins, reach 6):
lhu $v1,0x34($s0) ; sltiu $v0,$v1,0x5 ; beqz $v0,.L8017F074 ; jr $v0 <- 5-entry dispatch
jtbl_801AA880 = [ 0x8017EFB0, 0x8017F074, 0x8017EFF0, 0x8017F074, 0x8017EF3C ]
^interior ^epi ^arm ^epi ^FIRST body
8017EF3C: la $s1,D_801202A0 … 0x60-iteration sweep …
8017EFAC: addiu $s1,$s1,0x10C <- loop ends, NO `j` — falls through
8017EFB0: lw $v0,%lo(D_80126CC8)($v0) <- table entry[0] lands HERE, 0x74 into the arm
8017EFE8: j .L8017F074 <- *this* arm's `break`
THE LAW. Every word in the ADDR_VEC is a case LABEL. expand_end_case emits one word per value in [minval,maxval] and the bodies in SOURCE order (§55a), so:
- the lowest body address in the table names the arm written FIRST;
- an entry that lands strictly between two other entries' addresses splits that block into two arms joined by a
/* fall through */— the earlier-addressed case's body flows into the later one. Confirm it with thej: gcc emits aj <epilogue>for everybreakexcept the last-emitted arm, so an arm that ends without ajand is not the last arm is a fall-through arm. Read the shape above ascase 4: {sweep} /* fall through */ case 0: {if (D_80126CC8==a0) …} break;— case 4 first, because 0x8017EF3C < 0x8017EFB0.
WHY IT IS INVISIBLE — the fifth blindness row. Moving the sweep from case 0 to a fall-through case 4 changes zero instructions. Both spellings, pinned triple, masked vs asm/ov_SC06_011/nonmatchings/ov_SC06_011_jr_8017BEBC/func_8017EEEC.s:
| source | ins | masked diffs | emitted .rdata |
|---|---|---|---|
case 0: {sweep; if(D_…)…} … case 4: default: break; |
108 | 0 | [$L3, $L2, $L12, $L2, $L2] — entry[0]=sweep, entry[4]=default |
case 4: {sweep} /*fall through*/ case 0: {if(D_…)…} break; |
108 | 0 | [$L10, $L2, $L13, $L2, $L3] — entry[0]=interior, entry[4]=sweep — the target |
Two functionally different programs, one instruction stream. match_one masks relocations and never looks at .rodata at all; rtu_match masked-diffs the function out of the whole-TU object and is blind for the same reason. Both report MATCH on the wrong one. The error surfaces only after jtbl_carve puts the compiled table at the table's real address — as a 2-word whole-binary DIFF with a spotless per-function gate (§52b, with no residual to read).
THE DIAGNOSTIC TELL — three lines, before writing any C for a jr function.
- Take the table's DISTINCT non-epilogue entries and sort them by address. Lowest = the arm written first (§55a).
- For each entry, ask where it lands:
== an arm's start⇒ ordinary arm;== arm_start+4⇒ §165-42's stolen head insn, not a case;== the epilogue⇒ empty case (§162a2/§162a1 at the edges); strictly interior ⇒ case fall-through, and the enclosing arm's case value is the one written first. - Cross-check with the
js: count arms ending inj <epilogue>. It must equal (number of arms with abreak) − (1 if the last-emitted arm breaks). A missingjat an arm boundary that carries a table entry IS the fall-through.
BYTE EVIDENCE. func_8017EEEC (ov_SC06_011, 108 ins). Target table asm/ov_SC06_011/data/tail18.data.s:21-27. Both C variants compiled on the pinned triple (tools/bin/gcc-2.7.2-psx/cc1 -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker, maspsx --aspsx-version=2.56 --expand-div), masked_diff.structured_diff = 0 for both. The wave-5 draft .run/wave6/func_8017EEEC/func_8017EEEC.c (sha1 baae6aba…) is the first row and was blessed as a bankable MATCH by two independent agents and by rtu_match. Scope: one function, both directions byte-measured; the mechanism (ADDR_VEC words are case labels, bodies in source order) is structural, not statistical.
(NEW; evidence: byte-probed; from func_801805D4)
§167-02 — match_one DECIDES WHETHER TO PREPEND common.h BY GREPPING THE RAW DRAFT TEXT, SO THE DIRECTIVE SPELLED INSIDE A COMMENT SUPPRESSES THE REAL ONE. (NEW. §40a (L2610) and §42a (L2960) both state that isolation compiles are -Iinclude + a prepended common.h; each describes the prepend as a fact of the environment and neither says it is CONDITIONAL, let alone on what. §96/§104 establish the class — a scan that reads RAW text counts prose as code — for reconcile_tu and gather_externs; this is that class inside the gate proxy every drafter runs, and §165-13 is its sibling on the other side of the same file.)
The check, verbatim (tools/match_one.py:71-75):
src = masked_diff.strip_scalar_typedefs(src)
if '#include "common.h"' not in src:
src = '#include "common.h"\n' + src
strip_scalar_typedefs → cdecl.strip_provided_typedefs drops typedef STATEMENTS only; it never masks comments, so comment text reaches the test intact.
THE LAW. A draft that spells #include "common.h" anywhere — including inside a /* */ header block — satisfies the test, the prepend is suppressed, and cc1 compiles a TU with no scalar types at all. The diagnostic is a CC1 FAIL in which s32/s16/u32 and every parameter and local are "undeclared (first use this function)" — which reads as an engine_types / §163a declaration-conflict problem and has nothing whatever to do with the body.
BYTE EVIDENCE (vet-time, pinned triple, func_801805D4, ov_SC07_006 / jr_8017BEBC). Shipped draft .run/wave6/func_801805D4/func_801805D4.c, which paraphrases the directive → MATCH (212 ins). The SAME file with one comment word changed — (via its common.h include -> → (via its #include "common.h" ->, no code touched → CC1 FAIL: t.c:131: parse error before 't', :133: 's32' undeclared, :135: 't' undeclared, :136: 'u' undeclared, :136: 's16' undeclared, :136: 'arg1' undeclared.
BLAST RADIUS — the same defect is on a second rung of the ladder. tools/reloc_verify.py:71 carries the identical if '#include "common.h"' not in src: inside build_object() (the §88f relocation gate), so a draft that trips match_one trips reloc_verify the same way and for the same reason.
DIAGNOSTIC TELL. A CC1 FAIL in which the SCALAR TYPES THEMSELVES are undeclared is never a body problem and never a decl conflict — run grep -n 'common\.h' <draft> before reading a single line of C. If the hit is inside a comment, paraphrase it ("via its common.h include"). Tool-side fix is §104's two-text discipline: run the test against cdecl._masked text, never the raw source.
(NEW; evidence: byte-probed; from func_801832F8)
§167-03 — A short / CONSTANT DIVISION IS COMPUTED IN HImode, SO ITS QUOTIENT CARRIES AN EXTENSION — AND THE DESTINATION'S DECLARED WIDTH DECIDES WHERE THAT sll 16 ; sra 16 LANDS. (§1-I3 gives only the authoring rule for the magic multiply and one ÷10 pair; §164-04 covers pure powers of two, where the shortening leaves no residue. §164-37/§165-29 own the same get_unwidened/promotion axis for a SWITCH operand only. Nothing in the file states the division case, the position rule, or the −2 lever.)
Target shape — a magic-multiply quotient joined to a running total, with a 16-bit round trip on ONE side of the add:
sra $v0,$a3,3
subu $v0,$v0,$v1 <- the quotient, NOT truncated
addu $v0,$s1,$v0 <- + pan
addu $s1,$v0,$zero
sll $v0,$v0,16
sra $v0,$v0,16 <- the SUM is truncated
bgez $v0,...
THE LAW. c-typeck.c:2023-2033 (build_binary_op, the TRUNC_DIV_EXPR arm) sets shorten = 1 whenever the divisor is an INTEGER_CST != -1; c-typeck.c:2352-2356 then calls get_narrower on both operands. So *(s16 *)p / 20 is evaluated in HImode, the magic-multiply quotient is a 16-bit value, and it must be sign-extended before it can join an SImode sum. Where the sll 16 ; sra 16 pair lands is decided by the width of the variable the sum is assigned to, not by the division:
destination of pan + field/K |
emitted |
|---|---|
s16 |
quotient bare, extension rides the SUM (after the addu) — the target |
s32 |
extension rides the QUOTIENT (before the addu), sum bare |
THE −2 LEVER, AND THE CAST THAT IS INERT. Only a real s32 local kills the shortening — get_narrower strips a cast but cannot strip a VAR_DECL:
pan + out1.x / 20; -> 32 ins (shortened, quotient extended)
pan + (s32)out1.x / 20; -> 32 ins (IDENTICAL — the cast is inert)
s32 xv = out1.x; pan + xv / 20; -> 30 ins (no shortening at all)
Byte evidence. All on the pinned triple. func_801832F8 (ov_SC02_041), the agent's own isolators re-run: .run/wave6/func_801832F8/dbg/isolate.c (s32 pan, pinned) 35 ins with sll/sra on the quotient; isolate2.c (s16 pan, same pin) 35 ins with sll/sra on the sum; isolate3.c (s32 pan, unpinned) 32 ins, same shape as isolate — the pin is not the variable. Controls .../vet832F8/cA,cB,cC,cD give the table above; cD (/16) confirms the power-of-two path (§164-04) carries no residue because the sra already leaves the value in range. Target anchor: asm/ov_SC02_041/nonmatchings/ov_SC02_041_jr_8017BEBC/func_801832F8.s at 801833C4-801833D8 (pan) and 80183454-80183464 (vol, the same shape with sll alone for a sign test) — both accumulators in this function are s16.
THE DIAGNOSTIC TELL. A magic-multiply quotient with a sll 16 ; sra 16 pair on ONE side of the following addu/subu, count-neutral. Read which side. Extension on the SUM ⇒ the accumulator is a short; declare it s16 and assign to it. Extension on the QUOTIENT ⇒ the accumulator is an int. Neither ⇒ the dividend reached the division through a named s32 local. Do not reach for pins, (s32) casts or the permuter — the cast is provably inert and the width is one character of source.
Bound. Constant divisors only (shorten needs INTEGER_CST); a variable divisor takes the divu+break path (§1-I4) and never shortens. Unsigned dividends shorten unconditionally (TREE_UNSIGNED (orig_op0)).
(NEW; evidence: byte-probed; from func_801832F8)
§167-04 — A register-pinned struct base's field reads need explicit per-field temps to keep gcc's natural multi-register
§16N+1 — A register __asm__ PIN ON A STRUCT BASE SERIALISES THAT BASE'S OWN FIELD READS INTO ONE REGISTER; NAMED PER-FIELD TEMPS BUY THE PARALLEL FORM BACK. UNPINNED, THE TWO SPELLINGS ARE BYTE-IDENTICAL. (a sixth channel for §164-49's class — a pin whose damage is not the pinned register but the code AROUND it. §72/§74 price a pin as a preference and a call-crossing hazard; §165-47 prices singleton pins on one insn; §45-Lever-B/§76 own the REUSED-temp serialisation via local-alloc.c:472. None covers a pinned base's own field loads, and none names the cure.)
Target shape — three halfword fields copied off one base into a stack struct, all three loads ABOVE all three stores, in three different registers:
lhu $v0,0x6($s3)
lhu $v1,0xA($s3)
lhu $a2,0xE($s3)
addu $a1,$s0,$zero
sh $v0,0x20($sp)
sh $v1,0x22($sp)
jal func_8012EFB8
sh $a2,0x24($sp)
THE LAW. With the base declared register s32 base __asm__("$19"), writing the copies inline —
in2.x = *(u16 *)(base + 0x6); ×3 — emits a strictly serial lhu $v0 / sh / lhu $v0 / sh / lhu $v0 / sh chain: each load's destination dies into its own store, so one scratch is reused and sched1 has nothing independent to interleave. Naming the three values first —
{ u16 fx = *(u16 *)(base + 0x6), fy = *(u16 *)(base + 0xA), fz = *(u16 *)(base + 0xE);
in2.x = fx; in2.y = fy; in2.z = fz; }
gives three pseudos that are all live before the first store, forcing three registers and letting all three loads rise above all three stores — the target's shape.
⚠ THE LEVER IS CONDITIONAL ON THE PIN, AND IT IS NOT A LENGTH LEVER. 2×2 on func_801832F8 (ov_SC02_041), match_one vs asm/ov_SC02_041/nonmatchings/ov_SC02_041_jr_8017BEBC:
| base | field reads | result |
|---|---|---|
pinned $19 |
per-field temps | 111 ins, 40 mismatched |
pinned $19 |
inline | 111 ins, 54 mismatched |
| unpinned | per-field temps | 109 ins, 101 mismatched |
| unpinned | inline | 109 ins, 101 mismatched — byte-identical to the row above |
Unpinned, the edit is a no-op: spend it only when the base carries a pin. And do not price it as instructions — the pin costs +2 in both spellings and the temps recover none of it; they buy register identity only. (Files: .../vet832F8/w6_{base,inlinefields,nopin_temps,nopin_inline}.c.)
THE DIAGNOSTIC TELL. The target reads N fields off one base into N DIFFERENT registers and stores them afterwards; your draft emits N load ; store pairs through one scratch, same instruction count — and the base is pinned. Give each field a named local. If the base is NOT pinned, this residual is somewhere else entirely; do not buy the edit.
Scope, honestly: one function, one base, three fields, four cells. The mechanism (dying-into-store scratch reuse vs three overlapping live ranges) is reconstructed from the emitted register pattern, not from a -dl/-dg join; the cheap confirmation nobody has run is cc1 -da on the pinned pair and a diff of .lreg.
(NEW; evidence: byte-probed; from func_801874C0)
§167-05 — QUALIFYING BOTH MEMs OF A DISAMBIGUATED PAIR volatile IS THE ONLY WAY TO PUT A DEPENDENCE EDGE BACK. A LONE volatile IS BYTE-INERT. (NEW. Completes §16Z's ADDRESS-CLASS TABLE, whose rows 1-3 record "0 — not needed" in the escape column and therefore leave every disambiguated cell with no lever at all. §37, §16Xy, §136d-3, §162q and §165-46 are all edge-DROPPING levers; this is the file's first edge-CREATING one. §164-79/§165-44 name the two-base barrier only as a diagnostic. §16Z states the both-volatile conjunction only for read_dependence, i.e. load-vs-load.)
Target shape — a store and a later load through ONE base pseudo at two constant offsets, where the target keeps them in source order and every draft hoists the load:
TARGET (both volatile) MINE (every non-volatile spelling)
sh $v1,0xA($s0) addiu $a0,$sp,0x10
lhu $v0,0x6($s0) <- stays lhu $a1,0x6($s0) <- hoisted ABOVE the store
addiu $a0,$sp,0x10 …
sh $v1,0xA($s0)
THE LAW. All three dependence predicates are one disjunction (tools/reference/gcc-2.7.2/sched.c:817-838 true_dependence, :844-864 anti_dependence, :866-882 output_dependence):
(MEM_VOLATILE_P (x) && MEM_VOLATILE_P (mem))
|| (memrefs_conflict_p (…) && ! <the /s drop clause, written twice>)
memrefs_conflict_p (:613) is the first conjunct of the second term only. So on §16Z's rows 1-3 — two distinct symbols, $sp+K vs a symbol, and the same base register at two constant offsets whose byte ranges are disjoint — the second term is dead outright, and no /s grant, no statement order, no register pin and no scope/reuse edit can create the edge: the /s clause only ever removes one. The first term is the only remaining door, and it is a CONJUNCTION. sched.c's own header comment says it (:768-771): "If both memory references are volatile, then there must always be a dependence between the two references, since their order can not be changed. A volatile and non-volatile reference can be interchanged though."
BYTE EVIDENCE — the full 2×2, re-verified at vet time from the objects on disk (func_801874C0, ov_SC03_014, 241 ins; .run/wave4/func_801874C0/w_*/func_801874C0/t.o, pinned triple, every variant 241 ins):
| spelling | objdump lines differing from the no-volatile baseline |
|---|---|
neither (w_v6) |
— baseline, 8 mismatched vs target |
store only *(volatile s16*)(a0+0xA) = … (w_pA1) |
0 — byte-IDENTICAL |
load only *(volatile u16*)(a0+0x6) (w_pA2) |
0 — byte-IDENTICAL |
both (w_pA) |
16 — the lhu 0x6 drops below the sh 0xA, the addiu $a0,$sp,0x10 follows it: MATCH 241/241 |
Two controls that pin the mechanism to the alias oracle and to nothing else.
w_pA4— volatile on the store to0xAand on the load from0xA(the SAME address): 0 differing lines. A pairmemrefs_conflict_palready conflicts on gains nothing. The qualifier pays only where the oracle had disproved the conflict.- A lone
volatilealso defeats cse-hashing for that access, and it moves zero bytes — which rules out a cse story and leaves the scheduler conjunction.
Widening is not free — qualify the ONE pair, not the object. w_pA3 (store + all three loads) and w_pA5 (7 accesses) are byte-identical to the minimal pair; w_pV3 (14) = 246 ins (+5); w_pV1 (whole entity, 38) = 258 ins (+17).
THE DIAGNOSTIC TELL. A load and a store through the same base pseudo at two constant offsets, the target holding them in source order, your draft hoisting the load, and every structural respelling returning the identical mismatch count — the flat plateau that means you are not steering the pass that decides. Before writing an "unsteerable" verdict, evaluate memrefs_conflict_p by hand for the pair against §16Z's table: if it returns 0 you are not on a scheduling tie at all — you are missing an edge, and only the volatile pair can supply it.
⚠ HONEST SCOPE. volatile here is a compiler-steering construct, near-certainly not what the original author wrote; some non-volatile spelling yielding two distinct base pseudos would create the same edge (§164-79) but costs an address materialisation. 20 single-base variants — shared and per-field scratch temps (§162b), s16*/u16* pointer locals (combine folds them back), struct-vs-array buffers, a single V8 v[3], operand re-association, statement order, arg-address hoisting, pointer-typed prototypes, a copy of the parameter — all sat at exactly 8 mismatched. Unlike a register __asm__ pin (§165-47/§37), a volatile cast is ordinary C and is not special-cased anywhere in tools/dedup_propagate.py, so it should not forfeit the h_seq family — untested on a real sweep.
⚠ TWO LIST CORRECTIONS. (1) This law is the explanation of the §164z func_8017CA18 entry's observation half ("volatile applied to the RMW side is a no-op") — one-sided volatile is supposed to be inert. (2) The §165z func_801874C0 entry ("the residual is unreachable by respelling") is byte-superseded: the mechanism half was right and already banked as §164-79, but the unreachable verdict is now refuted by the w_pA object.
(NEW; evidence: byte-probed — 2×2 ablation plus two controls, re-verified at vet time from the on-disk objects; mechanism source-cited to the pinned tools/reference/gcc-2.7.2/sched.c; from func_801874C0)
(SHARPENS — sharpens §165-03 (L13716-13740), §163e (L11813-11821), §164-72 (L13412), §164-53; evidence: byte-probed; from func_8017C294)
§167-06 — EVERY RELOAD SPILL SLOT IS 8 BYTES, IN ANY MODE, BECAUSE alter_reg PASSES align = -1. (Derives the constant §165-03 measures and builds its frame formula on; BOUNDS §163e's 'a scalar s32 gets only 4-byte alignment' to DECLARED locals — carrying that sentence across to a SPILL mis-prices the frame by 4 per slot.)
Target shape. vars= exceeds the sum of your declared locals by a multiple of 8, none of the excess is $sp-addressed, and you are trying to price a spilled s16/s32.
THE LAW. reload1.c:657-658 walks for (i = LAST_VIRTUAL_REGISTER+1; i < max_regno; i++) alter_reg (i, -1); — ascending pseudo NUMBER (§163e), from_reg == -1 — and alter_reg allocates with x = assign_stack_local (GET_MODE (regno_reg_rtx[i]), total_size, -1) (reload1.c:2349; the slot-reuse path at :2382 passes -1 too). assign_stack_local's align == -1 arm (function.c:681-685) sets alignment = BIGGEST_ALIGNMENT / BITS_PER_UNIT and, uniquely among its three arms, rounds the size: size = CEIL_ROUND (size, alignment). MIPS sets BIGGEST_ALIGNMENT 64 (config/mips/mips.h:1080). ⇒ a spilled HImode pseudo, a spilled SImode pseudo and a combine-orphan all cost exactly 8 bytes, 8-aligned. The mode never reaches the frame. Only DECLARED locals see their own alignment — assign_stack_temp takes the align == 0 arm (function.c:675-680), which is the case §163e measured.
Corollary — the frame is a two-ended pseudo-BIRTH oracle. Ascending regno + expand minting in source order ⇒ the LOWEST spill slot belongs to the earliest-born pseudo (in an arg-spilling function, the incoming-argument pseudo) and the HIGHEST to whatever pass minted last; after expand only loop.c mints (§147-CORRECTED A). A value the target spills at the TOP of the block therefore cannot be a declared local at all.
BYTE EVIDENCE (.run/wave6/func_8017C294/, pinned cc1 -O2 -G0 -mips1 -mcpu=3000; two drafts, one formula):
dmp_fas/: declared aggregates32+32+32+8+8 = 112(every scalar is a register candidate and contributes 0),grep -c 'ST_REGS or none' t.i.lreg= 16,t.cc1.s=.frame $sp,312,$31 # vars= 256=112 + 8×(16 orphans + 2 real spills), exact.dmp_base/(same body pluss32 pad[4]): declared 128, orphans 14, samevars= 256=128 + 8×(14+2). Different split, same arithmetic.
DIAGNOSTIC TELL. Never price a spill by its C type. Count orphans with grep -c 'ST_REGS or none' t.i.lreg (§165-03) and real spills from .greg's regs to allocate minus the placed ones, then multiply both by 8 — vars = Σ(declared locals) + 8×(orphans + real spills).
(SHARPENS — sharpens §165-03 (L13738, the flagged ⚠ open detail), §165-02 (L13914), §147-B CORRECTED (L10117); evidence: byte-probed; from func_8017C294)
§167-07 — ONLY A DELETION THAT HAPPENS AFTER life_analysis CAN ORPHAN A PSEUDO — AND AN ORPHAN LEAVES TWO DIFFERENT RESIDUES IN -dc. (Closes §165-03's flagged ⚠ open detail and corrects its causal order; supplies the necessary condition behind §165-02's 'must come from MEMORY'; reconciles §147-B-corrected's (use) account with §165-03's 'present in no insn'.)
THE PASS ORDER, from toplev.c. cse (:2865) → loop → cse2 (:2926) → flow_analysis / life_analysis (:2983) → combine (:3004) → sched1 (:3033) → regclass + local_alloc (:3051-3052) → global_alloc (:3080) → reload. reg_n_refs is filled by life_analysis and is never recomputed before alter_reg reads it (reload1.c:2330, gated reg_n_refs[i] > 0).
THE LAW. A pseudo whose insns die in cse / loop / cse2 / jump is recounted to zero by the later flow pass and can never take a slot. A pseudo whose insns die in combine keeps its pre-combine count forever and is slotted. The recipe's real precondition is not 'memory' — memory is what makes the deletion a COMBINE deletion (there is a load for the widening to fold into). A redundant copy, an alias of a parameter, or a register-sourced short is killed by cse, upstream of the count, which is why no amount of retyping buys frame from it.
TWO RESIDUES, ONE OUTCOME. §165-03 says the orphan 'appears in no insn in -dc's combine dump'; §147-B-corrected says combine leaves (use (reg)) + REG_DEAD. Both forms exist in one function (.run/wave6/func_8017C294/dmp_fas/, 16 orphans):
(use)form, 14 of 16 —distribute_noteshas aREG_DEADnote with no home and plants a bare(use)(combine.c:10835-10847, §36).t.i.combine:396=(insn 719 191 193 (use (reg:SI 147)) -1 (nil)with(expr_list:REG_DEAD (reg:SI 147)on the next line;t.i.lreg=Register 147 used 4 times across 1 insns in block 4; ST_REGS or none.- Ghost form, 2 of 16 — nothing survives at all. Pseudos 103/104 (the head
ws/hs) appear as(set (reg:SI 103) (ashift …))+(ashiftrt (reg:SI 103) 16)int.i.cse2:200-206andt.i.jump:65-71, appear nowhere int.i.combine, and still printRegister 103 used 2 times across 2 insns in block 0; dies in 0 places; ST_REGS or none.— a count for two insns that no longer exist.
⇒ the (use) is not what keeps the count alive; flow banked the count before combine ran. The (use) matters only in that it carries no constraints, so regclass (after combine) records no class for either form, the all-zero cost vector converges on ST_REGS, and §165-03's allocation story runs unchanged.
BYTE EVIDENCE. Five dumps under .run/wave6/func_8017C294/: grep -c 'ST_REGS or none' t.i.lreg = 14 / 16 / 16 / 14 / 16 (dmp_base, dmp_fas, dmp_m2, dmp_s9, dmp_zbss) against grep -c '(use (reg' t.i.combine = 28 / 30 / 30 / 28 / 30 — a constant offset of 14 (the call-argument and return (use)s) ⇒ +1 orphan ⇔ +1 planted (use), five bodies, one function.
DIAGNOSTIC TELL. You added a narrow memory value and vars did not move: find out which pass ate it, not which type it had. grep -n 'reg:SI N' t.i.cse2 t.i.combine t.i.lreg — absent from t.i.cse2 onward ⇒ cse killed it, the count went with it, no slot exists at any spelling; present in t.i.cse2, absent from t.i.combine, still printed in t.i.lreg ⇒ ghost form, the slot is already yours.
(SHARPENS — sharpens L2266-2270 (§ giant-crack recipe item 2) — 'Declare each callee to match the ACTUAL call site — count the $a0–$a3 (+ stack) set before each jal, NOT the canonical sig; evidence: byte-probed; from func_8017EEEC)
§167-08 — AN ARGUMENT REGISTER THAT IS READ BEFORE THE jal IS SCRATCH, NOT AN ARGUMENT — COUNT THE DEFS, NOT THE MENTIONS. (sharpens the giant-crack recipe's arity rule at L2267, 'count the $a0–$a3 set before each jal', which is right about DEFS and is routinely misread as 'count the $aN mentions'; the complement of §164z's func_80186C4C refutation, which killed the inverse inference — a nop delay slot does NOT prove a 0-arg callee.)
Target shape (func_8017EEEC @8017EFC4, ov_SC06_011) — four $a-register mentions around one call, and the callee takes nothing:
lhu $v1,0x88($s0) ; lhu $a0,0x8A($s0) ; lhu $a1,0x8C($s0)
sh $v0,0x34($s0) ; sh $zero,0x5C($s0) ; sh $v1,0x6($s0)
sh $a0,0xA($s0) <- $a0 READ as a store SOURCE, before the jal
jal func_8017F364
sh $a1,0xE($s0) <- $a1 READ in the DELAY SLOT
THE LAW. $a0–$a3 are ordinary caller-saved registers; local-alloc hands them to any pseudo whose live range does not cross the call. expand_call materialises real arguments as the last defs before the jal (a move/lw/li into $aN with no intervening use of that register as a source). So: an $aN whose last event before the jal is a READ — it is a store's value operand, a compare operand, an address — died before the call and was never an argument. A store in the delay slot reads its operand before the callee runs, so it is not evidence either way on its own; the def/use direction is.
BYTE EVIDENCE — the counterfactual costs two moves and permutes the file. Same body, only the callee's arity changed, pinned triple:
| spelling | ins | masked diffs |
|---|---|---|
extern void func_8017F364(void); … func_8017F364(); |
108 | 0 — MATCH |
extern void func_8017F364(u16,u16); … func_8017F364(t1,t2); |
110 | 53, LENGTH-DRIFT |
The 2-arg build is unmistakable: gcc allocates the stored halfwords to $a2/$v1 (lhu $a2,138($s0) ; lhu $v1,140($s0)) and emits move $a0,$a2 ; move $a1,$v1 immediately before the jal — it will not source a store from a register it is about to load an argument into. The target has no such move pair, so the call is 0-arg. Corroborated independently by §166a's oracle: the destination TU src/ov_SC06_011/ov_SC06_011_jr_8017BEBC.c defines void func_8017F364(void) itself, ~60 lines below the splice point.
THE DIAGNOSTIC TELL. Before declaring a callee, walk backwards from the jal and mark each $aN: DEF with no later use before the call ⇒ argument. USE (source operand of a store/compare/ALU op) ⇒ scratch. Then stop at the first $aN that is neither — argument registers are filled contiguously from $a0. If the TU (or a sibling TU) defines the callee, that definition outranks the register read (§166a).
(SHARPENS — sharpens §164-12, §136d-2, §164-57, §164-73; evidence: byte-probed; from func_8017F17C)
§167-09 — A ?: IN AN if's CONTROLLING EXPRESSION IS EXPANDED ONE CONDITIONAL BRANCH PER ARM. HOIST IT ANYWHERE — TERNARY-INTO-A-LOCAL, if/else, OR CONDITIONAL OVERWRITE; ALL THREE ARE BYTE-IDENTICAL. (sharpens §164-12, which reaches the SAME do_jump COND_EXPR case only via fold-const.c:3276-3333's distribution over an ENCLOSING comparison and is stated for a ?: used as a comparison OPERAND — the ?: as the condition itself needs no fold and is not covered. Distinct from §136d-2 / §164-57 / §164-73 / §164-74 / §165-21, every one of which is about the select's VALUE DESTINATION — reg, MEM, call — and none about it being the test.)
Target shape — TWO branches, the one-instruction arm living entirely in the first branch's delay slot, and no j to a join:
andi $v0,$v1,0x2000
bnez $v0,.L8017F330
andi $v0,$v1,0x8000 <- the whole ELSE arm, in the delay slot
jal func_8012BEE8
addu $a0,$s0,$zero <- the THEN arm, falling through
.L8017F330: beqz $v0,.L8017F354 <- ONE consumer test
THE LAW. if (c ? A : B) reaches do_jump's COND_EXPR case (expr.c:9124-9151) directly: each arm gets its own do_jump aimed at the CONSUMER's true/false labels, so you pay one conditional branch per arm plus the drop-through — three conditional branches where the target has two, +2 ins. Take the select out of the controlling position and give it a name; both arms then expand as VALUES into one pseudo and a single test sits below the join.
The dial is POSITION, not spelling. Pinned cc1 (-quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float), one function, r from a preceding call so the prologue is already emitted:
| spelling | ins | conditional branches |
|---|---|---|
if ((r & 0x2000) ? (r & 0x8000) : h(a0)) |
20 | 3 |
c = (r & 0x2000) ? (r & 0x8000) : h(a0); if (c) |
18 | 2 |
if ((r&0x2000)==0) c = h(a0); else c = r & 0x8000; if (c) |
18 | 2 |
c = r & 0x8000; if ((r&0x2000)==0) c = h(a0); if (c) |
18 | 2 |
The last three are byte-identical up to label numbering. The +2 is the isolated figure; the crack note reported +1 from an in-function count — §165-22's precedent, do not use the in-function number as the tell.
⚠ DO NOT RE-BUY (byte-refuted at vet time). "The CALL must be the FALLTHROUGH arm — invert the source condition to choose." Putting the call in the else arm gives the same 18 instructions and the same 2 branches, differing only in label numbers: gcc inverts the test for you. (Arm order does move one byte in the degenerate shape with NO preceding call — 16 vs 17 — but that is the prologue's own sw $31 competing for the slot, not the select.) Likewise do not reach for §2-T4's polarity invert; the polarity is a consequence of the hoist, not a dial.
BYTE EVIDENCE. func_8017F17C (ov_SC02_026, 133 ins, banked at src/ov_SC02_026/ov_SC02_026_jr_8017C180.c:3723) uses the if/else-into-c form; the isolated 5-spelling A/B above reproduces the target's exact branch/delay-slot shape from the hoisted form and only from it.
DIAGNOSTIC TELL. THREE conditional branches where the target has two, +2 ins, and your extra branch tests a value one arm has just computed. Hunt for a ?: sitting in an if's controlling expression and bind it to a local — any binding will do. Reading the target from the other side: a branch whose delay slot holds a complete one-instruction arm, whose fall-through is the other arm, with no j to a join and a single test after the label, is a two-armed select assigned to a temp — not a threaded condition and not two independent tests.
(SHARPENS — sharpens §164-20, §160d, §165-02, §16Xy; evidence: byte-probed; from func_8017F17C)
§167-10 — A SIGNED-NARROW LVALUE COMPARED AND THEN RE-READ COSTS ONE move AND 8 BYTES OF FRAME — ONE EDIT, TWO SYMPTOMS. (extends §164-20's second-read law off the DELAY SLOT — §164-20's three-way tell is keyed on the slot's owner and has no route for a copy that sits between a load and a compare; gives that law a SIGNEDNESS gate; and supplies §165-02 / §16Xy the construct row they lack, joining the copy to the orphan slot. BOUNDS §162i1's "Δ multiple of 8 ⇒ declare s32 pad[Δ/4]".)
Target shape — no delay slot in sight; the copy sits between the load and a compare that clobbers the load's register:
lh $v0,0x102($s0)
addu $a0,$v0,$zero <- the copy IS the second source read
slti $v0,$v0,0x20 <- clobbers the load's register
...
addiu $v0,$a0,1
sh $v0,0x102($s0)
THE LAW. Reading ONE signed narrow (HI/QI) memory lvalue in a guard and again inside the guarded arm mints two things at once. cse rewrites the second load as (set P2 P1) and it survives as a bare move (§164-20's mechanism, off the delay slot). And the SImode extension combine cannot fold — a narrow use, the sh store-back, survives — is stranded as an orphan pseudo that alter_reg hands exactly 8 bytes of vars no instruction ever references (§165-02's rule, §16Xy's .greg mechanism). Hoisting the read into one s32 local deletes BOTH. Never diagnose them as two residuals.
The gate is the load opcode. Isolated on the pinned cc1 (-quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float), one 4-line function, if (*(T*)(p+0x102) < 0x20) { *(T*)(p+0x102) = *(T*)(p+0x102) + 1; g(p); }:
| T | emitted | vars | ins |
|---|---|---|---|
short |
lh + move + slt |
8 | 12 |
signed char |
same shape | 8 | 12 |
unsigned short |
lhu + sltu, no copy |
0 | 11 |
int |
no copy (cse collapses it) | 0 | 11 |
short, read hoisted into int n |
no copy | 0 | 11 |
unsigned short + a (short) cast on the compare |
lh + move |
8 | 12 |
The last row is the discriminator: the signed compare is the trigger, not the pointer's spelling.
Bounds, each byte-measured. An (s32) cast on the second read buys nothing (still 8/12 — §21, gcc re-derives). The guard must compare the SAME lvalue the arm re-reads: compare x / store from y ⇒ 0; two reads passed as call ARGS with no store-back ⇒ 0 (cse commons them); the read-modify-write with no compare at all ⇒ 0. Ordered vs equality compare is irrelevant (!= 0x20 ⇒ 8/12). A call in the arm is not required. Two independent sites ⇒ vars= 16, additive (§16Xy says the same of its own construct).
BYTE EVIDENCE. func_8017F17C (ov_SC02_026, 133 ins, banked at src/ov_SC02_026/ov_SC02_026_jr_8017C180.c:3723). Real cpp | cc1 pipeline: banked body .frame $sp,48 # vars= 16, regs= 4/0, args= 16, 118 cc1-insns. Hoist the 0x102 read into s32 n ⇒ vars= 8, 117 insns — one move and 8 bytes of frame from one character of source. Neutralise the guarded block entirely (if (0)) and vars drops 16 → 8, locating the 8 bytes.
⚠ DO NOT RE-BUY — the 8-byte hole above an align-1 local is NOT the struct's slot being rounded. Measured, 7 spellings: an 8-byte struct { u8 c[8]; }, address-taken and assigned from a global, reserves vars= 8 — identical to char s[8], int s[2], struct {int a,b;}; only a 16-byte struct reserves 16. And "the hole absorbed my s32 pad[1]" proves nothing: §162i1 byte-proves a one-element array is INERT everywhere.
DIAGNOSTIC TELL. A bare move / addu rD,rS,$zero between a narrow LOAD and a compare that CLOBBERS the load's register — no delay slot involved, so §164-20's three-way tell never fires — is a SECOND SOURCE READ, not an allocator artifact. Check the frame in the same breath: if you are also 8 bytes light, it is the same edit, and §162i1's "Δ multiple of 8 ⇒ declare s32 pad[Δ/4]" will send you to invent a dead local you do not need (measured here: s32 pad[2] ⇒ 0x38, FAIL). Then route by the second read's opcode: a second lw/lbu ⇒ §160d (keep the re-read, the load survives); addu …,$zero in a delay slot ⇒ §164-20; addu …,$zero before a clobbering compare ⇒ here.
(SHARPENS — sharpens §166a, §165-36 rule 2, §162g; evidence: byte-probed; from func_8017F2D4)
§167-11 — THE DESTINATION-TU ORACLE, STATED EXACTLY: the LAST component of the INCLUDE_ASM subdir is the TU stem, and the stem ALONE resolves the file.
§166a says "the third path component is the TU stem" and draws one shape. Both need fixing before an agent can apply it mechanically.
THE TWO SHAPES (they differ by the presence of an overlay component, not by the rule):
asm/<overlay>/nonmatchings/<stem>/<fn>.s -> src/<overlay>/<stem>.c (398 of 476 live TU dirs)
asm/nonmatchings/<stem>/<fn>.s -> src/<stem>.c ( 78 of 476 — the main exe)
THE LAW, ordinal-free. The TU stem is the last component of the INCLUDE_ASM subdir = the parent directory of the .s file. Never count from the left: in the overlay form the third component is nonmatchings, and §166a's src/<overlay>/<stem>.c template is wrong for every main-executable stub (asm/nonmatchings/libgte21/… is src/libgte21.c, not src/nonmatchings/libgte21.c).
WHY IT IS STRUCTURAL, not a convention that could drift. A splat c subsegment is a SINGLE token: - [0x56c04, c, ov_SC01_005_jr_8017ED5C] (config/splat.ov_SC01_005.yaml:125). That one token generates both <src_path>/<name>.c and <asm_path>/nonmatchings/<name>/. The paths cannot disagree unless the config is edited between the two.
BYTE EVIDENCE (whole tree, current HEAD).
- 13,630 / 13,630
INCLUDE_ASMrows insrc/: subdir's last component == the containing file's stem. Zero exceptions. - 13,630 / 13,630: the overlay component (when present) == the containing directory; the 2,002 rows with no overlay component all live in
src/root. - 476 / 476
asm/**/nonmatchings/<stem>/dirs holding.sresolve to an existingsrcfile. The only non-conformingasmdirs are*/data/(rodata carves) andasm/header.s— neither holds a function stub. - 4,153 / 4,153
src/**/*.cstems are unique repo-wide ⇒ the stem alone is a sufficient key. You do not need the overlay component to resolve the TU, which is what makes the ordinal-free phrasing safe across both shapes.
⚠ AUTHORITATIVE ≠ DURABLE (see §165-36 rule 2). The oracle binds the current asm/ tree to the current src/ tree. A subdir recorded in a note, a backlog row or a .run/*_ready.json is a stale path, not an oracle — splat moved func_8017F2D4 from …_jr_8017C340 to …_jr_8017ED5C and .run/jr48/wave1_ready.json still carries the old one. Re-glob asm/**/nonmatchings/*/<fn>.s before deriving anything.
THE DIAGNOSTIC TELL. You are about to splice, and the path in the prose has a different stem from the --asm-subdir you were handed ⇒ the prose is wrong, every time (a name grep hits callers and prototypes in sibling TUs and reads identical to a destination hit). One-line check, no build: ls src/$(dirname <asm_subdir_relative_to_nonmatchings>)— or simply find src -name "$(basename <asm-subdir>).c", which is unambiguous because stems are unique.
(SHARPENS — sharpens §164-62, §80 (R7), §158, §36 (KEEPALIVE KILLS THE DYING-HARD-REG SUGGESTION, L2498); evidence: byte-probed; from func_8017F3C8)
§167-12 — THE KEEPALIVE'S ANCHOR CAN BE volatile INSTEAD OF A SECOND OPERAND — and §36's bare single-input form is inert only because it is NON-volatile. (SHARPENS §164-62, which supplies exactly one anchor — "name a value whose def is at or below the consuming insn" — and never names this one, though its own mechanism paragraph contains the fact that makes it work; widens §80-R7, stated only at behemoth/pin scale; and identifies §158's range-extender as the same construct seen from the allocno_compare side rather than the qty_phys_sugg side.)
Target shape — §164-62/§80's in-place-reuse tell on an ordinary leaf, the value held in a callee-saved pin:
mine: sll $s1,$s1,16 <- destructive, in place; $s1 "dies" at this insn
target: sll $v1,$s1,16 <- preserving copy into a fresh scratch
THE LAW. local-alloc.c:1795-1834 records the dying hard reg in qty_phys_sugg unconditionally, with no death guard (§80), and find_free_reg (:2150) honours the suggestion only while that reg is free across the quantity's range — i.e. only while it dies at that insn. §80-R7's cure (keep it alive past the temp) is right, and §164-62 is right that a NON-volatile input-only asm is an ordinary schedulable insn carrying only its operands' deps, so written after the consuming insn while naming only the dying source it floats ABOVE and anchors nothing. §164-62 fixes that with a second operand. __asm__ __volatile__ is the other fix and needs only the one operand: stmt.c:1502 sets MEM_VOLATILE_P from the keyword and sched.c:1957 then takes the clobber-everything barrier path, so the asm cannot float above the insn it was written after, the death moves onto it, and the suggestion never fires. Materialise the comparison into a named boolean first so there is a statement boundary to sit on:
s16 cmp = sVar2 > sVar3;
__asm__ __volatile__("" :: "r"(sVar2)); /* the `volatile` is the whole anchor */
if (cmp) { … }
BYTE EVIDENCE — func_8017F3C8 (ov_SC06_011, 66 ins, NEAR 5/66, not banked). Eight A/B pairs on one base, one variable each: dropping __volatile__; placing the keepalive before the compare; inside either arm after the if; and wrapped as a GNU statement-expression evaluated before the compare — all regress to the in-place sll $s1,$s1,16 form at 11-13 mismatches, against 5 for the form above. Both the keyword and the position are load-bearing, independently.
DIAGNOSTIC TELL. §164-62's tell unchanged (one-register diff, target's dest a fresh $v0/$v1, yours the operand's own register, pseudo NUMBER identical across every -da dump and only its COLOUR differing). Before reaching for §164-62's second operand, check whether you wrote volatile. Free confirmation either way: grep -n '#APP' t.s — if the block sits ABOVE the insn you wrote it after, the asm is non-volatile and inert; adding volatile pins it there at zero bytes. Prefer the volatile form when there is no value defined at or below the consuming insn to name; prefer §164-62's second operand when you are inside a region where a new barrier would move a schedule (§47's rule).
Scope: one function, eight controlled pairs, measured as mismatch counts against the target rather than a gated MATCH — the function was still 5-off when the probes were taken. Re-confirm on a banked exemplar before treating the position rule as exact.
(SHARPENS — sharpens L2465 (§34 'Statement-position / type levers': "a leading pb = param_3; rides sched.c:3191-3215's 'don't delay getting parameters' pin so combine folds the parm-save in; evidence: byte-probed; from func_8017F76C)
§167-13 — A LEADING p1 = a1; DOES NOT RIDE THE bb0 PARAMETER PIN — IT TERMINATES IT, AND ONLY FOR THE COPIES AFTER IT. The dose is "break the run as EARLY as the target needs". (SHARPENS L2465 (§34's statement-position lever list), whose one line — "a leading pb = param_3; rides sched.c:3191-3215's 'don't delay getting parameters' pin so combine folds the parm-save into it (prologue order)" — names the pin but has the direction, the pass and the dose wrong for this shape; and BOUNDS docs/gcc-2.7.2-map/sched.md S8, "anything before the first non-param-copy insn is immovable — don't fight it".)
Target shape — the call's argument setup WOVEN INTO the prologue save/copy pairs:
target: sw $s3 / move $s3,$a0 / li $a0,0x3B / sw $s6 / move $s6,$a1 /
move $a1,$s3 / sw $s5 / move $s5,$a2 / sw $s4 / move $s4,$a3 / jal
mine: sw $s3 / move $s3,$a0 / sw $s6 / move $s6,$a1 / sw $s5 /
move $s5,$a2 / sw $s4 / move $s4,$a3 / li $a0,0x3B / move $a1,$s3 / jal
Same instruction count, same register letters, same everything else — the arg setup just sits BELOW every pair.
THE LAW. schedule_block (tools/reference/gcc-2.7.2/sched.c:3186-3213, guarded reload_completed == 0 && b == 0) walks the head of bb0 and sets INSN_REF_COUNT = 1 — "Keep this insn from ever being scheduled" — on the leading run of (set pseudo hardreg) parameter copies. The loop demands GET_CODE (head) == INSN, so any NOTE inside the run ends it, and a deleted insn is a NOTE. The prologue saves are threaded after global-alloc (toplev.c:3103) and weave at sched2, where every candidate here ties at priority 1 and at class 3 against the last-scheduled sw $ra, so rank_for_schedule falls all the way through to INSN_LUID (tmp) - INSN_LUID (tmp2) = stream order. Unlevered, all four parm copies are pinned, permanently out-LUID the arg setup, and win every tie — no statement order anywhere in the body reaches this.
THE LEVER. A leading s32 p1 = a1; makes cse DELETE the original a1 parm copy (insn 6 → NOTE) and rewrite the later p1 = a1 to read (reg:SI 5 a1) directly, so it survives as a HIGHER-UID insn further down the stream. That NOTE terminates the pin run after the a0 copy alone; a1/a2/a3 become schedulable; sched1's adjust_priority birthing boost (birthing_insn_p, REG_N_SETS == 1) lifts those single-set copies to 0x7f000001 while the arg setups stay at 1; sched1 is BACKWARD, so boosted = picked first = emitted LAST. Post-sched1 the stream is 4 (s3=a0) / 19 ($a0=0x3B) / 16 (s6=a1) / 21 ($a1=s3) / 8 (s5=a2) / 10 (s4=a3) / call = the target's bb0 verbatim, and because sched2's LUIDs are sched1's output order (sched.md S9) there is nothing left to undo.
THE DOSING RULE (the operational half, and not what you would guess). A leading copy frees only the parm copies after it. A/B, same file, one line each, pinned triple:
| leading copy | residual |
|---|---|
| (none) | 8 mismatched |
p3 = a3 |
8 — frees nothing |
p2 = a2 |
7 |
p1 = a1 |
MATCH 154/154 |
| all three | MATCH |
So the rule is "break the pin run as EARLY as the target needs", not "launder the parameter you happen to use". Also byte-inert here: retyping parameter 1 to u8 * (8).
DIAGNOSTIC TELL. A residual that is a pure SCHEDULE-REORDER confined to bb0 — instruction count exact, every register letter already correct — with the call's argument setup (li $aN,K / move $aN,$sN) sitting BELOW all the sw $sN/move $sN,$aN prologue pairs the target weaves them into. Do not reach for a register __asm__ pin (§36: pins wreck the save-birthing order) or §67's asm launder: this is a plain C statement, costs zero instructions, and keeps compiles_standalone so the draft still propagates to its family (§37).
⚠ BOUND ON L2465, recorded because the one-liner mispredicts this shape. The levered copy does not ride the pin in prologue order — in the cc1 -dS stream it lands BELOW the arg setup — and the deletion is cse's, not combine's. Read L2465 as naming the pin, not as describing the mechanism.
BYTE EVIDENCE. func_8017F76C (ov_SC02_026, 154 ins, MATCH, clean standalone compile, no pins, no permuter; draft + dumps at .run/wave4/func_8017F76C/, header L38-L92). The source half was re-verified at vetting time against tools/reference/gcc-2.7.2/sched.c:3186-3213. Second natural instance: the §160g STEP-0 sibling func_80181F88 (src/ov_SC03_098/ov_SC03_098_jr_8017D898.c:5153) carries the same odd-looking leading copy as its first line — reading a sibling's weird first line beat every scheduler analysis on this function.
(SHARPENS — sharpens §164-24 / §16x 'THE PLUS/MINUS CONSTANT MIRROR' (L12384-12405), incl. "A MINUS whose constant is FIRST comes back through the varsign == -1 code flip (:3766) to the sam; evidence: byte-probed; from func_8017F76C)
§167-14 — A TWO-TERM CON - VAR IS NOT THE SAME TREE AS -VAR + CON: fold's associate: PATH CANNOT FIRE ON A BARE CONSTANT OPERAND, SO THE SPELLING SURVIVES TO RTL AND PICKS THE OPCODES. (BOUNDS §164-24 / "THE PLUS/MINUS CONSTANT MIRROR" (L12384-12405), whose "A MINUS whose constant is FIRST comes back through the varsign == -1 code flip (:3766) to the same tree as the PLUS form" is byte-true for its THREE-term exemplars (a -= K - J ≡ a += J - K) and byte-FALSE for the two-term form; and bounds §164-16's "parentheses, operand order and same-mode casts are all inert", which holds only once associate: can run at all.)
Target shape — a negate feeding an add-immediate, with the negate in the branch delay slot:
bnez $v0,L
negu $v0,$s0 <- head of the FALL-THROUGH arm, taken into the slot
addiu $v0,$v0,0x400
j ...
L: addiu $v0,$s0,0x400
THE LAW. -base + 0x400 is PLUS (NEG (base), 1024) → subu $2,$0,$4 ; addu $2,$2,1024 (the negu/addiu pair). 0x400 - base stays MINUS (1024, base): split_tree (fold-const.c:882-950) fires only on a PLUS/MINUS node with a decomposable operand, and a bare INTEGER_CST beside an opaque VAR_DECL offers nothing to split, so associate: never runs, the tree reaches RTL as a reverse subtract, and MIPS — which has no reverse-subtract-immediate — must materialise the constant first: li $2,0x400 ; subu $2,$2,$4. Identical instruction COUNT, different opcodes, and a different insn at the HEAD of the arm — which is exactly what reorg moves into the delay slot.
BYTE EVIDENCE (re-measured at vetting time, pinned triple cc1 -O2 -G0 -mips1 -mcpu=3000, two files differing in one token):
s = -base + 0x400; -> bne $5,$0,$L2 / subu $2,$0,$4 / j $L3 / addu $2,$2,1024 / $L2: addu $2,$4,1024
s = 0x400 - base; -> bne $5,$0,$L2 / li $2,0x400 / j $L3 / subu $2,$2,$4 / $L2: addu $2,$4,1024
Consumer: func_8017F76C (ov_SC02_026, 154 ins, MATCH) — if ((rand() & 1) == 0) spd = -base + 0x400; else spd = base + 0x400; (the polarity half is §3-T4, read off the target's bnez).
DIAGNOSTIC TELL. A li $vN,K you never asked for occupying a branch delay slot where the target has negu $vN,$sM. Reading a target back to source: negu immediately followed by addiu +K ⇒ the original wrote -x + K; li K followed by subu ⇒ it wrote K - x. Do not sweep parenthesisations (§164-16: inert) and do not apply §164-24's mirror — with only one splittable term there is nothing to mirror.
(SHARPENS — sharpens §164-40 (byte evidence, L12714), §165-36 rule 2 (L14646); evidence: byte-probed; from func_8017F9AC)
§167-15 — PROVENANCE — the cookbook's cite for morph_lerp (…jr_8017BEBC.c:4018) is one line past the helper; the definit
⚠ Provenance (R37) — §164-40's morph_lerp cite is one line long, and every func_8017F9AC entry is gate-outstanding. L12714 cites the helper at src/ov_SC07_006/ov_SC07_006_jr_8017BEBC.c:4018; the definition is :3994-4017 (:4018 is the blank line after its closing brace) and func_8017F9AC's INCLUDE_ASM splice site is :4086, not the inherited notes' "~:4089". Re-read against the live tree 2026-08-12. This is §165-36 rule 2 firing on THIS campaign's own entries — coordinates decay inside a single phase, not just across phases; carry the reasoning, re-resolve the line. And the status line: func_8017F9AC (ov_SC07_006, 275 ins) is not banked — backlog row 64, match_one MATCH / gate rejected on TU plumbing, INCLUDE_ASM still at :4086 — so §164-05, §164-06, §164-07, §164-18, §164-40, §164-41, §165-14 and §165-38 all rest on a MATCH that is in-situ-proven (below) but has never faced the whole-binary arbiter. ⚠ The func_8017F9AC definition that does exist in src/ is ov_SC02_028_jr_8017D898.c:3586 — a different overlay, an unrelated body at the same address. Do not read it as this function banked.
(SHARPENS — sharpens §88f (L6718-6724), §136 self-verification (L9333-9339), the two offline oracles (L9395-9405), §42c / tools/rtu_match.py (L3033-3043); evidence: byte-probed; from func_8017F9AC)
§167-16 — THE IN-SITU PROOF: KEEP INCLUDE_ASM LIVE IN THE BASELINE AND THE WHOLE-TU .text sha1 BECOMES THE ORACLE. (sharpens §88f — this IS the "missing rung between match_one and the binary" it asks tools/ for, obtained without writing a relocation resolver; sharpens §136's self-verification (L9333-9339) and offline-oracle 2 (L9403-9405), whose collateral check must ALLOW a j-addend shift 'of exactly your function's size'; and BOUNDS §42c / tools/rtu_match.py, which NEUTRALIZES the stub (-DINCLUDE_ASM(a,b)=, rtu_match.py:7,137) and therefore cannot produce this baseline at all.)
Target situation. match_one/rtu_match say MATCH. That is a candidate, not a bank (§52b) — relocations are masked (§81/§84/§87) and file-scope decl damage is invisible. You want the strongest verdict obtainable without a gate cycle.
THE PROCEDURE. Scratch-copy the host TU and build two objects through the pinned triple (cpp → cc1 -O2 → maspsx --aspsx-version=2.56 --expand-div → as):
- baseline — the TU exactly as committed,
INCLUDE_ASMleft live.include/include_asm.h:7expands to.include "<folder>/<fn>.s", soasassembles the ORIGINAL game asm into the object at the function's exact size. - candidate — the same TU with the draft body spliced over that one
INCLUDE_ASMline.
Then compare the entire .text section byte-for-byte and set-compare objdump -r.
THE LAW. Because the splice replaces the stub in place, the candidate's function occupies the same byte range the original asm did — no size shift exists, so the collateral check collapses to exact .text equality with no addend allowance to reason about. And because the baseline's bytes ARE the shipped code as assembled, the body comparison is against the game, not against the .s text, with relocation ENTRIES (symbol + type) directly comparable — closing the wrong-jal-target (§81), wrong-%lo-symbol/addend (§84) and wrong-D_-symbol (§87) classes in one diff. Neutralising the stub throws all of that away.
BYTE EVIDENCE. func_8017F9AC (ov_SC07_006, 275 ins), artefacts at .run/wave6/func_8017F9AC/insitu/: base.text.bin and tu.text.bin are both 31,064 bytes, sha1 0cacfe12d5774ea408d822fec2e923ecda49bb19 — the whole TU .text identical spliced-vs-baseline, all 44 neighbour functions included; relocs.txt shows the function's 30 relocations agreeing symbol-for-symbol (11 D_ globals as HI16/LO16 pairs, 4× R_MIPS_26 func_8004787C, 2× internal R_MIPS_26 .text); cc1.err carries only the TU's pre-existing conflicting types for built-in function memcpy at :2305 — §165-13's warning-visibility check satisfied, zero new diagnostics.
DIAGNOSTIC TELL — and it discriminates the failure, not just detects it. One extra TU compile. Read the two shapes apart: .text differs only INSIDE your function's range ⇒ ordinary codegen, go back to the diff. .text differs OUTSIDE it ⇒ your probe layer changed the declaration environment (§8d/§161c) and the function's own bytes may be perfect — the class L9405 says 'the target function's own bytes cannot show'. Spend it on any draft where a wrong %lo or a file-scope collision would cost a gate cycle.
⚠ Still not the arbiter. The whole-binary SHA1 (G3/P9) is (§52b). func_8017F9AC passed every clause above and is still backlog row 64, unbanked.
Honest scope: n=1 function, procedure verified from the artefacts on disk; PROCESS, not a compiler law.
(SHARPENS — sharpens §165-40, §165-39, §165-14/§NNN (L13999-14029), §42; evidence: byte-probed; from func_8017FE38)
§167-17 — A READ IS PLACED BY ITS STATEMENT'S POSITION, NOT BY HAVING A STATEMENT OF ITS OWN: the own-statement-but-late spelling is BYTE-IDENTICAL to the inline one. (BOUNDS §165-40, which reads as 'sched1 hoists it to the top of the block regardless'; instantiates §165-14's LUID law on a GLOBAL load whose consumer is ~200 instructions away.)
Target shape — a global lhu consumed ~200 instructions later, yet emitted at insn 6-7, with the addiu that competes for $a0 pushed below the store that frees it:
/* 6 */ lui $v1,%hi(D_800B9A02)
/* 7 */ lhu $v1,%lo(D_800B9A02)($v1)
...
/* 14 */ sw $a0,4($a3) <- the 0x808080 constant KEEPS $a0
/* 15 */ addiu $a0,$a1,0xDC <- $a0 reused only after the store frees it
/* 16 */ sll $v1,$v1,0xE
THE LAW. Free the read from its consumer's expression and put the statement at the head of the block. cc1 emits each statement where it stands and rank_for_schedule falls through to INSN_LUID on the all-ties case (sched.c:2425, §165-14), so source position IS the schedule: an inline sub-expression inherits its enclosing statement's position, and a statement written beside the consumer inherits the same one. The boundary is not the lever; the POSITION is — and it is a plateau with a monotone tail, so sweep it (§67).
d = *(u16 *)&D_800B9A02; written … |
mismatched vs the MATCH |
|---|---|
| head of body — 1st, 2nd or 3rd statement | 0 — MATCH |
after *(u32 *)(pkt + 4) = 0x808080; |
7 |
after va = (u8 *)(p + 0xDC); · after *(u8 *)(pkt + 7) = 0x2C; |
11 |
its own statement, immediately above ot = …[d << 14]; |
12 |
inline: ot = (u32)&D_800A6610[(*(u16 *)&D_800B9A02) << 14]; |
12 — byte-identical .text to the row above |
BOUNDS §165-40. §165-40 (same function) says sched1 hoists an independent symbol-address pair to the TOP of its block and prescribes a bare __asm__ volatile("") fence to deny it. That holds only up to the LUID tie-break: an independent load moves ahead of dependent work but keeps source order against other independents, which is why eight positions give five distinct objects. Sweep the statement before you spend the fence.
BYTE EVIDENCE. func_8017FE38 (ov_SC07_001, 239 ins, banked at src/ov_SC07_001/ov_SC07_001_jr_8017BEBC.c:3976). One-statement A/Bs against the banked body on the pinned triple (cc1 -O2 -G0 -mips1 -mcpu=3000, maspsx --aspsx-version=2.56 --expand-div), masked-diffed against the banked object; ladder above. The inline and own-statement-late objects differ only in the embedded source filename.
THE DIAGNOSTIC TELL. A global lui/lhu pair the target places in the prologue region while your draft emits it beside its far-away consumer — with an unrelated addiu $aN,$aM,K sitting in the slot the target gives the load, and a materialised constant displaced out of $a0. Do not fence and do not pin: walk the read's STATEMENT up one statement at a time and take the plateau.
(SHARPENS — sharpens §78 third block (L6203-6209), §164-74 ⚠ (L13469), §67 refuted hypothesis (L5390-5397), §76; evidence: byte-probed; from func_8017FE38)
§167-18 — THE TARGET NAMES THE VARIABLE TO REUSE: a value sitting in a register that a DIFFERENT value just vacated was the same SOURCE variable reassigned. (§78 and §164-74 both prescribe 'reuse an already-busy variable' and neither says WHICH one; both leave 'maybe it is declaration order' open. This closes both.)
Target shape — count-neutral, one register, on a mask of a still-live operand:
mine: andi $a1,$a1,0xFFFF ; … ; addiu $a1,$a1,-0x100 ; sb $a1,0xD($a3)
target: andi $v0,$a1,0xFFFF ; … ; addiu $v0,$v0,-0x100 ; sb $v0,0xD($a3)
$a1 is y's own register — the value being masked. $v0 is the register the PREVIOUS temp (tp, stored two insns earlier at sh $v0,0x16($a3)) has just released.
THE LAW. A fresh local for the masked value is tied by local-alloc to the source it reads, so it lands in the operand's register. Reassigning the temp whose live range has just ended — tp = y & 0xFFFF; written after x -= tp << 6; — makes the allocator re-use the register that temp vacated, which is the target's. Read the target's register choice as a statement about variable IDENTITY: when the target's value sits in a register a now-dead value released, rather than in its own operand's register, the original reassigned THAT variable. This is the selector §78 and §164-74 do not supply.
DECLARATION ORDER IS NOT THE KNOB — negative control. Six spellings of a separate sv local (declared first / mid / last in the block, assigned at the load, assigned after the u0 store, block-scoped inside the if) give .text byte-identical across all of them — 4 mismatched every time. Only the reuse closes it, 4 -> 0. Second instance of §67's 'declaration order is likewise inert', now at HImode and in a call-free leaf. (A seventh probe that moves the ASSIGNMENT rather than the declaration costs 27 — that is §42 statement placement, a different axis; do not conflate them.)
BYTE EVIDENCE. func_8017FE38 (ov_SC07_001, 239 ins, banked at src/ov_SC07_001/ov_SC07_001_jr_8017BEBC.c:3976). Drop-one ablation against the banked body on the pinned triple: fresh sv = 4 mismatched at 239 ins; tp reuse = MATCH. Permutation set .run/wave4/func_8017FE38/S1…S6.c (S1/S2/S3/S5/S6 identical .text).
THE DIAGNOSTIC TELL. Count-neutral, one register, on a value produced by masking a still-live operand: if YOUR register is the operand's and the TARGET's is one a nearby store has just freed, you manufactured an allocno. Do not permute the declaration block and do not pin — rename the assignment onto the dead temp.
Scope: n=1 function, no .lreg dumped; the measurement is the law.
(SHARPENS — sharpens §166a (THE DESTINATION-TU ORACLE — 'prove it in situ … splice into a private copy of the real TU, one directory deep, with a shared symlink … run the pinned triple end-; evidence: byte-probed; from func_8017FFD0)
§167-19 — THE WHOLE-TU SYMBOL-LAYOUT ORACLE: keep INCLUDE_ASM LIVE, assemble from the repo root, and check every sized symbol's object offset against its SHIPPED VRAM delta. (SHARPENS §166a's in-situ recipe, which proves the function's BYTES and the collateral symbols' BYTES but never checks WHERE anything landed; and §8a, whose 'rtu_match … excludes the §8 jtbl rodata, so it MATCHes a body whose switch is subtly wrong' false-MATCH class this closes offline. Distinct from §137a oracle 2, which compares with-splice against without-splice — a SELF-comparison; this one compares against the SHIP.)
The recipe — three lines on top of §166a.
- Splice the draft over its own
INCLUDE_ASMand leave every OTHERINCLUDE_ASMlive. Do not neutralize them with-DINCLUDE_ASM(a,b)=— that is §42c'srtu_matchtrick and it is the opposite choice. Runaswith cwd = repo root so the stubs'.include "asm/<ov>/nonmatchings/…"paths resolve; the object then holds the TU's whole shipped text, not just your function. nm -Sthe object and drop zero-size symbols (§166a's.NON_MATCHINGalias trap, restated — an unsized alias has no offset worth checking).- Require, for every survivor,
st_value == VRAM(sym) − BASE(section), withBASE(.text)= the TU's first function VRAM andBASE(.rodata)= its first jtbl VRAM. Both bases and every symbol VRAM are already in the.sheaders /symbols.us.txt.
THE LAW. .text offsets are a running sum of sizes, so ONE wrong-length body shifts every symbol after it. Requiring the whole offset VECTOR to equal the shipped deltas therefore proves, in one command and with no target .s diff at all: your function's LENGTH, its POSITION (i.e. that nothing above it drifted), the length of every already-banked C sibling in the TU, and — because the .rodata base catches them — that the jump tables are the shipped size. That last clause is the point: §8a's false-MATCH class (func_80159C84's second jtbl 5 words instead of 6) shifts the following jtbl by 4 and fires here, offline, where rtu_match/match_one are structurally blind and the file's only prescribed remedy is a full whole-binary gate.
Byte evidence. func_8017FFD0 (ov_SC03_108, 196 ins) spliced over src/ov_SC03_108/ov_SC03_108_jr_8017F83C.c:2917, one directory deep with a shared symlink so ../shared/engine_core.h resolves, pinned triple end-to-end (cpp -Iinclude → cc1 -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float → maspsx --aspsx-version=2.56 --expand-div → as -march=r3000 -O1 -G0): masked diff 196/196, 0 mismatches, and 21/21 symbols at their exact shipped offsets, layout drift 0 — func_8017FFD0 at 0x794 = 0x8017FFD0 − 0x8017F83C. The 21 reconciles exactly against the tree: 15 .s under asm/ov_SC03_108/nonmatchings/ov_SC03_108_jr_8017F83C/, 4 col-0 C definitions in the TU (func_8017F83C:2759, func_801803E0:2932, func_80180814:3096, func_8018087C:3109), and 2 jtbls (jtbl_801A0510, jtbl_801A05D8).
⚠ Three bounds — this is a LENGTH/POSITION oracle, not a byte gate.
- It cannot see wrong bytes at the right length. §166a's masked diff is still the body check and the whole-binary SHA is still the arbiter (§52b).
- It is blind to the last symbol of each section — nothing follows it to shift. Close that by also comparing each section's total size against
VRAM(last) + size(last) − BASE. - Its live surface is your function + the TU's already-banked C functions + the jtbls. An
INCLUDE_ASM'd sibling is verbatim shipped assembly and cannot drift, so in a 100%-stub TU the oracle gives you back only your own function's length.
Evidence scope (honest). n = 1 and green-only: it was run on a MATCHing draft and never on a known-bad object. The negative control existed in the same note-set and was never fed to it — the barrier-stripped build of this very function is 189 ins, −28 bytes (§164-09's first table row), which would shift func_801802E0 and everything below. That the oracle fires on drift is arithmetic, not a measurement; run it once on a bad object before quoting it as a gate.
THE DIAGNOSTIC TELL. Offsets exact up to symbol K and uniformly off by a constant Δ from K+1 onward ⇒ symbol K is Δ bytes wrong, and K is the function to look at — even when K is not the one you spliced.
(SHARPENS — sharpens §152 (BYTE SIZE is the family key that name- and h_seq-grouping both miss — 'Byte size is an allocator-independent, name-independent, cache-independent family key … One c; evidence: byte-probed; from func_8017FFD0)
§167-20 — §152's BYTE-SIZE FAMILY KEY IS BAND-DEPENDENT: exact for WHALES, ~92% garbage under 0x100. (BOUNDS §152, whose 'one command, exact, no false positives on the case measured' was measured on a 0xECC / 947-instruction body — the one band where it is in fact clean. Does not refute it: size is still the right candidate GENERATOR.)
§152 keys structural families on the .s header size (grep -rl 'nonmatching .*, 0x<SIZE>' asm/*/nonmatchings/*_jr_<SPLIT>/). Measured over all 11,681 asm/*/nonmatchings/*/*.s, with identity taken as the SHA1 of the opcode-only stream (the mnemonic after each */, delay slots included):
| size band | size-groups (≥2 files) | files | groups holding >1 body | files outside the group's dominant body |
|---|---|---|---|---|
| < 0x100 | 62 | 9,103 | 62 | 8,367 (91.9%) |
| 0x100–0x200 | 64 | 1,897 | 64 | 1,641 (86.5%) |
| 0x200–0x400 | 91 | 460 | 89 | 291 (63.3%) |
| 0x400–0x800 | 21 | 67 | 15 | 21 (31.3%) |
| ≥ 0x800 | 4 | 14 | 0 | 0 (0.0%) |
THE LAW. Size is a sieve, and its selectivity IS the size. Above ~0x800 it is an exact key — which is why §152's whale exemplar had no false positives, and why that result must not be carried down. At ordinary overlay-function sizes it must be composed with a structural oracle before any remap is planned or any reach is reported.
Byte evidence — the case that found it. func_8017FFD0's family (ov_SC03_108, 196 ins, 0x310). The §152 command on 0x310 returns 5 files. Four share opcode-stream SHA1 9afa3e4a… — ov_SC03_108 func_8017FFD0, ov_SC03_110 func_8018035C, ov_SC03_112 func_80181F74, ov_SC05_001 func_80183C9C — and a full instruction-text diff with only symbol names and .L labels normalized is drift 0 across all 196 lines, which is what makes that remap a pure 4-symbol substitution. The fifth, asm/ov_SC04_015/nonmatchings/ov_SC04_015_jr_8017AE2C/func_8017E520.s, is an unrelated body: identical for four instructions (addiu;sw;addu;sw — every -O2 leaf prologue looks like this), then lhu where the family has lh, then addiu;sll;sra;sltiu where the family has slti;bnez — a §163b minval-biased s16 switch dispatch, not a guard.
THE CHEAP COMPOSITION (keep §152, add two steps). Size grep as the candidate generator → group candidates by the opcode-only stream → confirm each survivor with a full instruction-text diff, symbols and .L labels normalized. Steps 2 and 3 are grep-and-diff, zero tokens, and they turn a band-dependent sieve into an exact key at any size. §152's caution 1 still binds afterwards: match_one/rtu_match mask exactly the fields a remap rewrites, so only the whole-binary SHA gates the result.
DIAGNOSTIC TELL. A size-keyed reach whose members' FIRST branch tests different struct offsets, or where one member extends its switch index and the others do not ⇒ you have a size collision, not a family. Run §164-45's 10-second check across the whole candidate set, not just against the Ghidra seed.
(SHARPENS — sharpens §45 Lever A (merged accumulator variables; gcc-2.7.2 global-alloc has no coalescing, K8), §164-64 (a local-alloc'd expression temp can claim $a0 and evict parameter 1), §; evidence: byte-probed; from func_801805D4)
§167-21 — A REASSIGNED PARAMETER CARRIES ITS INCOMING ARGUMENT REGISTER THROUGH THE WHOLE FUNCTION: give the recomputed value its OWN local, and expect NO length change. (SHARPENS §45 Lever A, whose law — "gcc-2.7.2 global-alloc has no coalescing (K8), so one hard reg spanning disjoint regions can only come from one reused source variable" — is stated and byte-proven only in the MERGE direction; and §164-64, which supplies the set_preference/prune_preferences chain but at the opposite polarity, an anonymous temp EVICTING the parameter. Neither prices a reused PARAMETER, and neither warns that the residual is COUNT-NEUTRAL. Distinct from §161b/§162n2, which price an ALIAS of a parameter, not a reassignment of it.)
Target shape — one value recomputed between N inlined blocks, where the FIRST block reads the raw argument register and the later blocks read a callee-saved one:
mult $v0,$a0 <- block 1: the raw incoming parameter
...
jal func_8004787C ; addu $s0,$v0,$zero <- left operand parked across call 2
jal func_8004787C ; sra $v0,$v0,4
addu $s0,$s0,$v0 <- the recomputed factor lands in $s0
...
mult $v0,$s0 <- blocks 2 and 3
THE LAW. The parameter's allocno is seeded with a copy preference for its incoming hard register off the prologue's (set pseudo (reg 4)) (§164-64's citation, global.c:1535-1619). Reassign the parameter and both roles share ONE pseudo, so it holds $4 for its entire life and the recomputed value can never reach $s0. Two named variables give two allocnos, and the second is then free to take the register of the anonymous left-operand temp that must survive call 2 — which is what puts the accumulation's result in the same $s0 the park used.
BYTE EVIDENCE (vet-time, pinned triple; func_801805D4, ov_SC07_006, 212 ins; base .run/wave6/func_801805D4/func_801805D4.c). Baseline with a separate s32 u → MATCH 212. Delete u and reassign the parameter t instead, nothing else changed → mine=212, target=212, 76 mismatched, class REGALLOC-PERM, sig $a0>$s0,$a1>$a0,$a2>$a1,$a3>$a2,$t0>$a3,$t1>$t0,$t2>$t1,$t3>$t2. Objdump of both builds at offset 0x128 isolates the single causal instruction: baseline addu s0,s0,v0, reassigned addu a0,s0,v0; everything downstream is that one register shift propagating through first-fit.
⚠ The producing note's own mechanism is byte-refuted — do not repeat it. It reads "forced into a callee-saved reg for the WHOLE function → prologue addu $s0,$a0,$zero and a first loop mult $v0,$s0." The fused build emits no prologue parameter copy at all, its count is identical, and all three of its loops read mult $v0,$a0: the fused pseudo keeps $a0, it does not migrate to $s0. The prescription's direction is right; the register and the phantom extra instruction are not.
DIAGNOSTIC TELL. Equal instruction counts, no prologue copy, and the whole caller-saved file shifted by one slot ($a0>$s0,$a1>$a0,…), with the target reading a callee-saved register where you read the raw argument register in the later blocks only. That is one variable doing two jobs. Split it before touching register __asm__ pins, §148-C density sliders or the permuter — it is a one-line declaration edit. §45 Lever A is the same knob run the other way; read the target's FIRST block to decide which direction you need.
(SHARPENS — sharpens §164-05 (THE INLINE-EXPANSION FRAME ORACLE: frame - args - 4regs COUNTS THE EXPANSIONS, L11944), §162i1 (the unreferenced-local frame oracle); evidence: byte-probed; from func_801805D4)*
§167-22 — THE INLINE-EXPANSION FRAME ORACLE MUST SUBTRACT THE ALIGNMENT PAD BEFORE DIVIDING: an ODD SAVED-REGISTER COUNT BREAKS THE "exact integer" STEP. (SHARPENS §164-05, whose procedure — divide (frame − args − 4×regs) by the body count, "an exact integer is your answer" — is byte-proven on two functions whose saved-register areas are 0 bytes (func_8017DC1C) and 16 bytes (func_8017F9AC). Both are already multiples of 8, so no rounding can occur in either row and the oracle's failure mode is invisible in its own evidence.)
Target shape — the same static inline helper, in a caller that saves an ODD number of registers:
addiu $sp,$sp,-0x68
sw $s1,0x5C($sp) ; sw $ra,0x60($sp) ; sw $s0,0x58($sp) <- 3 saved regs = 12 bytes
... and NOTHING else in the function touches $sp
THE LAW. gcc-2.7.2/MIPS rounds the total frame up to 8. When args + vars + 4×regs is not already 8-aligned the frame carries a 4-byte pad, §164-05's residual absorbs it, and the quotient misses an integer by 4/N — which reads as "this is not an inline expansion" and sends you to §162i1's dead-local pad, the exact misroute §164-05 exists to prevent. Compute p = (8 − ((args + 4×regs) mod 8)) mod 8 — equivalently try p ∈ {0,4} — and divide (frame − args − 4×regs − p).
BYTE EVIDENCE (func_801805D4, ov_SC07_006 / jr_8017BEBC, 212 ins, MATCH). Frame 0x68 = 104; outgoing-args area 16 (the function makes four jals); saved regs 3 × 4 = 12 at 0x58/0x5C/0x60; the only $sp references in the entire function are the 8 prologue/epilogue words (grep -c '\$sp' = 8). §164-05 as written gives 104 − 16 − 12 = 76, and 76/3 = 25.33 — no integer. The true vars region is 0x10..0x57 = 0x48 = 72 = 3 × 0x18, i.e. the same 24-byte helper local area measured across 25 expansions on func_8017DC1C and 4 on func_8017F9AC, with 4 bytes of alignment pad sitting above the register saves. Same helper, same 24, N = 3 — the law itself reproduces exactly; only the arithmetic recipe needed the pad term.
DIAGNOSTIC TELL. The quotient misses an integer by exactly 4/N. Subtract 4 and re-divide before concluding the body is not an inline expansion; an odd saved-register count (3 or 5 sw $sN, $ra included) is the fingerprint that the pad is present.
(SHARPENS — sharpens §164-61 / §16Xd (L13164-13181) — the section being bounded, §48-B THE EBB RULE (L3468-3480) — 'cse resets its hash table at a label'; stated for copies that must SURVIVE,; evidence: byte-probed; from func_80181500)
§167-23 — THE ARG-COPY ARITY TELL IS LABEL-SCOPED, NOT BASIC-BLOCK-SCOPED. (bounds §164-61/§16Xd; reads §48-B's EBB boundary for a DELETED copy instead of a surviving one)
§164-61 scopes its "the nop carries no arity information" exemption to "the parameter's own basic block" and tells you to "ask which block the jal is in." Read literally that mis-predicts, because a conditional branch does not end the block that matters here.
Target shape — one function, one callee, one argument, the two halves 0x18 bytes apart:
/* 80181508 */ move $s0,$a0 <- the parm save
/* 8018151C */ bne $v0,$zero,.L8018153C
/* 80181524 */ jal func_80131C78 <- fall-through of a conditional branch:
/* 80181528 */ nop <- NO arg copy, delay slot is a real `nop`
.L8018153C: <- the FIRST CODE_LABEL in the function
/* … */ jal func_80131C78 <- same callee, same argument, past the label
/* … */ move $a0,$s0 <- and now the copy appears
THE LAW. The copy is deleted by cse, and cse's block runs label to label: cse_end_of_basic_block scans while (p && GET_CODE (p) != CODE_LABEL) (tools/reference/gcc-2.7.2/cse.c:8039). A JUMP_INSN is not a terminator, so the not-taken side of every conditional branch is still inside the parameter's own cse block — which is sound, that side has exactly one predecessor path. Inside it the incoming hard reg still carries the value, the arg-setup (set (reg $4) (parm-pseudo)) is redundant, and nothing is emitted. At the first CODE_LABEL the table resets (§48-B's EBB rule, stated there for copies you need to survive — this is the same boundary read for copies that get deleted) and every later call must materialise move $a0,$sN. §164-61's exemption is therefore wider than its own prose: it covers every call between function entry and the first label, however many conditional branches sit in between.
BYTE EVIDENCE (func_80181500, ov_SC03_107, 212 ins, match_one MATCH ×3; .run/wave6/func_80181500/{func_80181500.c,cc1.s}). The draft declares extern void func_80131C78(s32 a0); and calls it with one argument at four sites. cc1 emits the first (cc1.s:33 → 0x80181524) with no arg setup at all, and all three later ones (cc1.s:222, :352, and func_8012B030 at :359) with move $4,$16 in the jal delay slot. Same function, same callee, same argument — the only variable is which side of $L2/$L25 the call sits on. This is a second overlay and a second callee for §164-61, whose evidence was func_80186C4C/func_8012C218 alone; and note §164-61's own listing already shows bgtz v0,0x38 at idx 8 with the exempt jal at idx 10 — its exemplar was always the fall-through case its prose calls "the entry block."
DIAGNOSTIC TELL. Before reading arity out of a jal delay slot, find the first CODE_LABEL in the target .s — the lowest address any branch or jump names. A bare-nop jal above that address is arity-SILENT: take the arity from the destination TU (§166), a sibling overlay, or a later call site, and do not buy the §17a-1 ((void (*)(void))f)() cast to explain it. Only a bare nop on a jal below the first label is evidence, and only if $a0 has not been re-materialised in between.
(SHARPENS — bounds §164-61/§16Xd (L13164-13181, "THE ARG-COPY ARITY TELL IS BASIC-BLOCK SCOPED"), extends §48-B (L3468-3480) to deleted copies, guards §161c/§17a-1; evidence: byte-probed; from func_80181500)
(SHARPENS — sharpens §55a / §49-variant (L4099-4102), §162b1 (L11069, incl. tell #2), §162k1 (L11421), §164-48 (L12861); evidence: byte-probed; from func_80181948)
§167-24 — THE TEMP YOU ADD TO KILL THE BIRTHING BOOST IS ALSO A WIDTH DECLARATION: share it at the VALUE'S OWN WIDTH, or the boost-kill stops being zero-byte. (sharpens §55a's §49-variant (L4099-4102) — "route the load through a temp assigned in BOTH halves of a branch ⇒ reg_n_sets==2 ⇒ boost dead, at zero byte cost" — which names no TYPE for that temp; composes it with §162k1 (L11421) / §164-48 (L12861), which price a local's declared width but never as a PRECONDITION of a scheduling lever; and bounds §162b1's tell #2 (L11090), whose mask-cost route sends you to SPLIT the shared temp — which here re-arms the boost.)
Target shape — a jr-switch whose arms bump the same halfword state word, with a 2-insn transposition around a call and NO mask anywhere in the residual:
case 0: … sh $v0,0x34($s0) <- `state + 1` written back
case 1: … lhu/addiu/sh 0x34($s0) <- the same bump, other arm
residual: your `beqz` slot holds `move $a0,$s0`; the target's holds `addiu $v0,$a1,1`
THE LAW. The §49-variant S2 kill is zero-byte only when the shared pseudo's MODE matches the value's. Declared u16, every set is HImode, combine's reg_nonzero_bits union (combine.c:718-788, §162b1) stays inside 0xFFFF and nothing re-widens or re-ranks; declared s32 — one token, nothing else touched — the function repermutes. Take the width off the lvalue the value is stored back into, never off the arithmetic. The correction §162b1 would have you make (split it per arm) is the one edit you must not make: splitting restores reg_n_sets == 1 and the boost with it.
BYTE EVIDENCE (func_80181948, ov_SC01_077, 132 ins, match_one MATCH; probes and harness in .run/wave6/func_80181948/, generator mk3.py). One function-scope u16 t written in case 0 (t = state + 1; *(u16*)(a0+0x34) = t;) and case 1 (t = *(u16*)(a0+0x34) + 1; …) ⇒ two sets ⇒ no boost ⇒ 132/0 MATCH, and state moves $a2→$a1 for free. t3_s32temp.c, identical but for u16 t → s32 t: 131 ins, 113 mismatched. Reusing the function's existing s32 v0 for the same bump (t2_reusev0.c): the same 131/113. Measured NULLs on the same base, all 132/6 — per-arm block scope, memory re-read, decl-order swap, an extra dummy local, +=, <= 0x170, a $2 pin. register u16 state __asm__("$5") buys the REGISTER only and leaves the 2-insn swap (132/2), consistent with §164-50: the pin was on the mis-allocated value, not on the boosted insn's dest.
⚠ THE MECHANISM IS A HYPOTHESIS — do not repeat the originating note's version. It reads "the s32 temp forces the zero-extend back and wrecks the dispatch". The length went down by one, so a re-materialised andi is not what happened; 113 mismatched at LENGTH-DRIFT −1 is §164-48's declared-width allocno re-ranking signature. No -dl/-dS was taken of the s32 build. Ship the precondition, not the story.
THE DIAGNOSTIC TELL. You introduced a shared multi-set temp to kill an S2 boost and the mismatch count EXPLODED instead of dropping, with a length drift of ±1. Do not split it back and do not reach for a pin — re-declare it at the width of the lvalue it is written into, and re-gate.
(SHARPENS — sharpens §1-I3, §1-I4, §164-04, §28's div-by-constant magic table (0x66666667→/10, 0x55555556→/3); evidence: byte-probed; from func_801832F8)
§167-25 — Division idioms confirmed: x/20 uses gcc's /5 magic 0x66666667 with shift 1+2=3 (20 = 5·2^2); `(excess*127
§16N+2 — THE MAGIC CONSTANT NAMES ONLY THE ODD PART OF THE DIVISOR; THE sra COUNT CARRIES THE POWER OF TWO. Read them together or you will guess the wrong divisor. (sharpens §1-I3, which gives the authoring rule and exactly one pair (0xCCCCCCCD / shift 3 = ÷10) and never says the constant is reusable; and corrects the §28-imported table's 0x66666667 → /10 row, which is a divisor-ladder entry misfiled as a unique mapping. Disjoint from §164-04, which owns the pure-power-of-two bgez ; addiu 2^k-1 ; sra k path.)
Target shape — a reciprocal multiply whose magic you recognise but whose shift you do not:
lui $a0,(0x66666667 >> 16) ; ori $a0,$a0,(0x66666667 & 0xFFFF)
mult $v0,$a0
sra $v1,$v1,31
mfhi $a3
sra $v0,$a3,3 <- the shift is the whole message
subu $v0,$v0,$v1
THE LAW. expand_divmod factors the divisor as odd × 2^k, synthesises the magic for the ODD part only, and folds k into the post-multiply shift. So divisor = odd × 2^(shift − base_shift(odd)), and one magic covers a whole ladder.
| magic | odd part | shift → divisor |
|---|---|---|
0x55555556 |
3 | 0 → /3 |
0x66666667 |
5 | 1 → /5 · 2 → /10 · 3 → /20 · 4 → /40 · 5 → /80 |
0x51EB851F |
25 | 5 → /100 · 6 → /200 · 7 → /400 · 9 → /1600 · 13 → /25600 |
Byte evidence — 11 divisors on the pinned triple (cpp / cc1-2.7.2 -O2 -G0 -mips1 -mcpu=3000 / maspsx 2.56 / as), s32 f(s32 x){ return x / D; }, D ∈ {3,5,10,20,40,80,100,200,400,1600,25600}: the table above is read straight off the objdumps (.../vet832F8/dv.c). Target anchor: func_801832F8 (ov_SC02_041) uses both rows — 0x66666667 / sra 3 for out1.x / 20 at 801833B0-801833C4 and 0x51EB851F / sra 13 for (dist-0x100)*127 / 25600 at 80183438-80183454. Neither is a ÷10 or a ÷100.
THE DIAGNOSTIC TELL. You recognise the magic, so you write the divisor the table told you and the magic comes out right while the sra is off by k. Multiply the divisor by 2^k and write it literally — never hand-craft a magic, never respell as (x/5) >> 2 (that rounds differently and emits an extra bias). Read it the other way too: an unfamiliar magic plus a large shift is a compound divisor, and the shift alone tells you how many factors of two to strip before you look the odd part up.
(SHARPENS — sharpens §165-21 (L14195) — TWO COMPUTED ARMS WANT ONE SHARED TRAILING STORE; already prescribes this exact cure for this exact target shape (arithmetic in the bnez delay slot, ; evidence: byte-probed; from func_80183834)
§167-26 — WHEN THE DUPLICATED STORE'S TAIL FALLS THROUGH, THE DUPLICATE-vs-JOIN DIAL IS FREE AND COSTS ONLY THE SAVED-REGISTER ORDER. §165-21's "+2 instructions" is the two-j case. (sharpens §165-21, whose only measured cost for the duplicated spelling on COMPUTED arms is a length drift; and §48-A1 / §164-32, which own the duplicate↔join allocno dial for a LOCAL's init and a switch arm's BODY but never for a STORE's BASE POINTER. The discriminator is §50-B's cross-jump floor.)
Target shape — two computed arms updating ONE memory cell by ±K, the tail already merged, one arm falling through:
jal rand
…
andi $v0,$v0,1
bnez $v0,.L1
addiu $v0,$sX,-0x400 <- TRUE arm, in the slot
addiu $v0,$sX,0x400 <- FALSE arm, falls through
.L1: sh $v0,0x12($sY) <- ONE `sh`, reached by FALL-THROUGH, not by a `j`
THE LAW. Both spellings emit this identical 135-instruction stream. The only thing that moves is which of {base pointer, loaded value} gets $s1 and which gets $s2:
if (c) *(u16*)(obj+0x12) = ang-0x400; else *(u16*)(obj+0x12) = ang+0x400; -> BASE = $s1, VALUE = $s2
*(u16*)(obj+0x12) = c ? ang-0x400 : ang+0x400; -> VALUE = $s1, BASE = $s2 <- target
if (c) d = ang-0x400; else d = ang+0x400; *(u16*)(obj+0x12) = d; -> same as the ternary
Duplicating the store hands the BASE POINTER one extra REG_N_REFS at allocation time: jump_optimize (…, JUMP_CROSS_JUMP, …) runs after reload (pass order, §45-B), so the second sh is real to global.c:594 allocno_compare and is refunded before final. This is §48-A1's rule applied to a store's BASE instead of a local's VALUE, and §164-32's zero-byte ref-boost run subtractively — collapse to the join to LOWER the base, duplicate into the arms to RAISE it. (The refs reading is INFERENCE: no -dl/-dg dump was taken. What is measured is the direction and the zero length cost.)
BYTE EVIDENCE — func_80183834 (ov_SC01_077, 135 ins, banked src/ov_SC01_077/ov_SC01_077_jr_80183324.c:3357, TU has 0 remaining INCLUDE_ASM). Every probe preserved under .run/wave6/func_80183834/; base.c → varO.c is a one-hunk diff whose only change is the store spelling:
| spelling | file | result |
|---|---|---|
| ternary, ONE store | varO.c (= the banked body) |
MATCH 135/135 |
named d, store after the join |
varL.c |
MATCH |
| duplicated store, plain | base.c |
5 mismatched |
| duplicated store, nested-CSE load order | varK.c |
5 mismatched |
| duplicated store, inverted condition + swapped arms | varN.c |
5 mismatched |
duplicated store through a u16 * base (*obj) |
varR.c |
5 mismatched |
IT IS NOT A TIE-BREAK, AND PINS ARE STRICTLY WORSE. Declaration order (varA.c) and block scope (varB.c, varI.c) are inert — so §158 / §162o1's decl-order tie-break is not the dial, and the refs inequality is strict, not an exact tie. Pinning BOTH (varC.c: obj→$18, ang→$17) and pinning ang alone (varG.c) each cost +2 instructions and flipped a branch sense; pinning obj alone (varH.c) does MATCH but fails dedup_propagate.compiles_standalone (§37) and forfeits this exemplar's ×3 reach (§162p rung 3). Take the spelling, not the pin.
THE DIAGNOSTIC TELL — count the js into the shared store, not the instructions. A two-arm ±const update of one memory cell where your instruction COUNT is exact and the residual is a clean saved-register swap between the store's BASE and the VALUE it stores ⇒ re-spell the store; do not reach for a pin, an §47/§158 slider or the permuter. Which direction to try is read off the tail:
- one arm FALLS THROUGH into the
sh⇒ §50-B'sminimum=1path merges the duplicate for free, the two spellings are length-identical, and the dial is purely the register order — try both directions; - both arms reach the store by
j⇒ §165-21, the duplicate does not merge and you pay its +2 instructions, so only the join spelling is live.
⚠ Do NOT cite §164-69 as the opposite pole. Its seven losing join spellings are a call-bearing, multi-instruction merged TAIL whose mechanism is sched1's basic-block SCOPE; nothing there is a register permutation on a one-instruction store tail. The two-way framing already belongs to §48-A1, which states both directions outright.
(SHARPENS — sharpens §136g-1 (L9214-9218), §164-55 (L13035-13063), §164-47 (L12840-12851), §3-T4 (L90-103); evidence: byte-probed; from func_80187130)
§167-27 — WHEN EVERY ARM RETURNS, gcc-2.7.2 PHYSICALLY SWAPS THE TWO ARMS AND INVERTS THE BRANCH (jump.c:1806). THE ARM YOU WRITE FIRST IS THE ONE THAT LANDS NEXT TO THE EPILOGUE. (⚠ BOUNDS §164-55 and §164-47, whose "RTL block order == source statement order" / "only the last-written body can fall into the epilogue" is stated with no precondition; settles the internal contradiction in §3-T4 — its clause (a) "put the target's fall-through block in the if" and its clause (b) "if (ok){…return good;} return 0; makes return 0 the fall-through" cannot both hold, and (b) is the one that governs here; supplies the FORWARD-direction lever for the transform §136g-1 already names but only teaches how to DISARM.)
Target shape — a 2-way test where both sides return, the branch is taken forward into the block that sits last, and that block falls into the epilogue with no j:
bnez $v0,.L801871XX <- branch-if-condition-TRUE, forward
li $a0,1 <- delay slot stolen from the block it branches TO
j .Lepi <- the OTHER arm, inline, pays the jump
move $v0,$zero <- …its return value, in the j's slot
.L801871XX: <BODY> … li $v0,1 <- falls straight into the epilogue, no `j`
.Lepi: lw $ra,0x6C($sp) …
THE LAW. jump.c:1800-1875 — /* Look for if (foo) bar; else break; */ — is a real block-swapping transform in gcc-2.7.2. It matches condjump label1 / range1 / jump label2 / label1: / range2 / <uncond jump> / label2:, calls invert_jump (insn, label1), and then splices range1 and range2 past each other with raw NEXT_INSN/PREV_INSN surgery (jump.c:1866-1875). Its preconditions ARE the "every arm returns" shape:
label1= the if-join label, withLABEL_NUSES (label1) == 1;range1end(last insn of the then-arm) is a simplejump targetinglabel2 = next_label (label1)— i.e. the then-arm ends inreturnand the label after the if-join IS the return/epilogue label;range2endis a JUMP_INSN followed by a BARRIER — i.e. the other arm also ends inreturn;! first(a laterjump_optimizeround) andreload_completed ? ! flag_delayed_branch : 1.
So if (C) { P; return X; } Q; return Y; emits Q-then-P, branch-if-C to P, and P — the arm you wrote FIRST — is the epilogue-adjacent block with no j. Put the target's no-jump arm inside the if, never after it. The spelling is inert: … return Y; and … else { return Y; } are byte-identical, both ways round; only WHICH arm is the then-arm moves anything.
Byte evidence — func_80187130 (ov_SC06_018, 86 ins, banked commit:1713, src/ov_SC06_018/ov_SC06_018_jr_80186270.c:3243). Five variants through the pinned triple, re-run at vetting (cpp / cc1-2.7.2 -O2 -G0 -mips1 -mcpu=3000 / maspsx / as; probe artifacts .run/match/func_80187130.246967/ and .242624/):
| spelling | cc1 .s layout |
verdict |
|---|---|---|
if (c != 0) { BODY; return 1; } return 0; |
bne …,$L2 · j $L3 ; move $2,$0 · $L2: BODY … li $2,1 · $L3: |
MATCH — the banked body |
if (c != 0) { BODY; return 1; } else { return 0; } |
.s-identical modulo label numbers |
MATCH |
if (c == 0) { return 0; } BODY; return 1; |
beq …,$L2 · BODY … j $L3 ; li $2,1 · $L2: move $2,$0 |
DIFF, 86 vs 86 ins, 11 mismatched |
if (c == 0) { return 0; } else { BODY; return 1; } |
.s-identical to the row above |
DIFF |
then-arm falls to a shared tail (no return in the arm) |
beq …,$L2 · BODY inline · $L2: <join> — source order, no swap |
regime control |
The last row is the precondition failing: with the arm falling into an interior join, range1end is not a jump to the return label, jump.c:1806 never fires, and §164-55/§164-47's source-order law holds unmodified. The two laws are disjoint, not contradictory — the discriminator is whether the label after the if-join is the RETURN label, i.e. whether EVERY arm returns.
⚠ This also bounds a §164z verdict. §164z's func_80184944 entry reads "No relocation occurred … gcc-2.7.2 has no block-reordering pass; describing plain source-order emission as the compiler 'physically relocating' a block will send the next agent hunting for a pass that doesn't exist." That verdict is right for func_80184944 (interior join ⇒ precondition unmet) and its refutation stands, but the pass is not imaginary: it is jump.c:1806, it splices insn chains, and func_80187130 is the byte-proven instance. Read that sentence as "no bb-reorder" (gcc-3.x), not as "no block motion in 2.7.2".
THE DIAGNOSTIC TELL. A count-neutral residual — no LENGTH-DRIFT, which is what separates it from §164-55's +1 — in which ONE conditional branch flips beqz↔bnez and a 2-instruction j <epilogue> ; <return-value set> pair changes sides of it, sliding between the function tail and the slot immediately after the branch. match_one files it SHIFT-DRIFT / BRANCH-POLARITY and the index routes to §3-T4: do not invert the condition (the polarity you can see is invert_jump's output and carries no source information, cf. §165-23) and do not add a fence (jump.c runs long before reorg, §136g-1). Swap which arm is the if body: the target's no-jump, epilogue-adjacent arm goes INSIDE the if, and the condition is spelled so the branch tests it TRUE.
Cross-link — the disarm direction. §136g-1 needs this swap NOT to fire and buys that by putting any label between the if-join and the return label (wrap the body in an if with ONE trailing return), which also breaks LABEL_NUSES (label1) == 1. Same pass, opposite goal — read the target's block order and match it.
(SHARPENS — bounds §164-55 (L13035), §164-47 (L12851), §3-T4 (L90-103) and §164z's func_80184944 "no relocation" verdict (L13677); completes §136g-1 (L9214); evidence: byte-probed (5 variants, cc1 .s + relocation-masked .o, re-run at vetting) + source-read jump.c:1800-1875; from func_80187130)
(SHARPENS — sharpens §49 (L3528-3576, THE LUID DIAL) — incl. its Method note: '-dS -dR ... the ready lists, the computed priorities, and the chosen order ... if the priorities are equal, you ; evidence: byte-probed; from func_801874C0)
§167-28 — WHEN THE TARGET PICKS A LOWER-PRIORITY INSN THAN YOUR DRAFT, PRIORITY IS NOT THE DIAL: THE HIGHER-PRIORITY INSN WAS NOT READY, SO YOUR DRAFT IS MISSING A DEPENDENCE EDGE. (sharpens §49's Method note, which reads the ready lists only for the EQUAL-priority cell and prescribes the LUID dial; and the gcc-2.7.2-map/sched.md §4 recipe, whose ladder is "equal-pri ⇒ S1 / pri differs via a load-or-mul chain ⇒ S3 = intrinsic, route to the permuter" — this is the missing third rung, and it reaches the opposite verdict.)
THE LAW. schedule_block traverses each bb backward (sched.c:3144, "we are traversing the instructions backwards"), so picked earlier = placed later. READY = every successor already scheduled (schedule_insn, :2557). rank_for_schedule (:2385) returns INSN_PRIORITY(y) − INSN_PRIORITY(x) first, so the class rule and the LUID tie-break never run across a priority difference. And priority() (:1425) is max(1, priority(pred) + insn_cost − 1) over LOG_LINKS — a C edit can only ever RAISE a priority, never lower one (sched.md §1.3). Therefore: a target that places an insn your draft's ready list ranked BELOW another one cannot be explained by any priority you can reach from C. The higher-priority candidate was absent from the ready list — it still owed a successor. Re-read the residual as an alias/dependence question, not as a scheduling tie.
⚠ BOUND — rule out the one other way a high-priority insn gets passed over. schedule_select (sched.c:2616-2650) works the ready list in equal-priority groups: it queue_insns every member of the top group that actual_hazard blocks, and if the whole group is queued it falls through to the next, lower-priority group. That is a lower-priority pick with the higher-priority insn present and ready. The dumps separate the two cases: ;; blocking insn N for K cycles at that tick ⇒ §165-45's memory-unit story, not this one; no such line ⇒ non-readiness.
THE READING, step by step (worked on func_801874C0, ov_SC03_014, 241 ins). At the step after sh <sp18.vx>,0x18($sp) the matching build picks addiu $a0,$sp,0x10 (priority 1) over sh 0xA($s0) (priority 2 — the lhu 0xDE load edge into the subu costs 2). Backward ⇒ the addiu is placed last, and in the matching object it is (w_pA: sh 0xA → lhu 0x6 → addiu $a0,$sp,0x10); in the 8-mismatch draft it sits four insns earlier (w_v6). sh 0xA($s0) can only have been unready if something it must precede was still unscheduled, and the only insn between them is lhu 0x6($s0) ⇒ the original carried a store→load edge at 0xA/0x6 that our alias oracle refuses ⇒ §16Z-vol.
⚠ THE INSTRUMENT IS NOT NEW — do not re-introduce it. §49's Method note (cc1 -dS -dR → the .sched/.sched2 traces: "the ready lists, the computed priorities, and the chosen order"), sched.md §3 Dump tells and the cookbook at L12914 already name the ;; ready list at T-N lines, the INSN_PRIORITY column, (7f000001) = birthing boost, ;; blocking insn N for K cycles and ;; insn N has a greater potential hazard. What is new is the inference across a priority difference, and the verdict flip it produces: sched.md §4 currently sends the unequal-priority case to the permuter as intrinsic.
(SHARPENS — sharpens §49 (Method note), gcc-2.7.2-map/sched.md §4 recipe and §S3, §165-45, §25; evidence: byte-probed on one function (the w_v6/w_pA object pair), mechanism source-cited to the pinned sched.c; from func_801874C0)
(SHARPENS — sharpens §136g rule 15 (L8921-8925) — split an RMW into v = *p + 1; … *p = v; so its load RISES, §165-06 / §164-XX (L13829-13876) — (*p)++ vs *p += 1, the seven-spelling tab; evidence: byte-probed; from func_801874C0)
§167-29 — A MEMORY RMW WHOSE RESULT IS A CALL ARGUMENT WANTS THE HYBRID: RE-READ THE FIELD AT EVERY USE SITE, AND KEEP THE RESULT IN A LOCAL. (sharpens §136g rule 15, which splits an RMW into v = *p + 1; … *p = v; precisely to make its load RISE — this is the same dial at the opposite polarity, for a target whose load sits mid-block; and §165-06/§164-XX, whose seven-spelling table is about the STORE's SET_SRC and never about where the LOAD lands. §49 supplies the argument half. The drafter's citation of §162b/§76 is wrong — those are scope→allocno/combine levers and do not reach load placement.)
Target shape — ONE narrow load mid-block, an if/else that bumps the field by two different constants, and the result passed to a call:
lh $v0,0xDC($s0) <- one load, MID-block, not at its head
slti … ; beqz … ; nop <- the compare's delay slot is a real nop
addiu $v0,$v0,0x30 / 0x50
sll $a0,$v0,16 ; sra $a0,$a0,16 ; jal … ; sh $v0,0xDC($s0)
THE LAW, as a three-cell decision (all measured on func_801874C0, ov_SC03_014, pinned triple):
| spelling | result |
|---|---|
hoist to a block-head local — s16 ang = *(s16*)(a0+0xDC); if (ang<0x400) ang+=0x30; else ang+=0x50; |
240 ins, 201 mismatched. The lh now has no LOG_LINKS predecessor ⇒ priority() = 1 ⇒ under backward traversal it is picked LAST and placed FIRST; the sw 0xC then falls into the beqz delay slot the target leaves nop, and the whole block cascades. |
fully in memory — *(s16*)(a0+0xDC) += 0x30; + f(*(s16*)(a0+0xDC)) |
241 ins, 12. The load lands correctly, but the argument becomes sh; lh (store then re-load) where the target has sll; sra off the register — and the sh takes the jal delay slot. |
| HYBRID — read the field in the condition and in each arm, assign the sum to a local, store the local | 241 ins, 8. cse commons the three reads into the one lh at the FIRST read site (= the target's position), and the local keeps the argument a register sign-extension. |
DIAGNOSTIC TELL — two symptoms that pull in opposite directions.
- (a) A
nopin a compare's delay slot that your draft fills, with a single narrow load at the block head that the target has mid-block ⇒ you hoisted the RMW's load into a local. Delete the local; re-read the field at each use site. - (b)
shin ajaldelay slot followed by anlhof the same field, where the target hassll; sra⇒ you left the result in memory. Bind it to a local (§49; at the lvalue's own width, §165-06).
This shape needs BOTH fixes at once, which is exactly why the single-axis sweeps stall: §136g-15's hoist alone gives you (a), §165-06's operator table alone gives you (b).
(SHARPENS — sharpens §136g rule 15, §165-06/§164-XX, §49; evidence: byte-probed, three measured cells on one function (variants under .run/wave4/func_801874C0/); the 201/12 cells were not re-run at vet time; from func_801874C0)
(SHARPENS — sharpens L1808-1815 register-resident short truthiness bullet (sll 16 + branch; contrasts s16 vs int ONLY — no u16 arm), §164-30 (L12507) sll $aN,$aN,16 in place at a truthine; evidence: byte-probed; from func_80187F2C)
§167-30 — Field a0+0xFE must be declared s16, not u16, even though the machine load is lhu: MIPS LOAD_EXTEND_OP==ZERO_
§16?-NN — THE MEMORY-FIELD SIGNEDNESS ARM: --field == 0 EMITS sll 16 FOR s16 AND andi 0xffff FOR u16, AND THE MISS ALSO PERMUTES THE LOAD SCHEDULE. (sharpens §165-11 / §136 type-form rule 12 (L8904), which state the signed/unsigned narrowing pair only on the RETURN axis; sharpens L1808's register-resident-short truthiness bullet, which contrasts s16 against int and has no u16 arm; sharpens §164-30, which owns the lone-sll 16 shape for a PARAMETER; and BOUNDS §164z's func_80180128 refutation — 'the byte gate is BLIND to the signedness of that field' — by naming the precondition under which it is NOT blind.)
Target shape — an in-place decrement of a narrow field, tested against zero:
lw $v1,0xC($s0)
lhu $a0,0xFE($s0) <- `lhu` EITHER WAY (§136g: LOAD_EXTEND_OP==ZERO_EXTEND, mips.h:1163)
addu $v1,$v1,$v0
addiu $a0,$a0,-1
sh $a0,0xFE($s0)
sll $a0,$a0,0x10 <- the ONLY signedness carrier. NO `sra`.
bnez $a0,…
THE LAW. The load opcode carries zero signedness information for a HImode field — lhu is emitted for s16 and u16 alike. The whole declaration is readable at the surviving HImode→SImode use, and for a zero-equality test it is one instruction either way:
*(s16 *)(p+K) -> sll $r,$r,16 (`sra` deleted: `simplify_comparison` case ASHIFTRT, combine.c:7422-7433 —
"If this is an equality comparison with zero, we can do this as a logical shift")
*(u16 *)(p+K) -> andi $r,$r,0xffff (the high bits are load-bearing; nothing collapses it)
And the miss is not opcode-only. The u16 spelling also hoists the lhu above the neighbouring lw and swaps the two addus with it — ~5 mismatched instructions at LENGTH-DRIFT 0, which reads like a §135-4/§49 scheduling residual and is not one.
BYTE EVIDENCE (func_80187F2C, ov_SC03_118, 56 ins, banked at src/ov_SC03_118/ov_SC03_118_jr_801863CC.c:3937). Target read off the SHA1-green object, mipsel-linux-gnu-objdump -d build/ov_SC03_118/ov_SC03_118.elf @ 0x80187F80-0x80187F98. Controlled A/B on the pinned triple, the two files differing in exactly one character sequence (.run/wave4/func_80187F2C/experiments/v4.c = --*(u16 *)(a0+0xFE), v5.c = --*(s16 *)(a0+0xFE)), 110 cc1 lines each:
v5 (s16, = target): lw $3,12($16) ; lhu $4,254($16) ; addu $3,$3,$2 ; addu $4,$4,-1 ; sh $4,254($16) ; sll $4,$4,16
v4 (u16): lhu $4,254($16) ; lw $3,12($16) ; addu $4,$4,-1 ; addu $3,$3,$2 ; sh $4,254($16) ; andi $4,$4,0xffff
DIAGNOSTIC TELL. lhu ; addiu -1 ; sh ; sll 16 ; beqz/bnez on a FIELD ⇒ write the lvalue *(s16 *). The same run ending andi $x,$x,0xffff ⇒ *(u16 *). Do not read the lhu — it is the same in both. Bound: the missing sra is the equality-with-zero rule; a target that shows sll 16 ; sra 16 before a signed relational compare is the same s16 field with a non-equality test (§164-37/§163b), not a different width. And §164z's blindness result still holds for the shape it was measured on — a field that is only added to and stored back with sh has no surviving HImode→SImode use, so no spelling is observable.
(SHARPENS — sharpens §165-11 (L13956), §136 rule 12 (L8904), L1808, §164-30 (L12507); bounds §164z func_80180128 (L13680); evidence: byte-probed by the vet (objdump + pinned cpp|cc1 A/B); from func_80187F2C)
(SHARPENS — sharpens §164-56 (L13067-13086) — THE LAW's 'the subtract and compare happen in the OPERAND'S OWN MODE, so a short operand emits andi 0xffff + sltiu and an int operand emi; evidence: byte-probed; from func_80188348)
§167-31 — THE range_test MASK IS A FUNCTION OF THE BIAS SIGN, NOT OF THE OPERAND'S WIDTH. A short OPERAND WITH LO ≥ 0 FOLDS TO A BARE addu −LO ; sltu HI−LO, AND §164-56'S "COUNT THE andi 0xffff" TELL NEVER FIRES.
Target shape — a short counter that is BOTH range-tested and passed as a call argument:
lh $v1,0x70($s0) <- SIGNED load
slt $v0,$v1,8 ; bne $v0,$0,.Lskip
slt $v0,$v1,14 ; beq $v0,$0,.Lskip <- two independent signed `slt`; no bias, no mask
Yours, from if (cnt >= 8 && cnt < 14):
lhu $a1,0x70($s0) <- load flipped to UNSIGNED
addu $v0,$a1,-8 ; sltu $v0,$v0,6 <- the fold; NO `andi 0xffff` anywhere
beq $v0,$0,.L ; sll $a1,$a1,16
li $a0,0x1EA ; sra $a1,$a1,16 <- the sign-extension REMATERIALISED for the call arg
THE LAW (two halves; the first bounds §164-56).
- The mask is conditional on the sign of
LO, not on the operand's mode.range_testdoes rewriteVAR >= LO && VAR < HIto(unsigned TYPEOF(VAR))(VAR − LO) < (HI − LO)in the operand's own mode (§164-56) — but combine then DELETES the HImode truncation whenever it cannot change the answer. WithLO < 0the bias is a positiveaddu:x + |LO|carries above 0xFFFF forxnear 0xFFFF and the 32-bit result disagrees with the HImode one on the in-range side ⇒ theandi 0xffffis load-bearing and survives. WithLO ≥ 0the bias is a negativeaddu: everyx < LOwraps to a huge unsigned that is still ≥HI − LO⇒ the truncation is provably redundant and is removed. Ashortoperand with a non-negative low bound therefore emits exactly the same two instructions as anintoperand. - On a
shortoperand the fold is visible in the LOAD and in a rematerialised extension instead. The fold wants the value zero-extended, solhbecomeslhu; any OTHER use of the same value that needs it signed (a call argument, a narrow store) then pays a freshsll 16 ; sra 16pair. That pair, not theandi, is the tell in this family, and it costs +1 instruction over the split form.
BYTE EVIDENCE — six-way isolated A/B on the pinned triple (-quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float), same body both sides:
| operand | bounds | spelling | compare region emitted |
|---|---|---|---|
short local |
>= 8 && < 14 |
&& |
lhu ; addu −8 ; sltu 6 — no mask, 21 ins |
short local |
>= 8 && < 14 |
nested ifs |
lh ; slt 8 ; slt 14 — the target, 20 ins |
int local |
>= 8 && < 14 |
&& |
lh ; addu −8 ; sltu 6 — no mask, 19 ins |
short local |
>= −0x101 && < 0xF2 |
&& |
lhu ; addu 257 ; **andi 0xffff** ; sltu 499 |
short local |
< −0x101 || >= 0xF2 |
|| |
identical + bne — §164-56's own shape |
int local |
>= −0x101 && < 0xF2 |
&& |
lh ; addu 257 ; sltu 499 — no mask |
The mask tracks the bias sign in every row; the declared width tracks only lh vs lhu. §164-56's evidence (func_80185EF8, biases +0x101 / +0x4AA) sits entirely in the LO < 0 half — that is why it read the mask as the mode's signature.
⚠ THE SPLIT FORM DOES NOT DECLARE THE WIDTH. short cnt and int cnt compile byte-identically once the chain is split (20 ins, same instructions). The lh/lhu distinction exists ONLY in the folded form; do not read the target's lh as evidence for an s16 local here.
THE DIAGNOSTIC TELL — use this one when LO ≥ 0; §164-56's does not apply. You are LENGTH-DRIFT +1, the target has two slt/slti on one register, and you have one addu −K ; sltu M. There is no andi to count. Confirm on the LOAD: target lh, yours lhu, and yours grows an sll 16 ; sra 16 pair on that same register straddling the branch. Fix is §164-56's — nested ifs (§165-22's lever; reach for §164-19(b)'s goto ladder only if a 0/1 flag with a body is involved).
⚠ DO NOT CONFUSE WITH §164-37 / §163b. Both shapes contain addu −K, sll 16 ; sra 16 and an slt* on one register. Position discriminates them absolutely: the switch dispatch is addiu −MINVAL ; sll 16 ; sra 16 ; sltiu — extension between the subtract and the compare, one value with one use. The range_test residual is addu −LO ; sltu HI−LO ; … sll 16 ; sra 16 — extension after the compare, because it belongs to a DIFFERENT use of the value. sll/sra after the sltu ⇒ this section, and no switch is involved.
Provenance: func_80188348 (ov_SC03_118, 52 ins, S48 wave-6 confirmed, draft .run/wave6/func_80188348/func_80188348.c:21-29, sha1 8effd252f70f5d475300df0839e6db4b55e3e388) supplied the shape; the six-row A/B was run at vetting time on the pinned cc1 and is reproducible from that body. §164-56's core law is unchanged — only its mask rule and its tell are bounded.
(SHARPENS — sharpens §164-02 (L11888) — states the precondition as 'every register loaded with la sym' but never says what happens one add downstream, §164-01 (L11871) — the pointer_int_sum; evidence: byte-probed; from func_8018C3F8)
§167-32 — qty_const DECAYS AFTER ONE ADD: A SYMBOL ADDRESS THAT ALREADY CARRIES A RUNTIME TERM IS AN ORDINARY REGISTER, AND FIX A1 GOVERNS IT. (BOUNDS §164-02; supplies the discriminator against §164-01. Retires the 'a spill/reload kills the constant equivalence' story offered from func_8018C3F8 — see below.)
Target shape — two addus off the same symbol in one function, with opposite operand orders:
lhu $2, D_800B9A02($2) ; runtime index
la $3, D_800A6610 ; BARE symbol -> CONSTANT_P
sll $2, $2, 14
addu $2, $2, $3 <- INDEX first (cse swapped it; §164-02)
sw $2, 0x30($sp)
...
lw $10, 0x30($sp) ; same value, ONE add downstream
sll $17, $17, 2
addu $17, $10, $17 <- BASE first (no swap; plain source order)
THE LAW. §164-02's precondition is qty_const, and insert records it only from a class member whose elt->is_const is true — CONSTANT_P (x) || (RTX_UNCHANGING_P (x) && REG && REGNO >= FIRST_PSEUDO_REGISTER) || FIXED_BASE_PLUS_P (x) (cse.c:1313-1319; FIXED_BASE_PLUS_P, cse.c:575-586, is frame/arg-pointer only). A pseudo set from (plus (reg <la sym>) (reg <index>)) has a class holding exactly that PLUS — none of the three. The second arm of insert (cse.c:1383-1397) scans the class for p->is_const && GET_CODE (p->exp) != REG, finds nothing, and leaves qty_const unset; equiv_constant (cse.c:5699-5710) then returns 0 and the commutative swap at cse.c:5282-5289 never fires. The equivalence dies at the first add. p = &SYM[i]; makes p an ordinary register, and the next p + x is §10-Residual-A territory: write the operand you want in rs FIRST.
IT IS NOT THE SPILL — and it can never be. cse_main runs at toplev.c:2865 (cse1) and :2926 (cse2); reload runs inside global_alloc at :3080, with reload_completed = 1 at :3096. grep -rn reload_cse_regs tools/reference/gcc-2.7.2/ ⇒ 0 hits — 2.7.2 has no post-reload cse. Operand order is frozen before a stack slot exists, so no reload-time observation (lw 0x30($sp), register pressure, 9 busy callee-saveds) can ever explain a cse-time operand swap. If your story about an addu's operand order mentions a spill, the story is wrong.
BYTE EVIDENCE — one function, one symbol, both outcomes. func_8018C3F8 (ov_SC06_018, 236 ins, MATCH, banked; src/ov_SC06_018/ov_SC06_018_jr_80187AEC.c:4854). In the shipped build/ov_SC06_018/ov_SC06_018.elf:
8018c464: addu v0,v0,v1— v0 =sllindex, v1 =la D_800A6610. Index first, although the C at:4877is written pointer-first (&D_800A6610[(*(u16*)&D_800B9A02) << 14], whichpointer_int_sumalso canonicalises pointer-first per §164-01). Both source-level orderings lose: this is §164-02 firing, in the shipped bytes.8018c554: addu s1,t2,s1— t2 =lw 0x30($sp)(that same value), s1 =z << 2. Base first, matching the C at:4897,(s32) ot + (z << 2). The drafter's A/B is the dial:(z << 2) + (s32) otgaveaddu $s1,$s1,$t2, the one and only mismatch in a 236/236 body; swapping the addends made it MATCH. Two builds, one character.- Sibling control, same TU:
func_8018F694(:6776) spells the second add the OTHER way,(z << 2) + (s32) ot(:6810), and gets8018f7f0: addu s0,s0,a3— index first — while itsotis stack-resident too (8018f7d8: lw a3,0x38($sp)). Both functions spilled, both obeying source order, opposite results. Spilling is not the variable.
THE DIAGNOSTIC TELL. Before reaching for §164-02's self-set asm launder, ask how the base got into the register, not where it currently lives:
la SYMmaterialised in the same expression ⇒ §164-02. Source order and pins are inert; the__asm__("" : "=r"(e) : "0"(e))launder is the only lever.- A pointer local assigned a symbol-plus-runtime-index earlier ⇒ ordinary register. Try Fix A1 first — it is one character and costs one build.
- And spell that second add in
s32, not pointer, arithmetic.(s32) base + offkeeps it a plain binop where source order reaches RTL;ptr + offfunnels throughpointer_int_sumand §164-01 pins the pointer to operand 0 before RTL, making Fix A1 inert for a second, unrelated reason.
(SHARPENS — bounds §164-02, discriminates against §164-01; evidence: byte-probed in the shipped object, 2 functions + a 2-build A/B; from func_8018C3F8)
(SHARPENS — sharpens §164-08 (L12007) — the ASCENDING N-site form: &SYM[k*STRIDE] off one array symbol, tells LENGTH-DRIFT/+2 (N symbols) and -5 (walked pointer), §136 rule 5 (L8872) — ; evidence: byte-probed; from func_8018D654)
§167-33 — use_related_value ALSO RUNS DESCENDING, AND AT TWO SITES THE MISTAKE IS LENGTH-NEUTRAL: §164-08's LENGTH-DRIFT TELL CANNOT FIRE. (BOUNDS §164-08 (L12007), whose law, both refutations and both tells are stated for N ASCENDING sites off one array symbol; and resolves its "Tension to know about" §136-5 note in the OPPOSITE direction — at two offsets the pointer local is REQUIRED, not refuted.)
Target shape — ONE %hi/%lo pair built for the FAR offset, a zero-displacement load off it, and the plain &SYM later reached by a single NEGATIVE bump:
8018d698 lui $s2,%hi(D_80126B64) ; = D_80126B58 + 0xC
8018d69c addiu $s2,$s2,%lo(D_80126B64)
8018d6a4 lw $a0,0x0($s2) <- the far read: THREE instructions
...
8018d738 addiu $a0,$s2,-0xC <- &D_80126B58: ONE instruction
THE LAW. use_related_value (cse.c:1781, called at :6535) is symmetric in the sign of the offset: with SYM+K already live in a pseudo, cse rewrites a later plain SYM as reg − K. Two preconditions, and both invert §164-08's ascending case:
(a) the FAR offset must go through a POINTER LOCAL so the full address lands in a pseudo (§136-5, L8872). Spelled as a bare read of the higher symbol it is (mem (const (plus SYM K))), which this MIPS port accepts as a legal address and folds to lui/%lo — the register never exists and cse has nothing to relate. (§162l, L11482, is the same fold read from the MEM side.)
(b) the near address must be spelled as the same symbol (&SYM), or the related_value chain does not link.
WHY IT IS INVISIBLE — the drift cancels at N=2. Spelling the far offset as its own extern (extern s32 D_80126B64; — exactly what splat's symbol map hands you) costs −1 on the read (lui/lw %lo instead of lui/addiu/lw 0(reg)) and +1 on the address (lui/addiu instead of one addiu). Net 0. §164-08's tells — +2 for N separate symbols, −5 for a walked pointer — are counts over N≥3 ascending sites; at two sites, one read and one address, they sum to zero. The gate reports the right length with a scattered OPCODE-MIXED residual and nothing that names the mistake.
BYTE EVIDENCE. func_8018D654 (ov_SC06_018 / _jr_80187AEC, 135 ins, banked MATCH 135/135; src/ov_SC06_018/ov_SC06_018_jr_80187AEC.c:5411). The target's own bytes settle it without an A/B — 8018d698-8018d6a4 spends three instructions reading D_80126B64, 8018d738 spends one on &D_80126B58; the bare-extern spelling cannot produce the first.
DIAGNOSTIC TELL. A bare addiu $rD,$rS,-K feeding a call argument or an address, where $rS was built by a lui/addiu pair of a symbol exactly K bytes HIGHER and is read at displacement 0 ⇒ ONE symbol, the far offset in a pointer local, the near one as &SYM. Do not read the length — this class has none. Read the address-instruction COUNT instead: a symbol whose read costs three instructions lives in a pointer local; two means it is a bare global.
Companion, already law — do not re-derive: each cse REGION needs its OWN pointer local. The target rematerialises the same base at 8018d77c (lui/addiu $a1) because .L8018D77C carries four jump refs and starts a fresh cse table (§164-52, L12943); one shared C variable would stay live in $s2 across the join. That is §44-Lever-3 (L3263) / §76 / §136-1 / §162b1's split-vs-share law applied to an address.
(SHARPENS — bounds §164-08; evidence: byte-probed (banked MATCH + target read); from func_8018D654.)
Honest scope: the isolated single-variable A/B on the spelling was never run — the only bare-extern draft (.run/wave6/func_8018D654/v1.c) also carried the wrong struct layout, so its "135 ins, OPCODE-MIXED" number is confounded. The length-neutrality above is arithmetic plus the target read, not a gated pair. Two minutes to close it: respell the banked body's two lim locals as extern s32 D_80126B64; and gate.
(SHARPENS — sharpens §49 (L3527) — THE LUID DIAL: materialise a temp to shift an insn's expand-stream position and split a rank_for_schedule tie (stated for a sll/sra sign-extension pair,; evidence: byte-probed; from func_8018D654)
§167-34 — A NAMED TEMP IS A LOAD-ORDER DIAL: it moves the load out of the expression and ABOVE the operand it was subtracted from — and NO statement permutation substitutes. (sharpens §49 (L3527, the LUID dial — a temp shifts an insn's expand-stream position; stated for a sll/sra pair moving EARLIER) and §165-14 (L14021, "source order IS the schedule" for three mutually-independent STATEMENTS); bounds §164-43 (L12771), whose law is that splitting an expression into named temps changes the pre-reload stream "with zero change to the final instruction stream" — the COUNT is unchanged, the ORDER is not.)
Target shape — a block whose leading load group is a pure PERMUTATION of yours, same registers, same count:
8018d6e8 lh $a0,0x6($s1) <- self->f6
8018d6ec lui $v0,%hi(D_80126B62)
8018d6f0 lhu $v0,%lo(D_80126B62)($v0)
8018d6f4 lui $v1,%hi(D_80126B5E)
8018d6f8 lh $v1,%lo(D_80126B5E)($v1)
THE LAW. The loads are mutually independent, so rank_for_schedule ties on priority and on every dependence class and falls through to INSN_LUID (tmp) - INSN_LUID (tmp2) (sched.c:2425-2428 — the fallback §165-14 cites). LUID is expand-stream position, and a global's load is expanded at its FIRST source mention. Hoisting the global into a temp of its own moves that mention from inside the expression to a statement ABOVE it, carrying the load above the operand it was going to be subtracted from:
bx = *(s16*)&D_80126B5E; i = self->f6 - bx;
RTL lh B5E, lh f6, lhu B62 -> sched1 B5E, B62, f6 WRONG
i = self->f6 - *(s16*)&D_80126B5E; v[0] = *(s16*)&D_80126B5E;
RTL lh f6, lh B5E, lhu B62 -> sched1 f6, B62, B5E TARGET
Both emit the same 135 instructions in the same registers. Inlining also merges the two reads into the single lh the target has — cse commons them.
THE NEGATIVE CONTROL IS THE POINT. With the temp present, every legal statement order was gated: 240 permutations of the six statements (.run/wave6/func_8018D654/perm/res.txt), floor closeness 3 (BI1320, BI3120), never 0; 144 more on a second source shape (perm2/, all 137 ins) and 72 on a third (perm3/). Statement order cannot reach it, because the temp's assignment must precede its use — the dependence you are trying to break is the one the temp created.
DIAGNOSTIC TELL. The residual is confined to the first N instructions of one block, is a pure PERMUTATION (same opcodes, same registers, nins exact), and does not move under statement reordering ⇒ delete the temp and inline the global into the expression (or, in the other direction, hoist it into a temp to move its load UP). Do not open the permuter, a pin, an asm barrier or §47/§158's allocno sliders: those steer a schedule TIE, and this is upstream of the tie, in the RTL you handed sched1.
BYTE EVIDENCE. func_8018D654 (ov_SC06_018, 135 ins, banked MATCH; src/ov_SC06_018/ov_SC06_018_jr_80187AEC.c:5411). Ladder 116 → 11 → 5 → 3 → 2 → 0; the 3 → 0 step is exactly the temp deletion.
(SHARPENS — sharpens §49, §165-14; bounds §164-43's "zero change to the final instruction stream"; evidence: byte-probed, 456 gated variants + the banked MATCH, one function; from func_8018D654.)
⚠ Wording corrected at vet time: the source note says "the post-reload schedule preserves the group order it receives", but sched1 itself permutes — RTL f6,B5E,B62 comes out f6,B62,B5E. The sound statement is that the RTL order SEEDS the LUID tie-break, not that a pass preserves it. The -fno-schedule-insns2 dumps quoted verify sched1's output only.
(SHARPENS — sharpens §165-04 (L14622-14638), §165-02, §165z; evidence: single-instance; from func_8017C294)
§167-35 — §165-04 ('a zero-emission =r/0 re-tie on a memory-loaded s16 buys +8 bytes of frame per site') does NOT replic
⚠ §165-04 CONTESTED BY TWO LATER PROBES ON ITS OWN EXEMPLAR (P30 S48, waves 5 and 6). §165-04's entire byte record is one A/B on func_8017C294's pointer-walk body (vars 240 → 256 for two re-tie sites on memory-loaded s16s). Two independent agents re-ran the instrument on that same function later the same day and measured the opposite sign: wave 5 got vars 240 → 232 → 224 with instructions added (.run/wave4/func_8017C294/var/r_a*.c, var/z3_retie.c), and wave 6 independently reproduced the failure across 16 self-cast sites (.run/wave6/func_8017C294/var/). Neither ran §165-04's own prescribed 4-line reproducer, so the entry is contested, not refuted — but do not cite '+8 bytes of frame per re-tie site' as a dial, and do not spend a wave steering with it. The two laws it leans on are unaffected and both still stand: §165-02's memory precondition, and §165-YY's post-life_analysis deletion condition — which predicts that whether the re-tie buys a slot depends on which pass ends up deleting the widening, and therefore that its sign is body-dependent. Retest is still the 4-line reproducer, ~2 minutes; do that before either banking or retiring it.
(SHARPENS — sharpens §165-36 rule 1 (L14639-14646), §160g, §71; evidence: asserted; from func_8017C294)
§167-36 — WAVE STEP 0, PART 3: INVENTORY YOUR OWN OUTPUT DIR BEFORE THE FIRST WRITE. (§165-36 rule 1 sends you to find .run for drafts in OTHER waves' trees; the collision that actually destroys work is inside the directory the harness just handed you. PROCESS, not a compiler law.)
.run/waveN/<fn>/ is not empty just because this wave is new — the harness re-uses the per-function path, so an earlier wave's entire variant tree, including its best drafts, can already be sitting in it. On func_8017C294 the wave-5 agent opened by overwriting the top-level func_8017C294.c with the wave-3 draft and only found the pre-existing wave-4 tree's three 2-diff drafts in fin/ by accident at the end of the run; the prior_notes.json it was handed described the wave-3 11-diff state and never mentioned them.
THE RULE. Before writing anything: ls -la the assigned dir, and match_one-batch every pre-existing .c in it (they are cheap and local — §12/§19). Never overwrite the top-level <fn>.c; write your first draft under a new name. The handed-down notes are a summary of ONE earlier wave, not an inventory — the directory is the inventory.
Companion. §165-36 rule 2 still applies to whatever you find: carry forward its reasoning, never its coordinates.
(SHARPENS — sharpens §21 wave-distilled idioms, L1872-1881 — 'store a call result AND test/reuse it in one expression → combined assignment (T)(p+k) = local = f();' (evidence func_80142DC4,; evidence: single-instance; from func_80180450)
§167-37 — WHEN A CALL RESULT CROSSES ZERO CALLS AND ITS ONLY NON-STORE USE IS THE NEXT CALL'S SOLE ARGUMENT, NAME NOTHING: STORE IT TO THE FIELD AND RE-READ THE FIELD TWICE. (BOUNDS §21's *(T*)(p+k) = local = f(); bullet (L1872-1881) — its own exemplar func_80142DC4 has the result surviving a LATER call, the precondition it never states, and following it here costs +1. Adds a FOURTH row to §164-20's delay-slot discriminator table, which routes "copy missing from a jal delay slot" to §43 alone. Runs §164-82's re-read lever in the OPPOSITE SIGN. The MECHANISM is not new — it is §48-A4's ARG-register copy preference plus §50-D; cite those, do not re-derive.)
Target shape — one call's result stored, null-guarded, and handed straight to the next call:
jal func_8012C1B8
addu $s0,$a0,$zero
bnez $v0,.L288
sw $v0,0x20($s0) <- the STORE is in the GUARD's delay slot
…
.L288: lui $a1,%hi(D_SYM) ; addiu $a1,$a1,%lo(D_SYM) jal func_8001C214 addu $a0,$v0,$zero <- the ARG copy is in the CALL's OWN delay slot
THE LAW. $v0 reaching the sw, the bnez and the second call's $a0 with no intervening copy means the source named nothing. Write it with no local at all —
*(s32 *)(p + 0x20) = f();
if (*(s32 *)(p + 0x20) == 0) { g(p); }
else { h(*(s32 *)(p + 0x20), (s32)D_SYM); … }
— cse commons both re-reads onto the call's own pseudo, so each site is a bare register use, and dbr gets both slots (the store into the bnez's, the argument copy into the jal's). Bind the result to a local instead and that pseudo becomes a cross-block global allocno whose one copy use is (set (reg $a0) P): find_reg checks copy preferences FIRST (§50-D, global.c:1000-1030), and because the pseudo is defined after one call and dead before the next it crosses zero calls, so allocno_calls_crossed > 0 never fires to strip the caller-saved prefs (§48-A4, global.c:906). It takes $a0 at its definition: the copy collapses into a move $a0,$v0 above the guard, the guard tests $a0, and the jal loses its slot occupant. Measured +1 (63 vs 62).
⚠ THIS IS THE PRECONDITION §21's BULLET NEVER STATES. §21 (L1872) prescribes *(T*)(p+k) = local = f(); for this same asm shape, and its evidence func_80142DC4 is the same callee (func_8012C1B8) at the same offset (0x20) — but there the success arm reuses the value across later calls, which forces it callee-saved (addu $sX,$v0,$zero) and is exactly why naming it is free. Read the arm before choosing: a later jal between the guard and the last use ⇒ §21's named combined assignment. No later jal ⇒ this entry, name nothing.
AND NOTE THE SIGN INVERSION vs §164-20 / §164-82. There the re-read buys an instruction (a copy that survives into a branch delay slot, curing a -1) and the guarded value is a plain field LOAD. Here the same source edit deletes one (curing a +1) and the guarded value is a CALL RESULT already stored to memory. Same three-line shape, opposite direction — the discriminator is what defines the value.
BYTE EVIDENCE. func_80180450 (ov_SC06_008, 62 ins, banked, src/ov_SC06_008/ov_SC06_008_jr_8017C294.c:4576-4621; family ×7). Target read off the byte-identical sibling asm/ov_SC06_018/nonmatchings/ov_SC06_018_jr_8017C24C/func_8018025C.s — 62 ins, sw $v0,0x20($s0) in the bnez slot (80180270/80180274), addu $a0,$v0,$zero in the jal func_8001C214 slot (80180290/80180294). Named-local spelling: 63 ins, with move $a0,$v0 above the guard.
THE DIAGNOSTIC TELL — the fourth row of §164-20's table. An argument copy missing from a jal's own delay slot, LENGTH-DRIFT +1, and that same move $aN,$v0 sitting above the guard branch with the guard testing $aN instead of $v0 ⇒ you named the call result. Not §43 (that row is a narrow prototyped s16 param and shows no +1); not §164-20 (that row is a branch slot and a -1); not a pin (§164-22 — there is no copy insn to place). Delete the local: store and re-read.
Scope, honestly: n = 1, one A/B, and the decisive control is missing. The note reports the losing build only as "the named-local form was 63 ins" and never says whether that was the plain two-statement v = f(); *(p+K) = v; or §21's combined *(p+K) = v = f(); — and only the latter decides the bound on §21. The losing build was not re-measured at vet time. One probe settles it: compile *(s32 *)(p+0x20) = v = f(); if (v == 0) … else h(v, (s32)D_SYM); on this body and count. Until then treat the §21 bound as a PREDICTION and "name nothing" as the byte-proven half. Related caution: §165z byte-REFUTED a similarly-worded "unname the call result" claim on func_80181E18; that refutation does not reach this shape (no null-guard, no argument-register consumer), but do not widen this entry past its four preconditions — call result, stored to a field, null-guarded, sole remaining use = the very next call's only argument.
(SHARPENS — bounds §21 L1872, §164-20, §164-82; mechanism cited from §48-A4 / §50-D; evidence: single-instance; from func_80180450)
(SHARPENS — sharpens §163c (L11793-11800), §162a1 (L11030-11056), §162a2/§161a (L10968-10989), §165-28 (L14450); evidence: single-instance; from func_801810C8)
§167-38 — AN INTERIOR JTBL SLOT EQUAL TO THE DEFAULT/JOIN LABEL IS AN ABSENT CASE, NOT PROOF OF A WRITTEN case k:. (bounds §163c, whose "jtbl[k] == the default label is the fingerprint of a case label sharing the default body, and that empty label is LOAD-BEARING" reads as unconditional; and supplies the row §162a1's three-line edge table does not have — it rules on entry[0], on entry[N-1] and on "a real body", never on an interior slot.)
Target shape — a dense-looking 6-word table with one interior word pointing at the same address the range check's beqz targets:
801810E4 sltiu $v0,$v1,0x6
801810E8 beqz $v0,.L801812DC <- OUT-OF-RANGE goes to 801812DC
...
jtbl_801B2BD4 = [80181108, 80181114, 8018113C, 80181168, **801812DC**, 801812A0]
^ slot 4 == the default target
THE LAW. expand_end_case emits one word for EVERY value in [minval, maxval] and fills any value that has no case node with default_label. An interior slot pointing at the default is therefore the default, and carries no information about whether the source wrote a label there. The label §163c calls load-bearing is load-bearing only through its two side effects on the table's SHAPE: the live-case COUNT (vs case_values_threshold = 5 — §163c, §55a, §165-28) and minval/maxval (§162a1/§162a2). When the count already clears the threshold and the gap is strictly interior, the source can simply omit the case — and here it does.
BYTE EVIDENCE. func_801810C8 (ov_SC02_016, 209 ins, first-pass MATCH 209/209, closeness 0, zero iterations; draft .run/wave6/func_801810C8/func_801810C8.c, sha1 faaf37c3…; target asm/ov_SC02_016/nonmatchings/ov_SC02_016_jr_8017DC70/func_801810C8.s:11-18; table asm/ov_SC02_016/data/tail18.data.s:21-28). The banked switch writes case nodes {0,1,2,3,5} and no case 4: — 5 nodes, minval 0, maxval 5 ⇒ sltiu 6 + a 6-word table, first try. Slot 4 holds .L801812DC, which is both the join every arm js to and the target of the range check's own beqz.
DIAGNOSTIC TELL. Before adding an empty case k: on §163c's advice, ask two questions about k. Interior gap AND you already have ≥5 live case nodes ⇒ write nothing — the slot fills itself. Add the label only when (a) the node count is ≤4, where per §163c the extra label is the only thing that reaches a tablejump at all, or (b) k is an EDGE, where per §162a1/§162a2 the label moves minval/maxval and therefore the sltiu bound and the word count.
⚠ Bounds. The converse A/B was not run: case 4: break; was never compiled against this target, so this entry proves the OMISSION matches — not that the label is byte-inert. (Mechanically both spellings should land slot 4 on the same join, but that is inference, not a measurement.) One function.
(SHARPENS — sharpens §1-I3 (L54-58), §1-I4 (L60-65), §28a div-by-constant bullet (L2349), §164-04 / §1-I5 (L11934-11942); evidence: single-instance; from func_801810C8)
§167-39 — THE SIGNED DIV-BY-CONSTANT SHAPE IS mult ; sra 31 ; mfhi ; sra k ; subu, AND A BRANCH AGAINST THE RECONSTRUCTED PRODUCT IS x == x / K * K, NOT x % K == 0. (extends §1-I3, which gives only the UNSIGNED magic — lui 0xcccc ; ori 0xcccd ; multu ; mfhi ; srl 3; and promotes §28a's imported decomp.wiki bullet "0x66666667→/10" from a "worth trying" list to a byte-witnessed instruction shape with a C spelling.)
Target shape — six instructions of division, then the source's own multiply back, then an ordinary bne:
80181168 lui $v0,0x6666 ; ┐ the SIGNED magic for /10
8018116C lw $a0,0x1C($s0) ; │
80181170 ori $v0,$v0,0x6667 ; │ 0x66666667
80181174 mult $a0,$v0 ; │ `mult`, not `multu`
80181178 sra $v0,$a0,31 ; │ the DIVIDEND's sign word
8018117C mfhi $t0 ; │
80181180 sra $v1,$t0,2 ; │ shift 2, not §1-I3's 3
80181184 subu $v1,$v1,$v0 ; ┘ q = (hi>>2) - (x>>31)
80181188 sll $v0,$v1,2 ; ┐ synth_mult's ×10 — the SOURCE's, not the division's
8018118C addu $v0,$v0,$v1 ; │ ((q<<2)+q)<<1
80181190 sll $v0,$v0,1 ; ┘
80181194 bne $a0,$v0,.L801811B4 <- the ORIGINAL dividend against the product
THE LAW. Two halves. (1) For a signed operand gcc picks mult with the signed magic and corrects by SUBTRACTING the dividend's sign word (sra $x,31); that trailing subu of an sra 31 is the one-glance fingerprint separating the signed form from §1-I3's multu/srl unsigned form, and the mfhi shift differs too (2 here vs §1-I3's 3 for the same divisor). (2) Everything AFTER the subu is the source's own arithmetic. A sll/addu/sll chain reconstructing q*K followed by a branch comparing that product against the dividend is a literal transcription of x == x / K * K. Write it that way.
BYTE EVIDENCE. func_801810C8 (ov_SC02_016, 209 ins, first-pass MATCH 209/209, zero iterations; asm/ov_SC02_016/nonmatchings/ov_SC02_016_jr_8017DC70/func_801810C8.s:45-56; banked C if (*(s32 *)(a0 + 0x1C) == *(s32 *)(a0 + 0x1C) / 10 * 10), draft .run/wave6/func_801810C8/func_801810C8.c, sha1 faaf37c3…).
DIAGNOSTIC TELL. Magic multiply ⇒ reconstructed product ⇒ branch comparing the product to the dividend ⇒ write x == x / K * K and do not "clean up" Ghidra's spelling. A % would compute x − q*K and branch on zero: one extra subu and a compare against $zero instead of against $a0. Read the compare's operands before choosing the operator.
⚠ Bounds. The % half is a prediction — no x % 10 == 0 variant was compiled against this target, and the whole function landed first-pass, so nothing here was ablated. The signed-shape half is byte-witnessed once. §1-I4's divu/mflo/mfhi entry is a different regime (runtime divisor) and does not apply.
(SHARPENS — sharpens §164-04 / §1-I5 (L11934-11942), §1-I3 (L54); evidence: single-instance; from func_801810C8)
§167-40 — THE SIGNED /2^k BIAS LEAVES addiu's IMMEDIATE RANGE AT k = 16: LOOK FOR ori (2^k−1) ; addu, AND EXPECT THE sra k TWICE. (bounds §164-04/§1-I5, whose tell — "a bgez whose only job is to skip an addiu of 2^k − 1 immediately before an sra k" — structurally cannot fire for k ≥ 16, so an agent applying it as written mis-reads the block as a mask.)
Target shape — six instructions, no addiu, and the shift on both paths:
80181258 bgez $v1,.L8018126C
8018125C sra $v0,$v1,16 <- the UNBIASED shift, in the delay slot
80181260 ori $v0,$zero,0xFFFF <- the bias 2^16 − 1, MATERIALISED
80181264 addu $v1,$v1,$v0
80181268 sra $v0,$v1,16 <- the BIASED shift, second copy
.L8018126C: 8018126C negu $v0,$v0 <- the source's unary minus
THE LAW. gcc's round-toward-zero correction for signed x / 2^k adds 2^k − 1 on the negative path. addiu carries a signed 16-bit immediate, so that constant fits inside the instruction only for k ≤ 15 (2^15 − 1 = 32767). At k = 16 it is 65535, out of range, and must be materialised — ori $rD,$zero,0xFFFF ; addu — after which dbr fills the bgez's now-empty delay slot with the unbiased sra k, so the shift appears once in the slot and once on the biased fall-through. The block grows from §1-I5's 3 instructions to 5, and the addiu §1-I5 tells you to look for is absent. The C does not change: write the division literally.
BYTE EVIDENCE. func_801810C8 (ov_SC02_016, 209 ins, first-pass MATCH 209/209, zero iterations; asm/ov_SC02_016/nonmatchings/ov_SC02_016_jr_8017DC70/func_801810C8.s:108-114). Banked C: *(s16 *)(a0 + 0xFE) = -(D_801B42D0 / 0x10000); (.run/wave6/func_801810C8/func_801810C8.c, sha1 faaf37c3…), with extern s32 D_801B42D0;.
DIAGNOSTIC TELL. A bgez whose fall-through is ori $rD,$zero,M ; addu with M = 2^k − 1, plus an sra k on BOTH paths (one of them in the bgez's own delay slot) ⇒ a signed / 2^k with k ≥ 16. Do not read the ori 0xFFFF as a mask, do not respell the division as >> k (which loses the entire block — §1-I5), and do not reach for an unsigned divide (a bare srl, no branch). A leading negu on the result is the source's unary minus, not part of the division.
⚠ Bounds. k = 16 measured, on one function, first-pass (no ablation). k ≥ 17 is unmeasured — the bias would need lui+ori, so expect 6 instructions, not 5. The k ≤ 15 half is §1-I5's own byte evidence and is unchanged.
(SHARPENS — sharpens §165-43 (L14773), §88c (L6695), §162e (record_jump_equiv for a loop index), §164-52 (equivalence lifetime); evidence: single-instance; from func_80181948)
§167-41 — cse's TAKEN-EQUALITY CLASS REACHES A STORE'S SOURCE, NOT ONLY A COMPARE: sw $a0,%lo(sym)($at) inside an arm guarded by beq $a0,<1> is sym = 1;. (sharpens §165-43 (L14773), which states the same record_jump_equiv class only for a register-to-register bne and whose tell — "an ==/!= compare against a callee-saved register inside a guarded arm" — cannot fire on a store; and adds the ARGUMENT-register case, where the false reading is the incoming PARAMETER rather than a local.)
Target shape — a case arm reached by an equality test on the switch value:
beq $a0,$v0,.Lcase1 <- $v0 = 1; on this edge cse learns $a0 == 1
…
.Lcase1: …
sw $a0,%lo(D_801270C8)($at) <- reads as "store the parameter"; it is `= 1`
THE LAW. record_jump_cond (tools/reference/gcc-2.7.2/cse.c:5839, via record_jump_equiv :5791) merges the two operands of a taken EQ into ONE equivalence class for the rest of the path, and cse then emits the CHEAPEST member of that class at every use — a use being any operand, the compare §165-43 documents or the SET_SRC of a store. D_801270C8 = 1; therefore materialises no li at all and stores whichever register the dispatch already proved equal to 1. Per §164-52 the class survives until a label with >=2 jump references resets the table.
BYTE EVIDENCE. func_80181948 (ov_SC01_077, 132 ins, match_one MATCH; draft .run/wave6/func_80181948/func_80181948.c:42): case 1: opens if (*(s32*)(a0 + 0x1C) == 0x14) { D_801270C8 = 1; } and emits the target's sw $a0. (One sighting, one-sided: no counter-spelling was compiled.)
⚠ It is BYTE-NEUTRAL inside the arm — D_801270C8 = state; emits the same store, because cse proved them equal. What the reading buys is not writing D_801270C8 = param;, which $a0 invites and which does not match once the parameter is anywhere else.
THE DIAGNOSTIC TELL. A store whose source register is an argument register that this arm's own guard compared against a constant. Before transcribing it as a variable, ask what the guard proved on this path: §88c says a materialised constant proves nothing and §165-43 says its absence proves nothing — that now covers store sources, so let the guard, not the register name, decide.
(SHARPENS — sharpens §160d (L10936) — THE ASYMMETRIC INDEX RELOAD; the only store-then-reload entry, but its observable is an lbu/sll COUNT and its consumer is a table index, never a comp; evidence: single-instance; from func_80183834)
§167-42 — A SATURATING u16 DECREMENT-AND-TEST READS THE FIELD BACK, AND THE BRANCH'S DELAY SLOT IS THE TELL. (sharpens §160d, the only store-then-reload entry, whose observable is an lbu/sll COUNT for a table index and never a compare; and §164-20 / §164-82, whose re-read tells are both LOAD-guard-then-reload, the mirror construct. The ordering half is §164-29's register WAR fence, NOT §163d/§162j.)
Target shape — a timer field decremented and tested inside one arm:
lhu $v1,0x86($s0)
beqz $v1,.Lskip
…
addiu $v0,$v1,-1
sh $v0,0x86($s0)
andi $v0,$v0,0xFFFF <- the "reload", cse'd down to the STORED register, zero-extended IN PLACE
bnez $v0,.Lret
nop <- the slot stays EMPTY
THE LAW. Write the second test against MEMORY, not against the local:
t = *(u16 *)(p + 0x86);
if (t != 0) {
*(u16 *)(p + 0x86) = t - 1;
if (*(u16 *)(p + 0x86) != 0) { return; }
}
cse replaces the reload with the just-stored pseudo, and the HImode→SImode zero-extend the compare needs is done in place on that same register. The andi therefore re-SETs a register the sh READ, and sched_analyze_1 (sched.c:1714-1715) emits a hard REG_DEP_ANTI for exactly that (§164-29) — so the sh can never be moved below the andi, which is precisely what reorg would have to do to put it in the bnez's slot. Slot stays nop: 5 words. The local-variable form t = t - 1; *(u16 *)(p + 0x86) = t; if (t != 0) decrements in place (addiu $v1,$v1,-1), masks into an INDEPENDENT register (andi $v0,$v1,0xFFFF), frees the sh of the anti-dependence, and reorg steals it into the delay slot: 4 words, LENGTH-DRIFT −1.
THE DIAGNOSTIC TELL. On the SECOND branch of a decrement-and-test: a nop in the slot ⇒ the source re-read the field; a sh in the slot ⇒ the source tested a local. Read the slot before touching anything else — like §164-82's class this presents as a whole-tail shift, not as one missing instruction.
Byte evidence, and its limit. func_80183834 (ov_SC01_077, 135 ins) banks with the read-back form (src/ov_SC01_077/ov_SC01_077_jr_80183324.c:3393-3398), as does its family twin func_8018127C (src/ov_SC02_000/ov_SC02_000_jr_80180D6C.c:3044-3049); a corpus grep finds zero instances of the local-variable form anywhere in src/.
⚠ The counter-arm has no surviving artifact. Every preserved probe in .run/wave6/func_80183834/ (base.c, varA–varR) carries the read-back spelling, so the −1 is a transient measurement from the crack's front-half iteration and is not re-runnable. The mechanism above is a reconstruction — the crack note's own wording ("the sh is a live-def of the compare register") is wrong: the sh is a USE, and the edge is ANTI, not true. Re-run the A/B before leaning on the −1 number; the transcription rule and the slot tell are the durable halves.
(SHARPENS — sharpens §165-15 (L14033-14057), §163a (L11764-11780), §8d (L487-510), §17a-1 (L1445-1470); evidence: single-instance; from func_80183BC8)
§167-43 — THE BLOCK-SCOPE SOLVENT HAS A MOVABILITY PRECONDITION, AND A RETURN-TYPE-ONLY CALLEE CONFLICT WAS NEVER ITS JOB. (bounds §165-15's discriminator table (L14049-14057), whose conflicting types for 'X' row says "⇒ §163a's block-scope solvent is live (move BOTH the typedef and the extern into the block)" without stating the precondition; the cure that row should hand over is §17a-1 point 1, which already names this exact case.)
Target situation. Your body needs the callee's $v0 — v0 = f(a0); if (v0 != 0) … — while the destination TU already carries extern void f(s32 a0); at FILE scope, ABOVE your insertion point, feeding a function that is ALREADY BANKED.
THE LAW. §163a's solvent is a property of a PAIR of declarations: both at block scope warn, either at file scope is the hard-error cell (§8d byte-proved that direction: FILE(void *) -> BLOCK(int) -> conflicting types for 'D_801812A4' ERROR). You can only reach the solvent when you own BOTH decls. A file-scope callee extern that an already-banked function in the same TU compiles against is immovable — demoting it changes that function's declaration environment (§8d, the whole reason scope_data_externs.py exists) and buys an R22 blast radius for a one-function problem. When the only term that differs is the RETURN TYPE, do not redeclare at ANY scope: leave the TU's decl exactly as it stands and cast at the use site — v0 = ((s32 (*)(s32))f)(a0); — codegen-neutral, draft-only, no fleet edit. §17a-1 point 1 already states this as "Call-site casts, NOT redeclaration (the #1 recurring miss)" and names the form verbatim for a void-canonical callee whose $v0 is used.
BYTE EVIDENCE. func_80183BC8 (ov_SC02_041, 108 ins), banked whole-binary at src/ov_SC02_041/ov_SC02_041_jr_8017BEBC.c:6477. The TU declares extern void func_8012CBCC(s32 a0); at file scope :5732 — consumed two lines later by the banked func_80182484 through ((s32 (*)(void))func_8012CBCC)() — and repeats the identical spelling at :6470. The new body takes the return through v0 = ((s32 (*)(s32))func_8012CBCC)(a0); at :6509. The crack note's proposed fix (add extern s32 func_8012CBCC(s32 a0); at block scope inside the body, leaving :5732 in place) was never compiled and lands in §163a's file-scope cell. Cross-TU confirmation of the true arity/return: src/ov_SC02_011/ov_SC02_011_jr_8017AE2C.c:4933/4953.
THE DIAGNOSTIC TELL. match_one MATCHes standalone, the whole-binary build reports conflicting types for 'func_X', and diffing the two spellings term by term (§73's two axes) shows only the return type differs. Route it: return-only ⇒ use-site cast, zero blast radius (this entry). Params ⇒ §73's param row, cast at each use. Arity / too many arguments ⇒ §165-15, no scope helps. A decl you own at BOTH ends ⇒ §163a, move the pair into the block. Check who else compiles against the file-scope decl before you touch it — if the answer is a banked function, the solvent is off the table.
(SHARPENS — sharpens §43 (L3195), §164-48 (L12866), §163b (L11781), §164-30 (L12507); evidence: single-instance; from func_80184E4C)
§167-44 — THE sll $aN,$sX,16 ; sra $aN,$aN,16 PAIR IN THE SLOTS BEFORE A jal CAN BE A PARAMETER WIDTH TELL — §43's TRIAGE HAS NO BUCKET FOR IT. (SHARPENS §43 (L3195), whose tell is binary — in-place on $aN ⇒ s16 param, into a $v0/$v1 temp ⇒ cast-of-s32 — and §164-48 (L12866), which owns this exact shape but reads it for a LOCAL and forwards the PARAMETER reading to §163b, which is switch-dispatch only. The ANSI-vs-K&R half is already indexed (cookbook-index L29; §164-20's delay-slot discriminator, L12286); the SHAPE is not.)
Target shape — a narrow param whose live range crosses a jal, re-extended into the ARG register at the next call:
move $s1,$a2 ; move $s3,$a3 <- entry: the raw WORD is stashed, UNextended
jal <allocator>
beqz $s0,<bail>
sll $a2,$s1,0x10 <- the extension READS the callee-saved copy…
sll $a3,$s3,0x10
move $a0,$s0 ; move $a1,$s4
sra $a2,$a2,0x10 <- …and WRITES the arg register
jal <init>
sra $a3,$a3,0x10 <- second half rides the delay slot
THE LAW. This is neither of §43's two buckets. The parameter is HImode; its live range crosses a call, so the raw word goes to a callee-saved pseudo at entry; the widening is the tree-level conversion of that HImode value to the callee's s32 formal (convert_arguments → convert_for_assignment, c-typeck.c:1623/1737). Declare the enclosing function's parameter s16 in a plain ANSI prototype and pass it STRAIGHT THROUGH — f((u8 *)obj, a1, a2, a3) against extern void f(u8 *, s32, s32, s32); — and ordinary integer promotion emits the pair for free. No (s16) cast on top (and a cast on an s32 param cannot reach this shape, §43).
WHY THE PARAM STAYS NARROW INSIDE THE BODY (source-cited; §43 states this only as behaviour). config/mips/mips.h:1153 defines PROMOTE_PROTOTYPES but the port defines no PROMOTE_FUNCTION_ARGS, so assign_parms leaves promoted_mode = passed_mode (function.c:3324-3329) and an incoming short lives in HImode for the whole body — every SImode use pays its own extension, wherever it falls.
BYTE EVIDENCE. func_80184E4C (ov_SC03_014, 50 ins, banked whole-binary, S48 wave 6). src/ov_SC03_014/ov_SC03_014_jr_801848E4.c:2797 = s32 func_80184E4C(s32 a0, s32 a1, s16 a2, s16 a3, s32 a4); the shipped build/src/ov_SC03_014/ov_SC03_014_jr_801848E4.o 0x568-0x62c disassembles to the shape above (move s1,a2 @0x588, move s3,a3 @0x590, sll a2,s1,0x10 @0x5a8, sra a3,a3,0x10 in the jal func_8001CB6C delay slot @0x5c0). Both middle params flow untouched into func_8001CB6C's s32 formals; there is no cast in the body.
Scope, honestly: n=1 and NO A/B was run — the draft MATCHed first try, so the s32 twin was never compiled. The direction is not in doubt (dropping to s32 deletes both pairs by plain C semantics: LENGTH-DRIFT −4), and §164-48's one-character probe byte-proves the same width→extend-at-use coupling for a LOCAL. What is new here is the READING of the shape, not the coupling.
THE DIAGNOSTIC TELL. LENGTH-DRIFT −2 per narrow argument, the missing instructions a sll 16/sra 16 pair straddling the call's move $aN,… arg setup, on a function that copies raw $aN into $sX at entry. Ask whose value $sX holds before reaching for §164-48: a call result or a computed local ⇒ §164-48 (and expect an $s0/$s1 permutation with it); an incoming argument copied at entry with nothing done to it ⇒ this entry — widen nothing, narrow the DECLARATION. If the pair sits between a bias subtract and an sltiu, it is §164-37/§163b instead.
(SHARPENS — sharpens §20 scalar-global-RMW bullet (L1907-1918), §164-52 (L12943), §153 (L10463), §165-26 (L14399); evidence: single-instance; from func_80188F60)
§167-45 — A GLOBAL THAT IS STORED TO AND HAS ITS ADDRESS PASSED TO A CALL IN THE SAME BLOCK NEEDS A POINTER LOCAL — AND AN addiu $rD,$rB,-K OFF THAT BASE NAMES WHICH SYMBOL THE SOURCE ANCHORED ON.
Target shape — one global's address held in a register serving BOTH a zero-displacement store and the call's pointer argument, while its neighbours in the same record stay plain symbol stores:
la $16,D_800A5E98
sw $2,0($16) <- the store goes through the BASE
...
sw $2,D_800A5E9C <- the NEIGHBOUR keeps the plain %hi/%lo fold
sb $2,D_800A5EA4
move $5,$16 ; jal func_80028620
LAW 1 — THE ANCHOR. D_800A5E98 = -0x12; … f(1, &D_800A5E98); emits the §18 single-store fold (lui $at,%hi ; sw $v,%lo(SYM)($at)) plus a second, independent la into $a1 — two wrong instructions. s32 *rec = (s32 *)&D_800A5E98; *rec = -0x12; … f(1, rec); makes the store's address RTL the same pseudo the argument needs, cse ties them, and one la serves both. This is §20's scalar-global-RMW bullet with the READ replaced by a call argument: the trigger is not "read and written", it is "the address is needed in a register for something else in the same block." Check §153/§165-26 first — if the address is an argument to >=2 calls and there is no store, those apply instead and the declaration is what you DELETE.
LAW 1a — BOUNDS §164-52. §164-52 says a pointer-to-global local evaporates into lw SYM+k unless a >=2-jump-ref label sits between its init and its uses. Here rec is init'd and used on ONE straight path with no join at all, and it survives — because §164-52's fold is fold_rtx rewriting p[k], a dereference, into an absolute address. A use of the pointer VALUE — a call argument, a compare, a store of the pointer itself — has no absolute form to fold into and keeps the base alive on any path. Read §164-52's tell as scoped to all-dereference locals.
LAW 2 — THE READING HALF. la $rB,SYM ; addu/addiu $rD,$rB,-K, with $rD a pointer argument and $rB a store base, means the source anchored the pointer local on the LATER symbol and subtracted:
s32 *rec2 = &D_800A5E9C; *rec2 = 0x18; … f(1, rec2 - 1);
=> la $3,D_800A5E9C ; sw $2,0($3) ; addu $5,$3,-4
Anchoring on the earlier symbol and writing rec2[1] instead gives sw 0x4($base) and a bare la $a1 — 6 mismatched. The STORE's displacement is the discriminator: 0(base) means the anchor IS the stored symbol; K(base) means you anchored too low.
PER-SITE, NOT PER-BLOCK. Only the word the target routes through the base gets a pointer local; its neighbours (D_800A5E9C and the three sb bytes here) keep the plain symbol spelling. Converting the whole record to one struct-typed base is the failure mode — the same discipline as §162q1 and §165-27.
BYTE EVIDENCE. func_80188F60 (ov_SC02_000 + ov_SC02_003, jr_8018173C, MATCH, banked commit:1713 at src/ov_SC02_000/ov_SC02_000_jr_8018173C.c:3890); asm at .run/wave4/func_80188F60/t.s:90-94, 113-118, 308-309. Honest scope: the preserved rungs (v1.c 149-off -> v2.c 98-off) change FOUR things at once — this anchor, the branch polarity, the rec2 - 1 anchoring and a counter merge — so the direction is established by the banked body and the losing spellings are prose, not preserved probes. Re-measure single-axis before quoting a magnitude.
(SHARPENS — sharpens §164-80 Law 1 (L13566-13578), §20 cross-BB combine law (L2489), §165-06 (L13827), §164-24/§16x (L12405); evidence: single-instance; from func_80188F60)
§167-46 — §164-80 LAW 1 EXTENDS TO >>/+ ON A PLAIN REGISTER LOCAL, AND ITS SYMPTOM THERE IS A LENGTH DRIFT, NOT A REGISTER DRIFT.
§164-80 Law 1: t = (t & M) | x; evaluates the inner operation into an anonymous temp that takes a scratch, while t &= M; t |= x; expand in place on t's own pseudo. Its evidence is one and/or pair on a memory-fed packed word, and its scope note says "Re-measure before generalising Law 1 beyond and/or."
It generalises, and it costs something different. i = (i >> 5) + 0x10; on a register local mints a pseudo for i >> 5 that lands in $v0. Two consequences, both LENGTH:
- the following
addiunow carries a WAR dependence on$v0, so it can no longer sink into the branch delay slot; and reorgfills the preceding branch's slot by DUPLICATING thesra— the same insn then appears both in the delay slot and at the join. Net +1.
i >>= 5; i += 0x10; — both destructive on i — restores the target's stream. What matters is that each statement's DESTINATION is the variable itself; the op= token is the shortest spelling of that, not the mechanism (§164-24).
THE DIAGNOSTIC TELL (the new half). An instruction that appears BOTH in a branch delay slot and again at that branch's join, on a LENGTH-DRIFT +1, means your intermediate value is living in a scratch register. Make the statement destructive on the variable before touching scheduling pins, allocno sliders or the permuter. Discriminate against the one other cause of that exact signature: §20's cross-BB combine law (L2489) produces a duplicated sll at a loop preheader plus the loop-back delay slot when a narrow value crosses a BACKEDGE — that one is a fold-defeat you WANT, and its duplicate straddles a loop, not an if-join.
BYTE EVIDENCE. func_80188F60 (ov_SC02_000, MATCH, banked commit:1713): .run/wave4/func_80188F60/v2.c:67 (compound, 98 off) vs v4_MATCH.c:68-69 (destructive, MATCH). Honest scope: that rung also splits a decrement/test and block-scopes a pointer local, so the +1 attribution is the agent's reading of an intermediate diff, not an isolated A/B. The underlying fact — an expression temp is a distinct pseudo at expand — is §164-80's and is source-read.
§167z — REFUTED IN WAVES 5/6: do NOT re-derive
func_8017FE38— GENERALIZATION: when a long-latency load heads a dependency chain whose consumer is far away, the SOURCE STATEMENT BOUNDARY decides whether sched1 hoists it — an inline s Why: Byte-refuted as worded. I built the banked body withd = *(u16 *)&D_800B9A02;as its OWN statement placed immediately above its consumer (ot = (u32)&D_800A6610[d << 14];): 12 mismatched at 239 ins — and the.textis BYTE-IDENTICAL to the fully-inline spelling (objdump diff is only the embedded filename). A statemfunc_8017FE38— THE FORCED COPY MUST BE CONSUMED IN PLACE OR THE STORE SCHEDULES AWAY FROM IT — rewritingx -= (tp & 0xF) << 6;astp = tp & 0xF; x -= tp << 6;shortens the forced co Why: The EFFECT is real — I measured the in-place vs inline A/B on the banked body at 0 vs 10 mismatched (239 ins both), with thesh $rX,0x16($a3)moving four slots and a register cascade behind it. But the stated LAW is a coupling claim about the forced copy, and the coupling is byte-refuted: with the+ zrcopy REMOVEDfunc_8018C3F8— BOUND ON §164-02: when the constant base reaches the addu via a SPILL RELOAD (lw 0x30($sp)) rather than a livelapseudo, "a round trip through memory kills the const Why: The OBSERVATION is real and byte-confirmed by me in the shipped object (0x8018c554addu s1,t2,s1, base first, source order won). The stated MECHANISM is impossible and must not be banked in this form.
(1) PASS ORDER makes the spill story unreachable. §164-02's swap is a cse-time decision (fold_rtx, cse.c:5278-5304
func_8018C3F8— Corollary: "this ALSO explains why sibling func_8018F694 matched with the opposite spelling: thereotstays in a live register." Why: BYTE-REFUTED from the shipped, matching binary.func_8018F694reloads itsotfrom the frame exactly likefunc_8018C3F8does:8018f7d8 lw a3,0x38($sp)/8018f7dc sll s0,s0,0x2/8018f7f0 addu s0,s0,a3. Itsotdoes NOT stay in a live register — it is stack-resident at the add, the same as the claimed "spillfunc_8017F17C— Lever 3 — FRAME: MEASURE THE BASELINE BEFORE ADDING DEAD LOCALS. The 8-byte hole at sp+0x18..0x1F reads exactly like §162's unreferenced-local oracle but is not one: gcc Why: THE THREE ABLATION NUMBERS ARE RIGHT; THE MECHANISM IS BYTE-REFUTED AND THE PRESCRIPTION IS ALREADY BANKED THREE TIMES OVER. It must not enter the file in this form.
(1) MECHANISM REFUTED. "gcc already rounds the align-1 struct's slot" is false. I compiled 7 controlled spellings on the pinned cc1: `struct { u8 c[8]; }
func_801805D4— L8 — the accumulation must be ONE expressionf(x) + (f(y + K) >> 4), not two statements: gcc-2.7.2 evaluates the left operand first, parks it in $s0 across the second c Why: The prescriptive half — 'ONE expression, not two statements' — is BYTE-REFUTED at vet time, twice. Splitting into two statements with per-site block-scope temps ({ s32 x = f(..); s32 y = f(..); u = x + (y>>4); }) -> MATCH 212. Splitting with a multi-set accumulator (u = f(..); u = u + (f(..)>>4);) -> MATCH 212. §16func_80187F2C— The two field stores and thefunc_8012E688(a0,0x98F,0)call must be written call-LAST (store; store; call;). Interleaving (store; call; store) let gcc-2.7.2's delay Why: THE TARGET DESCRIPTION IS FACTUALLY FALSE AND I BYTE-REFUTED IT. Disassembling the banked SHA1-green object at 0x80187FE0:jal 8012e688/sh v0,254(a0)— the 0xFE store IS the call's delay slot, not anop. The whole claim is built on a target reading that does not exist, and the same TU's unbanked sibling `func_8func_80187130— When EVERY arm of a 2-way branch ends inreturn(no single main-body arm), gcc-2.7.2's RTL-order-is-source-order places the arm written LAST adjacent to the shared epil Why: BYTE-REFUTED — and refuted by the agent's own two builds, which are still on disk. I re-ran the pinned triple at vetting time (cpp / tools/bin/gcc-2.7.2-psx/cc1 -O2 -G0 -mips1 -mcpu=3000 / maspsx / as) on both surviving probes plus three new controls, and read cc1's own.s, not just the.o.
(1) THE DIRECTION IS EX
func_80183BC8— The accumulate must be spelled as the full raw expression twice —*(u16*)ptr = *(u16*)ptr + r;— and NOT as a+=compound assignment. Why: No A/B was run on the+=token; it is bundled into the same edit as the per-branch duplication and the fresh local, so it credits an untested variable — the precise failure mode §164z refuted twice in this campaign (func_80184944: 'the A/B moved TWO variables at once and credited the wrong one'; func_801823E8: 'the Afunc_80183BC8— INTEGRATION PRESCRIPTION: because the destination TU already declaresextern void func_8012CBCC(s32 a0);at file scope, bank the body by declaringextern s32 func_8012 *Why:* Three independent problems. (1) MOOT AND CONTRADICTED BY THE SHIPPED BANK: func_80183BC8 is already banked at src/ov_SC02_041/ov_SC02_041_jr_8017BEBC.c:6477 and the resolution actually used is the §17a-1 use-site cast — the file-scope decl is left asextern void func_8012CBCC(s32);` (:6470, an exact-text duplicate offunc_8017F3C8— FLAGGED CLAIM — 'a cross-block ternary-result allocno's global-alloc conflict is BLOCK-granularity, not instruction-precise': gcc-2.7.2 global-alloc records a conflict ag Why: BYTE-REFUTED FROM THE PINNED SOURCE, and the cited evidence cannot support the claim it is offered for.
(1) THE GRANULARITY HALF IS FALSE. global_conflicts (tools/reference/gcc-2.7.2/global.c:625-772) is an explicit per-INSN birth/death walk, not a block-granularity scan. Per block it (a) seeds allocnos_live/`hard
func_801832F8— FLAGGED: the §164-82 / §46-L2 'test-then-re-read' idiom transfers from memory LOADs to constant-division results — writingpan + out1.x/20again in every arm of an if/e Why: BYTE-REFUTED on the pinned triple (cpp / cc1-2.7.2 -O2 -G0 -mips1 -mcpu=3000 / maspsx 2.56 / as). I built the minimal repro the claim describes —register s16 pan __asm__("$17"), the fieldout1.xloaded through a real call, the clamp result captured intos32 panv,panvconsumed by the trailing call — in two spefunc_801832F8— Pinningbaseto ANY explicit hard register costs +2 instructions elsewhere in the SAME function — specifically it blocks fill_simple_delay_slots from duplicating thea *Why:* Byte-refuted, and already narrowed by the successor agent. In the shipped draft the pinned build emits the duplicatedaddiu $a0,$sp,0x20` in BOTH positions (indices 62 and 64 of the mine-side stream, matching the target's pair at 801833F0/801833F8) at 111 ins — i.e. the pin does not suppress the duplication at all. Thfunc_801832F8— A second, more novel use of the §5a barrier: as a pure SCHEDULING fence with no return statement attached — placed afterif (v<0) goto y_neg;to block thebltz's dela Why: Byte-refuted in its own function. Deleting exactly that barrier from the shipped draft and re-runningmatch_onegives a BYTE-IDENTICAL result — 111 ins, 40 mismatched, and the two diff reports arediff-clean against each other (.../vet832F8/w6_base.diffvsw6_nofence.diff). The barrier is doing nothing at the sfunc_801832F8— (derived during this vet, from residual B) The target's pan-clampaddu $v0,$s1,$v0 ; addu $s1,$v0,$zero— sum into a scratch, then a copy into the callee-saved home — c Why: Byte-refuted, and the cure is one deleted word. That copy is the deferred-truncation store ans16accumulator emits when it is ASSIGNED and then TESTED — and theregister __asm__("$17")pin onpanis what deletes it. Minimal repro, same call skeleton, pinned triple: pA `register s16 pan asm("$17"); pan = pfunc_80184E4C— MECHANISM: 'MIPS o32 leaves the upper bits of narrow-typed register params unspecified, so gcc re-extends on first actual use, even though the parameter was register-copi Why: The OBSERVABLE half — raw whole-word stash to a callee-saved pseudo at entry, extension deferred to the use — is already §43 word for word ('with the raw values stashed to callee-saved pseudos first (s3←a1 …) and re-extended per use after calls'), so it is COVERED, not new. The ABI ATTRIBUTION is wrong and must not befunc_8017EEEC— func_8017EEEC is a SECOND byte-verified instance of §163c's specific mechanism — an EMPTYcase 4:glued ontodefault:is the load-bearing label that crossed the thres Why: BYTE-REFUTED, and it matters: the draft is wrong and must not be banked. §163c's own diagnostic ('jtbl[k] == the default label is the fingerprint of a case label sharing the default body') falsifies the claim on sight. The target table is jtbl_801AA880 = [0x8017EFB0, 0x8017F074, 0x8017EFF0, 0x8017F074, 0x8017EF3C] (asmfunc_8017F5D4— (wave-5) FAMILY REACH: the ×2 sibling should remap mechanically — the family template is the (Morph_8017DC1C*, SVECTOR2*, SVECTOR2*, t) quadruple list; only the 11 per-ov Why: Byte-refuted, first by the wave-6 agent and then independently by me: the only other func_8017F5D4.s in the tree (ov_SC01_008) is an 18-instruction body with frame 0x18 and two jals — nothing to template. The wave-5 note asserted a mechanical ×2 remap without ever opening the sibling's .s, which is precisely the failurfunc_80187D0C— Sub-claim of the same note: the oracle generalises to<=— "slti+bnez-to-label almost always means the source used >=/<= against the constant, with the label holding th Why: BYTE-REFUTED, and it is the one part of the note that is not already on disk. The relational families split by polarity, not by strictness: the GREATER family (>=,>, and the operand-reversedK <= x) producesslti+bnez-to-else; the LESS family (<,<=) producesslti+beqz-to-else. I compiled all four onfunc_8017F2D4— ROOT CAUSE of the seven prior gate refusals was the wrong destination-TU citation, not a codegen residual — 'a banker splicing into jr_8017C340.c gets a no-op or a bad sp Why: Already byte-refuted inside the very entry this campaign banked from these notes. §166a's scope correction states it was measured:corpus.stubs()derives each stub's TU from the actual INCLUDE_ASM site andgate_stagesplices via corpus, so the harness was always editing the right file — only the PROSE was wrong. Affunc_8017F2D4— NEW-1: gcc-2.7.2 cross-jumping keeps the FIRST copy while the target keeps the LAST, which is why the 14-arm shared tail must be hand-written ONCE at the last arm withg *Why:* The MECHANISM is on §165z's refuted list under this exact function, in these exact words. jump.c keeps the LAST on every path —do_cross_jumpdeletes the stream precedinginsnandjump_chain` is built push-front, so the earliest jump pairs with the latest copy (§162g, read out of jump.c:2537/219-221). The real reafunc_8017F2D4— NEW-5: the surviving redundant compare at 0x8017F460 needs TWO levers TOGETHER — (a)__asm__ ("" : "=r"(t) : "0"(t))tied to SELF (tying to a fresh temp costs a real `m Why: Split verdict, and both riders the agent re-asserts are already byte-refuted. Lever (a) itself is SOUND and banked as §165-03 (cse's record_jump_cond carries EQUALITY across a branch, cse.c:5944-5990; barrier deleted -> 3 mismatched, BRANCH-POLARITY) — COVERED. But the rider 'tying to a FRESH temp costs a real move' isfunc_8017F2D4—s32 lt = mode < 0xA;must be an explicit local because gcc-2.7.2 has no gcse and the singlesltimust precede the branch both arms need it after. Why: On §165z's refuted list under this function, byte-refuted at vet time: deleting the local and inlining the compare at both use sites —if ((mode < 0xA) || (D_8011515A == 0x100))andif (!(mode < 0xA))— gives MATCH 279. The C form is byte-INERT here, so it is not a lever and must not be taught as one; the 'no gcse'
§168 — THE COUSIN TIER (P30 S49, 2026-08-12): h_seq's exact-hash brittleness, measured — and the similarity map above it
The finding (Drew's smell, byte-verified). The frontier's "4,513 unique singletons + 1,817
small families" picture was substantially a GROUPING ARTIFACT. h_seq — our loosest tier — is an
exact hash of the mnemonic skeleton, so a single inserted instruction makes two functions total
strangers. Per-location compilation inserts instructions constantly: a per-location constant
crossing the 16-bit boundary turns a 1-instruction li into lui+ori; a bigger jump table adds a
case; a dropped flag test deletes one. Probed on the full frontier (120-pair sample per band):
in the 0.85–0.99 similarity band, 86/120 near-pairs differ by PURE insertion/deletion (25 with
lui inside the indel block — the li-expansion tell); replace-heavy pairs are the minority.
Specimen: ov_SC06_010:0x8017bebc (753 ins, "singleton") is 0.987 similar to a MATCHED function
in the same binary — 12 small edit blocks, mostly 1–2-instruction insertions. Not a unique
function; the B2 walker family wearing a one-instruction disguise per site.
The map (tools/family_cousins.py → .run/family_cousins.json + docs/family-cousins.md). Distinct
open skeletons (one per h_seq class, ×N copies collapsed via the family map, R32-checked against an
independent sigs+stubs recount), streams via family_remap.stream_words, mnemonic-class tokens
(register/imm-blind, h_seq-granularity), K=8 shingle candidate index (posting cap 400), difflib
ratio, union-find at ≥0.85. Measured at S49 HEAD (whole-frontier, 11,627 instances / 584,448 ins —
totals exact by construction):
| category | units | open fns | open ins | lever |
|---|---|---|---|---|
| A-prop (unit holds a lane-A skeleton) | 197 | 1,286 | 68,729 | family_sweep propagation |
| seeded (≥0.85 MATCHED body exists) | 418 | 1,652 | 50,422 | crack by EDITING a proven body |
| cousin-multi (no seed, ≥2 instances) | 1,552 | 5,449 | 249,799 | 1 crack seeds the unit's rest |
| cold (alone at 0.85) | 3,240 | 3,240 | 215,498 | full-price crack |
The genuinely cold tail is 215k ins — 37% of the remainder, not 90% — and it is small-function shaped (2,692 of 3,240 cold units < 100 ins; only 43 > 300 ins). Corroborating probes: only 823 of 3,760 overlay singletons are truly alone at their vram address; main's "structurally barren" verdict HOLDS at the similarity tier (94% of main's stub mass < 0.70 to anything anywhere; ~72 tiny fns have seeds — a small correction, not a reversal); call-sequence grouping adds ~94 matches beyond content similarity (redundant — not a tier worth building).
The three laws:
- A cousin is a SEEDED CRACK, never a remap. Skeleton drift means the member needs its own
compile —
family_sweep/remap_hseqrequire an identical skeleton by construction. The seed's value is the draft: the agent edits a proven C body instead of writing one. (Prior-NOTES seeding measured 7/9 and 10/12; a full ≥0.98-similar matched body is a strictly stronger prior.) - Rank waves by UNIT weight, not family weight. A crack's real yield is its whole cluster — family members plus the cousins it seeds. The S49 slate: 40 targets = 33,304 unit ins vs 25,010 family-ranked — +33% on the same agent count. (The S29 pricing law, one tier up.)
- Discount short-function similarity. Shared prologue/epilogue boilerplate inflates ratios on small nins; the targets carry nins and seed-sim separately so wave sizing can discount seeds on <~40-ins functions. A 0.87 on 20 ins is a hint; a 0.95 on 400 ins is a body.
Ops: regen order is sigs → family_hseq.py → family_cousins.py (it fails loud on a stale
map, with the regen command in the error). --targets 40 --wave wave7 emits the crack_wave.js
slate; seed refs resolve to the matched body's location (engine_core.h DEFINE_ macro vs inline
src/<bin>/*.c def). The survey RANKS and SEEDS; only the whole-binary byte-gate banks (G3/P9).
§169 — THE MICRO-ADAPT LANE (P30 S49): edit a proven body, don't crack a new one
The lane. For a cousin (§168) whose drift vs an already-matched seed is ≤3 blocks / ≤6 mnemonic
tokens, the work is an EDIT, not a crack: copy the seed's byte-proven C, re-point every symbol at
the TARGET's .s, apply the one semantic change the diff implies (an indel of lui/ori/addiu
is almost always a changed CONSTANT — the value is readable in the card's member words), gate.
Tools: family_cousins.py --adapt-cards → .run/adapt_cards.json; tools/wave/adapt_wave.js.
Measured, pilot of 30 (S49): 25 agent-MATCH (83%, 0 refuted by the adversarial verifier) → 16 banked (64% MATCH→bank; 53% end-to-end) at 2.7M tokens, 28 haiku / 2 sonnet ≈ 30k tok per banked instance vs a crack wave's ~75k. The edit is small even when the body is not, so route by body size only to cover the COPY cost (haiku ≤60 ins), not the reasoning.
Three laws the pilot paid for:
- A banked cousin usually does NOT propagate. Cousins are byte-VARIANT by construction, so
there is typically no byte-identical sibling to stamp: 2 of 8 propagated (+4). The card's
reachcounts COUSIN members (future adapt fuel), NOT dedup copies. Price a card at ~1 instance plus optionality, never at ×reach. (R14 — I framed it as ×reach before measuring.) - The residual failure class is INTEGRATION, not codegen. 9 of 25 MATCHes died at the whole-binary gate, clustered per-TU (5 in one binary, 2 in another): standalone-MATCH, host-TU-rejected — the §52b/§161c declaration surface, which the reconcile ladder exists for. A per-TU recovery pass belongs between the wave and the scale-up.
- Never wrap a self-timing tool in a tighter outer timeout, and kill process GROUPS.
gate_stagescales its own propagation timeout (1800 + 1800×banks); a 7200s outer cap killed a healthy 5-bank group mid-fleet-write AND orphaned itsdedup_propagatechild, which kept rewritingsrc/through a subsequentgit checkout --. Symptom: a later gate opens onto a dirty tree it did not create. Diagnosis:pgrep -af dedup_propagate. The same 5 drafts banked 5/5 when re-run untimed. Generalizes S48's "guard the CAMPAIGN, not the process."
MEASURED TWICE (S49 waves 7a/7b) — and the spread law. 7a: 30 cards → 25 MATCH (83%) → 16 banked (64%). 7b (relaxed thresholds, 59 cards): 48 MATCH (81%) → 44 banked (92%), 75% end-to-end, ~157k tok/banked fn. The bank-rate jump is NOT the thresholds — it is TU SPREAD: 7a concentrated drafts in few destination TUs (5 in one binary) and its 9 gate failures were per-TU declaration collisions between sibling drafts; 7b spread 48 drafts over 35 TUs, mostly one apiece, and only 2 groups lost anything. Build slates that spread across destination TUs; when several drafts must share one TU, gate them together and expect the §161c reconcile ladder.
Sizing the lane (S49, seeded pool = 828 skeletons / 1,652 members / 50,422 ins): today's
≤3/≤6 cut captures 47% of the pool's instructions; ≤6 blocks/≤16 tokens captures 78% (+247
skeletons, +15,576 ins) and is still a holdable edit; past ≤8/≤24 the curve flattens (+4 skeletons)
— the remainder is genuinely different code, i.e. a seeded crack. Absolute thresholds mis-sort
large bodies (MIXED median is 35 ins with a 29% edit fraction — small bodies rewritten heavily —
while only 25 skeletons are big-body/small-edit), so union the absolute rule with edit_fraction ≤ 0.20 rather than replacing it. Note the tier boundary: a pure IMMEDIATE change leaves the
mnemonic stream identical ⇒ same h_seq ⇒ that member is family_sweep/imm_map work, never a
cousin. Every replace block in a cousin card is a genuinely different MNEMONIC.
§170 — THE A-PROP WORD-DIFF CARD (P30 S49): the lane that had no owner
The gap. family_cousins.py --adapt-cards builds cards only for seeded units, so lane A —
1,700 open functions / 76,419 ins, every one with a byte-proven matched sibling — was skipped by
construction. A lane-A member shares its family's h_seq with a MATCHED sibling, so the cousin
(mnemonic) diff is EMPTY: the differences live in WORDS — a struct field offset, a data symbol, a
register. The mechanical remap refuses exactly these (unresolved immediates / regalloc drift), which
is why the ≥16-reach head swept 0 banked / 245 failed / 188 refused.
The card (--aprop-cards → .run/aprop_cards.json): positional WORD diff member-vs-matched
sibling (same length by construction, so no alignment needed), each site classified IMM / REG /
OTHER and disassembled both sides, grouped BY FAMILY. One agent then learns the parameterization
once and emits N drafts — a 54-member family is ONE card, not 54. Measured on the head: 13 families
/ 433 members, median 2 differing words per member; 5 members were byte-IDENTICAL to the seed
(pure dedup_propagate work, not agent work — check for those first).
Measured (S49 calibration, 9 batches / 108 members, 4.5M tok): 98 agent-MATCH (91%) — the
best agent rate of any wave type — → 56 banked (57%), ≈ 80k tok per banked function vs the
per-member cousin card's 157k and a crack wave's 400k+. The agent notes are substitutions, not
decompilations ("src struct = D_801D0358"; "cb=func_8017FBA4; 0x12C, 0x4B0, 0x38E…").
⚠ The conversion gap. 91% agent → 57% gate is the WORST conversion measured (the per-member cousin wave ran 81% → 92%). 14 of ~50 groups banked zero.
Hypothesis, not yet proven: family-batched cards CONCENTRATE members into one destination TU by construction — precisely the §169 collision the spread law names, which held the 7a wave to 64%. Re-gate the unbanked drafts ONE PER TU before scaling a batched A-prop wave; if the spread law is the cause the same drafts bank, and the fix is to interleave families per gate batch rather than to redraft.STRUCK 2026-08-13 (S50) — REFUTED. Batching was never the discriminator (5-draft groups banked 5/5; the two "concentrated" groups banked 12/12 and 10/10 once fixed). The cause was a STALE SEED SYMBOL:
match_oneis blind to a relocation's target NAME, so a carried-overD_8018xxxxscores MATCH standalone and dies at link. 23 of 24 banked after a mechanical rebase; A-prop's true conversion is 87%, not 57%. See §171 — and note the test itself was never needed: the answer was already sitting in.run/harvest_failed.<binary>.classified.txt.
Three ops notes paid for on the head: (1) family_sweep --hseq defaults to --band substantial,
so tiny/mid families are silently OUT OF SCOPE — a "never attempted" verdict can be a band default,
not a difficulty (5 of 13 heads); (2) dedup_extend extends MACRO-backed groups only — for a matched
INLINE def use dedup_propagate --addr; (3) that tool names its own blocker precisely
(missing file-scope extern (CARRY-FIXABLE): D_…) — a data symbol the matched body's own TU never
declares at file scope blocks every sibling until it is carried.
§171 — THE STALE SEED SYMBOL (P30 S50, 2026-08-13): why §170's 91%→57% was never codegen
§170 left one hypothesis open — that family-batched A-prop cards CONCENTRATE members into one
destination TU and die of the §169 collision. It is REFUTED, by data that already existed and by a
direct test. Refuted three ways: (1) the recorded S49 verdicts show 5-draft single-TU groups
banking 5/5 twice, and 4/4 twice — batch size was never the discriminator; (2) 11 of the 35
unbanked drafts were single-draft groups, i.e. already gated one-per-TU, and all 11 classify
DIFF; (3) after the real fix below, the two "concentrated" groups §170 blamed — 12 drafts and 10
drafts into ONE .c each — banked 12/12 and 10/10.
The real defect, and it is one line per draft. A per-location data symbol (D_8018xxxx — a
function-pointer table, a jump table, a state array) is part of the seed's ENVIRONMENT, not its
logic. The adapt/A-prop lanes hand an agent a proven seed body plus a word-diff; the agent edits
the logic and carries the seed's symbol across unrebased. Then:
match_onescores it MATCH. It compiles standalone and compares instruction ENCODINGS.%hi(D_seed)and%hi(D_target)are the same instruction with a different relocation TARGET NAME, and the scorer is blind to the name.- The whole-binary gate kills it at LINK:
undefined reference to 'D_80181900'.
Standalone-MATCH / host-TU-link-failure. That class was 24 of the 24 concentrated A-prop
failures — the entire conversion gap. In the word-diff card the rename is visible only as an opaque
IMM site (the low half of a lui/lw %hi/%lo pair), which is exactly the site class an
agent reads as "an immediate to copy", not "a symbol to rebase".
Measured: all 24 were 1:1 rewritable, every one at the same seed→target vram delta (0x4128 —
one binary's data section offset from the seed's). Rebased mechanically: 23/24 banked (the 24th
is a genuine DIFF). A-prop's real conversion is 56 → 79 of 91 = 87%, not 57%; the agents'
91% MATCH claim was very nearly right and the gap was ours.
Two tools, one primitive (tools/aprop_symfix.py, imported by family_cousins.py):
- Post-hoc guard —
aprop_symfix.py <slate.json> --fixaudits every draft's vram-suffixed symbols against the symbols the TARGET's own.srelocates, rewrites the 1:1 cases, and emits agate_lane-shaped slate. Deterministic, no build, so it belongs BEFORE the gate, never after. - Pre-hoc annotation —
--aprop-cardsmembers now carrysym_map: the explicit{seed → member}renames, computed from the seed's C BODY (a matched seed has no.sof its own — it is compiled from C) versus the member's.s. It turns a puzzle into an instruction.
This is R34 in one line: match_one is a perfect codegen oracle and a NULL linkage oracle. The
symbol audit is the second oracle that can disagree with it, and it costs no build.
Two spellings, one bug, twice: sig_image writes hex lowercase, splat writes it uppercase, and
seed_body_ref builds func_%08X. Both defects found while wiring this (a NO_SEED_BODY on every
macro seed, an AMBIGUOUS on every 1:1 rename) were case mismatches — the same class §128/R35 keeps
naming. Compare function identities case-insensitively, and strip the DEFINE_ prefix, or the
seed's own name reads as a stale symbol.
§171-D — STALE-DELTA: the 1:1 rule generalized to n:n (P31 T1, 2026-08-14). The S50 fix
handled exactly one stale symbol ↔ one target symbol; everything n:m was AMBIGUOUS-refused. But a
seed's data cluster moves to the target overlay AS A BLOCK, so when the counts are EQUAL, every
symbol on both sides is vram-addressed, and the sorted-by-address zip has exactly ONE uniform
(target − draft) delta, the pairing is forced and aprop_symfix now rewrites it as STALE-DELTA
(n pairs, per-pair substitution proved, sequential re.subn safe because stale ∩ asm_only = ∅).
Anything else — count mismatch, a non-addressed name (MoveImage) on either side, per-pair deltas
that disagree by even 4 — stays AMBIGUOUS. Measured on its first live batch (the 11 undefined reference to D_* failures from the match108 never-gated set): 4 classified STALE-DELTA, 4/4
banked (func_8016BCC0 Δ−0x65450, func_8017F1C8 Δ+0x13FC0, func_80186BD8 Δ+0x165A0,
func_80186BF8 Δ+0x16590); the uniform-delta test correctly refused func_80186C1C, whose two
deltas differ by 4 — a hand-check had wrongly called it fixable, the rule was right. R39 negative
controls: synthetic probes (1:1 unchanged, uniform→DELTA, non-uniform/non-addressed/count-mismatch
→AMBIGUOUS) plus a classification-equality re-audit of the S50 snapshot over still-stub rows (zero
non-world-motion changes). Note the world-motion classes when re-auditing old snapshots: banked
fns read NO_ASM (stub gone), and STALE can decay to clean when a sibling's bank DEFINES the
once-missing symbol — 9 such drafts became gateable for free here (0 banked on re-gate, though:
stored drafts still re-gate at the ~8% A10 law, 0/23 this batch).
§171a — THE MECHANICAL A-PROP DRAFT (P30 S50): 256 members banked with no agent in the loop
The claim. A lane-A member shares its family's h_seq with a MATCHED sibling, so its body IS
that sibling's body with the per-location environment rebased. Two mechanisms had each taken a run
at that population and each left it on the table:
family_sweep --hseqremaps mechanically but CARRIES the seed's declaration layer, and its dominant failure is decl-agreement — 331 of the 458 verdicts in S49's post-repair ledger. The body was never the problem; the decls it dragged along were.- the A-prop agent wave re-derives the same body at ~80k tokens per banked function — and got the per-location symbol wrong every time it mattered (§171).
tools/aprop_autodraft.py does neither: seed body + family_remap.symbol_map (positional reloc
zip, three-oracle target spelling) + a minimal preamble synthesized from scratch — one extern
per symbol the body actually references, plus only those seed typedefs the body names and the
destination does not already define. Nothing else travels. 256 banked at zero agent tokens
(222 + 34), against ~20M tokens the same work would have cost as a wave.
THE SELECTOR IS THE TOOL. Everything below was learned by launching at scale and stopping when the verdicts disagreed with the plan. Each fix moves a failure from build time to generation time, and at ~1 min per gate group that difference IS the run:
| Refusal | Why a draft cannot work | Measured |
|---|---|---|
classify_member != PURE |
a rename reaches a RELOC site and nothing else | first 5 failures were 5/5 IMM or STRUCT |
| no definition after rename | the seed name was recovered by scanning the body for the first func_XXXX( — in a de-macroized block that is the first extern declaration, so a CALLEE got the member's name and the definition kept the seed's |
48 caught; the symptom was undefined reference to <member> |
| arity conflict | the destination already declares a callee with named parameters the body underfills | 17 on one symbol alone |
| undefined data | the draft references a symbol nothing in the DESTINATION binary defines — the seed's environment does not exist there | 43 + 33 on two symbols |
.s carries data or a jtbl |
the data lives INSIDE the stub being replaced, or the carve path refuses | 90 of 584 |
Macro seeds are the majority and they are a trap. 567 of 1,196 members sit behind a
DEFINE_<fn>() engine_core macro (all 3,737 de-macroize cleanly). Pasting the de-macroized block
whole reintroduces exactly the decl-agreement failure this design exists to avoid: measured
inline 145/213 (68%) vs macro 77/276 (28%), with the macro failures reading parse error before '*' and too few arguments. func_8016AB6C's macro block is 1,891 lines of which 108 are the
function. Take the DEFINITION; keep the block only as the decl source.
IMM is not a wall, it is a second engine. T2a's imm_map_tier1 resolves a per-location LITERAL
exactly as symbol_map resolves a per-location SYMBOL: 131 of 275 IMM members resolve with zero
unresolved sites. Only STRUCT (register/opcode drift, 238 members / 4,259 ins) genuinely needs a
per-member edit.
Negative-control every refusal before you ship it (tools/draft_prechecks.py, run against ALL
205 banked drafts of the first run: zero false positives, catches 39 of 67 known failures). That
control found two bugs in the checks themselves, both of which would have thrown away good work
silently: C89 f() declares UNSPECIFIED parameters, not zero — so decl 0 vs call 1 is no
conflict at all — and a member's own definition read as a call to itself. A pre-check that discards
good drafts is worse than one that lets a few builds fail; when in doubt make it CONSERVATIVE
(flag only decl > call, on named parameters).
§171b — THREE CARRIES THE MECHANICAL DRAFT NEEDS (P30 S50, banking the top-reach families)
Working the frontier's top three families end-to-end exposed three carries a seed-body draft needs beyond the symbol rebase. Each was found by reading a single compiler verdict, and each generalises.
1. DATA DEFINED INSIDE THE MEMBER'S OWN .s must be DEFINED, not declared. When a symbol's
bytes live between dlabel/enddlabel in the very .s the draft replaces, an extern links to
nothing — the data vanishes with the stub. Emit the seed's definition re-initialised with this
member's own bytes: they differ per location (the 0x801F1CD8 family carries four distinct
8-byte patterns across 42 members). Substitute only when the initializer is a flat byte list whose
count matches, and refuse otherwise — a partially-understood initializer silently mis-initialised
is a failure the gate catches but nobody can explain. Verdict that names it: undefined reference to 'D_…' on a symbol that visibly exists in the binary's asm.
2. SHARED TYPES the destination cannot see. MATRIX, SVECTOR and friends live in
engine_types.h, which ov_* TUs reach through engine_core.h and md_* TUs do not include at
all. A draft using one in an md module dies as parse error before 'm1' — 4 of the 9
0x8017D290 members, and the only thing between them and a bank. Carry the brace-matched typedef
block, vetoed by the destination (re-declaring a type it already has is a hard C89 error).
3. A POSITIONAL LITERAL MAP where the ordinal engine gives up. imm_map_tier1 refuses a value
that also appears at a NON-differing asm position (asm-ambiguous) — here 0x10, which collides
with the struct offsets param_1 + 0x10. But when the differing slots map 1:1 onto the C's call
sites in order, the substitution is exact: assert [C literals in order] == [seed slot values]
first, then rewrite positionally. That assert is the whole safety argument — it fails loudly if the
C and the asm ever disagree, and it took a family the generic engine had refused 10/10 to 9/9.
AND THE TRAP UNDER ALL THREE: body_text matched extern void func_X(s32, s16 *); at column 0
and returned the next function's body. Silent, and it had been shipping wrong seed bodies —
visible only as "no definition of the member after rename" skips, which read like a niche edge case
and were actually the symptom. A definition is confirmed by a { with no ; before it; a name
match alone never is.
§172 — THE ORPHAN-SLOT MECHANISM v2 (P30 S50-Max): the complete frame-residue model for gcc-2.7.2 MIPS
(v2 supersedes the S50 original in place: the Max-effort source reading corrected the producer list, the alignment math, and the strength of the impossibility claim. The instrument and the orphan rule are unchanged and re-verified.)
The instrument (tools/cc1_dumps.sh <draft.c> <tag>): run the pinned cc1 with
-dr -ds -dj -dc -dl -dg, then count standalone (insn N P X (use (reg:M R))) insns in the
.combine dump — count them across ALL modes, not just SImode. Each is one never-referenced
reload slot. On func_8017CE58: draft 12, target frame demands the equivalent of 16.
Slot producers in the spill region (complete list, from the compiler source):
- alter_reg pseudo slots (reload1.c): every slotted pseudo gets
assign_stack_local(mode, size, -1)— align −1 means BIGGEST_ALIGNMENT = 8 bytes, size rounded up to 8. This is why every orphan costs 8 bytes even in SImode. Slots are assigned in REGNO order: the a1-param pseudo (lowest) lands first, loop-opt-created pseudos (highest) last — the two ends of the spill region are position-pinned; everything between is order-free. - combine USE-orphans (combine.c:10835): the ashift intermediate of a short-mem→int
promotion triple, orphaned when the death-note walk (backward over plain insns only) hits a
CODE_LABEL or JUMP_INSN. Orphan rule, byte-verified both directions: the HImode load's reg
carries an extra HImode use (a
?:arm copy) → the load survives (or is REWRITTEN as(set (reg:HI) (subreg (reg:SI)))— combine does this rewrite, which is why an orphaning site can still show a singlelh) → the promotion folds separately → orphan. Single-use load → 3-way merge consumes everything → no orphan. Sites before the first label/jump (the function head) can NEVER orphan — the walk reaches insn 0. - caller-save areas (caller-save.c
setup_save_areas;-fcaller-savesis ON at -O2): allocated eagerly — oneassign_stack_local(SImode, 4, 0)= 4-byte slot per call-clobbered hard reg that carries a call-crossing pseudo at ANY reload iteration, whether or not a save/restore insn is ever emitted. Transient iteration-1 allocations that later respill leave never-referenced 4-byte areas. - spill_stack_slot (reload1.c): one reused 8-byte slot per hard reg that pseudos are spilled FROM ("Spilling reg N" in the .greg dump).
The three-layer canonicalization wall (measured, ~120 probe forms + a 200-variant sweep):
manufacturing an extra orphan with zero code drift requires an expression NOVEL to cse yet
PROVABLY sign-extended to combine. Three layers jointly close every reachable spelling:
fold-const normalizes the trees ((x<<16)>>16 arrives in RTL as the same subreg-promotion —
verified in the expand dump), cse1/cse2 canonicalize through REG_EQUAL/quantity classes, and
opacity that defeats cse (asm-laundered values/pointers) equally blinds num_sign_bit_copies
(the survivor emits real shifts or loads: the opaque-pointer probe reproduces the exact
16-orphan target frame at +7 insns). Scalar declaration order (20 permutations) and TU context
(both drafts run through the real whole-binary gate — first time ever for this class) are also
byte-refuted as levers. The wall statement, honest form: the residual is not reachable by
re-spelling the same computation; what remains is structurally different source with
coincidentally identical bytes.
Transferable diagnostics: (1) frame-off-by-8k residual → count combine-dump USEs first, all
modes; (2) slt-count changes in a probe = the ?: branch structure moved — the form is wrong;
(3) which chain operands RE-LOAD in a later pass is pinned by which HI temps the earlier pass
clobbered as ?: accumulators (the FIRST operand of each inner ?:), and the bytes pin that —
a select-chain's elision pattern is byte-determined end to end; (4) the a1-param spill position
pins where stratum-1 (declared locals/temps) ENDS — any dial that grows locals displaces it,
which is exactly the dead[8] 2-off signature.
§172a — TWO DECOMPILATION TELLS FROM THE SAME DIG (P30 S50-Max)
The lhu/lh tell. gcc-2.7.2 MIPS emits lhu for a plain HImode COPY (movhi — an s16-to-s16
assignment) and lh for a PROMOTION to int (extendhisi2_internal). A lhu+lh DOUBLE-LOAD of
the same address is therefore an s16 value used both ways in one region — the ?:-arm-plus-
compare shape. When typing variables from asm: lhu ⇒ the destination is s16; lh ⇒ the use is
promoted. (Corollary of force_not_mem: s32 x = shortmem and s16 t = shortmem; s32 x = t; are
RTL-IDENTICAL — the s16-temp spelling axis does not exist for the compiler.)
The macro-vs-inline tell. A select chain whose final code RE-EVALUATES its compares in the
arms comes from a TEXTUALLY REPEATING MACRO (#define MIN2(a,b) ((a)<(b)?(a):(b)) nested), never
from an inline function: gcc-2.7.2 evaluates inline-function arguments ONCE into parameter
pseudos, collapsing the repeats (measured on the 246-ins body: the inline form compiles to 209
ins). When a family's shape shows duplicated compare work, reconstruct it as nested macros with
repeated operand expressions — that redundancy is load-bearing for byte-matching.
§172b — THREE MORE TELLS FROM THE GCC READ (P30 S50-Max, banked on Drew's ask)
LANE ALIASES (grep bait — added P31 S59). The atlas lever labels a drafting card carries
(tools/atlas.py) do not appear anywhere else in this cookbook, so an agent that greps the label it
was handed finds NOTHING: measured over the tells waves, 108 failure transcripts grepped
extend-tell and 28 grepped swaprepeat against zero hits while the knowledge sat here under
different words. The map:
extend-tell → §172b-1 (+ the C recipes in §264) · swaprepeat-tell → §172a (macro-vs-inline
repeats) + §172b-2 · s16-div-tell → §172b-4. If you arrived by grepping a card label, this
is your section.
1. The sll/sra-16 pair tell (extendhisi2 is MEM-only). The single-insn sign-extending load
(extendhisi2_internal) exists ONLY for memory operands. A visible sll X,Y,16; sra X,X,16 pair
in the target is therefore a promotion of a REGISTER-held short — a multi-def s16 variable (a
branch-merged value like mw), never a fresh memory read. Conversely, if your draft emits the
pair where the target has a plain lh, your operand is living in a register (a temp/var) where
the original read memory — drop the temp. Extends to QImode (extendqisi2 same shape): a real
sll/sra 24 pair = register-held s8.
2. The swapped-arm select tell (cse comparison canonicalization is HALF-complete). b > a
canonicalizes to a < b at expand — the compare slt is SHARED — but the select around it is
not: (b > a) ? b : a and (a < b) ? a : b emit SEPARATE branch+move structures that cse never
merges (byte-measured: each swapped-arm recompute adds its own select code and its own spill
slots, vars up to +112 across a sweep). Forward tell: TARGET code with a re-evaluated select on
an already-computed compare = the source repeated the expression with SWAPPED arms (macro args in
the other order). Reverse tell: your draft emitting duplicate selects the target lacks = your
repeats disagree in arm order with the first mention — make every textual repeat of a ?:
operand-identical.
3. The ?: accumulator-order tell. Expand reuses the FIRST operand's register as a ?:
result accumulator, clobbering it — so in chained selects, exactly the first-operand values must
RE-LOAD when used again later, and second-operand values stay live in registers. From bytes:
which operands freshly re-load in a later pass tells you the operand ORDER of every inner ?: in
the original source, mechanically. (This is how func_8017CE58's max-chain reload pattern pins
its min-chain spelling end to end.)
4. Division-by-constant sign-correction reuse. In s16 / K via magic-multiply, the sign
correction can read the PRE-truncation intermediate (sra $r, (x<<16), 31 — shift counts folded
16+31→31) rather than the extended value. When hand-writing such a division and the sign sra
reads an "impossible" register, the compiler reused the promotion's <<16 intermediate — keep
the promotion as an expression (not a separate temp) so the intermediate exists to reuse.
§173 — THE STORED-PLUMBING RECOVERY RECIPE (P31 T6): symfix-first, per-group isolation, and where the verdicts have no drafts
The pool, honestly derived. tools/plumbing_groups.py (R38: the ledgers already name every
conflict) unions the classified failure ledgers newest-first and keeps still-open rows: the
"1,217 PLUMBING failures" of ledger legend collapsed to 237 still-open — SELF 109 (three
concentrated binaries), CALLEE 48, OTHER 48, DATA 32.
Law 1 — one TU-stage edit poisons every other group's gate (the probe's phantom 0/14). A
recovery stage that edits TU-A (demacroize, tu-scope) while TU-B's drafts gate makes EVERY
whole-binary build compile the edited TU-A: one draft's wrong extern int f() for a
DEFINE_-defined callee failed three other groups' gates with an identical phantom error, and
per-fn attribution smeared. Fix (now in recover_integration): per-group isolation — git-
checkout the binary's TUs to committed truth between groups (gate_lane's proven revert pattern;
engine_core.h untouched so a fleet-tier arity edit persists), apply the TU stages for that group's
fns only, capture banked_from_source per group BEFORE the next group's checkout.
Law 2 — symfix-first. The isolated re-run exposed the true dominant class: undefined reference to D_8018xxxx = the §171 stale-seed-symbol class. rtu_match scores these drafts
MATCH (it is blind to a relocation's target NAME; the R34 two-oracle split exactly as §171
documents), so a "byte-correct, integration-blocked" pile is not gateable until
aprop_symfix --fix (now with the §171-D n:n uniform-delta rule) rebases the seed symbols.
Measured: raw path 0/14 → symfix + isolation + the new stages → 9/14 (64%). The standing
pipeline for stored drafts: aprop_symfix --fix → recover_integration --stages macro-externs,demacroize,tu-scope (per-group isolated).
Law 3 — a ledger verdict without a stored draft routes to the FAMILY lanes, not recovery.
The sweep's biggest group (ov_SC02_037, 44 rows) had zero backlog drafts: its PLUMBING
verdicts came from transient family-sweep remaps that were never persisted. A recovery lane can
only recover what was stored; verdict-only rows are re-derivable by family_sweep/crack lanes
(the atlas already labels them) — do not count them as recovery fuel when pricing (R37).
The new stages (in tools/recover_integration.py): macro-externs (draft-tier §121 — rewrite
a draft's decl of a DEFINE_-defined callee to the macro's own definition head, via
family_sweep.macro_def_sig_map) · tu-scope (binary-tier §103 STU — move a contested file-scope
TU decl into its consumers) · plus the pre-existing demacroize/arity.
§174 — THE ADAPT-CARD WAVE RECIPE (P31 waves A/B, 2026-08-14): prevention beats recovery
The pipeline (measured): adapt cards (§169 emitter) → one cheap drafter per card (haiku ≤50
ins, sonnet ≤120; model routing on the card) → each agent reads the TARGET .s in full, edits the
proven seed body per the card's diff sites, and SELF-VERIFIES with match_one (≤6 iterations,
parallel-safe) → symfix-first audit → gate_lane (single-writer, spread by destination TU) →
targeted propagation. Wave A: 24 cards → 75% standalone → 12 banked (67% gate). Wave B (+1
prompt lesson): 48 → 79% standalone → 35/37 banked (95% gate), ~64–88k tok/bank e2e.
Law 1 — put §171 IN the drafter prompt, not just in the recovery path. "NEVER carry the seed's per-location symbols; spell every symbol from the TARGET .s's own relocations" produced 0 stale seed-symbols across 61 drafts — the class that was 24/24 of S49's gate failures and half of T6's, eliminated at the source for free. Prevention in the prompt beats symfix after.
Law 2 — the decl-matching lesson (67%→95% gate conversion in one wave). Wave A's six gate failures were ALL integration (3× a data extern typed by access width where the destination TU already declared it differently, 1 callee arity, 2 TU-context). The fix, verbatim in the wave-B prompt: before declaring a data extern or callee, grep the destination TU for an existing declaration and MATCH it exactly; only if absent, type by access width. One sentence, −28 points of gate failure.
Law 3 — bank the lesson between waves (the calibrate-then-scale cadence). 24-card wave A priced the lane and named the failure class; 48-card wave B applied it. The lesson transfer is the yield lever — never fire wave N+1 before reading wave N's failure verdicts (R38).
Law 4 — the DEF-side prototype is a wave-prompt law too (P31 wave-C probe, 2026-08-14).
Law 2 tells the drafter to match the TU's decls for callees and data. The probe proved the
same constraint binds the function being defined, and it silently costs the whole bank:
func_80181724 reached standalone MATCH (13 ins) and gated 0/1. Cause: the destination TU
already carried extern void func_80181724(s32, s32); (the §8b carried decl layer), while the
matching definition wanted s16 a0 to emit the entry sll/sra pair — a conflicting prototype,
so the whole-binary build fails while match_one (which compiles the draft ALONE) is blind to it.
This is §20's DEF-side wall arriving through the wave path, and it has a mechanical,
byte-identical answer — keep the TU's canonical signature and narrow at the USE site:
/* WRONG — conflicts with the TU's extern; match_one MATCHes, the gate refuses */
void func_80181724(s16 a0, s32 a1) { func_800291A0(D_80183A40[a0], a1 & 0xFF); }
/* RIGHT — same 13 bytes, no conflict: the cast emits the identical sll/sra */
void func_80181724(s32 a0, s32 a1) { func_800291A0(D_80183A40[(s16)a0], a1 & 0xFF); }
Re-gated 1/1. So the prompt clause is: before choosing your parameter types, grep the
destination TU for an existing func_<YOURADDR> declaration; if one exists adopt it verbatim and
move the narrowing into the body. Prevention again beats recovery — canon_sig_reconcile v3.2
performs this same rewrite after the fact, but a prompt sentence costs nothing and never
re-perturbs a correct draft (the §19 "sig_unify REGRESSES canonical drafts" trap).
Corollary for triage: a standalone MATCH that gates 0 is a declaration fact, never a codegen
fact — look at the TU's decls before touching the body.
Lane economics measured (P31 wave C, so future waves price correctly). The tell lane
(§172b LEN+N cards — functions that already survived one drafting attempt) runs ≈100k tok/card
at a 33% standalone rate (1/3 probe), and its misses are genuine compiler residuals
(register-ROLE swap, schedule cascade) that belong to the permuter, not to more agent effort.
The adapt lane runs ~64–88k tok/bank at 75–79% standalone / 95% gate. Tell cards are worth
running once because each bank is real, but budget the adapt pile first — and route every
tell-lane NEAR straight to the grinder with its named class (a count-exact near like
func_8017DAEC 113=113 with a pure v0/v1 role swap is prime permuter fuel).
Also from the probe: the card's tell is a hint about the SHAPE of the gap, not a diagnosis —
in 2 of 3 the real residual was something else (one draft had omitted an entire array lookup;
one had the promotion already satisfied and was actually a §2 cross-call address-cache
hoist-vs-remat). Re-diagnose from the live match_one diff; do not trust the tell attribution.
Ops notes: the drafter workflow makes NO tree writes (drafts land in .run/wave_*/;
match_one is per-fn isolated) so drafting runs concurrently with the grinder — but the GATE is
single-writer: stop the grinder (STOP sentinel) or wait, and note a finished --once grinder can
leave a lingering wrapper process that fools a bare pgrep (check the log's "once done" line).
The safety classifier can rate-limit under ~48-agent bursts — one agent lost its match_one run
to that (report it unverified; the gate arbitrates anyway). Wave NEARs carry named classes —
enqueue close≤3 drafts straight to the grinder queue as warmstart records.
§175 — A CALLER-SAVED REGISTER PIN CAN BE A CORRECTNESS BUG, NOT JUST A SCHEDULING CHOICE (P31 wave H, 2026-08-15)
register T x __asm__("$2") (or $3, or any caller-saved register) is not a hint — it forces
literal hard-register semantics. If the pinned value is written before a call and read after
it, gcc-2.7.2 is entitled to treat the pre-call store as dead across the call, because the
callee may clobber $v0/$v1. It then silently deletes the store and the post-call read gets
garbage. Nothing warns you; the draft just comes out one instruction short and the diff never
converges no matter how you shuffle statements.
Byte-witnessed: func_80182EB0 (ov_SC02_005). A first-pass draft pinned register s32 v0 __asm__("$2"), set v0 = -1 before func_8012AD44(...), and stored it after. gcc dropped the
addiu $v0, $zero, -1 entirely — mine 49 ins vs target 50, 25 mismatched, and the agent
stalled there. The fix was not a better pin: drop the pin (plain s32 pseudo) and remove the
cross-call live range at the C level — store the constant directly before the call
(*(s16*)(s0+0xAE) = -1;). Instruction recovered, 19 of 25 mismatches closed in one step.
The rule. Before pinning to a caller-saved register ($v0/$v1/$a0-$a3/$t0-$t9), check that the
value has no live range crossing a jal. If it does, either pin a callee-saved register
($s0-$s7 — the §17 call-crossing lever, which is safe precisely because the callee must preserve
it) or restructure the C so the value never crosses the call. A count mismatch of exactly one
instruction, on a draft carrying a caller-saved pin, is this bug until proven otherwise.
Why the gate doesn't save you cheaply: the draft is wrong code, not merely unmatched, so it fails standalone too — you pay a full diagnosis cycle. This is prevention, like §174's laws.
§176a — THE VERIFICATION-LAYER LAWS (P31 overnight, 2026-08-15). What each check can and cannot prove.
These are not matching idioms; they are the rules for believing a matching result. Every one was paid for in gate cycles this session.
1. match_one verifies INSTRUCTION SHAPE, not SYMBOL IDENTITY. It masks jal/HI16/LO16
relocations, so a draft that calls the WRONG FUNCTION or loads/stores the WRONG GLOBAL reports a
clean MATCH. Byte-witnessed: func_8002A234 (14 ins) stored v1→D_80078EE8 and 0→D_80078EE4
where the target does the exact reverse; match_one said MATCH, the whole-binary build differed in
2 bytes, and it cost five gate attempts. Same root cause as the invented PsyQ names in waves F/G
(S80131E00→Square0, Blk20_…→RotMatrixY, SRM_…→RotTransSV).
Rule: after MATCH, re-read the target .s relocation lines and check every symbol you wrote —
each callee, each data symbol, and which symbol each load/store touches. Shape ≠ correctness.
2. A detector is ADVISORY; the whole-binary gate is the ARBITER. aprop_symfix flags
STALE/AMBIGUOUS on LOCAL identifiers (typedef names, inline-asm macro names) and its
draft-symbol extraction misses some extern forms, so a name the draft does declare can still
report asm-only. Three wave-G drafts were withheld on such flags; all three banked unchanged
when finally gated. Never withhold a standalone-MATCH draft on a flag alone — gate it and let
the bytes decide (R39: a refusal check that silently discards good work is worse than one that lets
a few failures through).
3. A verifier that can pass WITHOUT BUILDING is worse than no verifier. gate_main read the
output binary's SHA after make build; when the build FAILED on a compile error the previous
binary was still on disk, so it returned the good hash and reported BYTE-IDENTICAL for a build that
never ran ("43 banked" on a TU that did not compile). The clean-fleet R22 caught it.
Rule: delete the artifact before building, and treat a non-zero build exit as no-hash/never-pass.
4. An all-zeros result is a NULL, not a finding. gate_lane swallows gate_stage's stderr, so
an unhandled corpus.CorpusError surfaced as banked 0, near 0, failed 0 — indistinguishable from
an honest "nothing banked", twice. A real gate always classifies its drafts. If every bucket is
zero, the tool did not run; run gate_stage directly to see the exception.
5. Before believing a measurement, run the control that would make it FAIL. The night's largest false conclusion — "main is blocked by a linker defect", complete with three hypotheses — died to one control: build with NO draft substituted at all. The "defect" reproduced with zero drafts, proving it was the build path, not the code. A null input, a known-answer population, or an independent oracle. R35 says fix the instrument first; this is the sharper form — confirm the instrument can answer, and that it answers correctly on a case whose answer you already know.
§176b — BATCH-GATING MECHANICS (P31): what changes when N drafts land in ONE .c
Gate cost scales with (binary, TU) GROUPS, not with drafts. Each group is one whole-binary
rebuild. Wave D was 42 drafts spread over 23 groups (~40 min of gate); waves F–N were 40–56 drafts
in 1 group. tools/build_wave_atlas.py packs a wave into few TUs for exactly this reason — it
is free throughput, purely a selection change.
Batched drafts must agree WITH EACH OTHER, not just with the file. Each draft is written to
compile standalone, so N drafts bring N independent extern sets into one TU:
- Type conflicts:
D_800A4ED4declareds16by one draft andu16by another;func_8001C9D0asvoid/void */s32across three. C rejects the TU. - Duplicate typedefs: every draft carries its own
typedef struct {…} SVECTOR;. Once one banks, that typedef lives in the.cforever and every later draft collides. Strip duplicates on substitution (harvest_verifyalready did;gate_mainnow does). - Compatibility compares TYPE SIGNATURES ONLY. Parameter names are irrelevant to C — a checker
that compares them wrongly discards good drafts (my first version dropped 2 that way). But the
DECLARATOR SUFFIX absolutely matters:
u8 D_xandu8 D_x[]are incompatible, and ignoring it let a real conflict reach the build (my second version). Too-strict and too-coarse are both defects. - Recovery, not rejection: a conflict-dropped draft is usually CORRECT. Adopt the other
declaration verbatim and adapt at the USE site —
extern u8 D_80076251;+(&D_80076251)[i], orvoid f(s32 a0)+D_x[(s16)a0]— which emits identical bytes (§174 Law 4).
A COMPILE error names its own culprit; only a BYTE mismatch needs a search. Bisecting a batch costs a full clean rebuild per step (a 41-draft bisect ran 28 minutes producing nothing) while the compiler had already printed the symbol and line. Read the error first.
§176d — THE CONFLICT TABLE MUST BE SEEDED FROM THE TU, AND KEYED PER FILE (P31 S52, 2026-08-15)
§176b's batch rule was right but under-scoped: it made drafts agree with each other and forgot the file they land in. Recovering the 11 conflict-dropped main drafts from waves J/K/L exposed both halves of the mistake, and the second one is the expensive one.
(a) Seed the symbol table from the DESTINATION TU, not from nothing. A draft can contradict a
declaration that is already in the .c — put there by a function banked three waves ago. Draft-vs-
draft comparison is blind to it, so the contradiction sails past the checker and surfaces only as a
compile error plus a bisect. Real case: src/800.c has carried extern void func_8001C9D0(void);
since func_8001C2C4 banked, while three wave-J drafts declared the same callee (s32) and
(void *). On the 11-draft recovery slate the TU-seeded check named 7 real conflicts the old
check missed entirely — and it found them iteratively: fixing one draft reveals the next
clash behind it, so re-run the dry run until it reports N -> N compatible, 0 dropped.
(b) Key the table PER DESTINATION FILE. A single slate-wide namespace makes two drafts landing
in different .c files illegally "conflict" over a symbol they are each entitled to declare
their own way. Separate TUs are separate namespaces (R39: over-refusal silently discards good work,
which is worse than letting a failure through to the gate that would catch it).
The recovery lever, extended — cast the CALLEE through a function pointer. §176b/§174-Law-4
covers adopting the TU's declaration for data and for your own parameters. The case they don't
cover: the TU prototypes a callee as taking no argument while your function must pass one. You
cannot pass an argument to a (void) prototype, and you must not change the TU's declaration
(other banked functions depend on it). Cast the function itself at the call site:
extern void func_8001C9D0(void); /* the TU's declaration, verbatim */
...
((void (*)(s32))func_8001C9D0)(a0); /* the call the target actually makes */
gcc-2.7.2 emits the identical jal with $a0 set — a direct call to a named symbol is unaffected
by the cast. All 11 recovered drafts re-verified MATCH after repair, across every variant used:
this function-pointer cast (×3), a pointer-type cast on an argument ((unsigned long *)), signed↔
unsigned data re-declaration (u16/s8, ×3), and array↔scalar (extern u8 D_x; + (u32)&D_x, ×3).
Zero of the eleven needed a codegen change — every one of them was a plumbing repair, which is
the standing P31 finding (matching-is-solved-integration-is-the-bottleneck) showing up once more.
§176e — SYMBOL IDENTITY IS COMPUTABLE OFFLINE (P31 S52): tools/reloc_identity.py
§174 law 1c says match_one masks relocations and therefore cannot see a wrong callee or a wrong
global. That was recorded as a caution to the reader ("check every symbol by hand after MATCH").
It is actually a computation, and it should never have been a human's job:
- the target
.scomment column carries the final linked word, so the true address behind a masked field is recoverable —jal:((w & 0x03FFFFFF) << 2) | (pc & 0xF0000000);HI16/LO16:(hi & 0xFFFF) << 16 + sext16(lo & 0xFFFF); - the draft's own object names the symbol it will bind to;
config/symbols*.txtmaps addresses back to names.
Compare the two and every wrong-symbol draft is named, with its fix, for $0 and no rebuild. On the
88-draft "match_one MATCH but the gate rejected it" pile this returned 12 MISMATCH / 68 AGREE,
and --fix mechanically repaired 10 of the 12 (the other 2 correctly refused).
The two failure shapes it separates, which look identical from the gate:
- Uniform-delta stale seed symbols (§171): every relocation off by the SAME delta —
func_80130D48all five by0xD1EC,func_8016D688all five by0x65450. One rebase fixes the draft. A rename is only safe when every mismatch implies the same corrected base; when a symbol implies two different bases the draft is wrong in more than one way and the fixer must refuse (it did, twice). - Wrong field offset — small deltas (
+0x4,+0xC,+0x10). Same symbol, wrong member.
FOUR TRAPS, all of which bit me while building it — this is a tool with sharp edges:
- Splat-derived names are not in the symbol FILES.
func_800123F0/D_800A4ED4/jtbl_*are generated for everything without a curated name; their ADDRESS IS THEIR NAME. Miss this and the checker resolves nothing, checks zero relocations, and reports clean (R32 — the exact shape of a checker that looks green while checking nothing). - MIPS o32 uses REL, so the addend lives IN THE INSTRUCTION, not the reloc entry.
objdumpprints a bareD_801F9DACfor what is reallyD_801F9DAC+1. Read the addend from your own object's hi/lo immediates, exactly as the linker does — otherwise every struct-field and array access becomes a fabricated "symbol error" (my first run turned one byte-array walk into three). - Index alignment is a precondition. Instruction i corresponds to target i only while the streams are the same shape; one inserted instruction shifts everything after it and manufactures phantom mismatches. Refuse (or mark advisory) when the shapes differ.
- A nearest-symbol label needs a TIGHT window. At 0x4000 the tool cheerfully called
func_8001C9D0"SsGetMute+0xC50". A wrong label is worse than no label — the reader acts on it.
And the honest limit, measured to completion the same session — this is the important half of the entry. Symbol-verified + shape-verified is still NOT sufficient for a bank. Re-gating 20 such drafts across 5 groups banked 1 — 5%, which is statistically the same as the project's existing A10 stored-verdict law (a blind stored-draft re-gate converts at ~0–8%; P31 T1 measured 0/23 on the same kind of pile). So the finding is a clean null with real value:
Symbol verification does NOT improve stored-draft re-gate conversion. The reason a stored "match_one MATCH" draft was rejected is almost never symbol identity — it is TU plumbing (§176d) or simple staleness. Do not spend rebuilds re-gating an old pile because a new filter made it look clean; the lane that banks is fresh drafting, and this oracle's real value is as a pre-gate check on FRESH drafts (where it costs seconds and removes a whole failure class before the rebuild) — not as a resurrection tool for the backlog.
R38 note to my future self: the phase log already recorded the 0/23 result before I ran this probe. The probe was still worth it — the reloc filter was a genuinely new discriminator and "does it move the A10 rate?" was unanswered — but the prior should have been ~8%, not the optimistic read I started with.
§176f — THE DECLARATION FORM IS A MATCHING LEVER, SO RECONCILE TOWARD THE FORM THE MATCH NEEDS (P31 S52)
§176b/§176d treat an in-TU declaration conflict as plumbing: pick one form, cast at the use site, move on. Wave O found the case where that advice is actively wrong.
Six drafts in one TU referenced D_80078D88. Three declared it extern s32 D_80078D88;, three
extern s32 D_80078D88[]; — and one of those drafts carried a comment explaining exactly why:
with
extern s32 D_80078D88;the global load is a plain scalar at asymbol_ref, so sched1 hoisted thelui/lwabove thepkt->uv3store; declaring it as an ARRAY makes gcc-2.7.2'salias.ctreat the access as possibly-aliasing and the hoist stops.
So the declaration form is load-bearing codegen, not style. Resolve such a conflict by asking which form the MATCHES need, then converting the other drafts toward it:
- array is the stronger form — it constrains gcc more, so scalar users can adopt it for free by
indexing
D_x[0](identical bytes: verified on all three here); - the reverse is NOT free — forcing an array user to scalar can re-enable the hoist and break it.
Result: 42/42 drafts compatible, 0 dropped, where the greedy keep-first rule would have dropped 5. Every one re-verified MATCH after conversion. Wave J/K/L each lost 5–10 drafts to this rule, and those were logged as "recoverable with cast-at-use" — for a scalar/array clash the right repair is usually not a cast at all, it is converting the whole TU to the array form.
Procedure (do this before every batch gate):
- dry-run the gate; for each conflict, grep which drafts declare which form and why;
- reconcile toward the form the matches need (array > scalar; the TU's own decl wins over both);
- re-verify EVERY converted draft with
match_one— the conversion is a codegen change, so it is only free if the bytes say so; - re-run the dry run until it reports
N -> N compatible, 0 dropped.
§176g — SIZE A WAVE BY INSTRUCTIONS, NOT BY CARDS (P31 S52 — the adopted doctrine)
The public metric is instruction-weighted, so a wave is worth what its instructions are worth — yet every campaign wave up to N was sized in cards, and drawn from the 12–42-ins cousin piles because those had the best seeds. That is ~1,400 ins/wave ≈ 0.011pp of fleet ⇒ ~440 waves to finish. Wave O carried 6,266 ins at the same gate cost and the same draft rate.
The draft rate barely decays with function size — this is the measurement that makes the whole doctrine work, and it was not obvious in advance:
| wave | avg ins/card | standalone MATCH |
|---|---|---|
| M | 51 | 98% |
| N | 65 | 92% |
| O | 128 | 96% |
So mass is nearly free: the same agent, on a function 2.5× larger, matches about as often. Size the
wave by --target-ins 6500 --min-ins 60 --max-ins 200, concentrated into ≤4 gate groups.
UNKNOWN IS NOT A DIFFICULTY LABEL. It means "the atlas could not name a lever", and it had been routed as needing its own bespoke lane. Wave O ran 22 UNKNOWN cards as an R37 probe: they drafted like any other lane. That is ~138k instructions — a quarter of everything open — reclassified as ordinary wave fuel by a single 22-card probe. Before building a lane for a labelled-hard pool, spend a probe asking whether it is actually hard.
With UNKNOWN included, 9,224 fns / 417,325 ins = 70% of all open instructions are agent- draftable, of which the 60–200-ins mass band alone is 164,357 ins ≈ 27 waves. Work that band first: it is where the instruction-weighted metric moves fastest per agent spent.
THE PRE-GATE PROTOCOL — five steps, each of which earned its place by catching something:
- re-verify every claimed MATCH yourself with
match_one(R14 — self-reports run optimistic); reloc_identity --batchfor symbol identity (§176e), whichmatch_onecannot see;gate_mainDRY RUN, iterated untilN -> N compatible, 0 dropped— conflicts surface one layer at a time and each fix reveals the next;- reconcile declarations toward the form the MATCH needs (§176f), then re-verify every converted draft — a declaration change is a codegen change;
- gate:
gate_main --applyfor main (one clean rebuild per slate),gate_lanefor overlays.
§176h — THE BATCH-SUBSTITUTION HAZARD MAP (P31 S52): seven holes, three wrong fixes, one law
Wave P drafted at 97% (58/60 byte-correct, 6,372 ins, zero symbol errors) and then cost a dozen clean rebuilds to bank. Not one of those rebuilds failed on a matching problem. Every single one failed on N independently-written drafts having to agree with each other and with a translation unit none of them can see. This section is that failure surface, mapped, so the next tool does not rediscover it.
A. The seven under-reporting holes (all in gate_main, all the same shape)
Each looked green while reading LESS than it claimed — R32's exact failure mode, seven times in one tool, because nobody had ever asserted the tool's coverage, only its verdicts.
| # | The checker never read… | Symptom |
|---|---|---|
| 1 | the destination TU's own declarations | draft contradicts a decl already in the .c; found only by the build |
| 2 | shared headers (engine_core.h's DEFINE_ macro bodies declare symbols!) |
file-scope array decl illegal against a macro's block-scope scalar |
| 3 | a draft's OWN function definition | the DEF-side wall: s32 func_X vs the TU's void func_X |
| 4 | typedefs/decls on lines with a trailing comment (;\s*$ anchoring) |
the commonest spelling of all; agents comment nearly everything |
| 5 | typedef aliases (short ≡ s16) |
R39 over-refusal: good drafts discarded as "conflicting" |
| 6 | the build's actual error text (bisected instead) | a full rebuild per step to rediscover what the compiler already printed |
| 7 | draft order vs FILE order | typedef stripped in slate order lands below its user |
Law: a batch-integration tool must be audited for what it DOESN'T look at. Its verdicts can be 100% correct on the inputs it reads and still be worthless, because the compiler reads more.
B. Typedef handling — the only strategy that survives contact
Each draft compiles standalone, so N drafts bring N copies of SVECTOR/SVEC8/OtBlk/Rec14.
Three strategies were tried; two are wrong and both look right:
- ❌ strip every duplicate — assumes the surviving definition sits ABOVE the insertion point.
It often does not (
src/800.cdefinesRec14at line 7336 while stubs wanting it sit at 7272 and earlier). Stripping then leaves the name undefined ⇒ implicit-int ⇒ a collision at the real declaration, reported asprevious declaration of D_800A4640— nowhere near the actual cause. - ❌ rename every duplicate — breaks drafts that share an IDENTICAL typedef: give each its own
name and their
extern <T> D_x[]declarations stop agreeing. (I shipped this; it broke three drafts at once, and it created a conflict the batch did not have.) - ✅ body-aware + position-aware, single pass:
- identical definition visible above the insertion point → STRIP and reuse the name;
- same name, different definition → RENAME (a typedef name is private to the draft, so the rename cannot move a byte);
- definition exists only below → never reuse it; keep the draft's own.
Recompute "visible above" per draft against the current text — the file grows with every
substitution, so offsets captured once go stale and mark a below-definition as reusable.
And do it in ONE pass over a snapshot: a rescan loop finds the definition it just renamed, calls
it a duplicate of itself, and deletes it (
parse error before '*'). That was my third wrong fix.
C. The limit that remains (recorded, not solved)
Conflict detection compares spelled type names. Three drafts each defining their own Slot54
with different layouts all declare func_80032A74(Slot54 *, …) and compare EQUAL. The real fix is
comparing struct LAYOUTS for locally-defined types. Until then, a rename can expose such a
conflict (which is a service) but the checker cannot predict it.
C2. RECONCILE BEFORE THE FIRST GATE — a parked draft gets HARDER to bank, not easier
The obvious plan after a wave is "bank the clean ones now, recover the conflicted ones later."
That plan is backwards, and it was measured: of 18 wave-O/P drafts parked and re-verified still
MATCH, only 1 survived resolve_conflicts once their wave had banked — versus 5 before it.
The mechanism is simple once seen. A banked draft's declarations become the TU's. So every
parked draft that disagreed with a SIBLING now disagrees with the FILE, which is the stricter
arbiter: sibling-vs-sibling can be settled by editing either side, but file-vs-draft can only be
settled by editing the draft (and gate_main reverts src/ before every build, so some cannot be
settled at all — see the immovable-declaration bucket). Worse, the auto-rename that reconciles a
cosmetic clash pre-bank turns into a DUPLICATE TYPEDEF post-bank, because the name it renames
to is now defined in the file too.
So: iterate the dry run to N -> N compatible, 0 dropped BEFORE spending the first rebuild.
Every draft dropped from slate #1 is worth more effort than it looks, because slate #2 will be
harder. Budget the reconciliation into the wave, not after it.
Corollary for the auto-reconciler: distinguish a COSMETIC clash from a REAL one by comparing struct
bodies, not names (OtBlk_80015498 vs OtBlk_80016450 are the same {s32 a; s32 b[4];} and
rename byte-identically; Elem12 vs B12 genuinely differ and must not be merged). That body
comparison is also the fix for §176h.C's spelled-name limit.
D. The measured cost shape, and what to build next
Drafting is cheap and solved; integration is expensive. Wave P: ~10M agent tokens produced 58
byte-correct functions on the first pass; banking them took a dozen 5-minute rebuilds and seven
tool fixes. So the next lever is NOT a bigger wave — it is a static pre-gate check: substitute
into a scratch copy and assert, on the text alone and with no make at all,
(1) no typedef used above its definition, (2) no type referenced that nothing defines, (3) no
draft extern contradicting the file, (4) no draft definition contradicting a prototype, (5) no
two drafts declaring one symbol differently. Every failure in this section was visible in that
text. Two seconds a look instead of five minutes.
And R39 applies to the tool you are fixing, not just the tool you are shipping. I spent the day adding negative controls to everything and then made three regressions editing a live tool between rebuilds without running one. Mask comments before any "is X used before Y" scan, too — a check that counts mentions inside comments reports 7 phantom failures (and one that only inspects names that ARE defined is blind to a name you deleted outright: my own R32 hole, inside the control I wrote to catch R32 holes).
§176i — WHAT A STATIC PRE-GATE CHECK CAN AND CANNOT PROVE (P31 S52, wave Q)
tools/pregate_check.py validates a slate in 0.7s instead of a 5-minute rebuild, and wave Q
was the first slate all session to reach the gate already reporting clean. It then failed the
build twice. Both failures are outside what any text-only check can see, and knowing that
boundary is the point of this entry — a tool whose limits are unknown gets trusted past them.
What it proves (all five checks are properties of the substituted text): typedef used above its definition · type never defined · duplicate typedef · one symbol declared two incompatible ways · definition contradicting a visible prototype.
What it CANNOT prove, with the wave-Q evidence:
- LINK-time undefined references. Wave Q died on
undefined reference `.L80050F24'. That label lives insidegfx2D_BG0_OBJ_698(0x80050EA4) and another function's.sbranches to it. Converting a function to C DELETES the local labels its neighbours jump to. No amount of reading the.creveals this; it is a property of the whole link. This one IS statically checkable, just not from the text: scan every other.sfor label references landing inside the candidate's[addr, addr+4·nins)range and refuse those candidates. Worth building — it is the split-file/jump-table class in a new costume. - BYTE mismatches. With the link fixed the binary BUILT and the SHA differed, i.e. a draft
that
match_onecalls MATCH is wrong in a way only the whole-binary gate sees (§174 law 1c).reloc_identityhad already named six suspects — includingfunc_80034C24storing toD_80078F20where the target referencescdReq_sectorHdrBuf+0xE0— which is exactly the division of labour to rely on: the text checker for shape, the reloc oracle for identity, the gate for truth.
Corollary — a clean pre-gate is a licence to build, not a prediction of success. It removes the failure modes that are cheap to remove. Budget one gate attempt for the ones that are not, and when the binary builds but the hash differs, BISECT: it costs wall-clock and zero tokens, which is the right trade whenever agent budget is the scarce resource.
§176j — STOPPING A WAVE MID-FLIGHT COSTS THE IN-FLIGHT TAIL (and how much is recoverable)
Wave Q was stopped early to save tokens. Measured consequence: 51/90 drafts verified MATCH (3,631 of 6,249 ins) against the 96–97% the same pipeline produced when allowed to finish.
But the loss is suspended, not destroyed — every draft persists on disk, and the stopped agents' partial work is closer than it looks:
| closeness | fns | ins |
|---|---|---|
| ≤10 | 15 | 833 |
| 11–30 | 10 | 729 |
| 31–60 | 10 | 699 |
| >60 | 4 | 357 |
Do NOT resume the workflow to recover this. resumeFromRunId replays cached agents and re-runs
the unfinished ones from scratch with the original prompt — full cost, no memory of their partial
work. The cheap path is a repair-only pass: feed the existing draft plus its diff to the
repair prompt (which is written to start from a draft, not from the .s), scoped to the ≤30-closeness
band. Most of those need a statement moved, not a decompile.
The decision rule worth keeping: before killing a long agent run, price the tail. If the median in-flight draft is near-matching, the tokens are already spent and stopping converts them from "nearly banked" into "needs a second, cheaper pass" — which is fine, but it is a deferral, not a saving.
§176j-2 — THE REPAIR PASS, MEASURED (do this instead of resuming)
Wave Q's 39 unfinished drafts were run through a repair-only workflow: one stage, no draft phase,
each agent handed its own on-disk draft plus that draft's measured closeness, with the prompt
opening THIS IS A REPAIR, NOT A REWRITE. Model routing deliberately cheap (6 haiku / 29 sonnet /
4 opus — opus only for the four >130-ins functions).
Result: 12 of 39 recovered, 579 instructions, taking wave Q from 51 verified matches (3,631 ins) to 64 (4,245 ins). Roughly a quarter of a stopped wave's tail comes back for a fraction of a fresh wave's cost.
Two calibration notes for next time:
- Closeness must be counted, not read off the first differing index. My first measurement sorted by the index of the first mismatch and reported six drafts at "closeness 0"; they were truncated drafts (agent stopped mid-write) that matched to instruction 35–48 and then simply ended. Count the differing instructions.
- The yield concentrates in the small-residual band. Of the 12 recovered, most came from the ≤15-differing-instruction band; the 35–40 band mostly stayed stuck (and much of what remained turned out to be §177's epilogue rule, not per-function work at all).
§176k — TWO SELECTOR BUGS THAT SILENTLY SHRINK A WAVE
Both found while building wave Q, both silent, both would have quietly cost instructions forever:
- Ranking gate groups by MEMBER COUNT collapses a wide band to the smallest functions.
build_wave_atlasranked(binary,TU)groups by how many candidates they held — correct for overlays, where each group costs its own rebuild. Formainthe gate cost is per SLATE, so that ranking filled the wave from the biggest-by-count group, which is the smallest-by- instruction one: measured 60 cards / 2,604 ins selected when 46 cards / 4,829 ins were available. Fixed with--rank mass. Whenever a selector ranks by a proxy, check the proxy still means what it meant when the cost model was written. - A selector that globs its own output poisons itself. Deriving the already-waved set from
glob('.run/wave_*_cards.json')matched the file the run was about to write, so re-running with identical filters counted the previous attempt's cards as spent: candidate pool 106 → 46. Fixed by excluding the output path. Any derive-from-disk rule (R33) must exclude the artifact it is about to produce.
§177 — 🔴 THE EPILOGUE RETURN-DELAY SLOT IS DECIDED BY YOUR SAVED-REGISTER SET, NOT BY SCHEDULING
(P31 S52 — source-confirmed in gcc-2.7.2/config/mips/mips.c; eleven functions were stuck on it)
The symptom. A draft sits at closeness 1–3 with the differing instructions clustered in the
epilogue: the target fills the jr $ra delay slot with a real body instruction while your draft
emits addiu $sp,$sp,N there (or the reverse). Wave Q's repair pass produced eleven of these
in the 800c/800c3 regions, and every agent independently filed it as an intrinsic scheduling
wall — "epilogue-delay-slot-unfillable", "gcc/maspsx structural". It is not a scheduling problem
and it is not a wall. It is a frame-shape problem, and it is steerable from C.
The rule, verbatim from mips.c:5376:
int mips_epilogue_delay_slots () {
if (current_frame_info.total_size == 0) return 1; /* no frame */
if (current_frame_info.mask == RA_MASK && current_frame_info.fmask == 0) return 1; /* only $ra */
return 0; /* otherwise */
}
So gcc-2.7.2 offers the epilogue a delay slot only when the function either allocates no stack
at all, or saves nothing but $ra (no callee-saved $s registers, no FP registers). In every
other case it returns 0, the slot is not offered to the scheduler, and the emitter puts the stack
restore there instead (mips.c:5276, the tsize > 0 path).
⛔ CORRECTED BY §188 (P31 S53) — READ THAT FIRST IF YOUR TARGET RESTORES 2+ REGISTERS
The table below is inverted for the multi-restore case. If the target's tail is
jr $ra+addiu $spwhile two or more callee-saved registers are restored just above it, that shape is not gcc's at all — it is GNUas -O2filling the return delay slot, and cc1 cannot emit it for any$s-saving frame (mips.c:5081/5174/5204: the only branch that putsj $31before the stack restore is the one whereload_only_r31holds). Saving an$sregister is precisely what makes that shape impossible, so "keep a value live across a call" is the wrong lever there. Row 2 applies only to the$ra-only / frameless case the rule above derives. This is why the S53 §177 lane converted 4 of 16. Usetools/oracle_reorder.pyto tell a C defect from an assembler artifact before spending an agent on it.
Therefore the lever is the CALLEE-SAVED SET:
| target does | means | your draft must |
|---|---|---|
jr $ra + a body instruction in the slot |
frame is $ra-only (or zero) |
need no value live across a call — no $s registers |
jr $ra + addiu $sp,$sp,N |
frame saves $s regs |
keep at least one value live across a call |
How to steer it in C (cheapest first):
- Fewer values live across calls. Recompute a value after the call instead of holding it; read
it back from the struct/global it came from. Each value whose live range spans a
jalcosts one$sregister, and the first one flips this switch. - More values live across calls, for the opposite direction: hoist a load above the call and use it after, instead of reloading.
- Only then consider register pins — and remember §176-C: a pin cannot schedule across a call, so pinning is the wrong tool for this residual entirely.
Why this matters beyond the eleven. They are ~600 instructions sitting three instructions from
banked, and they were all about to be written off as intrinsic. A residual that eleven independent
agents call "structural" is a signal to read the compiler, not to file a wall (R17): the answer
was forty lines of mips.c and it was already sitting in tools/reference/gcc-2.7.2/.
§178 — SIX LEVERS MINED FROM THE WAVE-P JOURNALS (P31 S52), each byte-proven and source-cited
Wave P's repair agents did something the campaign has rarely got: they read cc1 -dS/-da dumps and
then the gcc-2.7.2 source, and four of them refuted the first pass's own diagnosis. Every lever
below took a draft to MATCH; every one names the file and line that explains it. The recurring
meta-finding is stated first because it is worth more than any single lever:
"REGALLOC-PERM" is the most over-diagnosed class in this project. In four separate wave-P functions the visible symptom was a register swap and the actual cause was in
cse.corsched.c, decided before allocation — which is exactly why pins, declaration order and statement order all failed on them. When a pin sweep plateaus, stop pinning and dump the pass.
A. THE $0-ADD OPAQUE COPY defeats make_regs_eqv (func_80033398, 93 ins)
Symptom: srl $s6,$s7,16 where the target has srl $s6,$a0,16 — every pin combination left it
bit-identical. Cause (cse.c:826, make_regs_eqv): when the second pseudo of a copy pair outlives
the first and its live range escapes the cse block, it is head-promoted to qty_first_reg, and
canon_reg rewrites every later use of the parameter pseudo to it. No C spelling of a plain
copy escapes this. The lever:
register s32 zr __asm__("$0");
s7v = arg0 + zr; /* emits `addu $s7,$a0,$zero` — but the RTL is a PLUS, not (set reg reg) */
s6v = arg0 >> 16; /* so the srl still reads the parm pseudo, which dies here */
Because it is a PLUS, make_regs_eqv never merges the quantities; local-alloc's copy suggestion
then hands the parm $a0 and deletes the real copy as a no-op. MATCH on the first compile.
Bonus: with the opaque copy in place, 3 of the draft's 5 register pins became dead weight and were
removed — worth re-minimising pins after any cse-level fix.
B. A return <const> IS A PRIORITY-1 HARD-REG SET THE SCHEDULER PLACES FIRST (func_8001BE30, 92 ins)
Symptom: a clean $v1-for-$v0 swap on throwaway temps; six levers failed. Cause, read from the
-da dumps: (set (reg/i:SI 2 v0) (const_int 0)) — the return 0 — is a hard-reg set with
REG_N_SETS(reg 2) > 1 (the function has six return sites), so birthing_insn_p/adjust_priority
give it no boost; it sits at priority 1 while neighbours outrank it, and gcc-2.7.2's backward
list scheduler picks it last, i.e. emits it FIRST in the block. Hard $v0 is then live across
the temp's whole range at local-alloc time, forcing the temp to $v1. sched2+dbr later move
move $2,$0 into the j delay slot — which is why the SHAPE looked right while the register
stayed wrong.
Lever: delete the hard-reg return set from the block. Replace each in-block return 0; with
goto L_ret0; to one shared L_ret0: return 0; tail. The blocks then contain no set of hard $v0,
the temps take $v0, and dbr steals the shared addu $v0,$zero,$zero back into each delay slot.
C. SINGLE-SET TEMPS GET THE BIRTHING BOOST (func_8001D3FC, 196 ins)
birthing_insn_p (sched.c:2469) boosts an insn only when its destination has exactly one
static set (reg_n_sets[dest]==1, the discriminator at :2490). A three-set temp
(ub = expr; u = ub; ub = ub + w - 1;) gets no boost, so its whole chain is picked late and placed
early. Splitting off a genuinely single-set temp boosts the insn and drags its feeder chain down
with it. Byte-proven that the boost must land on the insn you care about: splitting one step
earlier reverted the schedule.
D. A NARROW TYPE BLOCKS COPY ELISION (func_8001D3FC — new idiom)
Once a temp is single-set, a plain same-mode u = ut copy is deleted by cse/coalescing, and u
becomes ut — which is what produced the "$a0↔$v1 swap" the first agent called irreducible.
Declaring the destination narrower (u16 u;) makes it an SI→HI mode-changing copy that cse
cannot propagate through and the allocator cannot coalesce, so the copy survives at its source
position and the entire register assignment falls into place. One type change, ~20 instructions.
E. THE ZERO-OFFSET ALIAS HOLE (func_80037028, 71 ins)
sched.c:memrefs_conflict_p, PLUS-vs-PLUS branch, falls through to find_symbolic_term(x/y) and
reports no conflict when the two symbols differ — but that path is only reachable for the plain
(plus reg symbol_ref) address form, i.e. a field at offset 0. A field at a non-zero offset is
(plus reg (const (plus symbol N))) and comes back conservative. Consequence: an offset-0 store
silently loses its dependence on later loads and floats to the bottom of the block. If a store at
offset 0 is scheduled wrongly, that is why — and giving the struct a non-zero-offset field to touch
restores the dependence.
F. MEM_IN_STRUCT_P ASYMMETRY IN true_dependence (func_80037144, 124 ins)
sched.c:817 skips a dependence when x is /s with a varying address, non-QImode, while
mem is non-/s at a fixed address. So D_800A463C[k].unk00 (struct, varying) does not
depend on plain scalar D_8007622C stores. Model your externs accordingly: struct-vs-scalar is a
scheduling decision, not cosmetics.
G. TWO MODELLING TRAPS THAT COST THESE AGENTS SWEEPS OF HUNDREDS OF COMPILES
sw $a1, D_80076244($a0)is ONE cc1 insn (symbol + scaled index). Thelui/addu/storetriples you see in the.sare gas-G0macro expansion, not cc1 output. Model at the cc1 level or you will chase a phantom. Only a struct array indexed by the slot produces that form; six parallelextern u8 D_800762xx[]make every access a distinctSYMBOL_REFand change the whole dependence graph.__asm__ __volatile__("" ::: "memory")is a FULL barrier — it clobbers all pseudos, so it sinks address chains below stores and can kill a delay-slot steal. When you only want memory ordering, that is the wrong tool; an empty non-volatile__asm__("")was the one that worked as a pure optimization barrier elsewhere (§B above, and the STORE_FLAG_VALUE fold defeat).
Statement-order sweeps are frequently worthless here and the agents proved it by exhaustion: a 2,240-variant sweep and a 5,040-permutation sweep each moved nothing, because the schedule was fully DAG-determined. When order does not matter, the answer is an alias or a set-count property, not a permutation.
§179 — IDIOMS MINED FROM THE WAVE P/Q JOURNALS (P31 S52, harvest pass)
Twelve readers mined the wave-P and wave-Q agent journals; 16 candidate findings survived their
novelty filter, and merging duplicates + dropping what §174–§178 already own leaves the eight
levers below. Every gcc/tool citation here was re-read against tools/reference/gcc-2.7.2/,
tools/maspsx/, and tools/masked_diff.py in this pass — three line numbers were wrong in the raw
findings and are corrected inline (marked ⚠). Ordered by how many functions each unblocks.
§179-A — 🔴 A LOOP-WALKED POINTER PARAMETER HANDS ITS ARGUMENT REGISTER TO THE GIV (9 byte-proofs)
Symptom. A NEAR whose only residual is a register rotation around a pointer loop: the draft has
a spurious move $tN,$aM that the target does not, the offset-K access lands on the argument
register $aM, and the base cursor lands on a scratch temp — the exact mirror image of the target's
allocation. cc1 -dg shows the argument register in the conflict set of the cursor pseudo
(N conflicts: … 7, M preferences: 7 for the $a3 cases).
Mechanism (all lines re-read and confirmed). When a pointer parameter is used undecorated as
a loop's biv and the loop also touches a fixed offset off that same pointer (so gcc mints a giv),
record_initial (loop.c:6327) records the biv's defining insn — which is assign_parms' own
incoming-argument copy (set P (reg $aN)) — as bl->init_set. At loop.c:3500 src = SET_SRC (bl->init_set) is therefore the hard register itself; valid_initial_value_p
(loop.c:4120, called at loop.c:3509) accepts it, since it returns 1 for CONSTANT_P or any
GET_CODE == REG (hard regs pass its REGNO < max_reg_before_loop test), given no intervening
call. bl->initial_value = src (loop.c:3511). Then emit_iv_add_mult (bl->initial_value, …, loop_start) (loop.c:3879) emits the giv's preheader init as new_giv = $aN + K — a fresh
reference to the hard register, inserted after P's own copy-from-$aN. That extends $aN's live
range past where P's copy would have let it die, $aN and P become simultaneously live, P loses the
hard-reg tie, the giv inherits $aN, and the dead move survives.
C lever — two spellings, both zero-byte, pick either:
/* 1. body-local copy: rename the parameter, route EVERY in-loop access through the copy */
void f(T *param0, …) { T *p = param0; for (…) { … p->fld … p++; } }
/* 2. identity re-tie, placed in the preheader BELOW the n!=0 guard so it can't eat the
guard branch's delay slot */
if (n) { __asm__("" : "=r"(p) : "0"(p)); do { … } while (--n); }
Spelling 2 works by the defeat route: the init insn's SET_SRC becomes an ASM_OPERANDS rtx,
which is neither CONSTANT_P nor GET_CODE == REG, so valid_initial_value_p returns 0,
bl->initial_value falls back to the freely-allocatable pseudo, and $aN is released.
Evidence (all MATCH). func_800262D8 (144→143, dead move t1,a3 removed), func_8002528C,
func_80025000 (164/154-wrong → 163/4-wrong from this lever alone), GsTMDfastF3GL,
func_80025EB8, func_80025A30 (re-tie spelling), func_80025504 (197 ins, poly pointer vs $a3),
func_80024DE8 (134/134, -dg confirmed the $a3 conflict), func_80025818.
Relationship to §70 (L5639) — read this before applying. §70 documents the same chain used in
the opposite direction: when the target wants the giv based on the argument register, walk the
parameter directly and do not copy it. §179-A is the mirror. Decide by what the target's giv is
based on, then choose: raw parameter (§70) vs. local copy / re-tie (§179-A).
Honest limit: why a second-level copy does not simply recreate the identical chain one level
removed was not re-derived from assign_parms' pseudo lowering; it rests on the source-verified
general mechanism plus nine consistent byte-proofs.
§179-B — 🔴 THE HAND-WRITTEN-ASM TRANSCRIPTION CHECKLIST (maspsx + masked_diff; 10 byte-proofs)
Symptom. A splat-flagged handwritten / PsyQ LIBGS routine transcribed as inline asm comes out
+N instructions (LENGTH-DRIFT), or the build dies with MASPSX FAIL: invalid literal for int() with base 10: '0x18', or masked_diff reports a huge mismatch on a body you copied verbatim.
Five rules, each source-verified, each a byte-miss or a crash if violated:
.setdirectives need a literal TAB.maspsx/__init__.py:844-848iselif line.startswith(".set\t"): if line.endswith("\tnoreorder"): self.is_reorder = False; elif line.endswith("\treorder"): self.is_reorder = True. A space-separated.set noreordermatches neither test, is forwarded toasunchanged, and leaves maspsx's tracker at its defaultTrue— after which it auto-appendsnop # DEBUG: branch/jumpafter every branch/jump it emits (:1054-1057, gated onis_reorder), clobbering your hand-filled delay slots. Write".set\tnoreorder\n"..entdoes NOT update maspsx's state.:856-859appends.set\tnoreorderto the output stream but never touchesself.is_reorder. Never rely on it; write your own tab-formed directive.- Every displacement and immediate in DECIMAL.
:970and:1038both call bareint(operand)(base-10 default) on the raw offset string before the magnitude comparison short-circuits, solw $2,0x18($3)throwsValueErrorregardless of range. Writelw $2,24($3). (.wordconstants are unaffected and may stay hex.) - maspsx's LOAD-delay nop is UNCONDITIONAL — do not write it yourself.
_handle_nop_before_next_instruction(:642-675, called from:908,931,964,991,1137,1181) contains nois_reorderreference anywhere in its body or call sites — unlike the branch-delay path, which is explicitly gated. So even inside.set\tnoreorder, alw $v0,56($t1)followed by a consumer of$v0still gets maspsx's own nop spliced in. This is the asymmetry that bites: you manage branch-delay nops yourself; you must NOT manage load-delay nops. - Prefix internal labels with
.L.masked_diff.py:34is_HDR_RE = re.compile(r"^[0-9a-f]+ <([^>]+)>:"), andinsns_from_object's collection loop (:141-146) setsinfn = (fn is None) or (h.group(1) == fn)on every objdump<label>:header. A plainsel_C94:becomes a real symtab entry that objdump prints mid-function, flippinginfnto False and silently truncating the rest of the function from the diff — a spurious byte-mismatch with no codegen cause. Write.Lsel_C94:.
⚠ Conflict with the already-banked trampoline rules (L1848-1858). That entry's rule 2 says "no
trailing .set reorder — it emits a stray epilogue nop." The wave-P/Q GTE bodies found the
opposite: close with a tab-formed ".set\treorder\n" as the last asm line so maspsx restores
its default and supplies the one nop gcc's auto-generated jr $ra epilogue needs. Resolution:
the trampoline's body ends in a real instruction gcc's epilogue can use, the GTE bodies do not. If
you are ±1 nop at the very end, flip this one lever and re-gate; it is the cheapest A/B in the file.
Evidence (all MATCH). func_80027200 (133→125 via rules 1+3), func_80025CBC (127 ins,
first-pass), GsTMDfastF4GL (162/162, 648/648 raw bytes), GsTMDfastG3GL (624/624 raw bytes),
GsTMDfastF3GL (129/129 words), GsTMDfastG4GL (rule 4: hand-written nop gave 155/156 LENGTH-DRIFT
−1; omitting it gave 156 + 624/624 bytes), func_8005D8B4 (43→3 mismatched), func_80059760,
func_80059234, MoveImage. The mechanism is also embedded as a code comment at
src/800b2.c:491-500.
§179-C — 🔴 A FUNCTION WITH NO EPILOGUE (falls into a sibling's shared tail) MUST BE FILE-SCOPE __asm__
Symptom. The target's disassembly for a symbol has zero trailing jr $ra — it ends
mid-basic-block, or every exit is a raw unlinked j SOME_OTHER_LABEL with live values in argument
registers, and the immediately-following symbol is nothing but a lw $ra / addiu $sp / jr $ra tail
restoring this function's frame offsets. Every C shape you try comes out exactly +2 instructions
(a phantom jr $ra / nop).
Mechanism. gcc-2.7.2 has no sibcall / cross-function tail-merge pass (absent from the pinned
config/mips/mips.c), and every function whose body cc1 compiles goes through normal function-exit
expansion (function.c:5224 expand_function_end, return emission at :5432-5437) with
function_epilogue (mips.c:5072) emitting the teardown text at final time. noreturn, a bare
tail j, and a function-body __asm__ __volatile__ all fail to suppress it — the append is a
property of the enclosing C function, not of the asm's RTL.
C lever. Write the whole thing as top-level (file-scope) basic asm, outside any C function, carrying its own directives — cc1 never runs function-expansion over it:
__asm__(".text\n.align 2\n.globl NAME\n.ent\tNAME\n"
"NAME:\n.frame $sp,N,$31\n.mask MASK,-8\n.fmask 0,0\n"
".set\tnoreorder\n"
"<body, decimal offsets, .L labels>\n"
".set\treorder\n.end\tNAME\n");
Three sub-rules: (a) use literal .ent\t / .end\t text — glabel (include/labels.inc:7,
include/macro.inc:10) is an assembler .macro, expanded by as after maspsx runs, so maspsx
never sees .ent and leaves reorder mode on, scrambling your delay slots; (b) in file-scope asm %
is literal, so write %hi/%lo un-doubled (the %%hi escaping of L1858 applies only to
function-body operand-template asm); (c) if the following symbol is the shared tail, emit a second
real .ent <nextsym> / label / .end <nextsym> immediately after this function's .end — the
harness's objdump-header boundary walk (§179-B rule 5, same _HDR_RE) then attributes exactly the
target's instruction count to each symbol. That is the same tooling fact used deliberately instead
of tripped over.
Bonus: raw .word bodies. When mnemonic transcription is hostile, a straight list of
.word 0xHHHHHHHH copied from the target's own words works — but stop the list before the
target's own closing jr $ra/nop: gcc supplies the frameless-leaf epilogue itself, so including
them is +2. (GsTMDfastF4GL: all 162 words → LENGTH-DRIFT +2; words [0..159] → MATCH at 162 ins,
648/648 bytes.)
Evidence. func_80059234 (71/71 real target instructions; a function-body noreturn asm left 2
dead trailing instructions, a glabel-based file-scope attempt produced a garbled 79-ins object),
func_80059FC0 (MATCH, size 0xA8, two raw j SYS_OBJ_E34 tails with $v0/$v1 live-out and no
$s0-$s3/$ra restore), MoveImage (42/42, closeness 0; all 7 referenced symbols verified against
the target's own relocation lines).
Follow-up worth a pass: func_8005BD7C, func_8005C1C0, SetGraphReverse are all NEAR in the
same slice and all diagnose this exact root cause without finding the fix. They are likely
re-crackable with §179-C verbatim.
§179-D — gte_stflg MUST CLOBBER "$12" OR THE WHOLE TEMP FILE ROTATES BY ONE
Symptom. The draft uses $t4-$t8 where the target uses $t5-$t9 — a clean one-slot shift of the
entire temp register file. Tell: the target never uses $t4 even though $t4 appears in the
cfc2.
Mechanism. gte_stflg reads the GTE FLAG register with a cfc2 hardwired to physical $12
(= $t4 in the o32/PSX ABI), outside its declared operands. With "$12" missing from the clobber
list, gcc treats $t4 as allocatable, parks a live pseudo (a loop counter, in both exemplars)
there, and the cfc2 destroys it — and the allocation order cascades from that point, shifting
every later temp.
C lever. Any inline-asm / raw-.word GTE macro that writes a fixed physical COP2-adjacent
register outside its operands must list it by number in the clobber list: : "$12".
Evidence. func_80025504 (MATCH, 197 ins — counter had been placed in $t4), func_80024DE8
(MATCH, 134/134). No gcc-source claim is made here; this is byte-evidence + ABI.
§179-E — A >2*MAX_MOVE_BYTES BLOCK COPY IS A STRUCT ASSIGNMENT, NOT A HAND LOOP
Symptom. The target shows [compute end address] + [loop: 4-word load/store group, compare, branch] + [straight-line leftover load/store group]. Element-by-element for loops and
hand-unrolled pointer-diff variants regress to 54–64 instructions with garbled 2-word sub-splits.
Mechanism. mips.c:2216-2217 defines MAX_MOVE_REGS 4 / MAX_MOVE_BYTES (MAX_MOVE_REGS * UNITS_PER_WORD). expand_block_move (mips.c:2331) dispatches any compile-time-constant,
word-aligned BLKmode copy larger than 2*MAX_MOVE_BYTES to block_move_loop (mips.c:2222, which
abort()s below that size at :2237). It computes leftover = bytes % MAX_MOVE_BYTES (:2240),
emits one movstrsi_internal-driven loop with a computed end-address compare/branch, then a
single straight-line movstrsi_internal for the leftover (:2286-2289). This is a different path
from the already-banked ≤2*MAX_MOVE_BYTES single-shot case (L2428) and the -O0 memcpy
library-call case (L2531). unroll.c is dead at -O2, so no hand loop reproduces it.
C lever.
typedef struct { s32 words[N]; } BlkNN; /* N sized to the exact byte count */
extern BlkNN SRC, DST;
void f(void) { BlkNN buf; buf = SRC; DST = buf; }
Evidence. func_80029274 → MATCH (42/42) after the struct rewrite.
§179-F — PINNING A LOOP-WALKED POINTER IS A TOTAL OFF-SWITCH FOR STRENGTH REDUCTION
Symptom. The target keeps a walked pointer as a single un-combined biv (addiu $s0,$s0,0x54
plus full literal field offsets at every access), and every C-source-order trick and asm fence still
lets gcc split it into two givs.
Mechanism. loop.c's IV machinery considers pseudos only. The biv scan requires
REGNO (dest_reg) >= FIRST_PSEUDO_REGISTER before calling basic_induction_var (loop.c:3301),
and the giv-collection loop does if (REGNO (dest_reg) < FIRST_PSEUDO_REGISTER) continue;
(⚠ loop.c:3572-3573, not :3571 as the raw finding claimed — :3571 is the dest_reg = SET_DEST (set); above it). So a hard-register pin makes the pointer categorically invisible to
combine_givs/strength-reduction, not merely de-prioritized. This is strictly stronger than the
banked zero-byte-asm anti-dissolution launderers (L2483), which only perturb the benefit heuristic.
C lever. register Ent *p __asm__("$16"); on the walked pointer — using the register the target
actually uses.
Evidence. func_8003324C → MATCH (54/54). Ablation: without the pin, p splits into a +0x2A0
call-arg giv and a +0x2EE address giv → 58 ins / 44 mismatched; with it, back to the single
addiu $s0,$s0,0x54 walk.
§179-G — 🟡 A PIN CAN CREATE A COMBINE LOG_LINK AND DELETE AN andi (sixth RC-5 channel, n=1)
Symptom. The target retains an andi $sN,$fp,0xFFFF narrowing mask; your draft folds it to a
bare move and is one instruction short — and you currently have a pin on the mask's consumer.
Mechanism. combine's LOG_LINKS never cross basic blocks (already-banked law, L2489, flow.c:2087),
so a (u16)y mask survives whenever y's def and the andi's consumer sit in different blocks —
combine has nothing to hang a link on. A register T x __asm__("$21") pin inserts a same-block
hard-reg move, which manufactures exactly that missing LOG_LINK; nonzero_bits reasoning then
legally folds the andi away. Removing the pin removes the link and the andi returns.
C lever. When a target keeps a narrowing mask on a pinned value, remove the pin first, before
anything else. This is a distinct causal channel from RC-5's four documented pin side effects
(init-copy, bad_spill_regs poisoning, pass-0 availability shift, range blocking) and from the fifth
(§164-49, pin-perturbs-scheduling) — all five of those are regalloc/sched1-shaped; this one runs
through combine.
Evidence. func_80032048 (152/152, MATCH): removing all three pins restored the andi and, with
a separate u8→u32 widening fix, closed it.
⚠ Banked with a flag: n=1, no independent A/B re-run, and no exact combine.c line number
exists for the pin→LOG_LINK creation claim itself — I could not derive one in this pass. Treat as a
strong heuristic, not settled law. Needs an exact citation + a second exemplar. (The neighbouring
set_nonzero_bits_and_sign_copies law at L11075 / combine.c:718-788 governs a related but
different case — a multi-set, multi-block pseudo's nonzero-bits record.)
§179-H — A MID-BODY .global LABEL PAIR SLICES A BYTE-COMPARABLE FRAGMENT OUT OF A LARGER ROUTINE
Symptom. A symbol classified NEAR/FRAGMENT that is provably not a real function — the target .s
shows no prologue, and a sibling's tail branches straight into it. Reconstructing the fragment alone
won't compile (no incoming register state); reconstructing the whole enclosing routine means the
non-fragment part must also be byte-exact, which it never is.
C lever. Write ONE C function that reconstructs enough of the enclosing routine to get the right
live values into the right registers (register T v __asm__("$N") pins for every live-in), then drop
__asm__ volatile(".global TARGET\nTARGET:") at the point corresponding to the real symbol's entry
address and a matching _END label at its exit. match_one's insns_from_object slices the object
between symbol labels, so only that slice must be byte-exact and the setup code is exempt.
Terminate the fragment's own exit with a raw __asm__ volatile("j OTHER_FUNC") — not a C call —
so gcc does not grow an $ra/$sN save/restore frame around a region whose real frame is owned by
the unreconstructed caller.
Evidence. SYS_OBJ_26EC → MATCH (47 ins), construction confirmed by reading
.run/wave_p31q/main/SYS_OBJ_26EC.c directly. The journal notes this is the "same technique as
SYS_OBJ_1DC0.c" — i.e. it was used at least once before and never fed back into the cookbook, which
is the gap this entry closes. No source-level citation; the claim rests on the byte-diff.
Considered and NOT banked
| Rejected | Why |
|---|---|
| §70's "walk the parameter directly" direction (L5639) | Already banked. Kept only as the explicit contrast note inside §179-A, which is its mirror — a future agent needs both directions side by side to choose. |
__asm__ __volatile__("" ::: "memory") as a CSE defeat |
Already banked at L1816-1819 (§21) and L13300, and §178-G2 already warns it is a full barrier. Deliberately stripped from the §179-H lever description. |
"=r"(x) : "0"(x) as an address-rematerialisation launder |
Already banked near-verbatim as §153 (L10463, "THE ADDRESS-REMATERIALISATION LAUNDER"). Note: §179-A uses the same instrument against a different governing predicate (valid_initial_value_p, not cse's qty_const), so only that new application is banked. |
≤2*MAX_MOVE_BYTES single movstrsi_internal copies; -O0 memcpy library call |
Already banked at L2428 and L2531. §179-E covers only the >2*MAX_MOVE_BYTES loop-with-remainder shape. |
The trampoline's three maspsx rules (no nop after jal; %%hi/%%lo escaping; no trailing .set reorder) |
Already banked at L1848-1858. The third one is contradicted by wave P/Q, so §179-B carries the conflict-resolution note rather than a restatement. |
maspsx "forces noreorder per function" (the func_80185B44 vetting note, __init__.py:857-859) |
Already banked — but it is misleading on its own, since .ent does not update self.is_reorder. §179-B rule 2 corrects it rather than re-banking it. |
| Everything in §174–§178 and §176a–§176j | Excluded by instruction: PsyQ names, match_one relocation masking, statement order around a call, pins-cannot-cross-a-call, declaration form as a scheduling lever, the $0-add opaque copy, return-const priority, birthing_insn_p, narrow-type copy elision, the zero-offset alias hole, MEM_IN_STRUCT_P, mips_epilogue_delay_slots. Several readers surfaced near-restatements of these; all dropped. |
§177's epilogue return-delay-slot class (mips.c:5376, L17156) |
Adjacent but genuinely different: that is a within-function scheduling gate steerable by callee-saved count. §179-C is a cross-function frame-sharing phenomenon with no compiler pass involved at all. Both kept, neither merged. |
The raw findings' loop.c:3571 giv-skip citation |
⚠ Corrected, not dropped — the test is at :3572-3573. |
The raw findings' loop.c:3499 SET_SRC citation |
⚠ Corrected to :3500 (the valid_initial_value_p call at :3509 and bl->initial_value = src at :3511 both check out exactly). |
"func_8005A9FC0" |
⚠ Typo in the raw finding; the real symbol is func_80059FC0 (confirmed from the draft path and asm/nonmatchings/800c/func_80059FC0.s). Banked under the correct name in §179-C. |
"expand_function_end unconditionally appends, with no noreturn guard" |
Softened. function.c:5224 and the return emission at :5432-5437 are confirmed present, and function_epilogue (mips.c:5072) is the final-time emitter — but the "no guard anywhere on the path" claim was not traced line-by-line. §179-C states the observable (three byte-proofs) and cites the location, not a derived proof. |
§176c — MAIN (SLUS_007.26) CANNOT BE GATED INCREMENTALLY
main's make extract runs the EXE-only psyq_integrate + ld_interleave steps, which rewrite the
linker script. gate_lane/gate_stage build incrementally, so they re-run that on an
already-rewritten .ld and produce a false diff — precisely the trap R22's own rationale
describes, and the reason main sat at 0.5% being treated as the project's hardest mass.
Byte-proven both ways: make extract BINARY=main && make build BINARY=main → 143dbb89…
BYTE-IDENTICAL; make build alone → c4546248… and a 2-byte jal diff, deterministically, even
with no draft substituted. Use tools/gate_main.py (substitute batch → extract → build → SHA);
ONE clean rebuild verifies a whole batch. main then drafts like any overlay (98–100%).
§176 — SEVEN LEVERS FROM THE P31 OVERNIGHT WAVES (2026-08-15): statement order, false regalloc, and the pin that fights back
Mined from six wave journals (wf_20e84237, wf_270d3ab1, wf_52bb381f, wf_5dce5df0, wf_7c8d0599, wf_b28660bc, wf_c2b96818, wf_e0e96c3b). Every lever below was re-verified against the banked C in the tree this session unless explicitly marked otherwise. Organised by the residual class you arrive with, not by function.
§176-A — "SCHEDULE / DELAY-SLOT / LENGTH-DRIFT ±1" ⇒ check STATEMENT ORDER around the call first
Three separate agents burned a full pass each on pins, polarity flips, goto restructuring and memory barriers before a second agent fixed the same residual by moving one C statement above a call. This is now the first thing to check for any residual adjacent to a jal.
The law it rests on (already in docs/gcc-2.7.2-map/sched.md:145-149, D1): fill_simple_delay_slots backward-scans from the slot owner. It never hoists an instruction emitted AFTER a call into that call's own slot. Everything below is a corollary of that plus argument-register liveness.
A1 — The arg-register proof (the strongest tell). If the target shows a store in a jal's delay slot whose address goes through one of that call's own argument registers — sw $v0,0x20($a0) where $a0 is arg 0 — that is positive proof the store's C statement sits before the call: the call would clobber $a0 otherwise. m2c habitually emits it after.
*(s32 *)(s0 + 0x20) = (*(s32 *)&D_8018AF28); /* store FIRST */
func_8001D0E8(s0, 0xDC, 0x98); /* s0 is arg 0 */
- Byte evidence:
func_80186764. First pass spent its budget onregister s32 s0 __asm__("$16")pins, dedicated locals and address hoisting, stalled at 7/92 mismatched, and filed it asregalloc-order. Second pass moved that one statement: MATCH 92/92, Law-1c relocation audit clean (38 symbol refs, identical order). No pin needed once the order is right — the pin was fighting a downstream symptom. Banked verbatim atsrc/shared/engine_core.h:62311-62312(DEFINE_func_80186764, also mirrored at :154022).
A2 — The −1 length drift with a genuine nop. Draft is exactly one instruction short and the target has a real nop in the delay slot of an unconditional jump that immediately follows a jal+store pair. Do not route this to the permuter as a dbr/LUID tie-break. Leaving the store after the call lets gcc fill the following jump's slot with it instead, deleting the target's genuine nop and producing a huge address-drift score from one missing instruction. Move the store above the call, per call site.
- Byte evidence:
func_80188528. Agent 1 tried 5+ variants (branch polarity, goto restructuring, memory barriers), converged on 101 ins vs target 102, 41/102 differing, and concluded "genuine dbr/LUID tie-break, needs §D3/permuter research". Agent 2 moved*(s32*)(s0+0x1C)=0;abovefunc_8012B23C(s0)→ 41→1, then*(s16*)(s0+0x5E)=0;abovefunc_8018876C(s0)→ 1→0. MATCH 102/102, plus a scratch full-TU integration compile (0 masked diffs, same 24 pre-existing warnings). Banked atsrc/ov_SC03_006/ov_SC03_006_jr_8017AE2C.c:9758— thecase 1:arm shows the fixed order. - 🔴 WALL REFUTATION (in-wave, not cookbook): this is a first-pass agent's "irreducible scheduler tie-break / permuter fuel" verdict cracked by a one-statement move. Treat any journal verdict of that shape on a −1 drift adjacent to a call as unproven until the store-order variant has been tried.
A3 — Keeping a call result out of a callee-saved register. If the target's jal delay slot holds a store of the previous call's return value, the C store statement can legally sit textually before the call expression; that keeps the value in $v0/$v1 instead of forcing a spill to an $sN to survive the call. func_8018DBF0: cut mismatches 11→8 as the first of three stacked levers (final MATCH, 43 ins). Agent-reported; the 11→8 delta was not independently re-derived this session. The other two levers in that stack are the already-documented birthing_insn_p / inline-asm-move family (§55a/§162/§167) — nothing new there.
§176-B — "REGALLOC-PERM, 1-4 instructions off" ⇒ it is usually NOT register allocation
B1 — The narrow-symbol alias tell (the highest-value entry in this section). A clean $a3-vs-$a2 perm on a value that is stored wide then reloaded narrow from what look like separate globals is a data-model bug. D_80126B5E/62/66 are not independent u16 globals — they are the high halfwords (sym+2, little-endian integer part) of the 16.16 words D_80126B5C/60/64. Declaring them as three separate extern u16 gives gcc's memory disambiguator three distinct SYMBOL_REFs with no dependency between the sw of the wide word and the lhu of the narrow one, so sched1 hoists the free load (large OPCODE-MIXED break). Naively fixing that by reusing one local restores the schedule via a WAR dependency but merges two live ranges into one pseudo that then conflicts with the $a2 arg setup → $a3 (the 2-insn residual).
extern s32 gVecZ __asm__("D_80126B64"); /* asm-label alias: keeps direct lw/sw */
gVecZ = out[2];
*(s16 *)(buf + 4) = *((u16 *)&gVecZ + 1); /* NOT a separate `extern u16 D_80126B66` */
One expression buys both properties: the shared base gives sched1 a true memory dependency (schedule fixed) and the two pseudos stay genuinely separate (live ranges fixed, $a2/$a3 fall out correctly). No locals, no pins, no barriers.
- Byte evidence:
func_80185548, MATCH 77 ins, verified twice including a full HI16/LO16 +R_MIPS_26relocation-resolve oracle against the target.o(0/77 word diffs, every relocation included). The%hi/%lopair computes againstD_80126B64with in-place addend 2, byte-identical to the target's own%hi/%lo(D_80126B66)form. Banked with its full derivation atsrc/ov_SC02_011/ov_SC02_011_jr_8017AE2C.c:7550-7620. - Standing check before any pin on a small REGALLOC-PERM: is the narrow symbol's address
== wide symbol's address + k,k < sizeof(wide)? If so, express it through the wide symbol.
B2 — Pin the interloper, not the contested value. Symptom: "target keeps value X in $vN across a run of unrelated stores; mine reuses $vN for a short-lived constant." Dump cc1 -dS -dl and check the .i.lreg: the birthing-boost (S2) can sink the long-lived value's def below a later multi-set store, making its range disjoint from a short-lived single-use constant, so first-fit local-alloc hands both the same hard register. Pinning the long-lived value to its target register reserves it function-wide and cascades. Pin the short-lived interloper out of the way instead:
register u8 one __asm__("$5");
...
one = 1;
*(u8 *)(d + 0xc0) = one;
*(u8 *)(d + 0x75) = one;
- Byte evidence:
func_8018CA74, 4-instruction residual → MATCH 63/63. Root cause confirmed by hand-runningcc1 -dS -dl: insn 75 (the0xC4load) and insn 87 (the const-1) both assigned$v0in.i.lreg, with the.i.schedready-list trace showing insn 75's boosted priority0x7f000001sinking it below theli/sb/sbgroup. Register-choice sweep confirms the mechanism, not a coincidence:$5→MATCH,$4→4 off,$3→18 off, pinning the LOAD itself to$2→17 off (worse). Banked atsrc/ov_SC02_005/ov_SC02_005_jr_80181D30.c:4951with the reasoning in-line. Distinct fromregalloc.mdRC-4 (copy-coalescing tie) and RC-6 (pressure-locked flat alloc).
B3 — Two arms sharing one local get cross-jumped. When if/else arms write the same struct field through a shared local pointer/temp, local-alloc gives both arms the same hard registers (it is literally one pseudo), the tail-store RTL becomes byte-identical, and jump.c's cross-jump merges them — dropping instructions and displacing the delay-slot filler. Fix: give each arm its own block-scoped, distinctly named locals.
if (...) { s32 *ptr1 = ...; s32 field1 = ptr1[1]; field1 |= 0x80000000; ptr1[1] = field1; }
else { s32 *ptr2 = ...; s32 field2 = ptr2[1]; field2 &= 0x7FFFFFFF; ptr2[1] = field2; }
- Byte evidence:
func_8018B4D0, NEAR (22 off) → MATCH 33 ins. Arms then received independent registers ($v0/$v1for the OR arm,$a0/$v0for the AND arm), matching target, and the AND-mask'sluischeduled correctly into thebeqzslot. Banked atsrc/ov_SC02_005/ov_SC02_005_jr_80181D30.c:4449. This is the local-variable-scope analogue of §165-10 (L13937), which documents the same cross-jump-identity mechanism over a global's address form.
B4 — A single pin can make it worse by widening coalescing. func_80029E30: NEAR (23/25) → a single register s32 a0 __asm__("$4") pin worsened it to 5 mismatches, because local-alloc coalesced the pinned variable's whole live range with a second, non-overlapping unpinned local (an early lui/ori magic constant). Fix was two pins at different scopes matched to the actual live ranges: the long-lived one function-wide ($3), the short-lived one scoped to its using block ($4). MATCH, 25 ins. Agent-reported; banked in src/800.c but the intermediate 2→5 regression was not independently re-derived this session.
Addendum (P31 S63 t5b-t5d, func_80180B44): B1's parenthetical — "naively fixing that by reusing one local restores the schedule via a WAR dependency but merges two live ranges into one pseudo that then conflicts with the $a2 arg setup → $a3" — is not universal, and on the very same alias family it runs the other way. After B1's own §37 asm-label alias for D_80126B5E/D_80126B62 (closeness 158 → 25) the residual is not a register perm at all but OPCODE-MIXED with move/nop swapped at one slot pair: a reg-to-reg copy of the aliased load stranded immediately after the subu that overwrites its source register, one instruction too early to be sunk into the following conditional branch's delay slot (which holds nop). That is a register-identity residual wearing a scheduling costume — a fresh single-purpose s32 d for the tail-guard difference gets an isolated allocno whose register the intervening subu is free to clobber, so reorg has nothing to sink, and ~15 naming/statement-order/early-return variants (v4–v11, w1–w3, b2e–b2l, x1–x3, q3/q4) all plateau at 21 with the same signature.
The two-step fix, and WHICH variable is the dial. Writing both tail-guard differences into ONE variable took 21 → 4 (REGALLOC-PERM/$v0>$a0 — shape already right, only the hard register left); writing them into the loop's already-live rand-result local instead of a fresh name merged both defs into the allocno that lands in $a0, the register the target actually uses, and the leftover move sank into the delay slot on its own: MATCH 170/170, whole-binary byte-gate accepted, law-1c re-walk clean (25 external relocations, both masked internal j pairs 0x16C×2 / 0x28C×2). So it is not merely whether you merge (§45 Lever A / §76 / §162b1) but which existing allocno you merge into — §156's donor law ("the preference travels with the allocno; reuse is what carries it") read at delay-slot granularity, and the same "reuse before pins or densities" route.
⚠ The pin is the wrong tool here, as in §176-B4 / §72 / §156 / §176-C: register s32 d __asm__("$4") on that same variable regressed 4 → 22 and back to OPCODE-MIXED.
New row for §164-20's delay-slot table (whose three rows are all copy MISSING): a copy present but one slot early, sitting directly under an instruction that clobbers its register, with the branch's slot left nop ⇒ do not reorder statements and do not pin — fix the register identity by reusing an already-live local. (Honest bound: why THAT local yields $a0, and why the pin disturbs downstream allocation instead of reproducing the effect, are established empirically by cc1-asm probing (z1→z2 regression), not read out of local-alloc.c/global-alloc.c.)
§176-C — 🔴 WALL REFUTATION: a hard-register pin CANNOT schedule around a call, because of a genuine gcc-2.7.2 bug
This refutes the universality of sched.md S11's protocol step (1) ("pin the callee-saved homes FIRST so scheduling levers can't cascade the alloc") and of the standing "don't conclude unsteerable — try register pins" reflex. There is a class where the pin is the defect, and the correct move is the inverse: unpin.
sched_analyze_1 (tools/reference/gcc-2.7.2/sched.c:1659) has two arms. The hard-register arm, at sched.c:1704, reads:
i = HARD_REGNO_NREGS (regno, GET_MODE (dest));
while (--i >= 0)
{
... reg_last_uses[regno+i] ... reg_last_sets[regno + i] ...
if ((call_used_regs[i] || global_regs[i]) /* BUG: `i`, not `regno + i` */
&& last_function_call)
add_dependence (insn, last_function_call, REG_DEP_ANTI);
}
i is the sub-word countdown index — 0 for any 1-word register. So the test is always call_used_regs[0], i.e. $zero, which MIPS marks call-used (config/mips/mips.h:1203-1210, already cited by this cookbook at L13504). Every hard-register SET in a block therefore gets a REG_DEP_ANTI on the last call, regardless of which register was actually pinned. The pseudo arm fifteen lines later (sched.c:1732) is guarded by reg_n_calls_crossed[regno] == 0 and is exempt for any value that legitimately crosses a call.
Practical rule: whenever a pinned value must schedule above or around a call in the same basic block and the pin isn't working — or is actively making things worse — drop the pin and let it stay a plain pseudo.
- Byte evidence:
func_8018D574. Dropping the$18pin on the loop counteriand sinkingi++belowfunc_80143BDCflipped it from priority-2/wrong-position to priority-1/correct-LUID-position, closing the last 2 mismatches: MATCH 107/107. The gcc source lines above were read directly fromtools/reference/gcc-2.7.2/sched.cthis session and are quoted verbatim. Banked with the full derivation atsrc/ov_SC02_011/ov_SC02_011_jr_8017AE2C.c:11872-11900. - What was already documented: only the pseudo-arm anti-dependence law (sched.c:1714-1715, cookbook L12491/L16060). The hard-reg-arm sub-word-index bug is new, and it explains a family of "the pin cascaded, route to permuter" verdicts.
§176-D — CSE-class levers used in reverse (two sharpenings of §153 and cse_expr §2)
D1 — §153's launder, applied to a MULTI-use pointer, forces PERSISTENCE (the opposite of its documented effect). §153 (L10463) documents the zero-emission re-tie tp = (T*)&SYM; __asm__ __volatile__("" : "=r"(tp) : "0"(tp)); as a way to force rematerialisation of a single-use address. Applied to a pointer with several uses it inverts: emptying the symbol's cse equivalence class forces every subsequent read to address off the pseudo, and the pseudo then takes a persistent callee-saved register (the la survives at the def site) instead of each use regrowing its own lui/%lo.
Use it whenever the target caches an address in an $sN across multiple reads but your draft rebuilds lui/%lo per use.
- Byte evidence:
func_80188790, 82 ins / 57 mismatched → 79 / 20 on applying the launder to the multi-usetpalone, then to 0 with a pin + statement placement + D2 below. MATCH 79/79, Law-1c relocation audit clean. Banked atsrc/shared/engine_core.h:61882-61883(DEFINE_func_80188790) —register u16 *tp __asm__("$19")with ~5 reads off it. Every existing §153-family entry (~12 sharpenings) describes the device only as remat-forcing; this inverted use is new.
D2 — cse_expr.md §2 gains a SIXTH boundary condition: a struct block-copy re-seeds the class and defeats the kill. The byte-proven output-only kill { void *q = &m; f(q); __asm__ __volatile__("":"=r"(q)); } is defeated when a struct block-copy (m = GLOBAL_STRUCT;) targets the same local: the block-move RTL expansion permanently seeds &m's cse equivalence class with a valid register, so the class still resolves after q is killed and the second call site keeps the cached address anyway.
Escape: don't let gcc emit a block move at all — hand-expand the copy as an explicit load-group/store-group sequence through an opaque source pointer, pinning the temps to the target's scratch registers, plus a trailing value barrier so sched1 can't hoist the last address computation above the final stores:
{ register Blk20 *s __asm__("$6") = &D_800AE620;
register s32 t0 __asm__("$3"), t1 __asm__("$4"), t2 __asm__("$5");
__asm__("" : "=r"(s) : "0"(s));
t0 = s->w[0]; t1 = s->w[1]; t2 = s->w[2];
m.w[0] = t0; m.w[1] = t1; m.w[2] = t2;
/* ... 3/3/3/3/2/2 groups ... */
__asm__ __volatile__("" : : "r"(t0), "r"(t1)); /* sched barrier */ }
- Byte evidence:
func_801888F4, MATCH 73/73, full relocation audit. The boundary was bisected across 8+ spellings — copy removed → remat OK; copy into a different local → remat OK; copy present in any form (assignment / initialiser /*(Blk20*)&m=/ via a killed or re-tied pointer local / union / DImode chunks / volatile struct /"=m"asm / sp-clobber asm) → folds. Pins are load-bearing: unpinned gcc picks a$v1base +$a2/$a3/$a1temps (19 mismatched); the trailing barrier closes the last 2 → MATCH. All three asms emit zero bytes. Banked with the probe table atsrc/ov_SC02_000/ov_SC02_000_jr_8018173C.c:4405-4500.
§176-E — Two cheap source spellings, both cc1-probed
E1 — & not % for a compile-time power-of-two divisor. For a signed dividend, gcc-2.7.2's % always emits the full sign-correction sra/subu dance; & emits a plain andi. Confirmed by a standalone cc1 probe that the two spellings diverge. Reserve the true % spelling for the one genuine runtime-variable divisor — and read that divisor field as signed (s16/lh, not u16/lhu) to match the load width.
*(u16 *)(param_1 + 0x2) = (rand() & 7) + 1; /* not rand() % 8 */
dx = rand() % *(s16 *)(param_1 + 0x4); /* the one true %, s16 read */
if (rand() & 1) { dx = -dx; } /* not rand() % 2 */
- Byte evidence:
func_8018D7C8, MATCH, all four modulo-like literals rewritten bitwise. Banked atsrc/ov_SC04_011/ov_SC04_011_jr_8017D494.c:6489. Existing §164-04 / §1-I5 cover true/division by a power of two, not%.
E2 — Split % into quotient form to free the shift's register class. When a signed-modulo guard's branch delay slot is a nop in your draft but the target duplicates the sra into it, and your single sra writes the callee-saved result register, write the remainder explicitly so the intermediate quotient gets its own pseudo (landing in a caller-saved $v-reg) instead of coalescing into the callee-saved variable — which is the precondition reorg needs to duplicate the sra into the bgez slot:
s1 = r2 - (r2 / 256) * 256; /* not r2 % 256 */
- Byte evidence: closed the final 4→0 on
func_80188790. Three C spellings converge (explicit-quotient local,qpinned to$2, and the inline arithmetic form); the inline form banked because it needs no extra local. Banked atsrc/shared/engine_core.h:61881. This is a source-level dial on register class, not a scheduling fix — do not touch the branch or reorg.
§176-F — Misdiagnosis triage: four residual verdicts that were lying
match_one's klass is a coarse similarity metric. These four cost a full agent pass each and were each fixed by a semantic correction with no codegen trick at all. Check these before reaching for pins.
| Verdict you get | What it actually was | Check |
|---|---|---|
regalloc-order, 20+ ins differ, function ends in a jal to a local callee |
Missing argument. Draft passed one arg; the callee's own .s saved both $a0 and $a1 into $s0/$s1. func_80187618: 23 → 2 → MATCH (32 ins) from the signature fix alone (residual 2 was a store-order swap). |
Open the callee's target .s and count the argument registers it saves/uses. |
regalloc-order, instruction counts coincidentally equal |
Collapsed pointer indirection. Draft folded a two-level chase into one scaled offset. func_80183648: 16 → 2 → MATCH (50 ins), no pins. A pin tried first made it worse (16→25). Banked at src/ov_SC04_002/ov_SC04_002_jr_8017BEBC.c:6141 — *(s16 *)(*(s32 *)(*(s32 *)(a0 + 0x64) + 0x20) + 0x12). |
If a sibling DEFINE_func_* macro in src/shared/engine_core.h already calls the same routine, copy its addressing shape verbatim rather than re-deriving. |
IMM-OFFSET, closeness 1 — all 44 instructions identical, one beqz displacement off by a small constant |
A dropped conditional edge. A trailing call was written after the if block's closing brace instead of as its last statement. func_801878B8: MATCH 44 ins after moving it inside. |
For a near-zero-mismatch IMM-OFFSET, trace which basic block each branch actually targets in the raw .s before assuming a scheduling nuance. |
prologue-regalloc / length-drift, an $sN the target saves is missing from your frame |
A discarded return value — i.e. a logic bug. Target's logic used the first call's result (XOR/equality against the second call's); the draft tested it for non-zero and threw it away, so $s0 was never allocated. func_8002A670: fixing the logic forced $s0 + the 24-byte frame; the residual +1 drift then closed via the already-documented if/else inversion (§3-T4/§32.2) so v0=0 folds into a delay slot. 21 → 19 ins, MATCH. |
Suspect discarded logic before suspecting a codegen quirk. |
F5 — a hand-written goto/label loop is invisible to loop.c. gcc-2.7.2's loop.c only works on NOTE_INSN_LOOP_BEG/END-bracketed for/while/do constructs. If the target's preheader shows hoisted invariants (a constant load, a mask), the source must be a real for/while — otherwise invariant-hoisting and combine_givs never run on it at all. Converting is simultaneously the invariant-hoist lever, the giv/IV-anchor lever, and (here) a scheduling fix, from one source change.
- Byte evidence:
func_80186A04, NEAR (12 off) two passes earlier → MATCH 54/54. Rewriting the outer goto-loop asfor (a1 = 0; a1 < 4; a1++)letloop.choist both the-1sentinel and theandimask into the target's exact preheader order ([a1=0][li -1][andi][giv init]), and a delay-slot swap resolved for free. Banked atsrc/ov_SC02_005/ov_SC02_005_jr_80181D30.c:3500. This sharpens, rather than replaces,loop.mdL300-303 / L7, which documents a different invalidation trigger (a recognised loop entered by agotofrom outside) — not the case of a goto-loop never being registered as a loop object in the first place.
What is NOT banked here
Mined but judged too thin, too specific, or contradicted by the tree. Recorded so it isn't silently lost:
func_80014E24— non-volatile opaque-asmandimaterialisation. The reported lever (give a masked-but-unused parameter a non-volatileasm("andi %0,%1,0xff")definition and consume it as a real call argument on both branch targets, so gcc must materialise it once before the branch;volatilewould also blockdbr_schedulefrom sinkingsw $rainto the branch delay slot, landing +1) is a plausible and well-argued composition, and the agent reportsmatch_one --json → {"status":"match","closeness":0,"nins":23}. Butfunc_80014E24is stillINCLUDE_ASMinsrc/800.c— the draft was never banked, and I could not verify the C this session. Re-derive before trusting it.func_80183074— the joint two-output asm (__asm__ volatile("" : "=r"(c0),"=r"(c1) : "0"(0x75), "1"((s32)a0));as one RTL definition with two destinations, to hoist a call's argument pair atomically where separate pins let the scheduler split them). Reported 14 → 5 mismatches in one step — genuinely interesting and unlike every existing one-value-per-asm zero-byte lever — but it never reached MATCH andfunc_80183074remainsINCLUDE_ASMatsrc/ov_SC02_005/ov_SC02_005_jr_80181D30.c:2941. Good permuter fuel; not a law.func_80182B20— "de-name both reads to defeat a sign-extension CSE fold". The function is banked and byte-matched (src/ov_SC04_000/ov_SC04_000_jr_8017BEBC.c:5838, MATCH 34 ins from NEAR/13), but the banked C contradicts the mined narrative: it keeps a namedval16local and uses a(s16)val16cast for the first signed compare, writing only the second signed read as a bare*(s16 *)(s0 + 0x18)deref — and there is an intervening store. Writing up "declare no locals for either access, no store in between" would be documenting a guess. The real rule needs a fresh bisect against that file.func_8018B4A4— "leave struct fields deliberately uninitialised where the target's frame never writes those bytes", plus re-deriving a reused field before each repeated call rather than caching it. Reported to have produced correct$s0/$s1allocation and exact 0x40 frame size on the first attempt with zero pins, by transplantingfunc_8018470C's declaration idiom. Plausible and adjacent to the well-covered §79/§136-6/§163e declaration-order material, but the non-initialisation angle rests on one function with no negative probe — no "I zero-inited it and the frame grew" counter-measurement was recorded.func_80186B1C— "control-flow shape defers an unrelated pointer's register commit." Two coupled fixes were required (collapse to one shared-variable if/else with a single trailing return, plusregister s16 *s0 __asm__("$16")); each alone left 14-31 mismatches, final MATCH 37/37. The agent itself flagged this at lower confidence as closely adjacent to the already-documented D3 own-thread idiom (§156/§167-27) and the §17 pin protocol. Nothing separable enough to state as its own law.func_8018549C(give a short early-exit tail its own label rather than folding it into a sharedgototarget — 2-insn DELAY-SLOT diff → MATCH 111/111) andfunc_80188AF4(*((u16*)&pos[i]+1)pointer-cast to force a stack reload instead of>>16on a cached register — MATCH 73/73). Both are single-instance observations with real byte results but no negative controls and no mechanism traced to a gcc decision point; §176-B1 above already covers the general "express the narrow read through the wide object's address" shape thatfunc_80188AF4is an instance of.func_8018A084(whether trailing arithmetic stays inside a ternary's expression or is split into a separate statement selects between a duplicated-tailjform and a fused join — onlyv[2] = ((rand()&1)?r:-r) - 48;matched, MATCH 64/64). The agent explicitly flagged this as a plausible variant of the already-documented §165-21/§164-73/§164-74 shared-trailing-store family rather than confidently novel, and did not verify it against that family's stated scope.
§180 — THE LEFTOVER-DRAFT HARVEST: RE-VERIFY WHAT YOU ALREADY HAVE BEFORE DRAFTING ANYTHING NEW
(P31 S53, 2026-08-16 — 34 byte-perfect functions were sitting on disk, unbanked, for two sessions)
The measurement. Before building wave R, every wave-P/Q main draft still on disk was re-verified with
match_one — not read out of the workflow journals, which are self-reports (R14). 141 drafts, one parallel
sweep, zero agent tokens:
| bucket | n | what it means |
|---|---|---|
| MATCH, still a stub | 34 (3,075 ins) | finished work nobody banked |
| NEAR, still a stub | 35 | grinder/repair fuel, honest closeness recomputed |
| "ERROR: no such .s" | 72 | already banked — not a failure at all |
For scale: a full 110-agent wave targets ~6,500 instructions. This sweep recovered 3,075 — for free.
The .s-existence oracle. Once a function becomes C, splat stops emitting its .s. So
FileNotFoundError: asm/…/<fn>.s from a verification tool is not an error class — it is the positive
statement "this one is banked", and it agrees exactly with corpus.stubs() (34/34 and 35/35 of the
still-stub verdicts were in the stub set; 72/72 of the "errors" were not). A scan that files those 72 as
failures reports a 51% failure rate on a pile that has none. Classify by the oracle, not by the exception.
Why the leftovers accumulate, structurally. Three independent mechanisms, none of them mistakes:
- a wave stopped mid-flight loses its in-flight tail (§176j);
- the gate drops drafts on declaration conflicts, and the drop is per-slate, not permanent;
- the atlas draws fresh cards and has no idea what is sitting in
.run/— the next wave never reconsiders the last wave's residue. So the pile grows every wave and nothing in the normal loop ever looks at it again. Sweep it at the START of a session, before spending a token on new cards.
§180b — WHAT THE PRE-GATE LADDER ACTUALLY FINDS IN A COLD PILE (the shape of integration debt)
Running the S52 ladder over those 34 byte-perfect drafts, in order:
| step | result | cost |
|---|---|---|
reloc_identity --batch |
33 AGREE / 1 MISMATCH (a draft naming D_80078F20 where the target references cdReq_sectorHdrBuf+0xE0) |
seconds |
fragment_check |
1 FAIL — MoveImage DEFINES SYS_OBJ_8F4, a stub that still has its own .s |
milliseconds |
reconcile_slate --apply |
11 compatible / 21 refused (7 TYPE, 5 SIGNATURE, 3 DIFFERENT-STRUCT, 3 BROKE-MATCH, 1 DEF-SIDE-RETURN) | minutes |
Two-thirds of finished, byte-perfect work was blocked on declarations. That is the same ratio the wave-P post-mortem found (97% drafted → 68% banked) reproduced on a completely independent pile, which makes it a property of the pipeline, not of any one wave. Budget for banking, not for cracking.
And do not bank the compatible subset first. §176h.C2 measured it: once a draft banks, its declarations become the TU's, so a sibling-vs-sibling clash (settleable by editing either side) hardens into a file-vs-draft clash (settleable only by editing the draft — or not at all). Of 18 parked drafts, 1 survived that transition versus 5 before it. Repair the refused set, then gate the whole pile in one slate.
§180c — WHEN A BINARY'S MASS BAND IS SPENT, THE FLEET-WIDE DRAW IS STRICTLY BETTER
build_wave_atlas --only-bins main --min-ins 60 --max-ins 200 --rank mass returned 917 ins against a
6,500 target: three waves had consumed main's mass band. Two probes (R37 — probe before costing) settled
the next draw:
| draw | cards | ins | gate groups | drafts per rebuild |
|---|---|---|---|---|
| main, widened to 30–400 ins | 44 | 4,600 | 10 | 4.4 |
| fleet-wide, 60–200 ins | 63 | 6,525 | 2 | 31.5 |
The fleet-wide draw hit the doctrine's 6k target and concentrated into two TUs, because the atlas ranks by gate-group concentration once the band is wide enough to have choices. A binary running dry is not the frontier running dry — it is a signal to widen the binary set, not the instruction band. Widening the band instead buys smaller functions at more gate groups, which is the wrong trade twice over.
§181 — WHAT A WAVE'S GATE ACTUALLY REJECTS (P31 S53, measured on wave R's 45-draft main pile)
Only ONE of 27 blocked drafts was wrong. The other 26 were correct and unbankable.
Wave R drafted 92/110 MATCH and its main pile carried 45 byte-verified drafts. 18 banked in the
first slate. Every rejection was catalogued; five distinct classes, four of which match_one,
reloc_identity, fragment_check and pregate_check are ALL structurally blind to:
| class | n | how it announces itself | who could have caught it |
|---|---|---|---|
| MIRROR-FRAGMENT | 2 | undefined reference to '.L80050D5C' at LINK |
nothing we had |
| duplicate typedef, TU's copy BELOW | 7 | C89 duplicate-typedef at COMPILE | pregate_check did |
| draft-vs-draft data-symbol type clash | 1 | previous declaration of D_80072784 |
nothing we had |
| draft-vs-file signature conflict | 4 | previous declaration of func_80032A74 |
nothing we had |
| genuinely byte-wrong | 1 | whole-binary SHA differs | only the build |
1. THE MIRROR FRAGMENT — the reverse of §176i, and a new refusal we do not yet compute.
fragment_check asks "does another symbol live INSIDE my range?". The opposite is equally fatal:
another stub's .s branches into a label inside YOUR range, and converting you to C deletes that
label. Two instances, both silent until the linker spoke:
gfx2D_BG0_OBJ_4D8—gfx2D_BG0_OBJ_1B4.sbranches to.L80050D5C/.L80050D70inside it;SYS_OBJ_26EC—SYS_OBJ_25C8.sbranches to.L8005B9B8inside it. The test is as cheap as the forward one: for each still-stubbed sibling.s, collect the.Ltargets it references and refuse any draft whose[addr, addr+4*nins)contains one. Shipped asfragment_check.branched_into(), negative-controlled on both known-bad drafts (it names them and the exact referring sibling) — and then measured across the corpus, which is the number that matters:
| binary | stubs owning a branched-into label |
|---|---|
main |
99 of 1,745 (5.7%) — incl. SaveLoadRoutine, GsSortFastBg, the gfx2D_BG0_* cluster |
ov_SC04_011 |
0 of 229 |
ov_SC03_028 |
0 of 194 |
So it is a main-specific hazard at ~1 in 18, and effectively absent from the overlay fleet. That
asymmetry is itself informative: main is where splat's symbol table names the most non-function
addresses. Two notes on the semantics — the check excludes labels a function defines itself, and it
excludes siblings that are IN THE SAME SLATE (converting both at once removes the referencing .s,
so the hazard evaporates). A population scan that puts every stub in one slate therefore correctly
reports zero, which is a right answer to a different question.
2. THE TYPEDEF-BELOW CASE. gate_main.strip_dup_typedefs reuses a definition only when it is
visible ABOVE the insertion point — correct, since stripping a below-survivor leaves the name
undefined (that trap is documented in its own docstring). But it then KEEPS the draft's copy, and
two definitions of one typedef name is a C89 error wherever they sit. Both horns are wrong; the
missing third option is to rename the draft's private copy, or hoist the file's definition (typedefs
emit no code, so hoisting is byte-neutral). 7 correct drafts are parked on this.
3. THE BISECT ECONOMICS ARE SETTLED. bisect_slate.py — null control first, then true binary
search — isolated the single byte-wrong draft (SYS_OBJ_1DC0) in 5 steps / 90 seconds, at ~13 s
per incremental build. gate_main's built-in bisect on a comparable slate ran 3 hours and named
nothing (§176i). Never use the built-in one; always pass --no-bisect and drive bisect_slate.
4. THE STRATEGIC READING. 26 of 27 blocked drafts are byte-correct work that only the plumbing rejects, which is the §180b ratio again from a third independent direction. The lesson is not "draft better" — the drafting is done. It is that every hour spent making the integration layer compute a refusal is worth more than an hour of drafting, because drafting is already at 84–93%.
§182 — §177's HONEST NEGATIVE: the epilogue lever cracked 4 of 16, and the 800c3 cluster held
Wave R ran §177 (the saved-register-set → epilogue-delay-slot law) as a dedicated 16-card repair lane
against pre-classified near-misses — 10 of them the exact addiu $sp / jr $ra / nop vs
jr $ra / addiu $sp signature, all in 800c3. Result: 4 MATCH, 10 still at closeness 2–3, 2
IMMOVABLE. Every survivor kept its original signature.
So §177's mechanism is source-confirmed and its lever ("change what is live across the call") is
not sufficient for this cluster. That is a refutation of the lever's reach, not of the rule. Do not
re-run this lane as-is (R38: the verdicts are recorded in .run/s53_epi_class.json and the wave
journals). ANSWERED by §188 (same session): none of the three — it is not gcc at all. The shape
is GNU as -O2 filling the return delay slot, and cc1 cannot emit it for an $s-saving frame, so the
lane was aimed at a compiler decision that was never the compiler's. Six of the affected functions are
additionally prebuilt SDK objects. Use tools/oracle_reorder.py before spending another agent-hour. A lane that converts 25% is a lane that needs a new hypothesis, not another pass.
§180d — THE pgrep BRACKET TRICK PROTECTS THE PATTERN, NOT THE COMMAND LINE
S52 banked "pgrep -f self-matches its own shell wrapper — use the [g]ate_main bracket trick." S53
found the trick's limit the hard way: a waiter written as
until ! pgrep -f '[g]ate_lane'; do sleep 20; done; tail -8 .run/gate_lane_sc02.log
never exits, because its own command line contains the plain string gate_lane in the tail
argument. The bracket only de-fangs the occurrence inside the pattern itself. Either name the file
nowhere else on that line, or match something unforgeable (pgrep -f 'tools/gate_lane\.py' from a
wrapper that does not mention the path twice). Cost here: a 46-minute loop against a job that had
already finished, and a false "still running" in two status reports.
§183 — THE DECLARATION-RECONCILIATION PLAYBOOK (P31 S53, measured on 20 byte-verified drafts)
18 of 20 reconciled while keeping the match. The two that did not are mechanism, not effort.
A dedicated lane took the 20 drafts that wave R's gate rejected on declarations — each carrying the tooling's exact refusal — and asked one agent apiece to make the DRAFT agree with its TU without losing a byte. 18 still verified MATCH afterwards; 10 banked in the first slate that followed. The moves that worked, by frequency:
| move | n | shape |
|---|---|---|
| TYPE-adopted-TU | 8 | take the TU's spelling verbatim, narrow at the use site |
| SIGNATURE-cast-at-call | 3 | adopt the TU's prototype, cast the arguments |
| STRUCT-view-cast | 2 | keep the TU's type on the declaration, cast the pointer at use |
| TAGGED-WORD-cast-at-call | 1 | raw word type + cast at the call (§181 law 4) |
| TYPE-shadowed-block-scope | 1 | move the private view into the function body |
1. THE NAME/SHAPE TRAP — adopting the TU's typedef NAME while keeping a different BODY is worse
than not adopting at all. Two drafts were told "the TU's Owner4EE8 is the incumbent" and both
renamed their struct to Owner4EE8 while keeping their own field layout. strip_dup_typedefs then
did exactly what it must — same name, different body, so it renamed the draft's copy to
Owner4EE8_8002C8F4 — and their extern Owner4EE8_8002C8F4 *D_800A4EE8; collided with the file's
extern Owner4EE8 *D_800A4EE8;. Adopt the SHAPE, or keep your own name and cast at the use site.
Never adopt the name alone.
2. §181 LAW 4 IS VALIDATED TWICE, INCLUDING FOR STRUCT-POINTER GLOBALS. SetGraphQueue declared
extern struct { u8 pad[0x34]; s32 (*field_0x34)(s32); } *D_80072780; — private, reasonable, and a
landmine for every sibling that spells the same global void *. Replacing it with
extern void *D_80072780;
...
(*(s32 (**)(s32))((u8 *)D_80072780 + 0x34))(1);
kept MATCH (43 ins). A global whose real type is a pointer-to-something should be declared in the rawest form any sibling might use, with the structure recovered at the use site.
3. &D_x MATERIALIZES A SHARED BASE REGISTER, AND THAT IS WHY THE CAST ESCAPE HATCH SOMETIMES
CANNOT WORK. func_80037028 needs Slot16A D_80076240[] while its TU declares W16 D_80076240;.
All three standard reinterpretations (pointer-cast-then-arrow, pointer-cast-then-bracket,
cast-to-array-pointer-then-deref) compile and all three lose the match identically — 68 ins vs 71,
diverging from instruction #1 of the function, not merely at the touch sites. Taking &D_80076240
gives gcc-2.7.2 a CSE-able address subexpression, so it materializes one base-pointer register and
reuses it, which perturbs allocation for the WHOLE body. The true-array form D_80076240[i].field
never creates that subexpression at all.
So DIFFERENT-STRUCT has a real, mechanistic limit: when the TU's spelling forces an address-of and
yours does not, no cast at the use site can recover the bytes. That case is a genuine TU edit, and it
is correct to report it as IMMOVABLE rather than grind.
4. THE TWO REMAINING IMMOVABLES ARE ONE-LINE TU EDITS, BOTH ARGUED FROM BYTES:
func_8001ABBC—src/800.c:4542declares itvoid, written for an earlier-banked caller that discards the return; the function has four load-bearing$v0-setting return paths. Edit the declaration tos32(byte-neutral: the sole call site ignores the value).func_80037028— change the TU'sextern W16 D_80076240;toextern Slot16A D_80076240[];and its one writeD_80076240.v = 0;toD_80076240[0].unk00 = 0;, then re-verifyfunc_80037144.
5. THE ITERATION ECONOMICS, AND THE TOOL GAP THEY EXPOSE. Ten banks cost four rebuild attempts,
because the compiler reports only its FIRST conflict, so each drop-and-retry reveals exactly one
more. pregate_check's CONFLICTING-EXTERN scan should have caught the D_80072780 clash before any
of them and did not: its declaration regex is single-line, and the offending declaration was a
multi-line extern struct { ... } *D_x;. A pre-gate check that sees 90% of declarations converts a
one-rebuild-per-conflict loop into a single pass — that is where the leverage is, not in the drafting.
Shipped the same session: pregate_check now scans brace-bodied externs (extern struct { ... } *D_x;) with the normalized body as part of the signature, so two identical struct declarations stay
silent while a struct-vs-void * clash FAILs. Negative-controlled four ways — and one of those
controls caught a trap worth its own line: a synthetic test using a fake symbol name (D_x) reported
CLEAN for a conflict the tool does detect, because sym_of only recognizes real project symbol
spellings. A negative control must use names the system would accept, or it tests nothing.
§184 — COMMENT-BLINDNESS IS A DEFECT CLASS, NOT A BUG (P31 S53: three tools, one root cause, one session)
Three independent tools were found comparing or scanning C text without masking comments, and each one was silently refusing byte-verified work:
| tool | what it did | what it cost |
|---|---|---|
pregate_check._typedefs |
scanned RAW text | a typedef quoted in a bank-note comment counted as a definition |
reconcile_slate._same_struct |
compared body TEXT | Slot16B vs Slot16 — same 7 fields — refused as DIFFERENT-STRUCT |
gate_main.strip_dup_typedefs |
compared body TEXT | renamed an identical struct to Slot16A_80037028, whose extern then contradicted the file's |
The root cause is the same sentence every time: the comparison was a DOCUMENTATION test, not a
layout test. Agents annotate every field with its address (s32 unk04; /* 0x80076248 */); the TU
usually does not. So two character-for-character identical layouts differ as text, and each tool
turned that into a refusal — the R39 over-refusal failure mode, inside the tools written to prevent it.
THE RULE: any tool that compares or scans C source must mask comments first. The project already
has exactly one masking oracle (cdecl._mask, length-preserving so offsets stay valid); every one of
these tools had it available and none used it in the path that mattered. When you find one instance,
grep for the others in the same session — this class does not occur alone.
§184b — A FORWARD TYPEDEF IS NOT A COMPETING DEFINITION. typedef struct Owner4EE8 Owner4EE8; in
a draft is the same type as the TU's full definition, written incomplete so the draft compiles
standalone for match_one. Renaming it (same name, "different" body) manufactured
extern Owner4EE8_8002C8F4 *D_800A4EE8; against the file's extern Owner4EE8 *D_800A4EE8; — a
conflict created entirely by the tool. gate_main now recognizes the forward form and strips it in
favour of the (hoisted) real definition. Two byte-verified drafts banked immediately.
§185 — EDIT THE SIDE THAT IS CHEAP TO VERIFY, AND CHECK A TU RETYPE AT ITS USE SITES
Three TU declaration retypes were attempted this session. The pattern in what worked:
| edit | verdict |
|---|---|
func_8001ABBC declared void → s32 |
byte-neutral (sole caller discards the return) |
W16 D_80076240 → Slot16A D_80076240[] (+ its one use) |
byte-neutral |
W32 D_8007622C → s32 D_8007622C[] (+ its one use) |
byte-neutral |
s16 D_800C5328[] → s16 D_800C5328[][2] |
REFUTED — compiles at the declaration, then breaks four banked assignments in two other functions (incompatible types in assignment) |
So a TU retype is byte-neutral only if every EXISTING USE SITE still compiles unchanged. Checking the declaration proves nothing; grep the uses first, and count them. A retype that forces edits to already-banked functions is not a retype, it is a re-match of those functions.
And prefer the cheap side. Verifying a DRAFT change costs one match_one (seconds, isolated);
verifying a TU change costs a clean rebuild (minutes) and risks every function in the file. So when a
draft and its TU disagree, push the edit into the draft by default — including adopting the TU's
single-field wrapper structs (D_80076228.v = x instead of D_80076228 = x, identical bytes at
offset 0). Reserve TU edits for the cases where the draft side is provably impossible.
§185b — THE BLOCK-SCOPE extern RETYPE CAN BE LOAD-BEARING, AND THEN NOTHING ELSE WORKS.
func_80031A98 needs extern s16 D_800C5328[][2]; inside the function so every access types as a
genuine 2-D array (§164-26: outer subscript variable, inner literal, no ADDR_EXPR pseudo, per-use
inline addressing). Both escapes fail, and fail the same way — a local s16 (*t)[2] pointer variable
AND an inline ((s16 (*)[2])D_800C5328)[i][0] cast each hoist a base load (§183.3 again). C forbids
the block-scope redeclaration against the file-scope flat type, and retyping the file breaks four
banked call sites. Recorded as genuinely blocked, with the exact cost of unblocking it: retype
those four assignments and re-verify their bytes. That is a real answer, not a failure.
§186 — CROSS-JUMPING RUNS AFTER SCHEDULING, SO NO C-LEVEL BARRIER CAN STEER IT
(P31 S53, wave S — source-cited from toplev.c + a -dR -dJ RTL dump; cost 8 wasted attempts before the refutation)
A wave-S near-miss was filed by its first agent as "cross-jump tail-merge granularity", and eight
attempts went into steering gcc's cross-jumper with the §5a __asm__ __volatile__("") barrier. All
eight failed, and the reason is structural:
toplev.c:3142— cross-jumping is thejump2pass, which runs AFTERsched2(toplev.c:3104). The merge boundary is therefore computed on the scheduled insn order, not on source order.jump.c:find_cross_jumpwalks the two blocks' suffixes backwards and stops at the firstrtx_renumbered_equal_pfailure. The RTL dump showed the scheduler hoisting alhuand alwABOVE the arm-distinguishingaddiu, leaving a 3-insn common suffix — so two insns duplicate.- A barrier cannot help: it constrains scheduling within a block, and the decision being fought happens after scheduling has already run.
The actual lever is source-level SHARED TAIL. The target was not cross-jumping at all — its arms
genuinely fall into one common basic block. Restructuring the C from "two arms each ending in the
same three statements" to "if (bit1) {...} else {...} followed by the common statements" dropped the
residual 36 → 10 mismatches and fixed the instruction count exactly. When you see a tail-merge
residual, write the shared tail in the source instead of hoping the compiler will merge yours.
§186b — A NO-SAVE 16-BYTE FRAME IN A LEAF FUNCTION MEANS s16 LOCALS, NOT A HIDDEN CALL
mips.c:compute_frame_size computes total = var_size + args_size + extra_size. HImode (s16) locals
contribute var_size even in a leaf function that saves no registers, producing the distinctive
addiu $sp,$sp,-0x10 … addiu $sp,$sp,0x10 pair with no sw of any register. An otherwise
identical body written with s32 locals compiles frameless and can never reach the target's
instruction count. So: a small frame with no saved registers is a type-width signal — widen or
narrow your locals rather than hunting for a call you cannot find.
§186c — WHERE A VALUE IS LOADED DECIDES WHICH ALLOCATOR OWNS IT, AND THEREFORE ITS REGISTER
gcc-2.7.2 runs local_alloc before global_alloc. A pseudo whose references all sit in ONE basic
block is handled by local_alloc, which — since MIPS 2.7.2 defines no REG_ALLOC_ORDER — takes the
default ascending order and lands on $v0. A pseudo live across blocks falls to global_alloc
and gets what is left (typically $v1). This is a placement lever, not a pin:
To put a value in
$v1, load it inside the ARMS (making it live across blocks). To put it in$v0, load it in the single block that uses it.
And when two values tie, global.c:allocno_compare ranks by floor_log2(n_refs) * n_refs / live_length
— which is why writing d += n instead of n = d + n can flip which one wins $v0. That last tie is
also where this function stopped: ~900 compiles across 11 generated sweeps could not rebalance the
ratio without costing more elsewhere. A residual that survives a 900-compile sweep of a documented
mechanism is a grinder/permuter target, not a hand-lever target — record it and move on (§182).
§187 — 🔴 "SAME SOURCE" IS NOT "SAME OBJECT": THE SDK BUILD AND THE GAME BUILD DISAGREE ON GTE NOPS
(P31 S53 — a refuted link opportunity, and three parsers giving three different answers)
The claim. A wave-S agent found that src/800b_7.c is not game code at all: its eight symbols are
GsSortBg + GsSortFastBg, i.e. PsyQ libgs 2D_BG0.o / 2D_BG1.o. It reported "526 ins, 0
relocation-masked diffs" and recommended banking all 1,022 instructions for free through the existing
psyq_integrate machinery instead of decompiling them. The reasoning was excellent and the symbol
names really do encode the SDK objects (gfx2D_BG0_OBJ_4D8 = "2D_BG0.OBJ + 0x4D8").
The refutation, in three steps — each stricter than the last:
| oracle | verdict |
|---|---|
the agent's masked_diff comparison |
0 diffs |
| my re-check, excusing any word that carries a relocation | 0 unexplained |
psyq_identify's objdump pattern, masking only the relocated FIELD |
271 mismatches |
The third is the one that matters, and its diff is diagnostic: the object's stream is 520
instructions where the EXE region is 526, and the mismatches begin as a one-instruction SHIFT around
COP2 words (0x005E001A, 0x17C00002). The game's copy carries six extra GTE hazard nops that the
SDK object does not. Same C source, different assembly — so the object can never fill the region
byte-identically, and psyq_identify was right to refuse it as "not linked by EXE".
THE LESSON IS ABOUT THE ORACLE, NOT THE LIBRARY. Two of the three checks agreed with each other and were both wrong, because both were permissive in the same way: they excused a whole instruction whenever it carried a relocation. A relocation licenses only its own FIELD to differ — 16 bits for HI16/LO16, 26 for a jump target — never the opcode or the register operands. A masked comparison whose mask is coarser than the linker's is not evidence of a match; it is evidence of nothing. Agreement between two loose checks is not corroboration (R34: oracles must be able to disagree).
What this costs and what it saves. It costs the 1,022-instruction shortcut: those eight symbols
stay decompilation work, and GsSortBg/GsSortFastBg remain fragment-merge targets (§181's mirror
class — five interior j targets each). It saves the far larger error of shipping a linked region
that is six instructions short, which would have failed the byte gate and been debugged as a linker
problem. Check the strictest available oracle BEFORE re-architecting around a byte claim (R35).
§188 — 🔴 THE jr $ra + addiu $sp TAIL IS AN ASSEMBLER ARTIFACT, NOT A FRAME SHAPE
(P31 S53 — found twice independently, from 800c3 and from 800c2; corrects §177 row 2, answers §182)
§182 asked what else forces those 800c3 frames. The answer is: nothing does — it is not gcc.
LAW 1 — cc1 cannot produce it, so no C lever reaches it. In mips.c, function_epilogue sets
noreorder = (epilogue_delay != 0) (:5081). Only that noreorder branch emits j $31 before the
stack restore (:5204, addu at :5209/:5214); the reorder branch emits addu $sp first and j $31
second (:5225-5238) — the A-form. epilogue_delay is non-empty only when
mips_epilogue_delay_slots() returns 1 (frame empty, or mask == RA_MASK && fmask == 0, :5378), and
on that same branch load_only_r31 is the identical predicate (:5174), so exactly one lw $31 is
emitted. Therefore: a tail with jr $ra + addiu $sp and two or more restores above it is
unreachable from cc1 for any C body whatsoever. §177's row 2 ("frame saves $s regs → keep a value
live across a call") is inverted for this shape: saving an $s register is what makes it impossible.
LAW 2 — two conditions, both necessary, measured as a 2×2. The B-form comes from GNU as filling
the return delay slot, and it needs an empty slot to fill:
- maspsx appends
nop # DEBUG: branch/jumpinto any emptyj $31slot, and force-emits.set\tnoreorderafter every.ent(tools/maspsx/maspsx/__init__.py:856-859), while its.set\tbranch (:844-848) updatesis_reorderbut never re-emits — so gcc's own.set reorderis swallowed. A TAB-formed.set\tnoreorder+ SPACE-formed.set reorderpair emitted just before the epilogue suppresses the padding (maspsx consumes the TAB form, passes the SPACE form toas). as -O2performs the swap;as -O1— which this project pins (Makefile:563,tools/match_one.py:62) — only pads withnop.
Measured on func_8005E3AC from ONE md5-identical .asm: marker + -O1 = 54 ins / 2 diffs · no marker
-O1= 54 / 2 · no marker +-O2= 54 / 2 (inert — noreorder wins) · marker +-O2= 53 / 0. So "as -O2flips it" is only half the cause; the marker is load-bearing, and it is byte-inert at-O1.
THE DIAGNOSTIC — tools/oracle_reorder.py (promoted out of gitignored scratch for exactly this
reason). Re-assemble the same draft bypassing maspsx with as -O2 and compare. Measured 2×2 on
func_80061FA8 (target 103 ins): maspsx+-O1 57 diffs/106 ins · maspsx+-O2 57/106 (inert) ·
bypass+-O1 97/107 · bypass+-O2 0 diffs/103 ins = MATCH.
If the bypass+-O2 cell is 0, the draft's C is already correct: file IMMOVABLE and stop grinding.
And do not scope this to epilogues — func_80061FA8's 57-diff cascade is unfilled beqz/j/jal slots
body-wide, including an la macro's %lo half.
DO NOT "FIX" IT GLOBALLY. as -O2 is byte-inert build-wide today, but maspsx's forced noreorder
does not cover INCLUDE_ASM'd hand asm — that content never passes through maspsx — so flipping the
flag changes the risk surface for every remaining stub. Treat -O2 as a diagnostic, not a build change.
LAW 3 — and 6 of the affected functions are SDK objects, found with the STRICT oracle. Running
psyq_identify.py (the field-masked oracle, §187) over the band first identifies: libpad
pdent3.o@0x8005D244, pdent4.o@0x8005D33C, pdent5.o@0x8005D410 (PadInfoComb),
pdmain1.o@0x8005D588, pdmaiini.o@0x8005D8B4, and libapi first.o@0x80061FA8. Those are
decomp work only by mistake — route them to psyq_integrate.py. The remaining ~24 in the band are
game-authored C whose bodies can be byte-exact today and are blocked only by Law 2.
(Unlike §187's refuted libgs claim, this identification comes FROM the strict oracle rather than being
checked by it — which is the whole difference.)
§189 — FIVE COMPILER LAWS MINED FROM THE WAVE R/S JOURNALS (P31 S53), each source-cited and re-derived by a second agent
🔴 THE INFERENCE DIRECTION IS BYTE-REFUTED — see §199-A/§199-E (next session's harvest). The SPLIT-TIMING half below survives (
-fno-schedule-insnsemits an unsplitliand no pair at all). What is FALSE is everything downstream of it: LUID adjacency does NOT imply emission adjacency, an interloper licenses NO conclusion about the source spelling, and the "no statement order and no pin" absolute is wrong. Byte-proof: the banked one-statement sliceprim.col[1] = 0x101010;compiles with SEVEN insns between itsluiandori; the two-step spelling prescribed below is BYTE-IDENTICAL (the fix is inert); moving an unrelated statement moves a third constant in and out of the gap; and one separated pair is0x88888889, gcc's own reciprocal magic for/ 0x3C— a constant with no source spelling at all, so "the target wrote two steps" is unsatisfiable there.rank_for_scheduletests INSN_PRIORITY first (sched.c:2395) and reaches the LUID tie-break only at :2428; the separator is the BIRTHING BOOST (birthing_insn_p, gated onreg_n_sets == 1), which the split pair can never have becausetry_splitgives its pseudo two sets. 8 separated pairs across 5 functions in 3 binaries.
§189-A — SPLIT-CONSTANT LUID ADJACENCY: an interloper between lui/ori proves the target wrote
TWO source steps. mips.md:3208's large_int define_split fires in sched1's per-block pre-pass
(sched.c:4830 try_split, reload_completed == 0) — before sched_analyze hands out LUIDs
(sched.c:2175). The two halves are therefore chain-adjacent with consecutive LUIDs, and since
rank_for_schedule's only live discriminator among equal-priority ALU constants is the INSN_LUID
tie-break (sched.c:2428), no statement order and no pin can put a third constant between them.
So if the target shows one there, the target did not write one constant.
Fix (both edits needed; either alone still scores 2): split it in source with a §30#3 zero-byte re-tie
between the halves (x = HI; __asm__("" : "=r"(x) : "0"(x)); x |= LO;) so the |= carries its own LUID,
and route the interloper through an already-multi-set / pinned variable so birthing_insn_p
(sched.c:2469) does not boost it below both halves. Four-form A/B on func_8001BBBC, identical
44-instruction multiset each time, only the li 4's slot moving: one constant → after both · two steps,
no re-tie → before both · two steps + re-tie + anonymous temp → below both · two steps + re-tie +
multi-set pinned temp → BETWEEN, MATCH 44/44 (src/800.c:5453, idiom at :5465-5476).
In-function control: the same function's 0xE1000040 is a one-statement split and its lui/ori sit
adjacent with nothing between — exactly as the law predicts.
Narrative correction: the re-tie does not stop cse folding the halves back into one CONST_INT
(A/B shows they stay separate without it, with only a REG_EQUAL note). It stops cse deleting and
re-materialising the HI set later in the chain, which is what lifts the lui's LUID above the
interloper. The comment in src/800.c:5430-5431 says the former and is wrong.
§189-B — SELF-ACCUMULATE OPERAND ORDER IS FIXED AT RTL EXPANSION, SO REORDERING THE C ADDENDS IS A
GUARANTEED NO-OP. optabs.c:399-421 swaps op0/op1 whenever target == op1 by rtx pointer identity;
for a local in a pseudo, expand_expr's VAR_DECL arm returns DECL_RTL itself (expr.c:4258) and
store_expr passes that same rtx as target. So x = b + x is swapped back to (plus x b) and emits
the identical addu $x,$x,$b as x = x + b. If the target shows addu $x,$b,$x, permuting the
addends will look like a refutation and prove nothing. Levers, both of which break the identity:
route through a temp ({ s32 xt = b + x; x = xt; }), or pin the destination to a hard reg.
This BOUNDS §10 Residual A / Fix A1, §164-02 and §167-32's "write the operand you want in rs
first" — all three are cse/front-end mechanisms with no target-identity constraint, and all three are
inert on a self-accumulating statement.
§189-C — A §30#3 RE-TIE THAT MUST LIVE IN bb0 NEEDS A "memory" CLOBBER. The boost-kill itself is a
sched1 effect, but the asm insn you added survives into sched2, where prologue saves, stack-arg loads
and ALU fillers all tie at priority 1 and fall through to INSN_LUID (§167-13). Inside bb0 a bare
__asm__("" : "=r"(x) : "0"(x)) floats through that interleave and rotates the filler chain one triple
early. Escalating to __asm__("" : "=r"(x) : "0"(x) : "memory") makes expand_asm_operands emit
(clobber (mem:BLK (scratch))) (stmt.c:1656-1663), which sched_analyze_insn routes through the
write-memory path (sched.c:2035-2042 → :1736-1790) and pins it. volatile over-fences.
Bounds §30#3, whose prescription ("place the re-tie in a LATER basic block") has no answer when the kill
must happen in the entry block.
§189-D — A NARROW PARAMETER IS BORN INTO TWO PSEUDOS, so its target shape is a callee-saved
COPY-OF-A-COPY with no extension anywhere. mips.h:1153 defines PROMOTE_PROTOTYPES (the caller
widens) but the port defines no PROMOTE_FUNCTION_ARGS and no PROMOTE_MODE, so for a prototyped
u16/s16 parameter nominal_mode(HI) != passed_mode(SI) and assign_parms takes function.c:3643
rather than the one-insn emit_move_insn at :3679 — emitting a tempreg (:3665/:3667) and a
parmreg conversion (:3669-3673). Target tell: move $sA,$aN then move $sB,$sA, the two halves
feeding disjoint use sites, with no sll/sra and no andi 0xffff at any of them.
§167-44's extension-based tell cannot fire on this — there is nothing widened to see.
§189-E — THE COMPARE-CONSTANT ROW FLIP: naming the constant changes the comparison's SHAPE, not just
its schedule. fold-const.c:4417/4430 rewrites X < CST → X <= CST-1 only when arg1 is an
INTEGER_CST. A named local is a VAR_DECL, so the fold never fires and three things flip together:
bare literal → li t,0xE0FF ; slt d,t,dzsq ; beqz d (constant−1, operands swapped, materialised at the
compare) versus named local → li t,0xE100 ; slt d,dzsq,t ; bnez d (constant verbatim, natural order,
materialised at its own def and therefore schedulable into an earlier delay slot). Sharpens §48-C4,
which treated this as a scheduling lever only.
§190 — THREE PRESCRIPTIONS FROM THE SAME HARVEST (weaker evidence than §189, honestly labelled)
§190-A — THE PREHEADER HAS THREE FIXED STRATA, and an init in the wrong stratum is not schedulable.
In stream order: (1) a source biv's own init, before NOTE_INSN_LOOP_BEG, never moved; (2)
move_movables' hoisted invariants (loop.c:1652/1708, called from scan_loop:966); (3)
strength_reduce's reduced-giv inits (emit_iv_add_mult(..., loop_start), loop.c:3879, called at
:976). Strata 2 and 3 both insert immediately before loop_start, so the pass order 966 < 976 is
what puts giv inits last. A register whose zero-init sits after a hoisted invariant cannot be a
source biv — it is a reduced giv of a stride-1 counter the source never named, and
maybe_eliminate_biv (loop.c:5952) then deletes that counter, which is why the target's exit test
reads slti rGIV, N*K instead of slti rCOUNTER, N.
Tell: count and content already match; the residual is a lone move rX,zero on the wrong side of the
preheader's la/lui block, with zero missing or extra instructions. Do not open the permuter on it.
Fix: n = 0; ... i = n * K; ... n++; — and i = n * K must be its own named statement, or every
&SYM + n*K becomes its own address giv and floods the preheader.
Proven twice: src/ov_SC04_011/ov_SC04_011_jr_8017D494.c:7740 (81/81) and :7882 (122/122).
Resolves §2-T2's open case; scope-fenced to the strata BOUNDARY — order within stratum 2 is §162e2,
within stratum 3 is §164-06.
§190-B — COMPILE THE PLAIN NATURAL ORDER FIRST: an interleave in the target is not evidence the source was interleaved. sched1 produces interleaving from natural order, so a hand-"pre-scheduled" draft is itself a defect class — the tell is a draft you deliberately reordered "to help gcc" sitting at a small stubborn residual filed as instruction-scheduling. Measured, and this is the new part: per-block rigidity is asymmetric within one function, so the winning set is a product-structured plateau, not a point. Permuting stores that feed later reloads is rigid (4 of 24 orders reach 0; 552/576 cells nonzero); permuting independent same-base writes whose values die locally is loose (8 of 24 byte-identical, natural among them). So "the natural order won" and "order is a live dial" are both true at once — do not generalise a loose block into §178's "statement-order sweeps are worthless", nor a rigid block into "you must hand-craft the order".
§190-C — A CALL-ARG CONSTANT WEDGED INTO A DEPENDENT LOAD'S DELAY SLOT IS A sched2 HOIST. Attribute
it first: if -fno-schedule-insns2 alone reproduces the target order, sched1 was already right and
post-reload sched2 is hoisting the load above the constant. The fence is both halves together — arg
registers pinned above a __volatile__ memory clobber: plain s32 locals get folded into the call's
own arg setup and sink below the fence (verified), and it is __volatile__ that makes the asm a barrier
at all (sched.c:1957, if (code != ASM_OPERANDS || MEM_VOLATILE_P (x)), gating flush_pending_lists).
§191 — WHAT THIS HARVEST DID NOT BANK (4 rejected, 4 narrowed) — recorded so it is not re-derived
The harvest ran 10 readers over 247 wave-R/S verdicts, then one adversarial verifier per candidate,
defaulting to REJECT: 18 candidates → 10 CONFIRMED, 4 WEAK, 4 REJECTED. Two verifiers rebuilt their
target from the ROM because the .s had been pruned on bank; one re-ran a four-form A/B rather than trust
the reader's sweep; one caught the src/800.c:5430 comment being wrong about its own mechanism.
Narrowed to their surviving core (banked above only in the corrected form, with the falsified half
named): func_80182E2C — roughly a third of the submitted rule was false · func_801836D4 — the
headline technique is byte-falsified, only the corrected form survives · func_801859F4 — bank only the
second half, as a sharpening of §164-73/§164-74 and a bound on §165-21, never as a new "LUID tie-break"
law · gfx2D_BG0_OBJ_4D8 — a register __asm__("$N") pin gets no caller-save, so the target's own spill
block must be hand-written as C statements (fragment byte-proof only, since the merged function is not
banked).
The standing rule this harvest reinforces: a reader's rule and a verifier's rule are different artifacts. Every confirmed entry above changed shape under verification — bounds added, a mechanism re-attributed, a sub-claim refuted — and the ones that did not survive were rejected for exactly the reasons §179 predicted: not banked, or banked C contradicting the narrative written about it.
§192 — THE PRE-GATE LADDER WAS MAIN-ONLY, AND NOBODY COULD SEE IT (P31 S54)
Three defects in one call path; the overlay slates that carry most of the wave work were being waved through
The symptom. Running pregate_check.py on an overlay slate printed:
slate 7 -> 7 after resolve_conflicts (0 dropped); checking 0 substituted file(s)
clean — no textual defect found; the batch is worth a rebuild
Seven byte-perfect drafts, a green light, and zero files examined. Every wave since O has been overlay work; the check that §181 credits with catching 4 of the 5 rejection classes has never once run on any of it.
DEFECT 1 — gate_main.resolve_conflicts and gate_main.substitute both hardcoded
corpus.stubs('main'). A non-main entry resolved to no stub, so resolve_conflicts compared it
against a '<unknown>' pseudo-file (never clashing with anything) and substitute dropped it on the
floor without a word. This is the R36 citizenship class again — a consumer structurally blind to a
real binary — and the slate shape already carried the answer (gate_lane records have a binary
field). Fixed with a memoized _stubs_for(binary); absent, the key defaults to main, so every
historical main slate behaves byte-identically.
DEFECT 2 — sym_of returned a KEYWORD for any pointer-to-function declaration. The regex took
the first identifier followed by [ or (, by position, so
extern void (*D_801923D0[])(void *);
reported its symbol as void — and then every such declaration in the TU "conflicted" with every
other one under that name: 192 phantom CONFLICTING-EXTERN failures on one overlay TU. Project
symbols are now matched by NAME first, C keywords are excluded from the generic branch, and the
parenthesised declarator T (*NAME[])(...) is read explicitly. Negative control across the whole
tree: 5,526,100 declarations, 189,301 changed verdicts, 0 regressions — every single change is a
keyword becoming the real symbol (void → D_800A4F24, s32 → D_801274D0), which also means
gate_main's own conflict table has been blind to function-pointer globals in main all along.
DEFECT 3 — void f() and void f(void) were normalized to the same thing. C89 6.5.4.3 makes an
unspecified parameter list compatible with any prototype (this project leans on it — §37/§124), but
an explicit (void) against f(s32) is a hard error. Collapsing both to () cost 40 more phantom
failures against a TU that compiles today. Now: unspecified → a UNSPEC wildcard, explicit (void) →
(), and the comparison is gate_main.sig_conflict, not !=. Seven synthetic controls, including
both directions of the pair that matters.
The residue is the point. After the three fixes the same overlay slate reports 2 failures, and
both are real: a D_80195AF6_t typedef defined twice, and memcpy declared (void*, const void*, u32) in the TU against a draft's (void*, void*, s32). Each of those is one lost clean rebuild —
which is the whole reason the ladder exists.
§192b — A CHECKER THAT CHECKED NOTHING MUST NEVER READ AS A PASS. Two guards, both earned above:
pregate_check now REFUSES (exit 2) when the slate is non-empty and zero files were substituted, and
it prints one [DROP] line per draft resolve_conflicts rejected — with the clashing symbol, the
side it clashed with, and both signatures. Those drops were already being computed and thrown away, so
a slate that lost every draft to declaration conflicts still ended with "clean — the batch is worth
a rebuild". The drop list is not a footnote: it is the §183 playbook's worklist, and it is now the
tool's headline output.
The generalizable law. A tool written against one binary is a tool with an untested hypothesis
about every other one. The main-vs-overlay split here is exactly the shape of R36's SC07 bug and
R32's build_engine_types bug: the code did not fail, it silently narrowed its own domain and then
reported success over the part it kept. When a checker's verdict is "clean", ask what it counted — and
make the tool answer that question in its own output.
§193 — THE WAVE-T HARVEST (P31 S54): 71 index_gap reports -> 9 laws, 5 rejected, 61 already-covered
Wave T byte-matched 70 of 71 functions and every agent was asked for its index_gap: the one thing
it had to work out itself that the cookbook should have told it. Five cluster readers mined those 71
reports; one adversarial verifier per candidate, defaulting to REJECT, then attacked each in a
fixed order — already in the cookbook? does the evidence say what it claims? is the citation real?
is there a second instance? what is the bound?
The headline number is 61. Sixty-one of the 71 gaps were answered by a section that already exists — the knowledge base is doing its job and the agents are not finding it. That is a RETRIEVAL problem, not a knowledge problem, and it is why §193-A (below) matters more than any of the compiler laws: the cards hand agents a pointer that cannot work.
Of the 14 candidates that survived reading, 9 CONFIRMED, 5 REJECTED — and every confirmed one changed shape under verification, exactly as §191 predicted. Two of them REFUTE existing sections in place (§43's absolute, §164-54's arm-count bound); one retires a mechanism the reader had wrong.
§193-A — The wave card ships only OPEN-set pointers and discards the atlas's BANKED one — exemplar/sibs are stubs 100% by construction (tools/atlas.py:96/657), while seed.ref (matched pool, atlas.py:505-536) is dropped at build_wave_atlas.py:143
⚠ CORRECTED IN PART BY §194-E (same session). Two things this entry did not know: (1)
exemplaris not merely un-banked — it names the card's OWN target on 42/73 wave-U and 36/71 wave-T cards (a consequence of--one-per-gidranking by TU-mass then nins), so half the time it is not a pointer at all; (2) theseed_refadded here is same-binary on 0 of 51, so after this fix the card still carries zero destination-TU locality — and the same-TU banked body is the highest-yield reading source there is (62% of wave-T targets shared >=2 callees/globals with one). Read §194-E before trusting either field.
THE WAVE CARD CARRIES THE ATLAS'S OPEN-SET POINTERS AND DROPS ITS BANKED ONE (measured 0/34 and 0/146 on wave T; a one-line tool fix)
The law (one line): a wave card's exemplar and sibs are drawn from the OPEN set by construction and are therefore never banked — while the atlas already computed a banked twin and the card builder throws its identity away.
Not a maturity effect — a construction invariant. tools/atlas.py:96-100 (load_open(): corpus.stubs(b)) makes every atlas group member an open instance; tools/atlas.py:657 (exm = max(members, key=…nins)) makes the exemplar the largest open member; tools/build_wave_atlas.py:142 copies it onto the card. Exemplar bankedness is 0% at any project maturity. This is not §40/§40a/§40c family-sweep drainage, and it does not strengthen over time. Do not spend a session re-measuring an early wave to "scope" this — it is a guaranteed null.
Measured, wave T (5 binaries, 34 cards), resolved with corpus.stubs, the oracle:
| card field | entries | banked at card-build time |
|---|---|---|
exemplar |
34 | 0 |
sibs |
146 | 0 (7 resolve banked today; git log -S dates all 7 to this wave's own gate-lane commits) |
atlas seed.ref — not on the card |
26 (ov_SC04_002) | 26 |
THE FIX, and it is one line. tools/atlas.py:505-515 builds pool as corpus.sig(b) minus corpus.stubs(b) — the MATCHED pool — and :525-536 stores seed_best[hs] = {"sim":…, "ref":…}; :592-600 also computes up to 3 M:-tagged matched kNN neighbours. tools/build_wave_atlas.py:143 keeps 'seed_sim': seed.get('sim') and discards seed['ref']; matched_n is carried nowhere. Ship the ref (and the M: neighbours). Byte-grounded: card func_80184FB4 got a stub exemplar (ov_SC03_007:func_8018283C) and a dry sibs list while its discarded seed ref was ov_SC02_011:func_8018C5A4 @ sim 0.951, banked at ov_SC02_011_jr_8017AE2C.c:11212 — a near line-for-line twin of the body the agent then hand-derived at ov_SC04_002_jr_8017BEBC.c:7611 (same +0x64/+0xFE==0x7FFF guard, same +0xFC vs +0x36 early-out into func_8012C218, same +0x20+0x10/+0x18 block copies, same +0x90 compare and func_8012F214(…,&out) else-arm). func_80182A6C @ 0.933 and func_801878B0 @ 0.648 — whose agent reported "exemplar = outright dead end" — were discarded the same way.
Until the ref ships, the corrected search order for a target in a MATURE (>50% C) destination TU:
- the atlas
seed.refif you can read.run/atlas.json— banked by construction, structural-similarity ranked; - same-TU banked body by EXACT SYMBOL JOIN — grep the target
.sforfunc_/D_tokens and find a banked body in the destination TU sharing ≥2. This works because the join key is exact: within one overlay the symbol tokens are literally identical, which no cross-overlay search can use. Measured on ov_SC04_002: 13/21 (62%) of open targets have such a body; - §136c's
engine_core.h DEFINE_*twin (exact and free when it exists); - §9543's cross-overlay distinctive-literal grep — measured 4/21 (19%) on this TU, because most functions carry no magic word (the surviving literals are
0x8000/0x2000/0x7fff); - the
.s; 6. the Ghidra seed (last, §136b). So for a mature TU step 2 moves ahead of §136c's header step and ahead of §9543's STEP 0 — §9543 stays correct as the cross-overlay reach channel, which is what it was written for.
And never spend a token on a card's exemplar/sibs without the body check first — this is §136e's precondition, mechanized: grep -nE '^[A-Za-z_].*\bfunc_XXXXXXXX\s*\(' src/<ov>/*.c (add DEFINE_func_XXXXXXXX( and the __asm__("func_XXXXXXXX") alias form, §9543/R33). One command; it would have saved 13 of 13 lookups on ov_SC04_002 and 34 of 34 wave-wide.
(CORRECTS a candidate that attributed the 0% to family-sweep drainage and predicted it strengthens with maturity — both refuted at atlas.py:657. SHARPENS §136e (L9137, the precondition), §136c (L9088, the search order) and §9543 (L9543, STEP 0). Evidence: .run/harvest_t/, wave T, 2026-08-17.)
BOUND. Where it does NOT apply:
-
Not to the atlas
seedfield.seed.refis banked 100% by construction (atlas.py:505-515pool =corpus.sig − corpus.stubs). The 0%-banked law is aboutexemplarandsibsonly. Do not generalize "atlas pointers are useless." -
Not to
main.load_open()routesmainthroughmain_open_stubs()and the seed pool explicitly skipsb == "main"(atlas.py:507), somaintargets get no seed ref at all — for main, step 2 (same-TU symbol join) is the top of the order. -
The step-2 promotion is bounded by destination-TU density. Measured on ONE
-O2jr-split TU (ov_SC04_002_jr_8017BEBC.c, pre-wave 117 stubs / ~141 bodies ≈ 55% C). On a sparse TU there is nothing to join against and the order reverts to §136c/§9543 as written. The candidate's "TUs already >50% C" scoping is the right guard — keep it, even though its stated reason for needing one was wrong. -
The ≥2-shared-symbol join is a high-precision recall FLOOR, not the whole channel. 3 of the 5 agent-reported same-TU wins fail it:
func_801821C4←func_80183648,func_8018167C←func_80184E2C,func_80182EA0←func_80182180all share <2 symbols and were found by shape, not symbols. So a 0-hit symbol join does not mean "no same-TU twin" — it means the cheap index missed; fall through to a structural read of the neighbours, do not skip to the.s. -
Symbol-join is overlay-local and cannot be lifted cross-overlay.
D_8018xxxxdiffers per overlay by construction; that is precisely why §9543's literal grep exists and why it remains the correct tool for cross-overlay reach. This law reorders the two, it does not retire either. -
engine_core.hDEFINE_*still outranks a symbol-join hit when it exists — an exact shared body beats a ≥2-symbol neighbour, and it is free. -
sibs"0 banked" is a statement about card-build time. Sibs of the same address in a near-clone overlay (theov_SC03_014/ov_SC03_015pair) get banked by the wave's own gate-lane propagation, so a post-hoc resolution will show non-zero. Re-resolving a card's sibs mid-wave is worth doing for exactly this reason.
SECOND INSTANCE. Both halves have independent second instances; I found them by measurement, not from the submitter's list.
For the same-TU symbol-join half (13/21 hits — four strong, reproduced by my own join, not by agent self-report):
func_801861D0→func_801862F0, 5 shared symbols, and it is the immediately-next function: the.sheader readsnonmatching func_801861D0, 0x120(72 ins), and0x801862F0 − 0x801861D0 = 0x120. Pre-wave banked at.run/harvest_t/tu_prewave.c:6973. (This is the submitter's instance, independently reproduced.)func_80184724→func_80185DA8, 4 shared symbols. (Also independently reproduced.)func_801858E4→func_80185668, 4 shared symbols. NOT in the submitter's list — found by my join.func_80182D2C→func_80183FE8, 4 shared symbols. NOT in the submitter's list — found by my join.func_80183FE8is additionally the top hit forfunc_80181B0Candfunc_80182A6C, i.e. one banked body indexes three separate open targets.
For the corrected/new half (the discarded banked seed.ref) — three instances, one byte-grounded:
func_80184FB4←ov_SC02_011:func_8018C5A4@ sim 0.951, banked atov_SC02_011_jr_8017AE2C.c:11212; near line-for-line twin of the agent's eventual body atov_SC04_002_jr_8017BEBC.c:7611(diff in.run/harvest_t/).func_80182A6C←ov_SC02_011:func_801890C8@ sim 0.933, banked.func_801878B0←ov_SC01_001:func_801835C8@ sim 0.648, banked — the card whose agent reported the exemplar as an outright dead end.- Plus
func_80186638←ov_SC06_018:func_80187320@ sim 1.000 andfunc_8017F534←ov_SC01_077:func_8014B12C@ sim 1.000 — perfect-similarity banked twins sitting in.run/atlas.jsonthat no card carried.
§193-B — A NARROW CAST OF A WIDE PARAMETER COSTS ONE EXTRA INSTRUCTION WHEN ITS STATEMENT SITS AFTER AN INTERVENING jal — AND THE DECIDER IS combine.c:929's CROSS-CALL GUARD, NOT REGISTER ALLOCATION
§X — A (s16) CAST OF AN s32 PARAMETER COSTS ONE EXTRA INSTRUCTION IF ITS STATEMENT SITS AFTER AN INTERVENING jal, AND THE DECIDER IS combine.c's CROSS-CALL GUARD — NOT REGISTER ALLOCATION. (byte-refutes §43's "the (s16)param_of_s32 cast form CANNOT reproduce this — it extends into fresh v0/v1 temps instead", and byte-refutes §164-30's closing sentence "add … a use after a jal, and you are back in §43/§99 K&R territory". Distinct from §176-A (stores/delay slots), §178-D (narrow DESTINATION blocks elision), §49 (LUID dial, zero-byte).
DIAGNOSTIC TELL. addu $sN,$aM,$zero in the pre-jal region, then after the call sll $sN,$sN,16 ; sra $sN,$sN,16 — the extension running on the callee-saved copy, not in place on $aM. That is a narrow cast of a wide parameter whose C statement sits after the call. The 2-instruction form sll $sN,$aM,16 ; sra $sN,$sN,16 (shift reads the arg register, no copy) is the same cast written before the call.
THE LAW. assign_parms emits (set (reg/v:SI P) (reg:SI $aM)) for every parameter, and it sits at the top of the stream in both spellings (verified in t.i.rtl and t.i.flow). What decides whether it survives is can_combine_p, combine.c:929: || (INSN_CUID (insn) < last_call_cuid && ! CONSTANT_P (src)), under the comment at :924-928 "Don't combine across a CALL_INSN". With no call between the copy and the sll, combine substitutes the hard arg register into the shift and deletes the copy — 2 instructions. With a call between them the guard refuses (src is a REG, not CONSTANT_P), the copy survives, and because the surviving pseudo now crosses a call local-alloc.c:2103-2106 (qty_n_calls_crossed[qty] == 0 ? fixed_reg_set : call_used_reg_set) denies it $aM/$vN and hands it $sN — 3 instructions. In this size class the pseudo is a local quantity; global.c:924 never sees it (both .greg dumps read ;; 4 regs to allocate: 72 86 95 100).
C LEVER. Keep the parameter s32 and place r = (s16)param; on the side of the jal the target's shape demands. Do not reach for a register __asm__ pin or an allocno-ranking lever — the decision is made in combine, before any allocator runs, and no pin can reach it.
COROLLARY (retires two absolutes). A raw $aM copied to $sN and re-extended after a call does not prove an s16 parameter. On func_80181368 the K&R s16 a1 definition and the plain s32 a1 + deferred (s16)a1 cast both compile byte-identically to MATCH (99/99). The bytes cannot distinguish them, so take whatever width the TU's canon-sig already declares — cast-at-use is a full §73 T0 escape even with a use after a jal.
Citations. combine.c:924-929 (can_combine_p, the cross-call guard — the decider). local-alloc.c:2103-2106 (find_free_reg, why the surviving copy's destination is callee-saved). mips.h:1153 PROMOTE_PROTOTYPES with no PROMOTE_MODE (§189-D) for why s32 a1 arrives raw. NOT global.c:917 — that citation is the wrong pass and the wrong allocator; see the bound.
Evidence. func_80181368 (ov_SC04_002, 99 ins, banked at src/ov_SC04_002/ov_SC04_002_jr_8017BEBC.c:6044): cast after the call ⇒ MATCH 99; cast as first statement ⇒ DIFF 97, LENGTH-DRIFT/-2, 74 mismatched; cast immediately before the call statement ⇒ byte-identical DIFF. Second instance func_80029B4C (MAIN, target 31 ins), independently drafted: both casts after the calls ⇒ 29 ins with move $s0,$a0 / move $s1,$a1 surviving and both sll/sra pairs running on $sN (target shape reproduced); both casts hoisted above the first call ⇒ 28 ins with sll $s1,$a0,16 and sll $s0,$a1,16 reading the arg registers and no copies at all.
BOUND. 1. A jal must actually intervene. combine.c:929 compares INSN_CUID (insn) < last_call_cuid. With no call between the parameter copy and the shift, combine fuses in both spellings and moving the statement is a 0-instruction no-op. The submitter listed this as a falsifier; it is not a refutation, it is the law's precondition. (This is also why §164-30's one-use/no-call case is a different animal.)
-
The gain is +1 only when the extension's result is not itself a call argument. Measured on
func_80029B4C: fora0(extension feeds anaddu, lives in$s0) the "before" form saves exactly 1 instruction. Fora1(extension feedsfunc_80029CD4's$a0) it saves nothing — the "after" form writessra $a0,$s1,16straight into the arg register, while the "before" form has to pay a compensatingmove $a0,$s0. Net across the function: 6 instructions vs 5, i.e. −1, not −2. If the cast's only consumer is a call argument, this lever is byte-neutral. -
The drift you SEE is not the law's number. The count claim is "+1 for the copy". Delay-slot absorption moves the visible total: on
func_80181368the "before" form'ssllandsrafill two load-delay slots (thelw $v0,0x20($s1)andlhu $a0,0x12($v0)shadows) that the target fills with anopand the copy respectively, so the observed drift is −2. Never quote the observed drift as the law. -
Not a type-recovery tell. K&R
s16 a1ands32 a1+ deferred(s16)a1are byte-identical here (both MATCH 99/99). The tell tells you where the statement sits, never what the parameter's declared width was. -
Size/allocator scope. In both functions probed, the parameter pseudo is a LOCAL quantity, so the register half is
local-alloc.c:2103. In a function with enough pressure that the pseudo becomes a global allocno the analogous test isglobal.c:924— same disjunction, different pass. The combine half (:929) is size-independent and is the part that decides whether the copy exists at all. -
Do not pin. The decision is made in combine, upstream of both allocators. A
register __asm__pin on the parameter or onrcannot restore the fusion.
SECOND INSTANCE. func_80029B4C — MAIN binary (asm/nonmatchings/800/func_80029B4C.s, 31 ins), unrelated to the ov_SC04_002 family, and a different parameter register ($a0 as well as $a1). Target carries the tell twice: addu $s0,$a0,$zero / addu $s1,$a1,$zero in the prologue, jal func_80029DB4, then sll $s1,$s1,16 ; sra $a0,$s1,16 and sll $s0,$s0,16 ; sra $s0,$s0,16 after the calls.
I drafted it from scratch (not banked; the function is still nonmatching) and ran both directions:
.run/harvest_t/vB_after.c— casts written at their uses, after the calls ⇒ 29 ins, bothmove $sN,$aMcopies present, bothsll/srapairs running on$sN. Register-for-register the target's shape; the only residual isttaking$s1instead of the target's$s2, an unrelated live-range issue..run/harvest_t/vB_before.c— same statements, casts hoisted into two locals above the first call ⇒ 28 ins,sll $s1,$a0,0x10 ; sra $s1,$s1,0x10andsll $s0,$a1,0x10 ; sra $s0,$s0,0x10reading the argument registers directly, both copies gone.
Same direction as the exemplar, on a second function, a second binary and a second argument register — and it is the instance that produced bound 2 (the a1 sub-case nets 0 because its extension feeds $a0).
Population check: a sweep of all 14,899 asm/**/*.s for addu $sN,$aM,$zero … jal … sll $sN,$sN,16 ; sra $sN,$sN,16 returns 38 distinct functions, 9 of them in MAIN (func_800134FC, func_80013694, func_8001382C, func_800139C8, func_80013B64, func_80013CFC, func_80029B4C, func_80029BC8, func_80029C44) and the rest spread over 20 overlays, split $s0←$a1 ×24 / $s0←$a0 ×14. This is a live, recurring shape, not a family artifact.
§193-C — gcc-2.7.2 cross_jump merges the SCHEDULED common SUFFIX only — there is no prefix/head merge, so §8/§48-A1's "duplicate into both arms and cross_jump refunds it" is a TAIL-only lever
§NEW — cross_jump MERGES THE SCHEDULED COMMON SUFFIX ONLY: gcc-2.7.2 HAS NO PREFIX MERGE, SO §8 / §48-A1 / §48-A4's "DUPLICATE INTO BOTH ARMS AND cross_jump REFUNDS THE BYTES" IS A TAIL-ONLY LEVER.
(sharpens §8 / L1893-1898, §48-A1/A4 / L3461, §50-B, §162g/h, §186 — every one of which bounds the refund by tail LENGTH, tail CALL-content or barrier placement, and none of which says the refund does not exist at the head. The backward walk itself is already stated in §5a / L219, §164-09 / L12032 and §186 / L18069; this entry is that walk turned into a source-writing prescription.)
THE LAW (read out of the pinned gcc, not inferred). find_cross_jump (tools/reference/gcc-2.7.2/jump.c:2371) walks its two streams strictly backward — i1 = prev_nonnote_insn (i1) / i2 = PREV_INSN (i2) (:2388-2391) — and stops at the first rtx_renumbered_equal_p failure. All four call sites anchor on a JUMP_INSN, i.e. a block END: :1941 (cond jump, minimum=2), :1978 (simplejump vs its own label, minimum=1), :1993 (jump vs jump to the same label, minimum=2), :2024 (RETURN chain, minimum=2). There is no forward equality walk anywhere in jump.c (next_nonnote_insn appears only in the if-conversion peepholes), no gcse.c, and no code-hoisting pass in 2.7.2. do_cross_jump (:2537) redirects and deletes insns only. Nothing in gcc-2.7.2 merges a common PREFIX.
THE PRESCRIPTION. Write a shared statement ONCE, above the if, when the target shows it once — and expect NO refund if you duplicate it at the head of both arms. §8's "duplicate the same call into both arms" only pays when the duplicate is the arms' last code.
⚠ THE INVARIANT IS THE SCHEDULED SUFFIX, NOT THE SOURCE POSITION (jump2 runs after sched2, toplev.c:3104/3142 — §186). Both halves of the source-position heuristic break:
- A head statement CAN be merged in part.
if(c){A=p[0];B=1;} else {A=p[0];C=2;}→ sched2 sinkssw $3,Ato the end of both arms, cross_jump merges it into one copy at the join; only thelwstays duplicated (10 ins vs 9 for the hoisted spelling). - A tail statement can FAIL to merge. Appending the same statement to the end of both arms of
func_801810E4cost +4 (bothli;swcopies live) because sched2 relocated arm A's copy into the middle of the block.
⚠ THE COST IS SMALLER THAN THE SOURCE STATEMENT. The value-computing half of a duplicated head statement is commoned anyway (one li, two sw). On func_801810E4 a 2-insn source statement cost exactly 1 insn when head-duplicated.
⚠ HOIST vs HEAD-DUP DIFFER IN LIVENESS, NOT JUST IN MERGING — AND THE HOIST CAN BE THE LONGER ONE. Hoisting a call above the if forces the condition to live across the call: if(c){g(p);B=1;} else {g(p);C=2;} is 17 ins (frame 24, saves $16) while g(p); if(c){B=1;} else {C=2;} is 18 (frame 32, saves $16+$17). Never predict this residual by counting instructions — read the frame size and the .mask too.
DIAGNOSTIC TELL. The target has a store/call ONCE, sitting in the delay slot of the arms' conditional branch (e.g. bnez $v0,.L… ; sb $zero,0x75($s0)), while your draft has two copies of it inside the arms and the SAME instruction count. dbr refilled the freed slots, so the count lies — read the delay slots, not the length. Fix: write the statement once, before the if.
BOUNDS — the law does not apply when:
- the arms are identical after the shared head (the head IS the tail; the whole
ifcollapses —if(c){A=5;} else {A=5;}→ 3 ins); - sched2 sinks the shared insn to the block end in BOTH arms (then it merges — the u3 case above);
- the shared head is a pure computation (constant, CSE-able load) — cse commons it across the branch regardless; only side-effecting insns duplicate;
- you try to use the residual's sign as the tell — it is not a length law.
BOUND. The law is about the SCHEDULED insn suffix, not source text, so four scopes are outside it. (1) Arms identical after the shared head — the head is then also the tail and the whole if collapses (if(c){A=5;}else{A=5;} → 3 ins, .run/harvest_t/verif_head/s8.i). (2) sched2 sinks the shared insn to the block end in both arms — then it IS a suffix and cross_jump merges it; measured on u3.i (sw $3,A once at $L4, only the lw duplicated). Symmetrically, a source-level TAIL duplication is NOT guaranteed to merge: sched2 can relocate it into the middle (in-tree func_801810E4 tail-dup control = +4, both copies live). (3) Pure value computation in the head — constants and CSE-able loads are commoned across the branch anyway, so a duplicated head statement costs FEWER insns than it contains (in-tree: 2-insn source statement, 1-insn cost). (4) It is not a length law. Observed cost directions: 0 (SHIFT-DRIFT/+1 at 69=69, func_801855A0), +1 (LENGTH-DRIFT, func_801810E4), and −1 (s3 head-dup 17 vs s4 hoist 18, where hoisting a call above the if extended the condition's live range and bought an extra callee-saved reg + 8 bytes of frame). The submitter's "the residual hides as a schedule shift rather than a length drift" is one outcome of three; the durable discriminator is the frame size / .mask and the branch delay slots, not the count. Also unscoped: nothing here was tested with a switch dispatch or with more than two arms.
SECOND INSTANCE. func_801810E4 (ov_SC03_112, 87 ins, wave-T MATCH) — a different overlay, a different function, byte-probed by me. Base .run/wave_p31t/ov_SC03_112/func_801810E4.c = MATCH (87). I inserted one novel statement *(s32 *)(a0 + 0xD4) = 0x1234; in two spellings around its if (v0 & 0x1) {…} else {…}:
- HOISTED once above the
if(.run/harvest_t/verif_head/i2_hoist.c) → 89 ins (+2 =li+sw). - DUPLICATED at the HEAD of both arms (
.run/harvest_t/verif_head/i2_headdup.c) → 90 ins (+3). Disassembly (.run/harvest_t/verif_head/w_i2_headdup/func_801810E4/t.o):li v0,4660appears ONCE (cse commoned it) andsw v0,212(s1)appears TWICE, at 0xf0 and 0x10c. Not merged — a second, independent in-tree instance of the head law, and simultaneously the evidence for bound (3). - Control, duplicated at the TAIL of both arms (
i2_taildup2.c) → 91 ins (+4), bothli;swcopies live (0xf8/0xfc and 0x134/0x138) — the tail refund FAILED here because sched2 moved arm A's copy into the middle of the block. That is the bound-(2) evidence.
Plus 8 independent synthetic probes I wrote and compiled with the pinned cc1 (.run/harvest_t/verif_head/{s1,s3,s5,s7,s9,t2,t4,u1}.i and their hoisted twins): in every one, at least one insn of the head-duplicated statement stays duplicated. s9.i shows both behaviours in one function — the head pair li 5 / jal g / sw 0($16) duplicated, the tail p[3]=9 merged into $L4.
§193-D — A COMPILER-GENERATED ARGUMENT COPY IS A optimize_reg_copy_1 TRIGGER: when the pointer's LAST use in a block precedes a call that takes the pointer as a register, sched1 hoists the implicit move $aN,$sN to the block top and local-alloc re-bases the WHOLE block's memory operands onto $aN. The only C dial is a label (the §165-24 goto-join) between the block and the call.
⚠ ITS C DIAL IS WRONG — CORRECTED BY §194-N (same session). The lever is a SURVIVING
CODE_LABEL(a label with a real incoming edge), not "a label between the block and the call":jump_optimizeruns long before sched1 and deletes any label withLABEL_NUSES == 0, rewriting a C user label into aNOTE_INSN_DELETED_LABEL— and a NOTE is not a basic-block boundary. A baretail:therefore changes nothing. Use §194-N's form.
§NEW — THE CALL'S OWN ARGUMENT COPY IS AN optimize_reg_copy_1 TRIGGER: when the pointer DIES at a call that takes it as a register, sched1 hoists the implicit move $aN,$sN to the block top and local-alloc re-bases the WHOLE block's memory operands onto $aN. The C dial is a label between the block and the call. (SHARPENS §165-24, L14297 — same goto-join dial, but §165-24 prices it only as a reorg.c delay-slot residual and leaves its closing "choose the spelling that leaves the rest of the block's registers alone" as a hint with no mechanism; this is that mechanism. SHARPENS §162j1 (L11392) / §165-33 (L14560) by adding the case where the copy is NOT in the source, so neither of their levers — in-place SET, hoist-the-use-above-the-copy — is expressible. Cousin of §164-69 (L13371), the other join-vs-duplicate sched1-block-scope lever.)
Target shape — a run of memory ops on a callee-saved base ending in a call that takes that base:
target: sb $v0,0xC0($s1) … sw $v1,0xC4($s1) <- 13 ops, ALL on $s1
jal func_8012AD50
addu $a0,$s1,$zero <- the copy is in the jal's OWN delay slot
mine: addu $a0,$s1,$zero <- hoisted to the block TOP
sb $v0,0xC0($a0) … sw $v1,0xC4($a0) <- all 13 re-based
jal func_8012AD50 ; nop
THE MECHANISM (four passes, each byte-witnessed in -da dumps).
- Expand emits the argument as a plain reg-reg copy
(set (reg:SI 4 a0) (reg/v:SI 72))immediately before thecall_insn, and at.combineit carriesREG_DEAD (reg/v:SI 72). - sched1 (
toplev.cpass order; per-basic-block) sees the copy has no dependence on the block's stores and relocates it to the top of the block. Flow's death note is destroyed by the move: every store is now a later use of the pseudo..sched:(insn 216 175 173 (set (reg:SI 4 a0) (reg/v:SI 72))) (nil)— no note. - That is exactly the precondition
local-alloc.c:1004-1006tests.update_equiv_regscallsoptimize_reg_copy_1(:700, call site:1007), which scans forward (:721) andvalidate_replace_rtx (src, dest, q)es pseudo → hard$aNat every use before the pseudo's death (:772), then moves the death note onto the copy..lreg: 26(reg/v:SI 72)mentions → 18, and every surviving store reads(reg:SI 4 a0). The#ifdef SMALL_REGISTER_CLASSESescape at:712-715is inactive —config/mips/mips.hnever defines it — which is why a HARD$aNis a legal DEST. Siblingoptimize_reg_copy_2(:874, site:1010-1015) is excluded by:870(both regs must be pseudos). - dbr then has nothing left to put in the
jal's slot, so the slot takes a store (or anop) instead of the copy — §165-24's delay-slot half, same edit, one pass later.
THE C DIAL. optimize_reg_copy_1 is disqualified at its CALL SITE, not out-scanned, if the copy keeps its REG_DEAD. Put the call in its own label-headed basic block — §165-24's shared-tail goto join — and the copy is alone in that block with no uses below it, so sched1 cannot move it, the pointer dies at the copy, and copy_1 never runs. Write the call inline at the end of the store block and the re-base fires. That is the whole lever; there is no in-source copy to hoist a use above (§165-33) and no arithmetic use to make in-place (§162j1).
THE THREE REAL PRECONDITIONS (the originating note's three were wrong on two counts — corrected here, byte-proven):
- No label between the store block and the call. A basic-block boundary is what sched1 cannot cross. (The originating note's extra clause "and the block has ONE predecessor" is FALSE: giving the store block a second predecessor via a
gotointo its head leaves the re-base intact. sched1 has no predecessor-count input.) - The pointer's LAST use must precede the call — not "the call is the block's last statement," which is FALSE. A statement after the call that does not touch the pointer changes nothing (
D_801AB570 = 0;after thejalstill re-bases). What kills it is a use of the pointer after the call: the death note lands past thejaland the scan breaks atlocal-alloc.c:732reg_set_p (dest, p), which is true at any CALL_INSN because DEST is a hard register. - The pointer must be passed AS A REGISTER, i.e. the argument expression is the pointer variable itself — not "its sole argument is the pointer," which is FALSE. A 3-argument call with the pointer first re-bases onto
$a0; the same call with the pointer second re-bases the whole block onto$a1. Arity and position are irrelevant. What matters is:1005'sGET_CODE (SET_SRC (set)) == REG: passing*(s32*)(p+0x20)expands tolw $a0,32(pseudo), not a copy, and copy_1 is never called.
BYTE EVIDENCE (func_80181F38, ov_SC02_027, 97 ins; files in .run/harvest_t/, gate tools/match_one.py --asm-subdir .run/waveT_asm_snapshot/ov_SC02_027):
| spelling | result |
|---|---|
base_1F38.c — shared tail: func_8012AD50(a0); after a goto join |
MATCH 97/97, all 13 ops on $s1, copy in the jal's delay slot |
F_nojoin.c / G_nojoin_armsame.c — call written inline at the block end |
98 ins, 30 mismatched, LENGTH-DRIFT/1, all 13 ops re-based to $a0 (byte-identical diffs ⇒ the discriminator is the join, not the call duplication) |
H_nojoin_notlast.c — a pointer store after the call |
101 ins, 19 mismatched, ops stay on $s1 |
I_nojoin_otherarg.c — argument is *(s32*)(a0+0x20) |
98 ins, 20 mismatched, ops stay on $s1 |
verify/K_multiarg.c — f(p, 6, 7) |
re-bases to $a0 ⇒ "sole argument" refuted |
verify/L_ptr_second.c — f(6, p, 7) |
re-bases to $a1 ⇒ any argument register |
verify/N_postcall_otherptr.c — non-pointer store after the call |
re-bases ⇒ "call is last" refuted |
verify/M_twopred.c — store block given a 2nd predecessor |
re-bases ⇒ "one predecessor" refuted |
ABLATION. -O2 -fno-expensive-optimizations on G puts (reg/v:SI 72) back at every .lreg operand and leaves the copy note-less ⇒ the mechanism is inside flag_expensive_optimizations, and since regclass.c:702/958 rewrite no RTL, it is local-alloc.c:1004-1015. This class cannot occur in an -O1/-O0 file (§116/§127).
DIAGNOSTIC TELL. A whole straight-line block's memory operands read $aN where the target reads $sN, coupled to the argument move sitting above the block instead of in the jal's delay slot, on a block that ends in a call taking that pointer. Do not reach for §17/§76/§186c pins (§178's over-diagnosis warning: the register map is a symptom, the block boundary is the cause) and do not triage the delay-slot residual separately from the register residual — §165-24 and this section are one edit. Count the source copies of the terminal call; if it is written once inline, give it a goto-join and a label of its own.
Symptom lines for the index: "my whole store block is based on $a0 and the target uses $s1" · "the argument move is at the top of the block instead of in the jal's delay slot" · "13 stores on the wrong base register and no pin moves them" · "joining the tail with a goto changed the register allocation, not just the delay slot"
BOUND. Where it does NOT apply.
-O1/-O0files.flag_expensive_optimizationsis set only at-O2(toplev.c:3391); the ablation proves the whole class evaporates without it. Same scope as §162j1/§116/§127.- The pointer is the incoming parameter still living in
$aN. Inside the parameter's own definition block gcc emits no copy at all (§16Xd, L13666) — no copy, no copy_1, nothing to steer. This law only bites once the pointer has been moved to a callee-saved register across a call/branch. - The argument is an expression, not the variable.
f(*(s32*)(p+K)),f(p+K),f((T*)q)where the cast forces a computation — any of these expands to something other than(set (reg:SI 4 aN) (reg pseudo))and:1005'sGET_CODE (SET_SRC (set)) == REGfails. Measured:I_nojoin_otherarg. - The pointer is used again after the call in the same block. Scan breaks at
local-alloc.c:732. Measured:H_nojoin_notlast. - Any label, loop note, or jump between the copy's hoisted position and the pointer's death —
:722-727breaks on CODE_LABEL, JUMP_INSN, NOTE_INSN_LOOP_BEG/END. This is the same escape §162j1's untesteddo{}while(0)wedge suggestion aims at, and it is why the goto-join works. - sched1 must actually be able to hoist the copy. If the pointer is computed inside the block, or a volatile/
asm volatilebarrier sits above the copy, the copy stays low, keeps its death note, and the re-base never fires. Untested here — predicted fromsched.c's dependence rules, so R14 applies to this clause. - The magnitude is not part of the law. How many operands get re-based is just "however many uses of the pointer sit between the hoisted copy and its death". Do not read "13 stores" as a threshold.
- The delay-slot half is §165-24's, and the two are not separable. Do not cite this section for a pure DELAY-SLOT/N residual with a correct register map — that is §165-24 alone.
- Not a licence to prefer the join. Three real targets in this tree (below) are compiled in the no-join form. The law is a two-way dial, not a prescription.
SECOND INSTANCE. Found, and it is not one — it is three, all in the same actor-init idiom, and they run in the OPPOSITE direction from func_80181F38, which is what makes them load-bearing. I scanned all 14,899 asm/**/*.s for move/addu $a0,$sN followed by ≥3 $a0-based stores terminating in a jal (.run/harvest_t/verify/scan.py): 99 sites, of which these are unambiguous:
asm/ov_SC02_005/nonmatchings/ov_SC02_005_jr_80181D30/func_801885BC.s@ 0x80188620.addu $a0,$s0,$zeroat the block top, then ten$a0-based ops at exactly the same offsets as our function (0xC0,0xB4,0xBC,0xAE,0x75,0xC4|=2, …), endingjal func_80132784with$a0=pointer,$a1=lw 0xCC($v1),$a2=6. A three-argument call — this target instance alone falsifies the "sole argument" precondition, independent of my K/L compiles.asm/ov_SC06_000/nonmatchings/ov_SC06_000_jr_8017AE2C/func_80183F38.s@ 0x80184014.addu $a0,$s0,$zero, nine$a0-based ops over the same field set, terminatingjal func_8012AD50— the identical callee as our exemplar — with the delay slot holding a real store (sh $v1,0xFE($a0)), not amove. This is the same function shape asfunc_80181F38compiled in the no-join spelling. Whoever cracks it must NOT reach for thegoto tail;join that madefunc_80181F38match.asm/ov_SC03_118|119/nonmatchings/…/func_80181E58.s@ 0x80181F38 (byte-identical pair) andasm/ov_SC03_014|015/…/func_80188670.s,asm/ov_SC03_024/…/func_80181DB0.s— same shape, 8 ops each.
So both poles of the dial are attested in real target bytes across five overlays, in the same engine idiom, with the same callee. The law is not a property of func_80181F38.
§193-E — A varying-address (pointer) load is re-emitted once per CSE-LIVE INTERVAL, and naming it in a C local is the only C-level lever over that count — no store SPELLING has any reach
A VARYING-ADDRESS (POINTER) LOAD IS RE-EMITTED ONCE PER CSE-LIVE INTERVAL, AND NAMING IT IN A C LOCAL IS THE ONLY C-LEVEL LEVER OVER THAT COUNT — NO STORE SPELLING AT A DIFFERENT ADDRESS HAS ANY REACH. (extends cse_expr.md §4b's table from "varying loads die" to a transcription arithmetic; BOUNDS §165-27, whose dial — the LOAD's declaration — does not exist for a pointer deref; generalizes the §21 L1883-1892 reload bullet from N=2 to N.)
Mechanism (read out of the pinned source). In cse_insn, every MEM destination goes through note_mem_written (cse.c:6991 → :7539), which sets writes_ptr->var = 1 at :7577 for any MEM store (only exception: a stack PRE_DEC/POST_INC auto-inc dest returns at :7551-7557 with sp=1 — impossible on MIPS). invalidate_from_clobbers (:7236 → :7599) then calls invalidate_memory (:1701) on var alone. Its load-side test is p->in_memory && (all || (nonscalar && p->in_struct) || cse_rtx_addr_varies_p (p->exp)) (:1711-1717) and the third disjunct is unconditional on the store. A pointer load *(T *)(reg + K) has a varying address, so it dies to any store — /s member, cast-over-PLUS, u8, a fixed global, even a store to a dead local. The whole /s grant/deny toolkit (§30a-1, cse_expr.md §4c, §165-27) is therefore INERT on a pointer load. The only surviving lever is REG-vs-MEM: bind the value to a named C local and it becomes a pseudo, which invalidate_memory never walks (p->in_memory only).
The transcription arithmetic. Count the target's lw <base> in the region. That number equals the number of CSE-live intervals you must write the raw expression across. A named local collapses every interval it spans into one load. Uniformly caching and uniformly repeating are both wrong whenever the target's count is strictly between 1 and N; the banked spelling is normally a MIX.
What ends an interval (all three, not just stores): (1) any store whose address is DIFFERENT from the load's; (2) any non-const CALL (cse.c:7246, invalidate_memory(&everything)); (3) a (mem (scratch))/BLKmode clobber. What does NOT end an interval: a conditional branch — cse runs over extended basic blocks, so a load survives an if with no store in it (byte-probed: one lw across an if). What RE-SEEDS the interval: a store TO THE LOADED ADDRESS. The entry is killed at :1716 and immediately re-inserted by the store itself, so the next read becomes the stored register and NO load is emitted — the count comes out one BELOW the interval count. (Byte-probed: 3 raw reads / 3 intervals / 2 loads.)
DIAGNOSTIC TELL. Your draft emits 1 load of a pointer base where the target emits N (SIZE-MISMATCH/short, N−1 lw plus their load-delay nops missing), or emits N where the target emits fewer (LENGTH-DRIFT +k, the extra lw landing immediately before a trailing store). Do NOT respell the stores and do NOT reach for /s, volatile, or a memory clobber: count the intervals, then split the base's uses between raw repetitions (one per interval you want a load in) and one named local (spanning the intervals you want collapsed).
BYTE EVIDENCE. func_80180440 (ov_SC03_112, 60 ins, wave-T MATCH). Target: four lw $v1, 0x20($s0) at 80180464/8018047C/80180494/801804AC = four intervals (three += 0x40 RMWs each closed by an sh, then the compare-plus-first-store cluster). Three single-axis spellings on the pinned triple, load-counts from the cc1 .s: banked mix (3 raw + 1 named) → MATCH, 60 ins, 4 loads; hoist the local to cover the three RMWs too → 50 ins (−10: three lw + six load-delay nops + a lhu pairing shift), SIZE-MISMATCH/short, 1 load; delete the local and raw-repeat the compare cluster → 64 ins, LENGTH-DRIFT/4, 6 loads (the two extras appear immediately before the 2nd and 3rd trailing sh; the compare's load still shares with the FIRST sh, no store between them). Model predicts 4/1/6; cc1 emits 4/1/6. Second instance: func_80184FB4 (ov_SC04_002, 93 ins, MATCH) — 6 raw *(s32 *)(s0 + 0x20) sites → 6 lw 0x20($s0), 4 raw *(s32 *)(s1 + 0x20) sites → 4 lw 0x20($s1), no named local for either base.
(SHARPENS — extends docs/gcc-2.7.2-map/cse_expr.md §4b (its table's "Lever reading" paragraph is scoped to a fixed-SYMBOL load and offers nothing for a varying one), BOUNDS §165-27 / L14428 (the LOAD's-declaration dial has no counterpart for a pointer deref), generalizes §21's L1883-1892 reload bullet from two loads to N; evidence: byte-probed; from func_80180440, func_80184FB4)
BOUND. 1. NOT "any store" — any store AT A DIFFERENT ADDRESS. A store to the very address being loaded re-seeds cse's table with its own dest MEM, so the following read is replaced by the stored register and the load count comes out one BELOW the interval count. Byte-probed (.run/harvest_t/m2.c t6b, pinned cc1): 3 raw reads, 3 store-free intervals, 2 loads — lw $3,32($4) / sh $2,24($3) / sw $5,32($4) / sh $2,26($5) (no reload) / lw $3,32($4) / sh $2,28($3). This kills the submitted falsifier clause "no store spelling can save it" as literally written, and it is a practically common shape (p->next = q; p->next->x = 1;).
2. "Store-free" is the wrong boundary predicate — the right one is "cse-live". A non-const CALL flushes the whole memory table (cse.c:7246) with no store present at all: two raw reads, zero stores, one jal → 2 loads (t7b). BLKmode / (mem (scratch)) clobbers likewise (note_mem_written :7558-7562).
3. A conditional branch does NOT end an interval — cse runs over extended basic blocks, so a load survives an if with no store in it (t8b: one lw across an if). Do not read "interval" as "basic block"; it is longer.
4. Only applies to a load cse actually caches. volatile loads are never hash-inserted, so they reload unconditionally regardless of intervals (this is the §21 surgical-reload lever and is a different mechanism). A load already inside a loop is additionally subject to loop.c invariant motion.
5. The "no lever" half is scoped to VARYING addresses. A load from a fixed symbol or a $sp+const frame slot is non-varying, and for those the /s toolkit and the LOAD's-declaration dial (§165-27, cse_expr.md §4b) DO apply. Do not carry the "no lever" verdict over to those.
6. Count-only, not placement. The rule fixes how many loads exist; WHERE each lands inside its interval is sched1/sched2 (§34/§37) and can still differ. And per §165-27's coupling note, converting raw→named also changes register pressure and can move the frame — re-verify the whole function, not just the load count.
7. writes_ptr->var = 1 has one early-return escape (stack auto-inc dest, cse.c:7551-7557, sp=1 then return). Unreachable on MIPS; noted so nobody re-derives "unconditional" from the source and gets contradicted on another target.
SECOND INSTANCE. func_80184FB4 — ov_SC04_002, banked wave-T draft at /home/musashi/bfm-decomp/.run/wave_p31t/ov_SC04_002/func_80184FB4.c, target at /home/musashi/bfm-decomp/.run/waveT_asm_snapshot/ov_SC04_002/func_80184FB4.s. I re-ran it: MATCH (93 ins). The C names no local for either pointer base and spells *(s32 *)(s0 + 0x20) raw at 6 sites (+0x12 read, the SV4x copy to +0x10, the +0x12 store, the SV4x copy to +0x18, the +0x2C store, the +0x4 store) and *(s32 *)(s1 + 0x20) raw at 4 sites — each site separated from the next by at least one store. The target contains exactly 6 lw $vX, 0x20($s0) and exactly 4 lw $vX, 0x20($s1). Different overlay, different base register pair, different offsets, 93 vs 60 instructions.
Supporting corpus evidence that the shape is not a one-off: 17 of the 70 wave-T snapshot .s files carry ≥3 identical lw $v?, 0xK($s?) from one base (e.g. ov_SC03_014/func_801867F8.s has 9 lw $v0, 0x20($s0), ov_SC06_032/func_80184B08.s has 5 of 0x64($s0), ov_SC02_027/func_80187288.s has 4 of 0x64($s0)).
Independent mechanism corpus (my own micro-probes, pinned cc1, .run/harvest_t/m1.c–m3.c): 8 further single-axis cases confirming the predicate, including the two that bound the law.
§193-F — §148-A2 — The threshold -= 3 STAIRCASE: move_movables admits a COUNT of invariants, not a boolean — two identical merged constants split hoisted/not-hoisted by LIST ORDER, and insn_count picks the rank cutoff
§148-A2 — THE threshold -= 3 STAIRCASE: move_movables ADMITS A COUNT OF INVARIANTS, NOT A BOOLEAN. TWO IDENTICAL MERGED CONSTANTS CAN SPLIT HOISTED / NOT-HOISTED PURELY BY THEIR ORDER IN THE MOVABLE LIST.
(SHARPENS §148-A (L10148) — which has the formula, the decay and the -dL read but frames every hoist as one movable, all-or-nothing; §2517's coalescable-copy insn_count bump and t7g-harvest L230-236, same single-movable framing; and docs/gcc-2.7.2-map/loop.md L51-53, which lists 29/58, -= 3 and insn_count *= 2 as scalars with no split consequence. DISTINCT from L4107-4109's n_times_set != 1 lever, which deletes the movable instead of raising the count — byte-separated below. NOT §164-67 (loop.c:3241, the giv threshold — a different number, do not mix). Evidence: byte-probed + tree-wide simulated; from func_8017E4B4, ov_SC03_112.)
THE LAW. scan_loop sets threshold = (loop_has_call ? 1 : 2) * (1 + n_non_fixed_regs) = 29 with a call in the loop, 58 without (loop.c:532, n_non_fixed_regs = 28). move_movables walks the movable list IN ORDER and admits each iff
already_moved[regno] || threshold * savings * m->lifetime >= insn_count
|| (m->forces && m->forces->done && n_times_used[forces->regno] == 1) (loop.c:1630-1633)
and after every actual move does threshold -= 3 (loop.c:1719 and :1904). combine_movables (loop.c:1281-1284) first merges textually equal invariants into one class, pooling savings and lifetime into the class LEADER and marking the followers done (they print done … matches N and never reach the test).
Consequence: the test gets strictly stricter as the pass proceeds. Two movables identical in every field the cost model reads — same pooled savings, same pooled lifetime — split verdicts on nothing but their position. For a savings-2 / lifetime-2 constant the admission bound falls 116, 104, 92, … so insn_count does not answer "does this loop hoist?" — it answers "how many of the ranked movables hoist?". The band in which exactly the first of two adjacent twins hoists is 3 · savings · lifetime wide (12 here; 3 · k · savings · lifetime if k other movables move between them).
THE DIAGNOSTIC TELL. The target hoists ONE constant into a callee-saved register and leaves its textually identical twin inline (here: addiu $s6,$zero,0x1 in the preheader, addiu $v0,$zero,0x2 twice in the body), while every spelling of yours hoists both — costing one extra callee-saved slot and shifting the whole $s-file. That is not a regalloc residual, not §162e3's candidate filter, and not a difference between the two constants. It is an insn_count reading, and it is arithmetic. Do not open §47 / §158 / §136 on it.
HOW TO STEER IT (both dials byte-witnessed here, each worth exactly +2 insns, each alone flips the second hoist): raise the loop's insn_count with source that costs nothing in the final binary —
- write the back-edge TWICE (
param_1 += 0xE; continue;on the early-out path and again at the bottom). loop.c counts both; jump2's cross-jumping tail-merges them later (§186 — cross-jumping runs after scheduling). Measured: collapsing them to one → 103 insns, second hoist admitted, match lost. - route a returned constant through an ALREADY MULTIPLY-SET variable (
a3 = 0x800; return a3;wherea3is set in four other places) instead of a fresh temp. Measured: single-assignment temps ({u32 t = 0x800; return t;}) → 103 insns, i.e. cse/jump propagate the copy away; the multiply-set variable keeps it → 105. (Distinct from L4108's dead-second-set lever, which makesn_times_set != 1and removes the movable entirely; here the movable survives — it is regno 86 in the dump — only the COUNT changes.)
READ IT, DO NOT GUESS IT. cc1 -dL writes <file>.i.loop. The free variable is the line §148-A never named: Loop from A to B: N real insns — that N is insn_count. Then per movable, Insn K: regno R (life L), … savings S moved to U / not desirable. Every input to the arithmetic is printed except loop_has_call (infer 29 vs 58 from the first verdict).
BYTE EVIDENCE. func_8017E4B4 (ov_SC03_112, 104 ins, MATCH 104/104, banked at src/ov_SC03_112/ov_SC03_112_jr_8017C294.c:3821). Banked dump: Loop from 14 to 257: 105 real insns. → Insn 95: regno 84 (life 2), move-insn savings 2 moved to 270 / Insn 106: regno 86 (life 2), move-insn savings 2 not desirable, with Insn 228 … matches 95 and Insn 239 … matches 106 proving the merges. 29·2·2 = 116 ≥ 105 → move; threshold 26; 26·2·2 = 104 ≥ 105 false → refuse. The whole predicted band measured on the pinned cc1: insn_count 103 → both move (105 ins, 63 mismatched, LENGTH-DRIFT); 104 → both move (the exact >= edge); 105 → exactly one moves (MATCH); 118 → neither moves. Target binary witness: .run/waveT_asm_snapshot/ov_SC03_112/func_8017E4B4.s:12 preheader addiu $s6,$zero,0x1, body :32/:76 addiu $v0,$zero,0x2.
TREE-WIDE VALIDATION. -dL over all 4161 src/ TUs → 33,412 loops, 16,174 with decided movables. A simulator of loop.c:1630-1633 + the -= 3 decay + the insn_count *= 2 doubling reproduces every moved/not desirable verdict in the tree: 0 mispredictions / 16,174. Deleting the decay from the model breaks 479 loops; deleting the two disjuncts breaks 383. Genuine identical-twin splits: 163 loop instances across 14 distinct banked functions.
BOUND. Six bounds, all read out of loop.c or measured in the tree scan. The first cost me a false positive during this verification and is the one that will burn the next reader.
-
forcesbypasses the arithmetic entirely — and it is the MAJORITY of apparent twin splits. A naive scan for "one moved, one refused, same life+savings" over the whole tree returns 1535 loops. After excluding movables whose dump line carriesforces N(them->forces && m->forces->donedisjunct, loop.c:1632) only 163 remain. I initially "confirmed" a second instance (func_8017D420, ic=30, insn 296 moved / 320 refused, both life 1 savings 1) that is nothing of the kind — insn 296 readsmove-insn forces 260 savings 1and moved for free. Never read a moved/refused pair as a threshold fact without checking the line forforces,force,condandglobalfirst. Same foralready_moved[regno]: a movable sharing a regno with one already moved is admitted unconditionally. -
insn_countis DOUBLED, permanently, for the rest of the call, the first time a movable's regno wasmoved_onceout of an earlier loop of the same function (loop.c:1609-1615,insn_count *= 2, dump printshalved since already moved). In a nested or multi-loop function twins can split for THIS reason instead of the decay, and the doubling persists for every later movable in that loop. Witnessed in the tree atfunc_8017E5F0(ov_SC02_011), whose outer loop shows all three movableshalved since already moved. -
The decay does NOT fire on the
m->partial && m->matchbranch (loop.c:1641-1668 — the zero-extend chain merge). That branch emits its move and printsmoved to Nwithout ever reaching athreshold -= 3. Zero-extend movables therefore hoist without stiffening the test for anything after them. -
Getting
loop_has_callwrong is a factor-2 error and the dump does not print it. 29 with a call, 58 without. §148-A's worked example uses 58; this one uses 29; they are the same formula. Infer the branch from the first verdict before doing any arithmetic, or you will predict the wrong band. -
Strictly earlier gates are not governed by this law.
not safe(them->cond+invariant_p/consec_sets_invariant_pfilter, loop.c:1585-1595) and §148-A/§162e3/§13504's candidate filters decide whether a movable exists at all;combine_movablesmerges only whenn_times_used == 1 && !global && !partialand the modes are compatible, so a constant also used outside the loop keeps its own unpooled savings/lifetime and will not present as a twin. An invariant that never reaches loop.c:1631 is out of scope — read the candidate filter first (§162e3). -
insn_countis loop.c's count at the timeloopruns — pass order expand → jump → cse → LOOP → cse2 → flow → combine → sched → regalloc → sched2 → crossjump. That is exactly why dial (1) is free (jump2 cross-merges the duplicated back-edge afterwards) — and it is also the bound: any construct you add to raise the count must be deleted by a pass that runs AFTER loop, never before. Anything cse1 or jump1 can fold is invisible to the count (measured: single-assignment temps → 103, not 105).
Untested edge, stated honestly: I measured the lower boundary exactly (104 both / 105 split) and the upper region at 118 (neither). The exact 116/117 crossing is predicted by the same arithmetic but was not compiled.
SECOND INSTANCE. Found many, in banked (byte-matched) source, all with the forces cases excluded — 163 loop instances across 14 distinct functions: func_8001E7E0, func_8013A380, func_8017BF14, func_8017C154, func_8017CD9C, func_8017D960, func_8017E4B4, func_8017E5F0, func_8017E778, func_80183D84, func_80186E24, func_80187354, func_8018ECA0, func_80192F64.
Three worked in detail:
func_8013A380 (src/ov_SC02_003/ov_SC02_003_jr_801380E0.c and ~30 other overlays — the cleanest instance in the tree, THREE identical movables):
Loop from 11 to 77: 26 real insns.
Insn 45: regno 77 (life 1), move-insn savings 1 moved to 84
Insn 51: regno 79 (life 1), move-insn savings 1 moved to 86
Insn 57: regno 80 (life 1), move-insn savings 1 not desirable
29·1·1 = 29 ≥ 26 → move; 26 ≥ 26 → move (the exact >= edge again); 23 < 26 → refuse. Three movables identical in every printed field, no forces, no cond, verdicts decided purely by rank. This is §148-A2 in four lines.
func_80187354 (src/ov_SC02_000/ov_SC02_000_jr_8018173C.c) — the instance that pins loop_has_call too:
Loop from 29 to 146: 50 real insns.
Insn 43: regno 84 (life 1), move-insn savings 1 not desirable 29·1 = 29 < 50
Insn 56: regno 90 (life 2), move-insn savings 1 moved to 158 29·2 = 58 >= 50 -> thr 26
Insn 77: regno 101 (life 1), move-insn savings 1 not desirable 26·1 = 26 < 50
Insn 91: regno 108 (life 2), move-insn savings 1 moved to 160 26·2 = 52 >= 50 -> thr 23
Insn 121: regno 122 (life 2), move-insn savings 1 not desirable 23·2 = 46 < 50
Insns 56, 91 and 121 are identical (life 2, savings 1, move-insn, no flags); the first two hoist and the third does not, and 46 < 50 < 52 brackets the decay tightly enough that no other threshold start reproduces the pattern. Also independently forces threshold = 29 (at 58, insn 43 would have moved).
func_8001E7E0 (src/800.c, resident): Loop from 54 to 276: 102 real insns. — Insn 88 (life 2) savings 2 moved … Insn 95 (life 2) savings 2 not desirable, split after three intervening moves have walked the threshold 29 → 20 (20·2·2 = 80 < 102, while 29·2·2 = 116 ≥ 102 would have admitted it first).
§193-G — §164-54's "scope to ≥4 arms" bound is byte-wrong — the dispatch-topology oracle goes live at THREE case nodes (balance_case_nodes splits at i > 2), but only for a signed-after-promotion index
§164-54-C — CORRECTION TO §164-54's "⚠ Scope to ≥4 arms": THE DISPATCH-TOPOLOGY ORACLE GOES LIVE AT THREE CASE NODES, NOT FOUR. (corrects the first bullet of §164-54's ⚠ Bounds block, L13027. §164-54's main claim — slti ⇒ write a switch, bare bne staircase ⇒ write a cached local + if/else-if — and its byte evidence on func_80184938 are untouched.)
THE FALSIFIED HALF (delete it): "Scope to ≥4 arms. At 2-3 arms emit_case_nodes degenerates toward bare equality tests, so a switch and an if-chain can both emit a bne staircase." The 2-arm half of that sentence is right; the 3-arm half is byte-wrong.
THE MECHANISM (pinned source, both arms read). balance_case_nodes gates the entire split on if (i > 2) (stmt.c:5361), and the boundary is an explicit named arm: /* If there are just three nodes, split at the middle one. */ else if (i == 3) npp = &(*npp)->right; (stmt.c:5395-5396), reached from expand_end_case (stmt.c:4905-4907) on the count < CASE_VALUES_THRESHOLD route (:4818). At 3 nodes the root becomes the median, acquires both a left and a right child, and emit_case_nodes must emit the ordering test. At 2 nodes i > 2 is false, no split happens, and you get bare equality tests. i counts case NODES, and pushcase allocates one node per case LABEL (stmt.c:4236) — so case 0: case 2: sharing one body is 2 nodes, not 1.
BYTE EVIDENCE (in-tree A/B, func_80186508, ov_SC06_032, 106 ins, 3 dense arms {0,1,2} on a u16). Target dispatch is the median tree:
lhu $v1,0x34($s0)
addiu $v0,$zero,0x1
beq $v1,$v0,.L801865CC <- MEDIAN case tested FIRST
slti $v0,$v1,0x2 <- the ordering test, in the delay slot
beqz $v0,.L80186540
beqz $v1,.L80186554
switch spelling → MATCH (106 ins). Same file, only the construct changed to st = *(u16*)(p+0x34); if (st==0) … else if (st==1) … else if (st==2) → DIFF 99 ins, 89 mismatched, LENGTH-DRIFT/-7, emitting bnez $v1 in source order with no slti anywhere. Re-ordering that if-chain into the tree's own order (1, 0, 2) → still 99 ins, still no slti (88 mismatched) — arm order is not the lever, the construct is.
CONTROLLED cc1 PROBE, pinned triple -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker, one file, unsigned short *p:
| arms | emitted dispatch | verdict |
|---|---|---|
{0,1} |
beq $3,$0 ; beq $3,$2 |
no ordering test — old bound holds at 2 |
{0,7} (gapped) |
beq $3,$0 ; beq $3,$2 |
still none at 2 nodes |
{0,1,2} |
beq $3,$4 ; slt $2,$3,2 ; beq $2,$0 ; beq $3,$0 ; beq $3,$2 |
median tree — ≥4 REFUTED |
{0,1,2,3} |
same shape + one leaf | unchanged |
if/else-if on 0,1,2 |
bne $3,$0 ; bne $3,$2 ; bne $3,$2 |
no ordering test |
READ IT BACKWARDS. §164-54's tell applies from three case nodes up, not four. A 3-arm dispatch carrying an slti is a switch; a 3-arm dispatch that is a pure source-order bne staircase is a cached local + if/else-if. Below 3 nodes the oracle is genuinely blind and §164-54's original warning stands.
(SHARPENS — corrects §164-54 (L13027); interacts with §135-1 / §165-29 (signedness of the median split) and §55a / §163c / §165-28 (table-vs-tree, CASE_VALUES_THRESHOLD = 5); evidence: byte-probed + cc1-probed; from func_80186508, corroborated by banked func_80128AF4.)
BOUND. Three conditions, two of which the submitter did NOT state — the first is a real hole in the law as submitted.
(1) SIGNED-AFTER-PROMOTION INDEX ONLY. This is the load-bearing bound. The corrected ≥3 floor holds for u8/s8/u16/s16/int operands (all promote to signed int, so 0 != TYPE_MIN and node_has_low_bound keeps the bound test — §165-29). It fails for u32/unsigned int: my probe switch (*(unsigned int*)p) over {0,1,2} emits li $2,1 ; beq $4,$2 ; beq $4,$0 ; li $2,2 ; beq $4,$2 — the median TREE is still there (median-first order 1,0,2) but every ordering test is elided, so there is no slti to read and §164-54's tell is blind at any node count. On a u32 discriminant the only surviving tell is median-first beq order vs source-order bne. Anyone applying the ≥3 correction to a u32 dispatch will chase a ghost — route to §135-1/§165-29 first.
(2) CONSECUTIVE LABELS SHARING ONE BODY COLLAPSE DOWNSTREAM, so node count ≠ visible tree shape. case 0: case 1: case 2: + case 5: is 4 nodes but emits a single slt $2,$3,3 range test and no median beq at all. Conversely case 0: case 1: + case 2: is only 2 nodes yet still emits slt $2,$3,2 (a bound test, not a median split). Practical consequence: the slti ⇒ switch direction is safe at 2 nodes as well as 3; the "no median beq ⇒ not a switch" direction is not. Non-adjacent shared labels do NOT collapse — case 0: case 2: / case 1: is 3 nodes and emits the full median tree, which is why func_80185464 (4 labels, 2 bodies) still shows beq $v1,$v0 ; slti $v0,$v1,0x2.
(3) use_cost_table picks a different arm — outcome unchanged, but the citation only names one path. stmt.c:5395-5396's else if (i == 3) is the else of if (use_cost_table). estimate_case_costs (stmt.c:5219) returns 1 when every case value is in [-1,127] with no iscntrl value, in which case the cost-bisect path runs instead. I probed both branches at 3 nodes — ASCII cases {65,66,67}, and {48,58,59} chosen to force n_moved == 0 into the lopsided early-return at :5384-5391 — and both still emit an slt, so the ≥3 floor is not sensitive to this. For BFM's typical dense state enums the point is moot: cost_table[1..7] < 0 (iscntrl; only 0/8/9/10/11/12/32 are overwritten with non-negative costs), so any switch containing case 1: gives use_cost_table == 0 and the cited arm is the live one.
Unchanged from §164-54: still confined below CASE_VALUES_THRESHOLD (=5) or the range > 10 * count disjunct — above that you get a jump table and none of this applies.
SECOND INSTANCE. func_80128AF4, src/ov_SC06_008/ov_SC06_008.c — BANKED (byte-exact; find asm -name func_80128AF4.s returns nothing in any of the ~134 overlays, i.e. no INCLUDE_ASM stub survives anywhere, so the whole-binary gate already covers it). Its C is a 3-dense-case switch on a u16 global:
switch (D_800B99F6) { case 0: … case 1: … case 2: … }
I compiled the real TU through the project's own pipeline (mipsel-linux-gnu-cpp with the Makefile's CPPFLAGS + -DINCLUDE_ASM(a,b)= → the pinned cc1) and the dispatch is the median tree, identical in shape to func_80186508's target:
lhu $3,D_800B99F6
li $16,0x00000001
beq $3,$16,$L65 <- median case FIRST
slt $2,$3,2 <- the ordering test
beq $2,$0,$L74
beq $3,$0,$L64
Because the function is banked, those bytes are the shipped ROM's bytes — this is a second target-verified instance of "3 case nodes ⇒ ordering test", in a different binary and a different TU from the submitter's exemplar.
Corroborating (label-counting clause only, not the 3-node floor): func_80185464, ov_SC06_032 — 4 labels in 2 bodies (case 0: case 2: / case 1: case 3:), and .run/waveT_asm_snapshot/ov_SC06_032/func_80185464.s:12-13 shows beq $v1,$v0,.L80185558 ; slti $v0,$v1,0x2, confirming non-adjacent shared labels each count as their own node.
Population check: a scan of banked src/**/*.c for 3-label single-level switches returns well over a hundred sites (func_8016CF04, func_80166690, func_80128A28, func_80130D48, func_8005E528, func_80031BE0, …), all of which the old ≥4 bound told a reader to treat as unresolvable. This is not a one-function curiosity.
§193-H — A pointer-derived base load *(s32*)(p+K) is uncacheable across ANY memory write or call (cse's third kill disjunct) — so the target emits one lw per store-separated RUN of spellings, and a run of stores sharing one lw is a C local you must declare
A base loaded off a POINTER — *(s32 *)(p + K) where p is a parameter/pseudo — is UNCACHEABLE by cse across any memory write or call, and no declaration can rescue it. The target therefore emits exactly ONE lw K($sP) per store-separated RUN of source spellings; a run of stores sharing ONE base load is a C local you must declare. (SHARPENS the un-numbered reload lever at L1883-1892 — which gives the same C dial with no mechanism and a needlessly narrow trigger; BOUNDS §165-27 — its "the dial is the LOAD's declaration, array→scalar to make it survive" is a $sp/non-varying-address rule ONLY, and does not exist here; CORRECTS the premise of the volatile-reload bullet at L1932-1946; complements §48-B/§164-52/L3257-Lever-2, which are the SYMBOL-base regime with the opposite prescription.)
THE MECHANISM (read out of the pinned source). note_mem_written (cse.c:7539) sets writes_ptr->var = 1 for every MEM store (:7577), and invalidate_from_clobbers fires the flush on that alone: if (w->var) invalidate_memory (w) (:7598). invalidate_memory (:1701) kills a cached entry iff p->in_memory && (all || (nonscalar && p->in_struct) || cse_rtx_addr_varies_p (p->exp)) (:1713-1717). cse_rtx_addr_varies_p (:2474-2496) returns non-varying only when the base REG's quantity carries a constant (qty_const[reg_qty[...]] != 0 — i.e. sp/frame/arg-pointer via FIXED_BASE_PLUS_P, or a la sym). A parameter pseudo has none, so the third disjunct fires unconditionally. It is an OR, so /s can only add kills — there is no declaration, cast, or struct spelling that keeps it. A jal is stronger still: cse_insn calls invalidate_memory (&everything) on any non-const CALL_INSN (:7245-7246).
THE CENSUS — count RUNS, not spellings. Loads emitted = number of maximal runs of spellings, where a run is broken by any memory write, any call, or a basic-block boundary. Spellings inside one run collapse to one load (they are the same MEM in the same hash class, nothing invalidated between them).
PRESCRIPTION, both directions.
- Target shows FEWER
lw K($sP)than your draft ⇒ your source re-spells inside a run. Declare a localg = *(s32 *)(p + K);covering that run and usegfor every store in it. - Target shows MORE ⇒ you cached across a store/call. Delete the local and re-spell at each site. You do NOT need to arrange an "unrelated field store" (L1883) — any store or call already there is enough.
- Cost is monotone and per-site: +2 ins per extra reload site (
lw+ load-usenop), and the residual class isLENGTH-DRIFTin both directions — so a LENGTH-DRIFT/±(2n) with thelws at the drift points is the fingerprint.
DIAGNOSTIC TELL. A run of sh/sw at DIFFERENT offsets off one $vX that was loaded by a single lw K($sP) — with $sP a parameter register/$s copy — and your draft emitting one lw per store. That is a missing C local, nothing else: do not reach for §165-27's declaration dial, do not reach for volatile.
COROLLARY — when volatile is actually required. The L1932-1946 surgical-reload bullet's premise ("gcc -O2 keeps a pointer field load cached across intervening calls/non-field stores") is false as stated; a call flushes everything. In its own evidence function func_801424E4 the single load came from the C local iVar2, not from cse. *(volatile T *) is needed only where the target reloads a pointer-derived base with nothing between the two sites — no store, no call, no label. Try plain re-spelling first; volatile is the fallback for the intra-run case only.
BOUND. THE SUBMITTER'S HEADLINE HALF IS BYTE-REFUTED, AND ITS PROPOSED SCOPE IS TOO NARROW. Both errors measured, not argued.
FALSIFIED HALF #1 — "one C statement spelling of the deref = one lw, always" / "the number of base reloads equals the number of distinct source expressions." FALSE. Spellings with nothing between them collapse. Probe pA (.run/harvest_t/vz_probe.c): two adjacent spellings of *(s32*)(p+0x20), no store between → ONE lw $2,32($4). Real banked instance: func_80183AC4 spells it 7 times → 4 loads. The correct reading direction is RUNS, not sites. Reading a target backwards under the submitter's rule would have you write 7 spellings where 4 runs are what the shape actually implies — this is exactly the "the advice worked, the reason was wrong, later sessions went into a dead lane" failure the §186/§188 rule is about. (docs/matching-cookbook.md:12282 already banks the collapse direction: a re-read with no intervening store becomes a reg-reg copy off the branch's pseudo.)
FALSIFIED HALF #2 — the submitter's own falsifier clause, "the law must be scoped to a store at a varying address." FALSE — the law is BROADER than claimed. note_mem_written sets ->var for every MEM store (cse.c:7577) and :7598 flushes on ->var alone, so the third disjunct fires regardless of the killing store's own address. Measured: probe pB — intervening li $2,1; sw $2,D_glob (fixed SYMBOL address) → TWO lw $2,32($4). Probe pD — intervening store into an address-taken $sp local → TWO loads. Probe pE — intervening jal noop → TWO loads. Do not write the "varying-address store" scope into the cookbook; write "any memory write, any call, any block boundary".
WHERE THE LAW DOES NOT APPLY (real scope):
- Symbol base. If the base itself lives at a constant address, cse keeps it: probe
pG,lw $2,D_ptr… varying store … second spelling → ONE load. This is the §48-B/§164-52 regime; the lever there is the join-label jump count, not a local. $sp-slot base. A frame-slot base load is non-varying, so the third disjunct never fires and §165-27's declaration dial is live: probepFusesloc[2](ARRAY_REF ⇒/s) and RELOADS across a varying store via thenonscalar && in_structdisjunct. Scalar-declare it and it survives. Do not apply this law to$spbases.qty_constdecay. §167-32 applies:p = &SYM[i]is an ordinary register again, so a symbol base that has already absorbed a runtime term falls back INTO this law.- Not a scheduling lever. This changes instruction COUNT. If your residual is a pure reorder with equal length, this is the wrong section.
- n=1 on the
-1.5 ins/sitefigure. f_allcache's −3 for two removed sites is not a clean −2/site (onenopwas already absorbed); only the +2/site direction is cleanly per-site.
SECOND INSTANCE. Three, one of them genuinely banked (the submitter's two are both unbanked INCLUDE_ASM stubs).
(a) BANKED — func_80183AC4, ov_SC06_020, src/ov_SC06_020/ov_SC06_020_jr_80180B04.c. Three *(u16*)(*(s32*)(param_1+0x20)+F) = same + *(u16*)(param_1+G); RMW statements plus an else-arm use. SEVEN source spellings. I extracted it verbatim to .run/harvest_t/vz_second.c and compiled with the pinned triple: lw $4,32($16); lhu 254($16); lhu 16($4); addu; sh 16($4); lw $4,32($16); lhu 256($16); lhu 18($4); addu; sh 18($4); lw $5,32($16); … sh 20($5); … lw $5,32($16); jal func_80017274; addu $5,$5,52 — 4 loads for 7 spellings, one per store-separated run. This instance simultaneously confirms the mechanism and refutes the census framing.
(b) TARGET ASM — func_80186508, ov_SC06_032 (.run/waveT_asm_snapshot/ov_SC06_032/func_80186508.s:29,35,60,66): lw $a1,0x20($s0); lhu 0x106($s0); lhu 0x10($a1); addu; sh 0x10($a1); lw $a1,0x20($s0); … — the reload lands immediately after each sh through the base. Two more at :98,106, but those are in the two arms of an if/else (block boundary, not the store rule).
(c) CONTROLLED PROBES, pinned triple — .run/harvest_t/vz_probe.c (pA/pB/pC/pD), vz_probe2.c (pE), vz_probe3.c (pF/pG). These isolate the trigger to a single axis each and are what established the two scope corrections above.
(d) Retrospective third-party corroboration — func_801424E4 (src/ov_SC03_099/ov_SC03_099_jr_80140608.c:1650, banked, the L1932 evidence function) is consistent with the corrected law and NOT with the cookbook's stated reason for it: its first volatile sits after a store (a plain re-spelling would have reloaded anyway) and its second sits where nothing intervenes (volatile genuinely required).
§193-I — A DECLARED AGGREGATE LOCAL HAS AN 8-BYTE FRAME STRIDE — CEIL(size,8), NOT size. N ARRAYS THEREFORE COST 8·k MORE THAN ONE HAND-LAID STRUCT (k = how many of them have size mod 8 ≠ 0), AND THE TELL IS IDENTICAL INSTRUCTION COUNT WITH EVERY $sp DISPLACEMENT SHIFTED BY THE SAME CONSTANT.
§1xx — A DECLARED AGGREGATE LOCAL HAS AN 8-BYTE FRAME STRIDE: it occupies CEIL(size,8), 8-aligned at its START, with the dead pad TRAILING. N separate arrays therefore cost 8·k more frame than ONE hand-laid struct holding the same fields, where k = how many of them have size mod 8 != 0. The tell is IDENTICAL INSTRUCTION COUNT with every $sp displacement shifted by the same constant. (the actionable inverse of §163e's per-local fact; inverts §82-2's prescription, which points the other way for SCALARS. CORRECTS §165-34's L15155 claim that a declared local takes the align == 0 arm — true for scalars, false for BLKmode.)
THE MECHANISM (read out of the pinned gcc; the submitted version of this law had it backwards and is corrected here). expand_decl slots a fixed-size non-register local via stmt.c:3411-3416 → assign_stack_temp (DECL_MODE, size, 1). assign_stack_temp mints a fresh slot at function.c:879:
p->slot = assign_stack_local (mode, size, mode == BLKmode ? -1 : 0);
So a BLKmode local takes the align == -1 arm of assign_stack_local (function.c:681-685) — alignment = BIGGEST_ALIGNMENT / BITS_PER_UNIT (= 8; config/mips/mips.h:1080 BIGGEST_ALIGNMENT 64) and, uniquely among the three arms, size = CEIL_ROUND (size, alignment). A declared addressable scalar takes the align == 0 arm (:675-679) and keeps its own mode alignment.
MIPS DOES NOT DEFINE FRAME_GROWS_DOWNWARD — config/mips/mips.h:1643 is /* #define FRAME_GROWS_DOWNWARD */, commented out. The live code is therefore function.c:697 frame_offset = CEIL_ROUND (frame_offset, alignment) followed by function.c:720 frame_offset += size. The FLOOR_ROUND at :695 and the frame_offset -= size at :706 are dead on this target. Consequence: each aggregate local STARTS 8-aligned and its slack TRAILS it. (Do not reason from the downward model — it predicts the pad on the wrong side, and §163e's practical pad-placement rule depends on getting this right.)
Members of ONE struct are laid out by layout_type at their own 2/4-byte alignments; only the aggregate as a whole gets the 8-byte stride. s16 mtx[10] + s32 pos[3] as two locals = 24+16 = 40 bytes; as two struct members = 20+12 = 32 with the next member landing on 0x20 exactly.
THE ARITHMETIC. Extra frame from the separate spelling = 8 · |{ aggregates with size mod 8 != 0, excluding the last-declared one }|. It is NOT 8 per aggregate — an SVECTOR-sized (8) or 16-byte array costs the same either way. The LAST-declared aggregate's pad is free: it is absorbed by MIPS's own 8-round of the total frame.
THE DIAGNOSTIC TELL (this is what makes the law worth its lines). match_one returns mine=N ins, target=N ins — instruction count IDENTICAL — with every callee-save store/load and every addiu $sp and every $sp-relative displacement off by the SAME constant, and that constant is a multiple of 8. Then: collapse your separate array locals into one struct in the same declaration order and strip the prefixes. It is a one-line fix.
DISAMBIGUATE FROM ITS TWO NEIGHBOURS BY INSTRUCTION COUNT: §161b and §162n2 also read as "frame 8 bytes too big", but BOTH carry ONE EXTRA sw $sN and therefore a CHANGED instruction count. Equal count + uniform displacement shift ⇒ this law, not those.
AND DISTRUST THE CLASSIFIER HERE. residual_class scores this IMM-VALUE [permuter] profile=cse — a permuter/regalloc verdict on what is a declaration edit. Do not spend permuter CPU on an IMM-VALUE residual whose mismatch set is entirely $sp displacements differing by one constant.
WHAT DOES NOT COUPLE (kills the obvious worry). Collapsing ARRAYS into a struct does not touch MEM_IN_STRUCT_P: stmt.c:3417/3432 set it from AGGREGATE_TYPE_P (TREE_TYPE (decl)), and an ARRAY_TYPE is already an aggregate. So §82-2's second-order aliasing warning applies to SCALAR→struct and is inert for ARRAY→struct. Byte-witnessed: the A/B's 34 mismatches are 34 displacements — zero register change, zero reordering.
(byte-proven func_80187368, ov_SC06_032, 96 ins: struct → MATCH, addiu $sp,$sp,-0x80; same body with the seven members as seven separate array locals → DIFF 96/96, 34 mismatched, addiu sp,sp,-136 = 0x88, uniform +8. Second instance: struct Fr_8016A290 in src/shared/engine_types.h:1130-1138, banked ×many.)
BOUND. Eight conditions, three of which materially narrow the submitted headline.
-
NARROWS THE HEADLINE — the cost is 8·k, not 8·N. k = the count of separate aggregate locals whose size is not already a multiple of 8. On
func_80187368only 2 of 7 qualify (20→24, 12→16); the other five are 8-multiples and are byte-identical either way. "8 bytes apiece" is wrong; "8 bytes per sub-8-granular aggregate" is right. -
NARROWS — the LAST-declared aggregate is free. MIPS rounds the whole frame to 8 (
MIPS_STACK_ALIGN), so a trailing sub-8 array's pad is absorbed and produces no tell and no delta. The law only bites where another local follows. -
DOES NOT APPLY TO SCALARS — the arrow reverses (§82-2). A declared addressable scalar takes
assign_stack_local'salign == 0arm (its own 4-byte alignment, size NOT rounded) and is slotted LAZILY at its first&, i.e. after every aggregate in the block. §82-2's "six GTE result words must be six separatelongs, not astruct" stands. A reader who applies this law to scalars will make the frame worse. Mixed field lists (struct { s16 xy[2]; s32 sp1c; s32 flag; },func_8016B234) are a judgement call, not a mechanical collapse. -
DOES NOT APPLY TO A ONE-ELEMENT ARRAY OR ONE-MEMBER STRUCT.
layout_typecollapses those to the element's mode (§162i1), so they are not BLKmode, never reachassign_stack_temp, are 4-aligned and are register-eligible. Two elements minimum (§164-71). -
DOES NOT APPLY TO VARIABLE-SIZE LOCALS.
stmt.c:3393's arm requiresTREE_CODE (DECL_SIZE (decl)) == INTEGER_CST; a VLA goes down theallocate_dynamic_stack_spacepath and none of this holds. -
THE TELL REQUIRES THE INSTRUCTION COUNT TO BE EQUAL. If the extra frame bytes also flip a callee-save decision or a register class, the count moves and you are in §161b / §162n2 / §83d territory instead. Check the
sw $sNcount first. -
THE STRUCT FIXES GRANULARITY, NOT ORDER. Member order inside the struct must still reproduce the target's declaration order (§163e / the L6240 "frame layout reads back declaration order" lever). Collapsing with the wrong member order trades an 8-byte error for a whole-layout error. And the struct's own total is CEIL_ROUNDed to 8 by the same
align == -1arm, so one struct can never beat the packed field sum. -
THE MEM_IN_STRUCT_P EXEMPTION IS ARRAY-ONLY. Safe for array→struct (both spellings are already
AGGREGATE_TYPE_P). If any of the fields you are collapsing is a SCALAR, §82-2's/s/aliasing coupling is live again and the edit is no longer frame-only.
Not tested and therefore out of scope: -O0 functions (frame-pointer prologue; §116), and functions where the aggregates are also passed by value.
SECOND INSTANCE. Found, in banked (matching) in-tree source, in a different function and a different overlay family.
src/shared/engine_types.h:1130-1138 — struct Fr_8016A290, used as struct Fr_8016A290 fr; in func_8016A290 and banked across many overlays (src/ov_SC05_003/ov_SC05_003_jr_8015C32C.c:6125, src/ov_SC02_027/ov_SC02_027_jr_8015C32C.c:6126, src/ov_SC06_008/…, …). Its commented member map is the law in action:
SVEC center; /* 0x00 -> sp+0x10 */
SVEC diff; /* 0x08 -> sp+0x18 */
s32 pos[3]; /* 0x10 -> sp+0x20 */ <- 12 bytes
s32 _pad1c; /* 0x1C -> sp+0x2C */ <- lands INSIDE pos[]'s would-be pad
s16 mtx[32]; /* 0x20 -> sp+0x30 */
SVEC v[4]; /* 0x60 -> sp+0x70 */
struct {u8 r,g,b,pad;} col[4]; /* 0x80 -> sp+0x90 */
u32 code; /* 0x90 -> sp+0xA0 */
As SEPARATE locals, s32 pos[3] would take a 16-byte stride and the next aggregate would start at 0x20, so nothing could ever be reached at sp+0x2C; as struct members, 0x10 + 0xC = 0x1C exactly. Same mechanism, same fix, independently banked. struct Fr_80167AE0 (:1124-1129) is a third, weaker witness (all members happen to be 8-multiples, so it costs nothing — a live example of BOUND #1).
Supporting but not counted as independent: struct { s32 x, mid, y; s32 _pad[9]; } arg; /* 0x30 @ 0x10 */ in the banked ov_*_after.c files (src/ov_SC05_003/ov_SC05_003_after.c:2398 and ~7 siblings), and struct { s16 xy[2]; s32 sp1c; s32 flag; } out; in the banked func_8016B234 (src/ov_SC03_099/ov_SC03_099_jr_8016AB6C.c:2082 ×many) — both are hand-laid frame structs, but both mix scalars in, so BOUND #3 applies and they do not test the pure N-arrays claim.
NOT accepted as a second instance: the submitter's ov_SC06_018:func_8018C190. It is byte-identical to func_80187368 apart from one per-overlay symbol and is still INCLUDE_ASM at src/ov_SC06_018/ov_SC06_018_jr_80187AEC.c:4814 — it is the same function, not a second one.
§193-REJECTED — what this harvest did NOT bank (recorded so it is not re-derived)
- An
addiu ±Kderiving one %hi/%lo label's address from a register holding another's proves the two labels are ONE source object, and the two-extern spelling is count-neutral — ATTACK 1 LANDED — this is a re-derivation of §167-33 (L15841), with its ascending half already at §164-08 (L12007).
I ran attacks 2-5 anyway (all of them pass), so the rejection is duplication, not falsity. Element-by-element against §167-33:
| candidate element | already stated at |
|---|---|
cc1 manipulates SYMBOL_REF, never numeric addresses |
§16Z row 1 (candidate cites it itself), |
__volatile__re-tie on a pointer-to-global keeps the address in ONE register with k(reg) displacements (claimed as a third escape from §164-52) — ATTACK 1 LANDED — the mechanism is already banked, in three places, and the submitter's "ruled out" list names none of them.
(a) THE CORE IS §176-D1 (docs/matching-cookbook.md:17705, "CSE-class levers used in reverse"), near verbatim: "§153's launder, applied to a MULTI-use pointer, forces PERSISTENCE (the opposite of its documented effect). ... emptying the symbol's cse equivalence class forces
- Repeated raw derefs across an ||-chain are load-bearing: emitted load count == number of cse regions spanned; naming the operand deletes all reloads, volatile explodes them — Attacks 1, 2 and the submitter's OWN falsifier all landed. Attack 3 (citation) is the only one that failed cleanly.
ATTACK 2 LANDED — the evidence refutes the headline count rule inside its own exemplar. The law says "the emitted load count equals the number of cse regions the derefs span." I counted labels in .run/waveT_asm_snapshot/ov_SC03_014/func_80187090.s: labels at lines 20/26/50/62/
- REFUTED (second time): the "memory barrier + de-naming defeats a sign-extension CSE fold" narrative — ATTACK 1 LANDED — every component is already banked, in four places. I stopped there per the protocol, but I ran attack 2 first anyway (it is the cheap decisive one) and the submitter's measurements are honest; the problem is that the finding is a re-derivation, not that it is wrong.
The candidate has three components. Each is already in docs/matching-cookbook.md:
- **"the `asm __vo
- The §137/§158 live-length slider is a STATEMENT-POSITION dial, not a spelling dial —
: "memory"on an input-only zero-byte keepalive is byte-inert — ATTACK 2 LANDED (evidence does not support the claim), and ATTACK 3 LANDED independently (mechanism is backwards). ATTACK 1 lands on the surviving half.
Attack 2 — DOES THE EVIDENCE SAY WHAT IT CLAIMS? NO. I reproduced the submitter's 2×2 exactly, to the integer — and then I ran the single-axis A/B the submitter did NOT run: the same output-free asm at five OTHER statement positions in the sa
§194 — THE WAVE-U HARVEST (P31 S54): 64 index_gap reports -> 14 laws, 5 rejected, 44 already-covered
Wave U is the first 100% wave in the project's history: 73 cards, 73 standalone MATCH, 73 banked. Its 64 gap reports went through the same mill as wave T's — cluster readers, then one adversarial verifier per candidate defaulting to REJECT — with one addition: the readers were seeded with §193 (which wave U's own prompt already carried as laws 21-27), so a restatement of those was dead on arrival.
44 of 64 were already answered by an existing section, against wave T's 61 of 71. The retrieval rate improved because the card now hands the agent a BANKED twin (§193-A) and the prompt has an explicit cookbook-search step — but two thirds of what an agent "works out" is still something this file already knew. Treat that as the standing measurement, and keep fixing RETRIEVAL, not prose.
Two of the 14 correct work banked earlier the SAME DAY (§194-E on §193-A's framing, §194-N on §193-D's C dial). A law written from one session's evidence and verified by one adversary is still a first draft; the next wave is its real review.
§194-A — A zero-byte scheduling fence goes AFTER the defining statement to make that computation emit FIRST in its block — and the barrier predicate is volatile-or-colon-less, not the "memory" clobber (COMPLEMENTS §165-40; does not refute it)
§NNN — A ZERO-BYTE SCHEDULING FENCE IS A ONE-WAY WALL RELATIVE TO THE STATEMENT YOU ARE STEERING: put it AFTER the defining statement to make that computation emit FIRST in its block. (COMPLEMENTS §165-40 (L14705) — its "immediately BEFORE the defining statement" placement is correct for its own, OPPOSITE goal (deny an upward hoist) and is NOT refuted here; what §165-40 lacks is the direction rule, because its PLACEMENT paragraph reads as general. GENERALISES §190-C's one clause "arg registers pinned above a __volatile__ memory clobber" from a sched2 call-arg-in-delay-slot symptom to any sched1 intra-block reorder on any value. BOUNDS §178-G2 (L17290), whose "__asm__ __volatile__("" ::: "memory") is a FULL barrier" invites the reading that the memory clobber is the ingredient — it is not. Run §167-17's statement sweep FIRST; this is what you spend when the sweep is exhausted.)
THE LAW. sched_analyze_2 (tools/reference/gcc-2.7.2/sched.c:1944-1971) takes the clobber-everything path — for (u = reg_last_uses[i]; …) add_dependence(insn, XEXP(u,0), REG_DEP_ANTI); if (reg_last_sets[i]) add_dependence(insn, reg_last_sets[i], 0); over all max_reg_num() regs, then reg_pending_sets_all = 1 and flush_pending_lists (insn). That is a two-sided barrier at its own position and nothing more: it forbids motion across itself and says nothing about the relative order of the insns on either side. So the fence is a one-way wall with respect to the statement you are steering:
- BEFORE the statement → the statement cannot float UP past the fence. (§165-40's case: keep an independent
lui/addiupair from being hoisted to the top of its block, shortening its allocno.) - AFTER the statement → the statement's PEERS cannot float up past the fence, so what is left above it is the only thing schedulable there and it emits FIRST. This is the placement that reproduces a symbol-address pair emitted ahead of its block peers, and it is the one §165-40 does not name.
THE PRECONDITION §165-40 has no analogue of — the region above the fence must be CLEAN. The fence does not privilege your statement; it privileges everything above it. Anything you leave between the block head (or the previous barrier) and the fence still competes on rank_for_schedule and can win. Byte-proven: moving one *(s16 *)(s0+0x28) = 0x300; store from below the fence to above it in the MATCHing source breaks the match, with that store's li v0,768 hoisted above the lui $s2 (6 mismatched, pure reorder).
THE PREDICATE, byte-discriminated — the "memory" clobber is NOT the active ingredient; volatile-or-colon-less is. sched.c:1953 is if (code != ASM_OPERANDS || MEM_VOLATILE_P (x)), and all four forms in the AFTER slot on func_80180780 obey it exactly:
| spelling | RTL | barrier? | verdict |
|---|---|---|---|
__asm__ __volatile__("" ::: "memory"); |
volatile ASM_OPERANDS |
yes | MATCH (the banked spelling) |
__asm__ __volatile__(""); |
ASM_INPUT |
yes | MATCH |
__asm__(""); (colon-less, no volatile) |
ASM_INPUT (stmt.c:1340) |
yes | MATCH |
__asm__("" ::: "memory"); (colons, no volatile) |
non-volatile ASM_OPERANDS |
no | DIFF, 6 mismatched |
Prefer the bare __asm__ __volatile__("") — the memory clobber buys nothing here and drags in §178-G2's address-chain sinking and §21's prologue pin.
ATTRIBUTE FIRST (§78/§80/§164-49, §190-C's own instruction). On the fence-free source, -fno-schedule-insns alone reproduces the target with 0 mismatched ⇒ sched1. (-fno-schedule-insns2 alone does not; §190-C's analogous after-placement case is the sched2 one.) A volatile asm is a barrier in both passes, so the fence works either way — but knowing which pass owns it tells you whether a plain statement move might have sufficed.
THE DIAGNOSTIC TELL. Count-neutral, register-identical, OPCODE-MIXED reorder in which a multi-insn computation your draft emits LATE sits FIRST in the target, ahead of a run of independent cheap peers, and no fence-free statement position wins because your statement is already at the head of its block.
BYTE EVIDENCE — two functions. (1) func_80180780 (ov_SC04_005, 111 ins, banked at src/ov_SC04_005/ov_SC04_005_jr_8017BEBC.c:5876-5877): fence AFTER → MATCH; fence deleted → DIFF 6; fence moved to §165-40's BEFORE placement → DIFF 6, byte-identical to no fence at all. (2) func_8017B614 (ov_SC04_005, 101 ins, src/ov_SC04_005/ov_SC04_005_jr_8017AE2C.c:2948-2949, cloned in ≥7 sibling overlays): bare __asm__ __volatile__("") AFTER a group of lh loads → 0 mismatched vs the committed object; deleted → 15; moved BEFORE the last load → 16.
Scope: n=2 functions, one binary (second instance replicated verbatim in ≥7 overlay clones). Direction rule and predicate table independently re-run by the verifier; attribution by -fno-schedule-insns. match_one is not the arbiter (§8a) — instance 2 was compared against the gate-green build object.
BOUND. Conditions under which this does NOT apply, or must not be reached for:
- Do not reach for it before §167-17's statement sweep. The fence is the expensive lever. Walk the defining statement up one position at a time first; only when the statement is already at the head of its basic block (as in both instances here — verified by two fence-free repositionings that both DIFF) is the sweep exhausted.
- Intra-block only. The barrier is a position in one block's dependence graph. It cannot pull a computation across a label or a branch, and it cannot beat a real data dependence.
- The region above the fence must contain ONLY what you want first (back to the block head or the previous barrier). Byte-proven failure mode: one peer statement left above the fence hoists ahead of the target computation and breaks the match.
"memory"is not the ingredient and can hurt. A non-volatile__asm__("" ::: "memory")is NOT a scheduling barrier (sched.c:1953), and the volatile memory-clobber form drags in §178-G2's address-chain sinking, §21's prologuesubu sppin, and §21's forced global reloads. Use the bare__asm__ __volatile__("")unless you separately need the clobber.- §164-36 still governs the slot: never at the head of a block whose first insn the target steals into a delay slot.
- Collateral, per §47/§158: the fence is +1 static insn at global-alloc time, adding +1
reg_live_lengthto every pseudo spanning it — any other exact-tie allocno pair straddling the point can flip. Re-verify the whole function, not just the reordered window. - It does not make §165-40 wrong. BEFORE is still the correct placement when the goal is to deny an upward hoist and shorten an allocno. Choose the side by the goal, not by a default.
- Attribution is not optional. If
-fno-schedule-insns2alone reproduces the target, you are in §190-C's sched2 case and its arg-register-specific advice (plains32locals get folded into the call's own arg setup and sink below the fence) applies on top of this rule. - Unmeasured: which direction is "commoner". No census exists; the submitter's claim to that effect is stripped.
SECOND INSTANCE. func_8017B614, ov_SC04_005, 101 ins — banked at src/ov_SC04_005/ov_SC04_005_jr_8017AE2C.c:2948-2949 (found by grepping banked C for a defining statement immediately followed by a zero-byte volatile asm; the same shape is independently banked in ≥7 sibling overlays — ov_SC01_084 :2950, ov_SC02_017 :2941, ov_SC02_028 :2947, ov_SC03_024 :2947, ov_SC03_029 :2941, ov_SC03_090 :2948, ov_SC03_099 :2949).
The banked source is a group of global/pointer loads, then the fence, then the store block:
v78C = *p78C;
v78E = D_801BF166;
v790 = D_801C1988;
__asm__ __volatile__(""); <- AFTER the defining statements
D_801BF3E8 = 1;
D_801BF0F4 = 0x1E;
D_80126990 = v794; ...
Different value class from instance 1 — three register-held lh loads, no lui/addiu symbol-address pair — and a different symptom, but the identical direction rule and the identical before-vs-after asymmetry.
I compiled the whole TU myself with the pinned triple and compared func_8017B614 against the committed, gate-green build/src/ov_SC04_005/ov_SC04_005_jr_8017AE2C.o:
- as banked (fence AFTER) → 0 mismatched / 101 ins (my recompile reproduces the built object exactly, so the harness is trustworthy)
- fence DELETED → 15 mismatched, count-neutral pure reorder: the post-fence store block (
li v0,1 / lui at / sb zero / lui at / sh v0) migrates above the pre-fencelhloads - fence moved to §165-40's BEFORE placement (immediately above
v790 = D_801C1988;) → 16 mismatched, the wholelhchain shifted by one register
So the after-placement is load-bearing and the before-placement is not merely inert but actively wrong for this goal, on a second function.
§194-B — A addu $rA,$rB,$zero copy feeding ≥2 sh stores is a SECOND, 16-BIT-DECLARED local — the width, not the clamp or the join liveness, is what keeps the copy (and func_80183094's pin + "memory" clobber are both provably inert)
A addu $rA,$rB,$zero copy whose result feeds TWO OR MORE sh stores is a SECOND LOCAL DECLARED 16-BIT, assigned from a wider (32-bit) local. The 16-bit declaration is the whole mechanism — not join liveness, not the clamp, not the allocator.
Write it as: s32 t; … t = expr; if (t > K) t = K; { s16 v = t; } *(s16*)(p+A) = v; *(s16*)(p+B) = v;
Three C facts, each isolated by a one-variable A/B (all vs .run/waveU_asm_snapshot/ov_SC04_005, func_80183094, 72 ins):
- THE SECOND LOCAL MUST BE DECLARED 16-BIT.
s16MATCHes;u16MATCHes (signedness is free);s32LOSES BOTH COPIES (71, LENGTH-DRIFT/-1,nopin the delay slot) — identical to writing one variable. A written cast on a wide local (s32 v = (s16)t;) does NOT substitute: it emits a realsll/srapair (73, +1). The lever isDECL_MODE, not the expression. - THE NARROW LOCAL NEEDS ≥2 SURVIVING CONSUMERS. One
sh⇒ copies gone (70, -2). Twoshto the SAME offset ⇒ one store is eliminated, copies gone (70, -2). Two distinctsh⇒ MATCH. Three distinctsh⇒ both copies still there, sole delta is the extra store (73, +1, only 5 mismatched). - THE CLAMPED TEMP MUST STAY 32-BIT AND BE CLAMPED IN PLACE. One 32-bit variable storing directly ⇒ -1 (
addugone,nopin the slot). One 16-bit variable ⇒ +2 (sll/sraaround theslti). A two-armif/elsewriting the narrow local directly in each arm (if (t>K) v=K; else v=t;) gives the right COUNT but the wrong PLACE — onemove v1,v0hoisted ABOVE theslti(OPCODE-MIXED, 9 mismatched) because cse/jump sees a plain select. So both the copy's existence AND its position are source-shape facts.
FALSIFIED HALF — do not carry the submitter's mechanism forward. "v is live out of the join and t is not, so each arm must materialise into v's register" is refuted twice: V_sib32.c and V_sib1store.c have exactly that liveness and emit no copy. Correct pseudo-level story (HYPOTHESIS, not source-verified — nobody read tools/reference/gcc-2.7.2 for this): s16 v keeps DECL_MODE == HImode, so v = t is a MODE-CHANGING set that copy-propagation will not fold into an SImode use, and with ≥2 uses there is no single site to sink it into. s32 v makes it a same-mode reg-reg copy that cprop deletes. Refute that, do not cite it.
REFUTATION HALF (fully reproduced, independent of the above). func_80183094 is banked at src/ov_SC04_005/ov_SC04_005_jr_8017BEBC.c:6998 with register s32 result __asm__("$2") plus __asm__("" : "=r"(v) : "0"(v) : "memory"). Four-way ablation: deleting the pin alone ⇒ MATCH 72; deleting "memory" alone ⇒ MATCH 72; deleting the re-tie alone ⇒ DIFF 71. Only the re-tie's cprop-blocking SET is load-bearing, and the plain-C sibling shape does that job with zero asm. So §189-C's "a bb0 re-tie needs a "memory" clobber" does NOT carry here. Per §37/§162p (L11712) the pin fails dedup_propagate.compiles_standalone and forfeits family-remap; the sibling func_80184238 in ov_SC04_007 is still INCLUDE_ASM while the same symbol is banked in ov_SC02_031 and ov_SC01_077 — a real unpaid cost. Re-bank func_80183094 from .run/harvest_u/V_sibling.c (asm-free) and re-run the whole-binary SHA1 gate.
⚠ CORRECTS §136-3 (L8862-8866). Its closing absolute — "An unpinned pseudo always coalesces the pair away" — is byte-false. An unpinned SECOND LOCAL keeps the copy whenever it is declared 16-bit and has ≥2 uses. Read §136-3's pin as sufficient, never necessary; try the width lever first.
BOUND. The law does NOT apply, and the copy will NOT appear, when any of these hold:
- The second local is 32-bit.
s32 v = t;coalesces away (V_sib32.c: 71 ins, both copies gone). This is the falsifier the submitter himself proposed, and it fires.u16is fine — the bound is width, not signedness. An explicit(s16)cast onto a 32-bit local is NOT a substitute and costs a realsll/srapair instead (V_sibcast.c: 73 ins). - The narrow local has fewer than two surviving consumers. One store ⇒ no copy (
V_sib1store.c: 70). Two stores to the same address ⇒ one is eliminated first, then no copy (V_sibsame.c: 70). The count is of consumers gcc keeps, not of consumers you type. - The clamped temp is itself narrow. A single 16-bit variable regresses badly (+2 ins,
sll/sraaround theslti) — the source of the copy must be SImode. - The join is written as a two-arm select.
if (t>K) v=K; else v=t;produces the same instruction count but the copy MOVES above the compare (V_ifelse2.c, OPCODE-MIXED). If your residual is a correctly-countedmovein the wrong place, this is the spelling you used. - A pass with a stronger claim already owns the shape. In a LOOP, §32's
loop.c:5556giv-init fence emits the identicaladdu dst,ba,$zerofor a different reason — this law is straight-line only. When the destination is MEMORY and there are two constants, §164-73 owns it and the cure is TWO STORES, not a second local. - UNTESTED, so out of scope: consumers other than
sh(a call argument pair, answ, a compare). I could not isolate that insidefunc_80183094without changing the function's shape, so the "twosh" clause is stated as tested — do not assume it transfers to a call-argument pair without re-running the A/B. - Whole-binary gate not run. Per the harness constraints I ran only
tools/match_one.py(standalone, relocation-masked).V_sibling.cMATCHes at 72 ins standalone; the SHA1 gate on ov_SC04_005 has NOT been run and must be before the asm is removed from the banked source.
SECOND INSTANCE. For the clamp instance specifically: WEAK — it is one source template cloned, not independent sightings. A regex scan of every banked .c/.h in src/ for if (X > K) { X = K; } V = X; returns exactly two hits, and they are the same actor-height function in two overlays: src/ov_SC04_005/ov_SC04_005_jr_8017BEBC.c:6967 (func_80182E54, banked) and src/ov_SC04_007/ov_SC04_007_jr_8017BEBC.c:7316 (banked twin). A structural scan of the entire asm/ tree for addiu $rB,$zero,K ; addu $rA,$rB,$zero returns exactly one hit, asm/ov_SC04_007/nonmatchings/ov_SC04_007_jr_8017BEBC/func_80184238.s:73 — the still-unbanked sibling of func_80183094, with a byte-identical tail (80184328-8018434C). Four functions, one original.
For the underlying narrow-second-local law: STRONG — four independent witnesses in unrelated code. Scanning asm/ for addu $rA,$rB,$zero immediately feeding two-or-more sh $rA (29 raw hits; I discarded the func_80144B9C cluster as -O0-shaped, every statement reloading through $fp):
asm/ov_SC02_027/nonmatchings/ov_SC02_027_jr_8017D898/func_80185C84.s:22-25—slt $v0,$v0,$v1 ; bnez $v0,.L ; addu $a0,$v1,$zero ; sh $a0,0x1C($s1) ; sh $a0,0x1A($s1) ; sh $a0,0x18($s1). A clamp-to-limit with the copy in the delay slot and THREE consumers. Different overlay, different function, same tell.asm/ov_SC03_012/nonmatchings/ov_SC03_012_jr_8017AE2C/func_8017DCB4.s:73-76—addu $v0,$a1,$zero ; sh $v0,0x1C($v1) ; sh $v0,0x1A($v1) ; sh $v0,0x18($v1). No clamp anywhere. This is the cleanest proof that the clamp is incidental and the narrow-local-with-N-stores is the law.asm/ov_SC06_016/nonmatchings/ov_SC06_016_jr_8017C8D0/func_80180BF0.s:43-45—addu $v0,$s2,$zero ; sh $v0,0x18($sp) ; sh $v0,0x10($sp), stores to a stack struct.asm/ov_SC03_091/nonmatchings/ov_SC03_091_jr_8018326C/func_801889EC.s:68-70—addu $v0,$a0,$zero ; sh $v0,0xE4($s0) ; sh $v0,0x100($s0), post-call, with thesrl $v1,$a0,16half beside it doing the same thing.
All four are still INCLUDE_ASM, so they are live targets this law can be spent on.
§194-C — A CALLER-SAVED loop counter proves its live range crosses ZERO calls — so it cannot share a pseudo with any value LIVE ACROSS a call (but it may freely share one with values that merely sit between calls)
A CALLER-SAVED LOOP COUNTER PROVES ITS PSEUDO CROSSES ZERO CALLS: it can never be the same source variable as anything LIVE ACROSS a jal. (It CAN be the same variable as something merely used between calls — that half is byte-refuted.)
(Read-direction oracle on §150's parent rule. Uses §48-A4/§193-B's mechanism but inverts it: those explain an instruction after the fact, this reads a variable-count constraint off the target .s before any C is written. §45-A/§167-21 are the same knob on CALLEE-saved registers and state only the merge/split lever, not the class read.)
THE TELL (one observation, no symmetric block pair needed). A loop counter in the target — the addiu rC,rC,K / slti $v0,rC,LIMIT / backward bnez triple — whose register rC is caller-saved ($v0-$v1, $a0-$a3, $t0-$t9).
THE LAW. gcc-2.7.2 does no live-range splitting (§150: one pseudo holds one hard reg for life). Both allocators strip the entire caller-saved set on the first attempt for any allocno/qty with a nonzero call-crossing count:
global.c:922-927—if (accept_call_clobbered) used1 = call_fixed_reg_set; else if (allocno_calls_crossed[allocno] == 0) used1 = fixed_reg_set; else used1 = call_used_reg_set;local-alloc.c:2103-2106— the identicalqty_n_calls_crossed[qty] == 0 ? fixed_reg_set : call_used_reg_setchain infind_free_reg.
So a caller-saved counter is a hard statement that its pseudo's live range contains no CALL_INSN. Any source variable that also holds a value across a jal is therefore a different variable. Split before drafting.
WHAT IT DOES NOT PROVE (byte-refuted — do not over-read it). It does not prove the counter is its own variable or is scoped to its own loop. reg_n_calls_crossed counts only calls where the reg is LIVE, so a variable that is re-defined after each call and dead across every one keeps calls_crossed == 0 and legally shares the caller-saved register with the counter (this is §48-A4's zero-crossing direction). Byte proof: func_80182EA0 (ov_SC04_002, 71 ins) — the target's $v1 is the call-free copy loop's counter AND *(arg0+0x20) in the post-call region, and the separate-i/p20 spelling and the merged single-i spelling are both MATCH 71, byte-identical.
BYTE EVIDENCE (the positive direction).
func_80182E54(ov_SC04_005, 144 ins,src/ov_SC04_005/ov_SC04_005_jr_8017BEBC.c:6893). Target: sweep counter initsaddu $a2,$zero,$zero, call-free 0x60-entry loop; probe/grid counters$s2/$s0, both loops crossingjal. Baseline → MATCH 144. Deletes32 k;and run the sweep on the call-crossingi→ DIFF 144/144, 12 mismatched, OPCODE-MIXED, COUNT-NEUTRAL:sw s2,40(sp); move s2,zeroreplacesaddu $a2,$zero,$zero, and the prologue save order +bne $v0,$a3/bne $v0,$t0constant registers cascade behind it.- Minimal controlled probe (pinned triple,
-O2): two loops, one call-free sweep + onejal-bearing loop. One sharedi→ sweep counts in$16(callee-saved). Separatek→ sweep counts in$4(caller-saved), and the prologuesw $31/sw $16order flips too. Same cascade, 8 instructions of C.
PRECONDITIONS (check all three before trusting the class read).
- No stack bracket in the counter's live range.
flag_caller_savesIS on at-O2(toplev.c:3394), and both allocators retry withaccept_call_clobbered=1when the first pass found nothing (global.c:1078needsbest_reg < 0;local-alloc.c:2205-2209is on thefail:path) ANDCALLER_SAVE_PROFITABLE(refs,calls)=4*calls < refs(regs.h:165) holds. That retry really fires in this game — 7sw rC,K($sp) / jal / lw rC,K($sp)brackets exist in the tree (libgs6/PRESET_OBJ_8CC,800b_7/gfx2D_BG0_OBJ_4D8+_698,ov_SC02_003:func_801888F4). A caller-saved register with save/restore around thejalsays nothing about variable identity. - Not a spill reload. A frame-resident counter shows up in a caller-saved register only at its increment. Tell:
lw rC,K($sp)immediately before theaddiu/sltiandsw rC,K($sp)immediately after, with the init spelledsw $zero,K($sp). Example:ov_SC07_002:func_8017DC80,$a2at 8017E1A4 over0x30($sp). - It is a counter, not an argument.
$a0-$a3written as the last def before ajalare arguments (L15195); the oracle keys on theaddiu rC,rC,K+slti+ backwardbneztriple only.
THE CONVERSE IS FALSE. A callee-saved counter does NOT prove a call crossing — 21 of 463 callee-saved counters in the tree sit in call-free loops (first-fit pressure, no free caller-saved reg). The oracle runs one way only.
WHY IT MATTERS OPERATIONALLY. The residual is count-neutral and OPCODE-MIXED (144 vs 144), so no length oracle, no slti-shape tell and no permuter reaches it, and per §150 every pin/slider/permuter probe on a wrong-variable-count draft is structurally inert. Read the counter's register class off the .s during the first pass over the target, alongside the frame oracle.
DIAGNOSTIC TELL. A count-neutral OPCODE-MIXED diff where your move $sN,$zero + an extra sw $sN,K($sp) sits where the target has addu $aN,$zero,$zero, and the whole prologue save order shifts behind it. That is one variable too few, not an allocator tie.
Symptom lines for the index: "my counter is in $s2, the target's is in $a2" · "count-neutral diff, prologue save order shifted" · "one extra sw $sN in the prologue and no length change" · "how many loop counter variables did the original have".
BOUND. The half that is falsified. "It cannot be the same source variable as any counter used in a call-bearing loop; caller-saved counter => its own variable, scoped to its own loop." The scoped-to-its-own-loop / its-own-variable framing is byte-wrong. reg_n_calls_crossed accumulates only over insns where the reg is LIVE, so a variable re-defined after every call and dead across each one keeps calls_crossed == 0. Refutation: func_80182EA0 — merging the caller-saved $v1 counter with p20 is MATCH 71, byte-identical, and the target itself uses $v1 for both roles across two jals. The surviving claim is only "nothing in this pseudo is live across a call".
Where the surviving half does not apply:
- The caller-saves retry (the submitter's own falsifier — real, not hypothetical).
flag_caller_saves = 1at-O2(toplev.c:3394). A call-crossing value CAN take a caller-saved register viaglobal.c:1078-1090/local-alloc.c:2205-2209, gated onbest_reg < 0(or thefail:path) AND4*calls < refs(regs.h:165). It fires 7 times in the tree. Precondition: nosw rC,K($sp) … jal … lw rC,K($sp)bracket. For loop counters specifically it fired 0 of 231 times — a counter'srefs(~4-6) rarely beats4×calls, and MIPS's 9 callee-saved registers makebest_reg < 0rare — but a high-ref counter in a register-starved function is the live escape hatch. - Spilled counters. 1 of 231 (
func_8017DC80,$a2over0x30($sp)). The caller-saved register at theaddiuis a reload temp, not the variable's home. Under heavy pressure gcc-2.7.2 prefers the frame to caller-saves: my 13-live-value probe put the counter in$s1and spilled 10 values to$spslots rather than take the retry. - The converse. Callee-saved does not imply call-crossing (21 of 463 in call-free loops). Never run the oracle backwards.
- Argument registers.
$aNwhose last def precedes ajalis an outgoing argument (L15195), not a counter. Key strictly on theaddiu rC,rC,K/slti/ backwardbneztriple. - Nonlocal-goto functions (
local-alloc.c:2199-2202,global.ccomment at :1074) never put call-crossing pseudos in registers at all — the class carries no information there. Not present in this codebase, but stated for completeness. - Citation bound.
global.c:906/:917-922as cited (and as already banked in §48-A4 L3455 and L15935) do not contain the predicate; useglobal.c:922-927. §193-B L18461 already has it right.
SECOND INSTANCE. Three, of increasing strength.
(a) A second BANKED ablation — func_80182EA0 (ov_SC04_002, 71 ins, src/ov_SC04_002/ov_SC04_002_jr_8017BEBC.c:6891), asm at .run/waveT_asm_snapshot/ov_SC04_002/func_80182EA0.s. This is the one that produced the narrowing, not a confirmation: baseline MATCH (71 ins); merging the caller-saved-$v1 copy-loop counter i with p20 is still MATCH 71. Files: .run/harvest_u/v_2EA0_base.c, .run/harvest_u/v_2EA0_merged.c.
(b) A minimal controlled compile probe (independent of any target). .run/harvest_u/syn_a.c (one shared i for a call-free sweep and a jal-bearing loop) vs .run/harvest_u/syn_b.c (separate k), compiled with the pinned triple via .run/harvest_u/cc.sh:
- shared → sweep counter in
$16($s0, callee-saved),sw $16,16($sp)beforesw $31,20($sp) - split → sweep counter in
$4($a0, caller-saved),sw $31,20($sp)beforesw $16,16($sp)Same register-class flip and same prologue-order cascade asfunc_80182E54, from 8 lines of C with no game data involved.
(c) A whole-tree statistical test of the read-direction (.run/harvest_u/lawtest.py). Over all asm/**/nonmatchings/**/*.s plus both wave snapshots, I extracted every hardware loop counter (addiu rC,rC,K → slti on rC → backward bnez) inside a function containing a jal, and asked whether the loop body between the branch target and the branch contains a jal:
body has jal |
body call-free | |
|---|---|---|
| caller-saved counter | 1 | 230 |
| callee-saved counter | 442 | 21 |
The single caller-saved-with-jal case is ov_SC07_002:func_8017DC80's $a2, and it is a frame-spilled counter (lw $a2,0x30($sp) / addiu / slti / sw $a2,0x30($sp), init sw $zero,0x30($sp)) — caught by precondition 2, so with the preconditions applied the read-direction is 230/230. The bottom-left cell (21) is what proves the converse false.
Additional whole-function corroborations read by hand: ov_SC06_033:func_80190E64 ($a0 counters in two call-free loops at .L80190EE4/.L80191044, $s0/$s1 counters in the rand-heavy loops) and md_SC07_003:func_801A026C ($v1 counters in the two call-free delay loops, $s1 counters in the two jal-bearing loops).
§194-D — Declared width of a computed-value local is a sched1 dial (count-neutral) — a replication + attribution correction of §165-17, NOT a new argument-register law
MERGE INTO §165-17 (L14091) as its replication + attribution correction — do NOT open a new section.
§165-17, corrected and replicated (n=1 -> n=3 sites, 2 functions, 2 overlaid shapes). The declared width of a local that RECEIVES A COMPUTED VALUE (u16/s16 vs s32) is a sched1 dial, not a local-alloc one. x = <load> + K written into a 16-bit local re-plans its basic block; the identical value in an s32 local is byte-free, even when the truncation is written explicitly (s32 b = (u16)(load + K); and s32 b = (load + K) & 0xFFFF; both MATCH). The knob is therefore the MODE OF THE PSEUDO, not the truncation semantics and not the value.
Three things are inert and must not be blamed:
- Live-range extension / number of named locals. k
s32locals with all k loads hoisted and all k stores deferred to the end of the block — the maximal-live-range spelling — MATCHES. So does the same with ku16locals when the arithmetic sits outside them. "Naming values in locals raises pressure and costs a register" is FALSE on this exemplar (it is also the explanation written into the banked C's own header comment atsrc/ov_SC03_090/ov_SC03_090_jr_8017CA80.c:7594-7605— that comment is wrong and should be corrected in place). - Where the arithmetic is.
u16 b = load; store = b + K;MATCHES;u16 b = load + K; store = b;does not. Move the arithmetic OUT of the narrow local — that is the one-token fix, and it keeps the narrow declaration if you want it. - Store order — a separate, orthogonal residual (reversing it gives IMM-VALUE/cse, no register churn).
ATTRIBUTION (ablation-established, replicated on two functions). Recompile both spellings with cc1 -fno-schedule-insns: they collapse to byte-identical .s, including the register allocation. -fno-schedule-insns2 does NOT converge them. toplev.c:3028 (sched1) runs before :3049 local_alloc / :3077 global_alloc, so the register differences are DOWNSTREAM of sched1's reorder, not an allocator preference. This corrects §165-17's "reorders the block quantities in local-alloc" and closes its open §164-48 hypothesis in the negative: do not hunt an allocno_compare/global.c:594 story for this class. The deeper RTL cause (why a HImode pseudo ranks differently in rank_for_schedule) remains UNESTABLISHED; the birthing_insn_p (sched.c:2468-2500) suspect is refuted-by-evidence, not merely unverified — reg_n_sets is 1 in both spellings, and the effect reproduces on a shift result with no birthing asymmetry. State "sched1 owns it, mechanism open" and stop.
DIAGNOSTIC TELL (corrected). Count-EXACT residual (never length drift), memory operands keep their $sp/$sN base, and the diff is a pure permutation of loads/arith/stores within one block — plus, ONLY when the block ferries k>=3 values at once, an extra caller-saved register (here $a2) appearing as a data carrier with the call's arg copies pushed to the block top. Sweep the declared width of every local in that block that holds a COMPUTED value before reaching for register __asm__ pins, §165-24 goto-joins, or the permuter. The edit is free and keeps the family (§37/§162p3).
FALSIFIED HALF OF THE SUBMISSION (do not file it). "An ARGUMENT register gets drafted as a data ferry and the call's arg copies hoist to the block top" is NOT the law — it is the k>=3-ferry symptom of one exemplar. On the second function the identical width edit produces a bare two-instruction transposition with no extra register and no arg-copy movement; on a third site (a shift, same function) likewise. Filing "$aN carrying DATA" as this class's index symptom would misroute future agents exactly the way §193-D's symptom line misrouted this one.
BOUND. The dial only bites where sched1 has freedom to move. Byte-proven inert case in the SAME function: u16 d = t1*2 - t2; *(s16*)(a0+0x102) = d; between two jals -> MATCH, identical to the s32 spelling. A block wedged between calls, or with no independent neighbour insns, will not respond to the width at all — a null result there is NOT a refutation of the law, and you must not conclude "width is inert in this function" from one site (it fired at +0x52 and not at +0x102, in the same 117-instruction function).
Other bounds, all measured:
- Not a load-width law.
*(s16*)vs*(u16*)on the SOURCE is a separate axis (it selectslhvslhu); my first maximal-live-range probe read 3 mismatched WIDTH/lh!=lhu purely from that, and matched once the loads were spelled*(u16*). Change one axis at a time or you will misread the result. u8is not a free third rung. Substitutingu8for the narrow local truncates the value for real: 118 vs 117 ins, LENGTH-DRIFT, 80 mismatched. The law is HImode-vs-SImode only; QImode is a semantic change here, not a dial.- Count-neutrality is a precondition, not a bonus. Every firing case is exactly count-exact. If your residual carries length drift, this is a different class (§164-48 is the +1-drift/
sll 16; sra 16promotion sibling; §16Xy is the(u16)-on-a-HImode-local frame-cost sibling — neither fires here, and(u16)applied to an SImode SUM stored into ans32local is byte-free and frame-free, consistent with §16Xy's own "s32 s-> 0" row). - §193-D discrimination stands (verified, not just asserted): §193-D re-bases the block's memory operands onto
$aNand drifts +1; this is count-exact and the operands keep their$spbase. - n is still small for the RTL story. The C dial and the sched1 attribution are n=3 sites / 2 functions / 1 overlay. No
.lreg/.greg/RTL dump was taken (cc1-dc -dSproduced no dump files in this harness), so the pseudo-mode ->rank_for_schedulelink is inference from the ablation, not read out of a dump.
SECOND INSTANCE. func_8018440C (ov_SC03_090, 84 ins, banked and MATCHing at src/ov_SC03_090/ov_SC03_090_jr_8017CA80.c:5898-5967). Its target has the same family shape — three lhu/arith/sh narrow RMWs plus a three-halfword ferry into 0x12/0x16/0x1A($sp) ending in jal func_8012B77C, with $a1 already carrying data in the TARGET.
Base re-extracted by me -> MATCH (84 ins). Perturbing ONE statement, *(s16*)(a0+0xA) = *(u16*)(a0+0xA) - 0x100;:
u16 q = *(u16*)(a0+0xA) - 0x100; *(s16*)(a0+0xA) = q;-> 4 mismatched, SCHEDULE-REORDER, 84 vs 84 inss16 q = ...-> byte-identical 4-mismatch outputs32 q = ...-> MATCHs32 q = (u16)(...)-> MATCH (the explicit-truncation control)u16 q = *(u16*)(a0+0xA); *(s16*)(a0+0xA) = q - 0x100;-> MATCH (arithmetic outside the narrow local)
Residual is a pure transposition: mine lhu v1,10(s0) / lhu v0,6(s0) / addiu v1,-256 / addu v0,v0,a1, target lhu v0,6(s0) / lhu v1,10(s0) / addu v0,v0,a1 / addiu v1,-256. No extra register, no argument register drafted, no arg-copy hoist — which is what falsifies the submitted headline while confirming the underlying dial.
Attribution replicated here too: -fno-schedule-insns makes the two spellings byte-identical; -fno-schedule-insns2 leaves a 3-line reorder.
Third site (same function as the exemplar, different shape): u16 s = (u32)ang >> 8; *(s16*)(a0+0x52) = s; -> 4 mismatched SCHEDULE-REORDER, count-neutral; the s32 spelling MATCHes. A shift, not a load + K, and not inside the k-ferry block — so the law generalises past the submitter's stated shape.
§194-E — The wave card's exemplar is the TARGET ITSELF on 42/73 wave-U and 36/71 wave-T cards — a construction consequence of atlas.py:657 (exemplar = max-nins OPEN member) meeting build_wave_atlas.py:166 (one card per gid); and the shipped §193-A seed_ref is same-binary on 0/51, so no card field can ever name a destination-TU sibling
x — THE CARD'S exemplar IS OFTEN THE TARGET ITSELF (42/73 wave U, 36/71 wave T), AND THE SHIPPED §193-A seed_ref IS SAME-BINARY 0/51 — SO NO CARD FIELD CAN NAME A DESTINATION-TU SIBLING
The law (one line): a wave card's exemplar is not merely un-banked (§193-A) — it names the card's OWN target on roughly half the cards, and the seed_ref §193-A added to fix it is same-binary on 0 of 51, so after the §193-A fix the card still carries zero destination-TU locality.
Mechanism (corrected from the submission). atlas.py:657 sets the group exemplar to max(members, key=nins) over OPEN members (verified: exemplar == max-nins member on 73/73 wave-U gids). build_wave_atlas.py:143 copies that group-level dict onto the card, and :166 --one-per-gid then keeps ONE representative per group ranked (TU-mass, nins, fn) — TU mass first, size second. The self-pointer fires whenever the TU-mass winner is also the size winner. Two regimes with different arithmetic:
--one-per-gid(waves T, U, R, and every wave since): rate = P(TU-mass rep is also max-nins). Measured 57.5% (U), 50.7% (T), 91.8% (R). Decomposed on wave U against.run/atlas.json: 16/16 singleton-member groups self-point (mathematically forced), 26/57 multi-member groups do (45.6%).- no
--one-per-gid(waves p31j/k/l/o): at most one card per gid can self-point, so the rate is bounded bygids/cards— J 14 gids/40 cards → 15.0%, L 14/44 → 18.2%.
The locality half. atlas.py:509 skips main from the seed pool and :505-515 dedups the matched pool by h_seq with no same-binary preference plus an explicit ov_SC01_077 preference that biases away from the card's binary. Measured: seed_ref.binary == card.binary on 0/51. Combined with the self-pointer and the 30/73 cross-binary exemplars, no card field ever points into the destination TU.
COROLLARY (mine, not the submitter's). atlas.py:658-659 keys the seed on the exemplar's h_seq (ex_hs = exm[2]; seed = seed_best.get(ex_hs)), and 18/73 wave-U groups span >1 skeleton. On 9/73 cards the shipped seed_ref/seed_sim therefore describes a different skeleton than the card's own target — e.g. ov_SC04_005:func_80184338 (hs 3b9adf…) carries seed_sim 1.0 computed for exemplar ov_SC05_010:func_80187198 (hs 9a68b1…). Read a card's seed_sim as the exemplar's similarity, not the target's, unless exemplar.fn == card.fn.
PRESCRIPTION (tool-side, one line each).
build_wave_atlas.py:143— emit'exemplar': Nonewhenex.get('name') == fn and ex.get('b') == b. A self-pointing field reads as a live pointer and costs a lookup before it is recognised as a no-op.atlas.py:512— prefer the card's own binary when several matched instances share anh_seq, and emit a second, same-binary ref alongside the global-best one.atlas.py:659— key the seed on the MEMBER'sh_seq, falling back to the exemplar's.
WHAT NOT TO RE-BANK — the falsified half. The prescriptive conclusion ("open the destination TU; same-TU siblings outrank cross-overlay seed similarity") is already §193-A — its corrected search order puts same-TU banked body by exact symbol join at step 2 for mature TUs (measured 13/21 = 62%), and BOUND 4 already covers the shape-found siblings the wave-U agents reported. This section adds the measurement of the card fields, not the retrieval advice.
(SHARPENS §193-A (L18396) — which measured exemplar bankedness on this same wave-T data and missed that 36 of its 71 cards self-point; and BOUNDS §193-A's own fix by measuring the shipped seed_ref as 0/51 same-binary. Evidence: .run/wave_u_cards.json, .run/wave_t_cards.json, .run/atlas.json, tools/atlas.py, tools/build_wave_atlas.py, reproduced 2026-08-17.)
BOUND. Where it does NOT apply:
- The ~50%+ self-pointer rate is regime-bound, not universal. It holds only under
--one-per-gid(build_wave_atlas.py:166). Without that flag the ceiling isgids/cardsand I measured 15.0% (wave J, 14 gids/40 cards) and 18.2% (wave L, 14/44) — materially below the submitter's "~50% construction invariant". The submitter's own falsifier named waves T and R, both of which are--one-per-gidand both of which pass (50.7%, 91.8%), so the candidate survives the test it proposed but not the fuller sweep. The MECHANISM is invariant; the RATE is a slate + flag artifact. Any future citation must state the regime. - Not a claim that the exemplar is worthless. §193-A already gives the correct handling (never spend a token on it without the same-TU body check first). This law only removes the ~57% of lookups that are provably no-ops before the grep.
seed_ref0/51 is not "cross-binary by construction" in the strict sense the submitter claimed — nothing inatlas.py:505-536forbids a same-binary hit; the pool simply has no same-binary preference and an anti-preference towardov_SC01_077. It is cross-binary in measurement (0/51 on the only wave that carries the field), not by proof. A same-binary ref is possible whenever the card's own binary holds a matched twin of the exemplar'sh_seqand wins registry order.- Not applicable to
main.atlas.py:509skipsmainfrom the seed pool entirely and:100/:103routes it throughmain_open_stubs(), so main cards get noseed_refat all — §193-A BOUND 2 already covers this. - The 9/73 wrong-skeleton corollary is bounded by
n_skel. 55 of 73 wave-U groups are single-skeleton, where the exemplar'sh_seqis the target's and the seed number is honest. The defect only bites on the 18 multi-skeleton groups. - This is a tooling law, not a compiler law. It has no bearing on any C spelling and no byte consequence; its entire value is tokens saved per card. It should be retired the moment the three one-line tool fixes land, not carried as a permanent matching idiom.
SECOND INSTANCE. Second instance — wave T, an independent slate built from an independent atlas run: 36 of 71 cards (50.7%) have exemplar.fn == card.fn. Concrete self-pointers, all in ov_SC04_002: func_80181D44 (127 ins), func_80182044 (79), func_80182D2C (78), func_801821C4 (74), func_801826E8 (72) — each card's exemplar is {'binary': 'ov_SC04_002', 'fn': <the same fn>}. Wave T is decisive because it is §193-A's OWN evidence wave: §193-A resolved these exact cards' exemplars against corpus.stubs and reported "34 entries, 0 banked" without ever noticing that half of them named the target itself. Wave T also lacks the seed_ref key entirely (sorted(keys) has seed_sim but no seed_ref), which independently confirms the §193-A fix shipped between T and U and that the 0/51 locality measurement is a genuinely post-fix finding.
Eight further instances, all computed by me over .run/wave_*_cards.json: p31w 10/10 (100%), p31v 6/6, p31u 12/12, p31q 89/90 (98.9%), p31p 59/60 (98.3%), p31r 67/73 (91.8%), p31s 65/71 (91.5%), p31f 66/96 (68.8%), p31o_all 33/49 (67.3%), p31n 42/48 (87.5%). The low-rate counterexamples (p31j 15.0%, p31l 18.2%, p31k 22.7%, p31o 23.8%, p31e/g/i ~33%) are the non---one-per-gid regime and are what forced BOUND 1.
For the (rejected) locality half, the submitter's own func_8018440C instance verifies in the tree but is redundant with §193-A's four already-banked same-TU instances (func_801861D0←func_801862F0 5 shared symbols, func_80184724←func_80185DA8, func_801858E4←func_80185668, func_80182D2C←func_80183FE8).
§194-F — if ((*p = v = f()) == 0) is an expand-time pseudo SPLITTER (store_expr's want_value && MEM path), not a fold — it partitions one value between local_alloc and global_alloc. BOUNDS §21's L1872 bullet, whose stated direction is byte-wrong on 3 of 4 instances, and CLOSES the control §167-37 asked for.
x — if ((*p = v = f()) == 0) IS AN EXPAND-TIME PSEUDO SPLITTER, NOT A FOLD — AND ITS DIRECTION IS PER-FUNCTION
(BOUNDS §21's L1872-1881 combined-assignment bullet, whose stated direction is byte-wrong on 3 of 4 instances measured. CLOSES the control §167-37's scope note explicitly asked for. SHARPENS §186c — same allocator split, but reached by expression nesting rather than by load placement, and the second register is an ARG register won through set_preference, not $v1 won by exhaustion.)
THE MECHANISM (source + RTL verified). Writing the store and the test as ONE expression does not fold anything — it mints two extra copy pseudos:
| spelling | expand RTL | .lreg |
.greg |
|---|---|---|---|
v = f(); *p = v; if (v==0) |
(set 73 $v0), (set mem 73), (ne 73 0), later (set $a0 73) |
73 used 4× no block ⇒ global | 73 in 2 ($v0) or 73 in 4 ($a0) — free |
if ((*p = v = f()) == 0) |
(set 74 $v0), (set 73 74), (set 75 74), store+test on 75, later (set $a0 73) |
73 used 2× (global) + 74 used 4× in block 0 | 74 in 2 ($v0), 73 in 4 ($a0) |
Two source sites do it. The inner v = f() expands with want_value=1 (expr.c:2445 expand_assignment → :2661-2665), so store_expr returns the call's own temp rather than the variable — that is pseudo 74. The outer *p = … is also an rvalue, so store_expr takes expr.c:2734-2742 — else if (want_value && GET_CODE (target) == MEM && ! MEM_VOLATILE_P (target) && GET_MODE (target) != BLKmode) / "If target is in memory and caller wants value in a register instead, arrange that" (fallback copy_to_reg (target) at :2955-2958) — that is pseudo 75.
The split is then between allocators. 74/75 are block-0-local, so local_alloc owns them and its own copy suggestion homes them on $v0 (combine_regs sets qty_phys_copy_sugg from (set 74 $v0) at local-alloc.c:1808-1834; find_free_reg ORs it in first at :2144-2151). Pseudo 73 is cross-block, falls to global_alloc, and its only hard-reg preference is now the later (set $a0 73) arg copy — set_preference (global.c:1580-1616, fires in either copy direction) records $a0, and find_reg tries hard_reg_copy_preferences before anything else (global.c:~1000-1030). So 73 takes $a0 at its definition, and the copy materialises as move $a0,$v0 above the branch instead of at the consumer. The split spelling keeps ONE pseudo, which carries both the $v0 def-copy and the $a0 use-copy preference — whichever it wins is the register the target's sw/beqz reads.
THE ACTIONABLE FORM — a per-function dial with three reaches. Compile both, always.
- 0 — the mint is collapsed back before
.lreg; the two spellings are byte-identical. - register / schedule permutation — same
nins,sw/beqzswap$v0↔$aN, and themove $aN,$v0slides across the branch. - +1 instruction — the forced 73/74 split needs a real copy where the single-pseudo form needed none.
DIAGNOSTIC TELL. Read two things off the target: (1) which register the sw/beqz pair uses, and (2) whether a move $aN,$v0 sits above the guard branch or down in the consumer jal's delay slot. $v0 + copy-below ⇒ try SPLIT first. $aN + copy-above ⇒ try FUSED first. Then compile the other one anyway — three of the four measured instances disagreed with their sibling in the same TU.
⚠️ DO NOT CARRY A DIRECTION OVER FROM A SIBLING — INCLUDING FROM §21. src/ov_SC03_024/ov_SC03_024_jr_8017DF84.c banks both directions on the same callee at the same offset: func_80184040, func_80186A58, func_80184210 are banked SPLIT; func_80185730 is banked FUSED. §21's L1872 bullet prescribes the combined form unconditionally and predicts the two-statement form "emits the copy-to-$sX BEFORE the store and loses the bare sw $v0" — that is byte-false here: on func_80184040 it is the fused form that emits the extra copy before the store (102 vs 101 ins), and the plain two-statement form that keeps the bare sw $v0 in the bnez delay slot. §21's direction is an artefact of its exemplar (func_80142DC4, whose success arm reuses the value across a later call, the precondition §167-37 named). Read §21's bullet as one arm of a dial, never as a rule.
BYTE EVIDENCE (all ov_SC03_024, all vs .run/waveU_asm_snapshot/ov_SC03_024, all re-run at vet time):
| fn | split | fused | reach |
|---|---|---|---|
func_80184040 |
MATCH 101 | 102 ins, 87 mism, LENGTH-DRIFT/1? |
+1 — move a0,v0 hoisted above bnez |
func_80186A58 |
MATCH 99 | 99 ins, 2 mism, REGALLOC-PERM/$v0>$a0 |
registers — beqz/sw $v0 vs target $a0 |
func_80184210 |
MATCH 176 | MATCH 176 | 0 — mint dies before .lreg |
func_80185730 |
3-line diff | banked MATCH | schedule+registers, fused required |
Plus an out-of-family synthetic (alloc_thing, offset 0x40) that reproduces the whole pseudo/disposition/asm chain: .run/harvest_u/vet_gen/.
(BOUNDS §21 L1872-1881; ANSWERS §167-37's scope note; SHARPENS §186c and CORRECTS its REG_ALLOC_ORDER causal story — local_alloc homes the temp by copy suggestion, not by ascending scan order; evidence: byte-probed, 4 tree functions with both directions banked + 1 synthetic; from func_80184040 / func_80185730.)
BOUND. Where it does NOT apply — five conditions, three of them source-derived and one byte-derived.
-
THE POLARITY HALF IS FALSIFIED AS UNCONDITIONAL — this is the named falsified half. "Fusing keeps the store/test on the block-local
$v0" is not a property of the fused spelling. It requires the expand-time mint to survive tolocal_alloc. Onfunc_80184210the mint happens exactly as predicted (t.i.rtlinsns 13/15/17 mint pseudos 76 and 77, store and test both on 77) but by.lregboth are gone — collapsed back into 73 by cse/regmove — and.gregreads73 in 4($a0) in both spellings. The fused build'ssw/beqztherefore read$a0, and the byte reach is exactly 0. The check is one grep: dump-dland look for a second pseudo markedin block Nbetween the call and the store. No second block-local pseudo ⇒ the dial is dead for that function, do not spend compiles on it. -
Non-volatile MEM lvalue only (
expr.c:2734).! MEM_VOLATILE_P (target)guards the branch that mints the third pseudo, so avolatilestore target never enters this path. LikewiseGET_MODE (target) != BLKmode— an aggregate assignment does not mint. And the lvalue must be memory:*p = v = f()into a register-homed local takes a differentstore_exprexit entirely. -
The C variable must have a later CROSS-BLOCK hard-register copy use. The whole effect is
set_preferencerecording$aNon the global allocno. Ifv's only remaining uses are ordinary arithmetic, or if they all live in the same block as the call, there is no(set (reg $aN) 73)copy, the global allocno has no competing preference, and both spellings converge. The reach is created by the consumer, not by the spelling. -
Do not extend this to the unnamed form — that is §167-37's dial, and it is a different axis. This law compares two named spellings. Deleting the local entirely (
*(p+K) = f(); if (*(p+K) == 0) …; else h(*(p+K), …)) is a third option governed by §167-37, whose own four preconditions are narrower (call result, stored to a field, null-guarded, sole remaining use = the very next call's only argument) and whose evidence isn = 1. Three options, not two. -
The
local_allochalf is deterministic; theglobal_allochalf is not. Pseudo 74/75 →$v0is forced by a copy suggestion and will hold. Pseudo 73 →$aNis a preference, andfind_regabandons it under conflict (AND_COMPL_HARD_REG_SET (hard_reg_copy_preferences[allocno], used)), underallocno_calls_crossed > 0stripping caller-saved prefs (§48-A4,global.c:906), or whenallocno_compareranks a competitor higher (§186c's tie). In a hot body with several live allocnos, expect the fused spelling to land the variable somewhere other than$aNand re-measure rather than re-reason. -
Scope of the byte record. Four functions in one TU on one callee (
func_8012C1B8→ field0x20→ null test), plus one synthetic I wrote. The mechanism is source-general (it is astore_exprbranch, not a peephole) and my out-of-family synthetic confirms that; the reach distribution (0 / registers / +1) is measured only on this idiom family. Do not quote "three possible reaches" as a frequency claim.
SECOND INSTANCE. Found two ways — attack 4 does not land.
(a) In the banked tree, with BOTH directions byte-gated inside a single TU. src/ov_SC03_024/ov_SC03_024_jr_8017DF84.c banks the split spelling at :5516 (func_80184040), :7218 (func_80186A58) and :5638 (func_80184210), and the fused spelling at :6077 (func_80185730). Same callee, same offset, same file, opposite spellings, all four byte-matched. That single fact is the law's strongest evidence and is independent of any A/B I ran. Tree-wide the fused spelling is banked in ten TUs: ov_SC02_011:13817, ov_SC02_027:4411, ov_SC02_028:4205, ov_SC03_014:5164, ov_SC03_015:4666, ov_SC03_024:6077, ov_SC03_118:4518, ov_SC03_119:4494, ov_SC04_011:8460, ov_SC06_000:7456.
(b) A synthetic outside the family, written by me at vet time — /home/musashi/bfm-decomp/.run/harvest_u/vet_gen/g_fused.c and g_split.c. Different callee (alloc_thing), different offset (0x40), different consumer (use_b(v, 7) rather than func_8001C214), no engine headers, 12 lines. It reproduces every link of the chain:
fused .lreg : Register 73 used 2 times across 3 insns (global)
Register 74 used 4 times across 4 insns in block 0 (local)
.greg : 72 in 16 73 in 4 74 in 2
.s : jal alloc_thing / move $16,$4 / move $4,$2 / bne $2,$0,$L2 / sw $2,64($16)
($L2 arm: jal use_b / li $5,7 — the arg copy is gone from the arm)
split .lreg : Register 73 used 4 times across 4 insns (global, no block)
.greg : 72 in 16 73 in 2
.s : jal alloc_thing / move $16,$4 / bne $2,$0,$L2 / sw $2,64($16)
($L2 arm: move $4,$2 / jal use_b / li $5,7 — the copy stays at its consumer)
Both 17 instructions here, so the reach on this body is schedule+placement rather than length — which is itself useful: it shows the +1 seen on func_80184040 is the contingent outcome (the hoisted copy could not be absorbed there), not the characteristic one.
Beyond the family, the mechanism is general by construction: store_expr's want_value && MEM branch (expr.c:2734) has no callee-, offset- or type-specific guard, only ! MEM_VOLATILE_P and != BLKmode.
§194-G — Reading the integer half of a 16.16 stack aggregate: REGISTER-LIVENESS, not the C spelling, decides sra vs lhu — and the break is count-neutral
x — The 16.16 integer-half read: REGISTER-LIVENESS decides sra-vs-lhu, and the break is COUNT-NEUTRAL
Shape in the target. sw $v0,0x68($sp) immediately followed by lhu $a2,0x6A($sp) — a narrow reload of the very word just stored, at +2 (little-endian integer half of a 16.16 fixed-point stack aggregate). Movement/collision code accumulates 16.16 positions and hands the integer halves to an SVECTOR/u16 buffer; this recurs across the engine.
LAW. For an s32/u32 stack aggregate, whether the integer-half read becomes a register shift or a narrow memory load is decided by register liveness, not by how you spell it:
- Word stored in the SAME basic block ⇒ there is no memory read to be had.
cse(tools/reference/gcc-2.7.2/cse.c:1181,lookup:if (mode == p->mode && ((x == p->exp && GET_CODE (x) == REG) || exp_equiv_p (x, p->exp, GET_CODE (x) != REG, 0)))) hits on the recorded SImode MEM and hands back the stored pseudo, so any shift spelling —x >> 16,(s16)(x >> 16),(u32)x >> 16— lowers to a registersra/srl. Only a narrow-typed lvalue at +2 emits the target'slhu:*(u16 *)((u8 *)v + 2)or*((u16 *)&v[i] + 1)(byte-interchangeable). A HImode MEM query can never hit an SImode MEM entry —mode == p->modeis the first conjunct — and the +2PLUSaddress failsexp_equiv_p(cse.c:2068,if (code != GET_CODE (y))) anyway. gcc-2.7.2 has no partial-MEM / sub-word forwarding. - Word NOT stored in that block (loop-invariant, or across a
jal/label/branch) ⇒ the shift spelling narrows into memory by itself, and the pun buys you nothing. There the only question is signedness:x >> 16→lh(this is §136-11 firing),(u32)x >> 16→lhu, the pun →lhu. The unsigned shift and the pun are byte-identical.
Pointer-cast signedness is inert; shift signedness is not. *(s16*) and *(u16*) at +2 give identical bytes because LOAD_EXTEND_OP(MODE) ZERO_EXTEND (config/mips/mips.h:1163) makes a plain HImode load lhu and the SImode extension is dead into a sh sink. That is a target-macro fact, not gate blindness.
DIAGNOSTIC TELL — this is why the entry earns its place. The break is count-neutral: 124 ins vs 124 ins, so no length drift ever warns you. Read it off match_one's class instead: WIDTH [structural] sig=WIDTH/addiu!=lw (register form) or sig=WIDTH/lh!=lhu (memory form, 1 mismatch). The register form is loud out of proportion to its cause — two sras displace a whole lw/addu/sw group and cost 15-16 mismatches. And one spelling can produce three different opcodes across three sibling expressions in the same statement group (sra, sra, lh), because liveness differs per word — so never sweep the three siblings uniformly. Fix per-word: pun the register-live ones; either pun or (u32)x>>16 the invariant one.
Prior art this promotes. docs/matching-cookbook.md:17777 parked func_80188AF4 for lacking negative controls and a gcc decision point; both are now supplied. §136-11 (L8899) is the not-register-live case and predicts lh correctly — it is a sub-case here, not a conflict. §176-B1 (L17639) owns the same +2 insight for globals with a memory-disambiguator payload. L9225 supplies the mips.h:1163 half.
BOUND. Where it does NOT apply — five conditions, three of them measured this session.
- The exclusivity does not hold off-block (MEASURED, this is the falsified half). If the wide word was not stored in the current extended basic block,
(u32)x >> 16matches the pun byte-for-byte (mix1u→ MATCH 124). Do not reach for the pointer cast reflexively — check first whether theswis actually in the block. The pun is only mandatory for register-live words. - "Register-live" means cse's extended-basic-block reach, not textual proximity. Any
jal, label, or branch between theswand the read moves you into case 1. In the evidence functionpos[1]'s store is only a few source lines above the read but sits outside thedo{}whilebody, and it behaves as the off-block case. - Signedness inertness of the pointer cast is scoped to a dead extension (NOT tested beyond it).
*(s16*)==*(u16*)here only because the sink isshinto au16array, so the SImode extension is dead andmips.h:1163emitslhueither way. Where the HImode value has a live SImode consumer — a compare, an add into an s32, asll 16/sra 16pair — expectlhandsll;srato reappear and the two casts to diverge. Re-scope tosh-destination sinks; the submitter predicted this and I did not test it. - Says nothing about globals. For a narrow symbol at
wide_sym + k, §176-B1 governs and its payload is memory-disambiguation and live-range separation, notsra-vs-lhu. Do not carry this entry's reasoning onto aSYMBOL_REF. - Says nothing about non-16.16 offsets or non-
+2halves. Both instances read the high half of a little-endian s32 at +2/+1-in-u16-units. A+1-byte or+0-half read is a different rtx and was not probed.
Sharpest one-line caveat: the register-live half is byte-proven on one function (two words) plus one tree-verified sibling function; the off-block half is byte-proven on one word. The claim "this idiom recurs across the whole engine" is plausible from the two instances but is not measured — treat the corpus-frequency assertion as unverified.
SECOND INSTANCE. func_80188AF4 — src/ov_SC03_006/ov_SC03_006_jr_8017AE2C.c:10108 (banked, MATCH 73/73 per docs/matching-cookbook.md:17777; a different overlay and a different TU from the candidate's ov_SC03_029).
Same shape end-to-end: RotMatrixY on a copied D_800AE620 matrix, func_800484EC producing an out vector, a u32 pos[3] 16.16 accumulate (pos[i] = *(s32*)(a0+4/8/0xC) + out.vx/vy/vz;) — all three stores in one basic block — and then the integer halves read with the narrow pun into a u16 sink:
roy = *((u16 *)&pos[1] + 1);
roz = *((u16 *)&pos[2] + 1);
*(u16 *)(rotOut + 0) = *((u16 *)&pos[0] + 1);
This is the register-live case, and the banked C uses the pun — exactly as the law predicts. The cookbook's own note on it says the pun was needed "to force a stack reload instead of >>16 on a cached register", which is the law stated in the submitter's own words by a different agent months earlier.
Caveat on this instance: I could not byte-re-run it — it is banked, so splat emits no .s and there is no .run/*_asm_snapshot/ov_SC03_006/ directory. It is tree-verified (I read the C), not gate-verified by me. I closed that gap a different way: I proved the two pun spellings are byte-interchangeable inside the function I can gate — punB1 rewrote func_80184008's three reads as *((u16 *)&pos[i] + 1) (func_80188AF4's exact spelling) and got MATCH (124 ins). So the second instance's spelling is confirmed to be the same lever, even though its own bytes were re-verified only by the earlier session.
A third instance for the global variant exists at §176-B1 / func_80185548 (*((u16 *)&gVecZ + 1) on D_80126B64, MATCH 77 ins) but is out of scope per bound 4.
§194-H — §164-29's WAR fence runs again at SCHED2 ON HARD REGISTERS: an in-place v &= K before a branch fences the store that reads v, and when TWO independent stores compete for the one delay slot the fence picks the winner at ZERO length drift (a WIDTH/sh!=sw swap, not a nop). Corrects §167-42's reconstruction (pseudo arm → hard-reg arm) and supplies its first re-runnable A/B.
§164-29-b — THE WAR FENCE RUNS A SECOND TIME, AT SCHED2, ON HARD REGISTERS; WITH TWO STORES COMPETING FOR ONE DELAY SLOT IT PICKS THE WINNER AT ZERO LENGTH DRIFT. (SHARPENS §164-29 (L12491), whose only stated bound is "sched1 is per-basic-block" and which never mentions the post-reload pass; CORRECTS §167-42 (L16060), whose reconstruction pins the same coupling on the pseudo arm and whose counter-arm is self-declared non-re-runnable — this entry is that A/B, re-run and preserved. The register-identity half is §164-80 Law 1 measured beyond and/or, as its own scope note requested.)
Target shape — two independent stores, an in-place mask, a branch:
sw $v1,0x1C($s0) <- reads $v1 …
andi $v1,$v1,0x7 <- … and the mask re-SETS $v1: WAR fence
bnez $v1,.L
sh $v0,0xA($s0) <- the OTHER, unfenced store takes the slot
THE MECHANISM (pass-corrected). -O2 sets flag_schedule_insns_after_reload (toplev.c:3397-3398) and sched2 runs after reload, on HARD registers (toplev.c:3105-3117). sched_analyze_1's hard-reg arm — sched.c:1695-1696, for (u = reg_last_uses[regno+i]; u; u = XEXP (u, 1)) add_dependence (insn, XEXP (u, 0), REG_DEP_ANTI); — is the same loop §164-29 cites at :1713-1714 for pseudos. So the fence is decided after register allocation: whichever store's SOURCE register the andi overwrites is fenced above the mask, and sched2 floats the other store down adjacent to the branch. reorg then simply steals the nearest non-conflicting insn (reorg.c:2907-2925, insn_references_resource_p (trial, &set, 1) — it never reaches the fenced store). Do not look for a shared pseudo in the losing spelling: there is none. Cite :1695-1696 + sched2, never :1714-1715, for anything downstream of reload.
THE PRESCRIPTION (cheap probe, contingent trigger). Two adjacent independent stores, a mask test, a branch, length identical to the target, and the two stores appear on the wrong sides of the branch (match_one prints class: WIDTH sig=WIDTH/sh!=sw, a misleading label — nothing is the wrong width). A/B the mask spelling; it is a two-character edit:
v &= K; if (v == 0)— mask in place ⇒ fences the store that readsvABOVE the branch, the other store takes the slot.if ((v & K) == 0)— mask into a temp ⇒ the temp first-fits onto$v0; if and only if $v0 is the other store's source register, that store is fenced instead and the two swap sides.s32 m = v & K; if (m == 0)is byte-identical to the implicit form — naming the temp is inert (refutes §162d1's anonymous-temp-is-single-set distinction for this shape). Source ORDER of the two stores is also inert (ablated, both spellings). Legal only wherevis dead afterwards; in func_8018118C the next test re-reads*(s32*)(a0+0x1C)from memory, so destroyingvis free.
⚠ THE FALSIFIED HALF — the flip is NOT a reliable consequence of the C spelling. The submitted claim "the mask's destination register decides which neighbouring store is anti-dependence-pinned" is refuted as a general rule. In two independent synthetics of this exact shape (.run/harvest_u/av_synth.c, av_synth2.c) the temp was coalesced back onto the masked value's OWN hard register (andi $2,$2,0xf in both spellings) and the two spellings compiled byte-identically — no register-identity effect, no slot effect. The C spelling only decides whether the mask lands in v's register or in the first free scratch; whether that scratch collides with the OTHER store is an allocation outcome you cannot steer from C. Treat this as a 2-character probe to try when the tell fires, never as a prediction.
BOUND. Does NOT apply, and the probe is inert, when any of these hold:
- The mask's first-fit scratch is the masked value's own register. If
vdies at the mask and local-alloc reusesv's hard register for the temp (andi $2,$2,K), both spellings emit identical bytes. This is what happened in both of my synthetics and it is the common case in small functions — the flip in func_8018118C needed$v0to already be holding the OTHER store's source. Unsteerable from C. - Only one store is adjacent to the branch. With a single candidate the observable is §167-42's polarity instead: fenced ⇒ slot stays
nop, LENGTH-DRIFT −1. This entry's zero-drift SWAP tell requires two competing stores, and §167-42's drift tell can never fire on it. vis live after the test.v &= Kdestroys it; the edit is only legal when the next reader re-reads memory (orvis otherwise dead).- A label or branch separates the store from the mask. sched2 is per-basic-block, same as §164-29's bound (1).
- Anything before reload. The pseudo arm (sched.c:1713-1714) governs sched1; the losing spelling has distinct pseudos there and no fence exists. Reasoning about this shape at sched1 is a dead lane.
- The other store's source is
$zeroor an immediate-materialised constant. No register to fence; severalasm/sightings of the surface shape (sh $zero,…) are of this inert kind. - Compiled without
-fschedule-insns2. The flip vanishes entirely (ablated). Any variant build that drops sched2 invalidates the entry.
SECOND INSTANCE. One byte-proven instance and one real-but-untested sighting; the generality probe FAILED.
- Banked, byte-proven (n=1):
func_8018118C, ov_SC03_029,src/ov_SC03_029/ov_SC03_029_jr_8017DC70.c:3669, target.run/waveU_asm_snapshot/ov_SC03_029/func_8018118C.sidx 14-17. - Second sighting, unbanked prediction: I scanned all 14,647
asm/**/*.sfor the discriminating quadruple (store whose source register == an IN-PLACEandi's dest, thenbnez/beqz, then a second store with a different source). Exactly one hit besides the banked one:asm/ov_SC01_009/nonmatchings/ov_SC01_009_jr_8017E590/func_8017FED8.s@0x8017FF78 —sh $v0,%lo(D_801F32D8)($at) ; andi $v0,$v0,0x1 ; beqz $v0,.L8017FF90 ; [slot] sh $t3,0x0($a1). The entry predicts its source isD_801F32D8 = v0; v0 &= 1; if (v0 == 0) …, NOTif ((v0 & 1) == 0). Falsifiable when that function is cracked. (37 raw quadruples matched the surface shape; 36 were prologue$ra/$sXspills or$zerostores where the fence cannot discriminate.) - Synthetic counterexample (kills the general claim):
.run/harvest_u/av_synth.candav_synth2.c— the same shape built from scratch, two independent stores off one base pointer. Both spellings compile to identical bytes in both files. The lever is inert there.
No second banked A/B exists, and the corpus is thin enough (2 sightings in the entire unmatched tree) that this shape is rare.
§194-I — §16N+2's magic-per-odd-part ladder has exactly one broken row — read the divisor arithmetically instead: d = round(2^(32 + post_shift) / magic_read_as_unsigned)
§16N+3 — THE SIGNED DIV-BY-CONSTANT MAGIC IS NOT INVARIANT ALONG THE ×2 LADDER. READ THE DIVISOR ARITHMETICALLY: d = round(2^(32 + post_shift) / magic_read_as_unsigned). §167-25's 0x55555556 → /3 row is the CLAMPED bottom rung — it covers /3 and nothing else, and the rest of the 3-ladder lives under a different magic. (BOUNDS/CORRECTS §167-25 (L15586, the malformed "§16N+2" header) on two counts: its odd-3 table row, and its stated mechanism. Sharpens §1-I3 (L54), §28's imported table (L2349). Disjoint from §167-39 (L15971), which gives the SHAPE not the arithmetic; from §164-04 (pure powers of two); from §167-03 (the HImode sll 16 ; sra 16 wrapper, which rides on top unchanged).)
Target shape — a reciprocal multiply whose magic is not in any table you have:
lui $v1,0x2AAA ; ori $v1,$v1,0xAAAB ; the magic — 0x2AAAAAAB
mult $v0,$v1
sra $v0,$v0,31 ; the sign word — NOT the post-shift
mfhi $a1
sra $s0,$a1,11 ; <- post_shift = 11
subu $s0,$s0,$v0
THE LAW. expand_divmod's signed TRUNC_DIV arm calls choose_multiplier (abs_d, size, size-1, …) with the full divisor (expmed.c:3034) — it does not synthesise the magic for the odd part. choose_multiplier builds mlow/mhigh from lgup = ceil_log2(d) and then reduces to lowest terms with for (post_shift = lgup; post_shift > 0; post_shift--) (expmed.c:2432). Two exits: the ml_lo >= mh_lo break (the interval has collapsed — the ladder's normal, magic-invariant case), and the post_shift > 0 clamp, which stops one halving early and returns a magic exactly 2× the ladder's canonical one. At precision = size - 1 = 31 the clamp binds for exactly one divisor: d = 3 (proved: unique in 2..399; odd part 3 is the unique ladder-breaker among all odd parts 3..63). So:
signed /3 -> 0x55555556, post_shift 0 <- the clamped rung, a one-off
signed /(3 * 2^k) -> 0x2AAAAAAB, post_shift k-1 for every k >= 1
signed /(6 * 2^k) -> 0x2AAAAAAB, post_shift k (same statement, easier to use)
Do not look the magic up. Compute:
d = round(2^(32 + post_shift) / magic) # magic read as UNSIGNED
2^43 / 0x2AAAAAAB = 12288.000 → sra 11 is /12288, not /6144. This one line covers every divisor gcc gives a reciprocal multiply on the signed path, including the add-back form (mfhi ; addu $x,op0 ; sra k ; sra 31 ; subu, magic >= 2^31): 2^34 / 0x92492493 = 6.9999999971 → /7. It also covers negative divisors (the magic is for |d|; only the trailing subu operands swap) and short / K (§167-03's HImode form uses the same SImode magic inside its sll 16 ; sra 16 wrapper).
THE DIAGNOSTIC TELL. You see a magic-multiply you half-recognise and reach for a table. Don't. Read the sra that follows the mfhi (never the sra …,31 — that is the sign word), plug it and the lui/ori constant into the formula, write the divisor literally, and re-gate. A wrong power of two shows up as a single IMM-OFFSET mismatch on the post-multiply sra with the instruction count already correct — that residual class means "divisor off by 2^n", nothing else.
BOUND. The law is exactly as stated for the signed path. Five conditions where it does not apply, or applies only after an adjustment:
- FALSIFIED HALF — the unsigned
mh != 0add-correct form. The submitter claims the formula is exception-free across "all three emitted forms". There is a fourth. Whenchoose_multiplier(d, 32, 32)returns mhigh >= 2^32 (expmed.c:2878,mh != 0with d odd), gcc emitsmultu ; mfhi ; subu ; srl 1 ; addu ; srl kand the naive read is wrong by 4×: unsigned /7 emits0x24924925with a trailingsrl 2and the formula returns 28. There the true multiplier is2^32 + magicand the true post_shift isk + 1:2^35 / (2^32 + 0x24924925) = 7. Measured failures in a 137-divisor sweep: d = 7, 127, 455 — all unsigned, allmh != 0. Tell: thesubu ; srl 1 ; addutriple between themfhiand the final shift. - Unsigned with a pre-shift.
mh != 0with d even takes thed >> pre_shiftpath (expmed.c:2884-2891) and emits a baresrl jBEFORE themultu(unsigned /14 →srl 1 ; multu 0x92492493 ; mfhi ; srl 2). Multiply the recovered d by2^j. - Very large divisors. For d ≳ 2^30.4 the rounding is no longer injective: d = 1610612737 recovers as 1610612736, d = 2147483647 as 2147483646 (both off by exactly 1). Irrelevant to game code (every real BFM divisor found is < 2^15) but the formula must be treated as a seed you recompile to confirm, not an oracle.
- Powers of two never reach this path — they take §164-04's
bgez ; addiu 2^k-1 ; sra k. d = 1 and d = -1 are folded away entirely. - Read the right shift. The
sra …,31is the sign word (size - 1), not the post-shift; in the add-back form there are twosras after themfhiand the post-shift is the one on themfhi+op0sum. Getting this wrong silently feeds a 2^31-scaled divisor into the formula.
Also corrected in passing: §167-25's causal sentence ("expand_divmod factors the divisor as odd × 2^k, synthesises the magic for the ODD part only") is false for the signed arm — full abs_d goes in at :3034. The ladder is an emergent property of the halving loop. Keep the ladder as a mnemonic; do not reason from it.
SECOND INSTANCE. Byte-proven, different overlay, different function, different k: func_80187420 in src/ov_SC03_001/ov_SC03_001_jr_801870B0.c:3364 writes q = t / 24. It is BANKED — a real C definition with no INCLUDE_ASM, and asm/ov_SC03_001/nonmatchings/*/func_80187420.s has been removed from the tree (the project's matched-function invariant). /24 on the pinned triple emits 0x2AAAAAAB + sra 2; the formula gives 2^34 / 0x2AAAAAAB = 24.000 ✓, while §167-25's 0x55555556 row gives 12.
Independent target-side third instance (unbanked, so shape-only): asm/ov_SC07_002/nonmatchings/*/func_8017FCA8.s at 8017FD70-8017FDC8 — lui/ori 0x2AAAAAAB ; mult ; mfhi $t0 ; sra $a2,$t0,7 ; sra $a1,$a1,31 ; subu. Formula → 2^39 / 0x2AAAAAAB = 768 = 6·2^7 (probe of /768 reproduces 0x2AAAAAAB + sra 7 exactly; /384 gives sra 6). The ladder rule would have said 384. The same function carries two more 0x2AAAAAAB multiplies at sra 7, and the source ratios that fall out (512/768 = 2/3, 800/768 = 25/24) are the clean ones.
Further banked 3·2^k sites (each a live consumer of the corrected row): /6 src/ov_SC01_084:3788, /12 src/ov_SC03_092:4454+4523, /48 src/ov_SC03_006:8291 and src/ov_SC02_011:11943, /24 in ov_SC03_001, ov_SC04_019, ov_SC05_017, ov_SC03_002, ov_SC04_020, ov_SC03_125, ov_SC04_015, ov_SC03_124, ov_SC04_018, ov_SC05_018, ov_SC06_010, a second /12288 at src/ov_SC01_084:4057. 0x2AAAAAAB appears in 69 files under asm/; 0x55555556 in 43 (those are the genuine /3 sites and one data tail).
§194-J — Back-to-back identical stores: flow.c's last_mem_set deletes the first, and only volatile saves it
gcc-2.7.2 deletes a store only when the very NEXT memory reference in the same basic block is the identical store — volatile on either one is the exemption clause.
SYMPTOM. The target has two stores of the same width to the same address, literally back to back (sh $v0,0x12($sp) / sh $s1,0x12($sp)), and your draft is LENGTH-DRIFT -2: the first store AND its producing insn (addiu $v0,$s1,-0x22) both vanish.
MECHANISM (byte-proven, pass-dump isolated). flow.c propagate_block scans each basic block BACKWARD holding last_mem_set (flow.c:281 — "used to eliminate consecutive stores to the same location"). insn_dead_p (:1726-1727) kills a SET whose dest is a MEM iff last_mem_set is non-null AND ! MEM_VOLATILE_P (dest) AND rtx_equal_p (dest, last_mem_set). Because the scan is backward, last_mem_set is the NEXT store in program order — so the deletion window is literally back-to-back. mark_set_1 (:1961-1974) records a store only if ! side_effects_p (reg), and clears it when a reg in its address is written. It is destroyed by: any MEM used as a source (mark_used_regs case MEM, :2376-2379, clears it unconditionally — the source comment says "We could do this only if the addresses conflict, but this doesn't seem worthwhile", so a provably-disjoint load counts); a store to a different address (which replaces it); a call (:1630); and the start of every basic block (:1391). Isolated by RTL dump: cc1 -dt -df shows both stores present in .cse2 and the first replaced by NOTE_INSN_DELETED in .flow.
FIX. Spell ONE of the two stores *(volatile T *)&lvalue = v;. Either end works: on the first it trips ! MEM_VOLATILE_P in insn_dead_p; on the second it trips ! side_effects_p in mark_set_1 so nothing is ever recorded.
DO NOT reach for these instead — all four are measured NO-OPs, because rtx_equal_p runs on RTL after cse/copy-prop has already collapsed the C-level difference: a second aliasing pointer variable (short *q = p; p[1]=a; q[1]=b;), a different spelling of the same address (*(p+1)), a variable index (p[i]=a; p[i]=b;), or taking &buf for one of them. Statement order is a no-op too — moving a real foreign store between them saves the store but costs you a SCHEDULE-REORDER/2 (measured on the exemplar).
WHY IT LOOKS LIKE A FRAME BUG BUT ISN'T. flow.c:1969-1973 refuses to record a store whose address mentions stack_pointer_rtx, which reads as protecting stack slots — it does not. instantiate_virtual_regs (toplev.c:2811) runs before flow (toplev.c:2983) and maps virtual_stack_vars_rtx -> frame_pointer_rtx (function.c:2675/2726/2745), so a C local's slot is (plus (reg:SI 30 $fp) K) at flow time, not $sp; the $fp->$sp elimination is a reload-time rewrite. Frame slots are deleted like any other MEM (measured).
NOT §21 L1920 (that is cse constant store-FORWARDING of a known literal into a global, restoring a folded lhu reload), NOT §21 L1933 (a load-side reload across calls), NOT §27 L2315 (aggregates, which flow does not touch). This is the store-side liveness pass; §164-78's "redundant write deleted, attributed to cse/§48-B, flagged as inference" (L13538) now has a proven home.
BOUND. Measured, not asserted. The law does NOT apply — the first store SURVIVES untouched — when any of these hold:
- ANY memory reference falls between them, however harmless: an unrelated load (
b += q[3]), a store to a different address (p[0]=0), or a call (ext();). Verified g2/g3/h1/h5. A load from a provably-disjoint constant offset of the SAME base also counts (h7) — flow.c does zero conflict analysis here, by explicit comment. - The intervening load must survive CSE to flow time. THIS IS THE TRAP, and it is not in the candidate.
short buf[4]; buf[3]=1; buf[1]=a; b+=buf[3]; buf[1]=b;— the "intervening load" is a value cse knows, so cse folds it toaddu $5,$5,1and no MEM survives to reachmark_used_regs. Result: ONEsh, byte-identical to the no-load control (k1 vs k2). Corollary: "insert a load between them" is NOT a reliable C lever; onlyvolatileis. - The two stores are in DIFFERENT basic blocks.
last_mem_setis reset per block (flow.c:1391), so anif-guarded second store (n1) or a store after a 2-predecessor label (n2) both keep the pair. This bounds the law tightly: it is a straight-line-code rule only. - Either store is
volatile(the lever itself), by either predicate. -O0.stupid_life_analysis(toplev.c:2974) runs instead offlow_analysisand does no DSE — both stores emitted. Relevant to the project's known -O0 regions (boot,ov_SC01_077_o0*, the whale_o0b, 0x8013B568..0x8013C98C). Fires at -O1 and -O2 alike, so it is not an -O2-only rule.
Where it DOES apply, more widely than claimed: all three widths (sb/sh/sw — h8/h9), pointer-based and frame-slot destinations alike (h4), and it CASCADES — three identical stores in a row leave only the last (h2, p[1]=a; p[1]=b; p[1]=c; -> one sh $7).
Frequency bound (so nobody over-invests): 31 adjacent same-width/same-offset/same-base store pairs across all 14,647 files in asm/. This is a rare shape, worth ~0.2% of the remaining frontier — but it is a total blocker at each of those sites, and it is a one-token fix.
SECOND INSTANCE. func_8018392C (ov_SC01_084) — a NEW crack, byte-proven, produced by the law alone. It is still INCLUDE_ASM in the tree (src/ov_SC01_084/ov_SC01_084_jr_8017CA80.c) and carries the same fingerprint at asm/ov_SC01_084/nonmatchings/ov_SC01_084_jr_8017CA80/func_8018392C.s:
sh $v0, 0x12($sp)
sh $s1, 0x12($sp)
I derived it from the exemplar by constant-substitution only (+0x400 not +0x200; s2 = 0x180 literal instead of the D_801C774A load; D_8018ABB0; arg 6; 0x28) and ran both arms myself:
.run/harvest_u/vfy_392C_vol.c -> MATCH (84 ins)
.run/harvest_u/vfy_392C_novol.c -> DIFF 82 vs 84, 45 mismatched, LENGTH-DRIFT -2
Same -2 signature as the exemplar: addiu $v0,$s1,-0x22 and its sh both die together. Two independent functions, same lever, same failure mode without it.
THIRD SITE, same shape, NOT cracked here (different body, would need a full draft): func_80183BD0 (ov_SC01_084), sh $v0,0x12($sp) ; sh $s1,0x12($sp) at 0x80183CA4.
POPULATION beyond the family — I scanned all 14,647 files in asm/ for adjacent stores with identical width, offset and base register and found 31 sites across ~20 functions in 15 different binaries, in shapes structurally unlike the card family, e.g.:
md_MAIN_027 func_800CB4A4 sw $v0,0x10($s1) ; sw $zero,0x10($s1) (and again at 0x14, 0x18)
md_MAIN_046 func_800CD288 sw $v0,0x234($s0); sw $zero,0x234($s0) (and again at 0x238, 0x23C)
ov_SC06_032 func_8018750C sb $v0,-0x2($a2) ; sb $t0,-0x2($a2) (byte width)
ov_SC03_121 func_80180E64 sw $v0,0x1C($s0) ; sw $a0,0x1C($s0) (×4 sibling fns)
ov_SC05_008 func_80181B84 sh $v0,0x6($s0) ; sh $v1,0x6($s0)
NEGATIVE CONTROL, in the same TU: func_80183790 has two sh 0x12($sp) (snapshot lines 46 and 58) with lw/lhu between them — banked and MATCHing with NO volatile. The law predicts exactly that, and the tree agrees.
§194-K — Blind sched1's alias oracle with a second SET of a pointer pseudo — the first zero-byte, non-volatile, dependence-CREATING lever (corrects §167-05's "volatile is the only door"; fourth consumer of reg_n_sets)
§NEW — BLIND sched1's ALIAS ORACLE WITH A SECOND SET OF A POINTER PSEUDO: the file's first ZERO-BYTE, NON-VOLATILE, EDGE-CREATING lever, and a FOURTH consumer of the reg_n_sets knob.
(CORRECTS §167-05 (L15119), which states that on §16Z rows 1-3 "no /s grant, no statement order, no register pin and no scope/reuse edit can create the edge" and that the both-volatile conjunction is "the only remaining door" — false on row 1 whenever one side's address is a pseudo; this closes the hole §167-05's own HONEST SCOPE flags as costing an address materialisation, at zero cost. Distinct from the three banked consumers of the same one-liner: §30#3/L2390 sched1 birthing boost = PRIORITY; §52b RC-7/L4007 update_equiv_regs = REMAT; §164-02/L11899 cse qty_const = OPERAND ORDER. Evidence: two instances, both ov_SC02_028.)
Target shape — a leaf store through a pointer-to-global, followed by loads of different globals; the target keeps the store first, every ordinary draft hoists the loads:
TARGET MINE (no lever)
sb $v0,0x0($a1) <- stays first lbu $v0,%lo(D_801D3101)($v0) <- hoisted
andi $v0,$v0,0xFF lbu $a0,%lo(D_801D3102)($a0)
lbu $v1,%lo(D_801D3101)($v1) sb $v1,0x0($a1)
lbu $a0,%lo(D_801D3102)($a0) andi $v1,$v1,0xFF
THE LAW. init_alias_analysis (tools/reference/gcc-2.7.2/sched.c:399-438) fills reg_known_value[r] only for a pseudo whose single_set carries a REG_EQUAL note and has reg_n_sets[REGNO]==1 (the gate at :426), or a REG_EQUIV note. REG_EQUIVs are minted by update_equiv_regs inside local_alloc, which runs AFTER sched1 (toplev.c:3028/3033 schedule_insns vs :3049/3052 local_alloc), so at sched1 the reg_n_sets==1 arm is the only live one. p = &SYM; compiles to (set (reg p) (symbol_ref SYM)) carrying a REG_EQUAL note of that symbol — so with one set, canon_rtx (:371) rewrites the store's (mem (reg p)) into (mem (symbol_ref SYM)), memrefs_conflict_p (:614) reaches its CONSTANT_P arm (:775-778), proves it disjoint from the other symbols, anti_dependence (:844-864) drops the edge, and the higher-priority load chains hoist over the store. Give p a SECOND set and the gate fails: reg_known_value[p] falls to the :437-438 fallback (reg p), canon_rtx is a no-op, memrefs_conflict_p has no arm that relates an opaque pseudo to a symbol and walks to its terminal return 1, the anti-dependence appears, and the store is pinned first.
THE LEVER — zero bytes, non-volatile, generic constraints (no $N pin, sweep-safe):
p = &SYM;
__asm__("" : "=r"(p) : "0"(p)); /* 2nd SET of p; emits nothing */
BYTE EVIDENCE — instance 1, func_80182060 (ov_SC02_028, 77 ins), and instance 2, func_80182688 (same overlay, 67 ins), independently drafted. All A/Bs on the pinned triple via tools/match_one.py against .run/waveU_asm_snapshot/ov_SC02_028, one file varying by one line:
| variant | func_80182060 |
func_80182688 |
|---|---|---|
| with the re-tie | MATCH 77 | 67/67, 6 mismatched, REGALLOC-PERM (order group fully correct) |
| re-tie deleted | 77/77, 10, OPCODE-MIXED | 67/67, 13, OPCODE-MIXED |
p = &SYM; written twice in C |
77/77, 10 — identical (cse deletes it) | — |
__asm__ __volatile__("" ::: "memory") at the same point |
77/77, 10 — identical | — |
re-tie WITH a "memory" clobber |
MATCH 77 (clobber byte-neutral) | — |
re-tie on a DIFFERENT live local (s0) |
77/77, 10 — identical | — |
__asm__ __volatile__("" : : "r"(p)) (real barrier) |
77/77, 10 — identical | — |
RTL PROOF, not inference (cc1 -da, dumps in .run/harvest_u/vfy/d_{retie,noretie}/x.i.{combine,sched}). The .combine dump (input to sched1) shows (insn 108 (set (reg/v:SI 84) (symbol_ref "D_801D3100"))) (expr_list:REG_EQUAL (symbol_ref "D_801D3100")) — one set + REG_EQUAL, i.e. the :426 arm live — and with the lever a second (insn 110 (set (reg/v:SI 84) (asm_operands …))). The .sched dump is decisive: without the lever the two loads sit above the store and carry LOG_LINKS (nil); with it the store (insn 127) precedes them and both loads carry (insn_list 127 (nil)). The edge is created, at sched1, at the alias oracle.
WHY IT IS NOT "asm opacity". sched.c:1957 — if (code != ASM_OPERANDS || MEM_VOLATILE_P (x)) — a NON-volatile ASM_OPERANDS does not flush_pending_lists and is not a barrier; only volatile asm / ASM_INPUT / UNSPEC_VOLATILE / TRAP_IF are. Byte-confirmed twice above (a real volatile fence at the same point buys nothing; the same re-tie on a different live local buys nothing).
THE DIAGNOSTIC TELL — state it structurally, NOT by residual class. A load GROUP and a store GROUP are swapped as blocks, the store's base is a la-materialised symbol held in a register, and the loads are of other symbols. residual_class files this OPCODE-MIXED [structural] (both instances), not REGALLOC-PERM — the register swap that rides along is a symptom of the order swap. Before filing "structural / rewrite the draft", check whether the store's address is a pointer local: if it is, this is one line.
⚠ NOT A CLEAN SINGLE-CONSUMER DIAL. reg_n_sets is also read by update_equiv_regs (local-alloc.c:1021, from local_alloc at :408), so the SAME edit also disables address rematerialization. Invisible in both instances above (77==77, 67==67), but in a same-base control the identical one-liner changed the instruction COUNT by -6. A length drift after applying this lever is expected, not a broken draft.
⚠ NO-OP when store and loads share the SAME base pseudo at disjoint constant offsets — memrefs_conflict_p disambiguates (reg r) vs (plus (reg r) K) on byte ranges regardless of opacity. Verified in the sched1 dump: the loads still hoist and still carry (nil). That row remains §167-05's volatile-pair territory.
⚠ Order only. On instance 2 the colouring did NOT follow for free — a REGALLOC-PERM residual survived the fix.
⚠ Dead-asm elision. A re-tie whose output is immediately overwritten is DELETED by gcc and your "control" then contains no asm at all. Always grep -c '#APP' the .s before trusting an asm-based control (baseline for this project is 1, from common.h's .include).
BOUND. DOES NOT APPLY / DOES NOT FIRE:
-
Needs a pseudo-held address on exactly one side. The gate is
reg_known_value[r], filled only from asingle_setcarrying REG_EQUAL withreg_n_sets[r]==1(sched.c:424-426) or a REG_EQUIV. So the pointer must survive to sched1 as a pseudo initialized once from&SYM. If cse already substituted the symbol into the MEM (no pseudo base), or if the pointer is a parameter / call return / arithmetic result with no REG_EQUAL note,reg_known_valueis already(reg r)(the :437-438 fallback) and there is nothing to blind — the edge is present anyway. -
NO-OP when both MEMs share the same base pseudo. I ran the submitter's own structural falsifier (rewrote the loads as
p[1]/p[2]so store and loads share base p). Prediction held, verified in the sched1 dump: with the re-tie the loads (insns 130/133) STILL hoist above the store (insn 127) and STILL carry LOG_LINKS(nil)—memrefs_conflict_pdisambiguates(reg 84)vs(plus (reg 84) 1)on disjoint byte ranges and the re-tie buys nothing. Do not reach for this on §16Z row 3; that row is still §167-05's volatile-pair territory. (Caveat on reading that experiment: the final .s DID change order there, but for an unrelated downstream reason — see bound 3 — so judge this bound from the sched1 dump, not the emitted asm.) -
NOT a single-consumer lever — it is coupled to local-alloc, and it is not always zero bytes.
reg_n_setsis also read byupdate_equiv_regs(local-alloc.c:1021, called from local_alloc at :408). The same one-liner therefore also disables REG_EQUIV address rematerialization. In func_80182060 and func_80182688 that is invisible (77==77, 67==67 ins), but in my same-base variant the identical edit swung the instruction COUNT by SIX (remat of the address at each use disappeared, LENGTH-DRIFT/-6). A length change after applying this lever is expected behaviour, not a broken draft. The submission's "ruled out (b) — update_equiv_regs is unrelated" is right that local-alloc cannot have produced the ORDER, but wrong to present the two consumers as independent: one edit moves both. -
The asm must be NON-volatile and must SET the pointer. A volatile asm is a full
flush_pending_listsbarrier (sched.c:1957-1969) — a far bigger hammer with its own side effects, and empirically it does not buy this order (my__asm__ __volatile__("" : : "r"(p))control: same 10 mismatches). A re-tie of any OTHER variable at the same point does nothing (control on lives0: same 10 mismatches). And beware dead-asm elision: a re-tie whose output is immediately overwritten is DELETED by gcc — checkgrep -c '#APP'in the .s before trusting such a control (this is how the submitter's control 5 was void). -
Not spellable in plain C.
p = &SYM; p = &SYM;is byte-identical to the single assignment — cse deletes the redundant set before flow recountsreg_n_sets. Confirmed. -
Order only; colouring is a separate fight. On the second instance the lever fixed the whole order group and left a REGALLOC-PERM base-register residual. Expect to still owe a colouring lever afterwards.
-
Direction. This creates an anti-dependence pinning the STORE ahead of later loads because the store is written first in source. It is
sched.c:1735-1759walkingpending_read_insns(§165-44's predicate); the edge's direction is whatever you wrote (§164-10). It cannot pull a store later.
SECOND INSTANCE. FOUND — func_80182688 (ov_SC02_028, 67 ins), the mirror decrement routine to func_80182060, drafted independently by me from its snapshot .s (.run/harvest_u/b88_retie.c / b88_noretie.c, differing by exactly the one re-tie line).
Discovery was mechanical, not cherry-picked: I scanned every .s under .run/waveU_asm_snapshot/ for the shape "a store to 0x0($reg) followed within 6 instructions by a %lo(D_...) load". Exactly two targets matched in the whole snapshot set — func_80182060 and func_80182688.
Result:
- without the re-tie — 67/67 ins, 13 mismatched, class OPCODE-MIXED/addressing,width. The residual is the tell verbatim: mine emits
lbu D_801D3101 ; lbu D_801D3102 ; addiu ; sb 0($a1), target emitsaddiu $v0,$v1,-0x10 ; sb $v0,0x0($a0) ; lbu %lo(D_801D3101) ; lbu %lo(D_801D3102). - with the re-tie — 67/67 ins, 6 mismatched, class REGALLOC-PERM/$v1>$a0>$v1. The entire load/store order group snaps to target order; the only survivors are the base register's colour ($a0 vs $v1) and the three instructions that read it.
- Zero bytes: 67 == 67 in both builds.
Two things this second instance settles that one instance could not:
- The lever is not an artifact of
func_80182060's particular block — it reproduces on a differently-shaped function (different control flow, a call in the else arm, decrement instead of increment, a second callee-saved register live) with a draft the submitter never saw. - It falsifies the submission's "the register colouring then follows the order for free" — here the colouring did not follow, and the function still owes a separate colouring lever. It also falsifies the triage claim: the PRE-fix residual is OPCODE-MIXED [structural] in both instances, never REGALLOC-PERM; REGALLOC-PERM is what the fix leaves behind.
Weakening note, stated honestly: both instances live in the same overlay and touch the same three globals (D_801D3100/1/2), so they are siblings rather than fully independent sightings. The mechanism half is nevertheless proven independently of both, from the cc1 -da RTL dumps and the gcc source gate.
§194-L — §88b and §189-E are BOTH half-wrong, but not the way the candidate says: the compare-constant shape is a 2-D lookup (cmp_info ROW × constant-in-window), and naming matters on OPPOSITE sides of the window for the GE/LT rows vs the GT/LE rows
THE COMPARE-CONSTANT SHAPE IS A 2-D LOOKUP: cmp_info ROW × IN-WINDOW, and naming matters on OPPOSITE sides of the window for GE/LT vs GT/LE
(SUPERSEDES the inference half of §88b (L6686) and the causal half of §189-E (L18247), which are each right on exactly one row-family and backwards on the other; sharpens §48-C4 (L3502), which had the force_reg but no window. §88c's ==/!= exclusion is untouched and still correct.)
Three passes, not one. (1) fold-const.c:4417-4435 rewrites X >= CST → X > CST-1 and X < CST → X <= CST-1, gated on TREE_CODE (arg1) == INTEGER_CST && tree_int_cst_sgn (arg1) > 0 — so it fires for a POSITIVE BARE LITERAL only: never for a named local (a VAR_DECL), never for a zero or negative literal. (2) gen_int_relational (config/mips/mips.c:1734) looks the test up in the cmp_info table (:1755-1764); if cmp1 is a CONST_INT inside [const_low, const_high] it stays immediate and const_add is added back (:1842-1861), otherwise force_reg materialises it (:1824) and reverse_regs may swap the operands (:1863-1868); the branch sense comes from invert_const vs invert_reg (:1826). (3) A later RTL constant-propagation re-folds a materialised constant back into an slti/sltiu immediate — but only if it landed on slt's RIGHT operand and fits the signed-16 field. Pass (3) is what makes naming invisible, and it is the half neither §88b nor §189-E saw.
Two row-families, opposite behaviour.
| row | table entry | bare literal | named local |
|---|---|---|---|
GE, LT, GEU, LTU (const_add 0, reverse_regs 0) |
GE {LT,-32768,32767,0,0,1,1,0} |
in [-32768,32767]: slti x,CST verbatim. CST > 32767: fold fires → out of window → li CST-1 ; slt K,x swapped, branch inverted. CST < -32768: fold does NOT fire → li/ori CST ; slt x,K natural |
always the reg path; pass (3) re-folds → byte-identical to the bare literal for every CST in [-32768,32767] and for every CST < -32768. Diverges ONLY for CST > 32767, where it gives li CST ; slt x,K natural + uninverted branch |
GT, LE, GTU, LEU (const_add +1, reverse_regs 1) |
GT {LT,-32769,32766,1,1,1,0,0} |
in [-32769,32766]: slti x,CST+1 (a third shape §88b/§189-E never mention), branch inverted. Outside: li CST ; slt K,x swapped |
reg path → reverse_regs puts the constant on slt's LEFT, so pass (3) can never fold it: li CST ; slt K,x verbatim + uninverted branch at every -O. Diverges from the literal for every CST INSIDE the window; converges outside it |
THE READING RULE (what to do with a target). li K ; slt K,x — constant materialised on the LEFT of slt — does not decide naming on its own. Read the branch sense and arithmetic first:
li CST-1 ; slt CST-1,x ; b<inverted>on a GE/LT-shaped test ⇒ a bare positive literal>= CST/< CSTwith CST > 32767. (§88b says "not a literal" here and is WRONG.)li CST ; slt CST,x ; b<natural>where CST ≤ 32766 ⇒ a> CST/<= CSTwritten with a NAMED local. (§88b is right here; §189-E's "bare literal → li CST-1, swapped" is WRONG here.)li CST ; slt x,CST ; b<natural>, constant on the RIGHT, CST > 32767 ⇒ a named local on a>= CST/< CST.slti x,CSTverbatim ⇒>= CST/< CST, and the spelling is UNDECIDABLE — literal and named local are byte-identical. Write the literal.slti x,CST+1⇒> CST/<= CSTas a bare literal.
THE WRITING RULE. On >= / < with a limit in [-32768, 32767], never invent a named local to chase a shape — there is no shape to chase. On > / <= with an in-window limit, the named local is the ONLY way to get the swapped li K ; slt K,x, and it survives every optimisation level. On >= / < with a limit above 32767, the naming choice is real and visible.
BOUND. 1. BRANCH CONTEXT ONLY, for the GE/LT family. Everything above is measured with the compare feeding a conditional branch (p_invert != 0, mips.c:1830-1836). In VALUE context (return G >= 57600;) the GE divergence vanishes: .run/harvest_u/adv2/val.c v5 (bare) and v6 (int k=57600) both emit li $2,0xe0ff(57599) ; slt $2,$2,$3 — byte-identical. The GT/LE divergence survives value context (v3 slt $2,$2,31 ; xori 1 vs v4 li 30 ; slt $2,$2,$3). So §189-E's whole exhibit is a branch-context artefact and must not be applied to a compare feeding an assignment or a return.
2. ORDERED COMPARISONS ONLY — §88c's bound stands. EQ/NE take rows {XOR, 0, 65535, 0, 0, …}; MIPS has no beqi, so equality constants are ALWAYS materialised regardless of spelling, and none of the above applies.
3. DEGENERATE CONSTANTS ARE NAMING-INVISIBLE ON BOTH FAMILIES. Where the test collapses to a compare-against-zero branch (bltz/bgez/blez/bgtz) — signed CST ∈ {-1, 0, 1} depending on the row, unsigned CST ∈ {0, 1} — bare and named produce identical bytes even on GT/LE. Probed: s_gt_0, s_gt_m1, s_lt_0, s_lt_1, s_le_0, s_le_m1, u_gtu_0, u_ltu_1, u_leu_0 all bare==named. Don't read a naming signal out of a zero-branch.
4. u >= 0u is folded away entirely (u_geu_0_* emit no compare at all), and u < 0u emits nothing — no row is consulted.
5. The window is on the POST-FOLD constant, not the source constant. For a bare >= CST the value tested against the GT row is CST-1, so the observable threshold is CST > 32767, not CST > 32766. Off-by-one here mis-predicts exactly at CST = 32767.
6. A named local's own placement is a separate axis. §76/§88b's live-range point still holds: if the local's range spans a call it takes a callee-saved register and the li hoists — that changes WHERE the li sits, orthogonally to the shape rule above.
7. Only -O1/-O2 verified. At -O0 pass (3) does not run and every named local stays a register on both families; the law describes optimised code only.
8. Not tested: long long (the TARGET_64BIT clause at mips.c:1815-1823 is dead for us), float compares, and constants above 65535 on the unsigned rows beyond 65536.
SECOND INSTANCE. Two byte-matched banked functions, different overlays, exercising OPPOSITE halves — this is not one function.
Instance A — GE, in-window, BARE literal → slti verbatim (falsifies §189-E). func_8017FD30, src/ov_SC02_028/ov_SC02_028_jr_8017D898.c:3830 writes bare if (D_801D30C4 >= 30). Target .run/waveU_asm_snapshot/ov_SC02_028/func_8017FD30.s:87-88: slti $v0, $v0, 0x1E ; bnez $v0, .L8017FE84. No named local anywhere in the function. §189-E as written would have pushed a drafter to invent one and produce li 29 ; slt.
Instance B — the SAME FUNCTION carries both halves, and it kills §88b in the tree. func_8017C338, src/ov_SC01_080/ov_SC01_080_jr_8017AE2C.c:3364, banked and matched (no nonmatchings/func_8017C338.s remains). Source line, both operands bare literals:
if (rx >= -0x7fff) { i2o = 0x7fff; if (rx < 0x8000) i2o = rx; }
Disassembly of the built object build/src/ov_SC01_080/ov_SC01_080_jr_8017AE2C.o at func_8017C338+0x100:
1610: 28c28001 slti v0,a2,-32767 <- >= -0x7fff: NEGATIVE literal, fold's sgn>0 gate never fires, -32767 in the GE window -> immediate, VERBATIM, invert_const=1 -> bnez
1614: 14400006 bnez v0,1630
1618: 24027fff li v0,32767 <- < 0x8000: POSITIVE literal, fold -> <= 32767, LE window [-32769,32766] -> OUT -> force_reg
161c: 0046102a slt v0,v0,a2 <- reverse_regs=1 -> constant on the LEFT
1620: 14400004 bnez v0,1634 <- invert_reg=1
That li 32767 ; slt 32767,rx is exactly §88b's "the limit is in a REGISTER on the LEFT of slt ⇒ K was NOT a literal in the source" signature — produced from a bare 0x8000 literal, in a byte-matched function, four instructions after a bare literal that took the immediate path. §88b's inference is refuted inside the project's own banked tree, not just in a synthetic probe. The same construct is replicated and banked across ~15 overlays (ov_SC02_000, ov_SC02_003/004/005, ov_SC03_003/007/012/023/028/126, ov_SC04_021, ov_SC05_019, ov_SC07_010, …), so it is a load-bearing family shape, not a curiosity.
Both instances also confirm the candidate's §194 mechanism half while refuting its scoping clause, since neither involves a named local at all.
§194-M — A STORE in a CONDITIONAL branch's delay slot proves its C statement DOMINATES the branch — reorg can never pull a store out of either thread (gcc-2.7.2, -mips1)
A STORE SITTING IN A CONDITIONAL BRANCH'S DELAY SLOT IS A DOMINANCE FACT ABOUT THE SOURCE: reorg CAN NEVER PULL A STORE OUT OF EITHER THREAD, SO THE C STATEMENT IS EXECUTED ON BOTH PATHS — WRITE IT ABOVE THE if, NOT INSIDE THE ARM
(the single-arm, reorg-side twin of §193-C / L18493-18510, which states the same tell and the same prescription for the TWO-ARM head-duplication case under a different mechanism — jump.c has no prefix merge. §193-C explains why a head-dup is not refunded; this explains why the store cannot reach the slot at all. Read both; they compose. Distinct from §L3/L3339 (a store merged into a shared TAIL), from L12282 (a cse'd reg-reg COPY in a beqz slot — a copy, not a store), from L1873 (a store in a jal slot, unconditional by construction), and from §5a/§34/§164-36 (BLOCKING a slot steal with an asm — the opposite direction).)
THE LAW (read out of the pinned gcc, and it is a proof, not a heuristic). In fill_slots_from_thread (reorg.c:3268), the only gate that admits an insn from a thread into a conditional branch's slot is reorg.c:3373-3376:
if (condition == const_true_rtx
|| (! insn_sets_resource_p (trial, &opposite_needed, 1) && ! may_trap_p (pat)))
opposite_needed is produced by exactly one call, mark_target_live_regs (opposite_thread, &opposite_needed) (:3293), and that function begins by hardcoding res->memory = 1; — "We have to assume memory is needed" (:2462-2463). (When the opposite thread is the function end it takes end_of_function_needs, whose .memory is also 1, :4256.) mark_set_resources sets res->memory = 1 for any MEM in a destination (:619-624), and resource_conflicts_p returns 1 on (res1->memory && res2->memory) (:713-716). So insn_sets_resource_p is unconditionally true for every store and the gate fails on its FIRST conjunct — may_trap_p is never consulted, and the address does not matter (a store to a fixed global is refused exactly like a store through a pointer). The same gate guards the two steal routes (:1658, :1747), and the forward scan in fill_simple_delay_slots is fenced by target == 0 (:3054), false for any JUMP_INSN. The annul escape at :3388-3410 is live at compile time for the false-thread (mips.md's define_delay for type "branch", mips.md:125-128, has a non-nil annul-false element, so genattrtab does define ANNUL_IFFALSE_SLOTS — the #ifndef stubs at reorg.c:137-143 apply only to eligible_for_annul_true), but it is dead at run time: eligible_for_annul_false requires the branch_likely attribute, a (const) attribute equal to "no" whenever mips_isa < 2 (mips.md:102-107), and our -mips1/r3000 build never emits a *l form.
Therefore the ONLY way a store reaches a conditional branch's slot is the backward scan of fill_simple_delay_slots (reorg.c:2798), which takes insns that already DOMINATE the branch.
THE READING RULE (asm → source, one-way). See a store in a conditional branch's delay slot ⇒ its C statement is executed on both paths ⇒ write it above the if. 2,482 such sites exist across asm/**/*.s.
THE TRIAGE NOTE (why it costs attempts). The residual is misclassified. Same instruction count both ways (dbr refills the freed slot from the arm), and match_one reports ADDRESSING [structural] sig=ADDRESSING/move!=sw profile=cse — a statement-dominance defect that reads as a cse/addressing defect. When you see move != sw at a slot index with equal lengths, relocate the statement before you touch registers, casts, or barriers.
THE ANTI-LEVERS (do not spend attempts here). A goto label, a :::"memory" barrier, and register pins all fail — none of them makes a store eligible, because the refusal is opposite_needed.memory, not a register or an ordering fact.
BOUND. Where the law does NOT apply, or applies only one-way:
-
CONDITIONAL branches only. For an unconditional branch
condition == const_true_rtx,opposite_neededisCLEAR_RESOURCEd (reorg.c:3289-3290) and the gate short-circuits — a store CAN be pulled into aj's slot from the thread. Call (jal) slots are likewise outside it (ajalslot store is unconditional by construction — §L1873). Never read aj/jalslot as a dominance fact. -
STORES only — do NOT generalize to "anything in a conditional slot dominates the branch". Non-store insns are freely stolen out of the arm: byte-shown twice here,
addu $a0,$s2,$zerotaking the freed slot infunc_80181EDC, and L12282's cse'd reg-reg copy filling abeqzslot. Loads are also outside it (a load sets no memory resource; onlyunch_memory/register conflicts apply). -
One-way only. Dominance ⇏ slot. Writing the store above the
ifdoes not force it into the slot — the backward scan still needs no resource conflict with the branch's own operands and the store must be the last eligible candidate. Byte-shown: variantB_belowonfunc_80182744puts the store after the wholeif, and the slot degrades to a realnopat the same 69-instruction length. So the law reads asm→source as a proof, and source→asm only as a necessary condition. -
"Above the
if" is loose; the invariant is DOMINANCE. Admissible spellings include the condition's own side-effecting subexpression,if ((p->f = g()) != 0) {…}, and any position in an enclosing block that both paths pass through. Do not force the statement to be lexically adjacent to theif. -
Target-scoped to -mips1/-mcpu=3000. At
mips_isa >= 2thebranch_likelyattribute flips to "yes",eligible_for_annul_falsestarts succeeding, and a store from the TRUE thread can legitimately occupy an annulled (*l) slot. The law degrades to a heuristic on any ISA-2+ build. (Falsifier check for a future session: grep the target for anybeql/bnel/blezl/bgtzl— the tree has none.) -
It is not a length law and the cost direction is unbounded. Both exemplars happened to land at ±0/+1, but the relocation changes liveness: on
func_80182744the in-arm spelling additionally let cse constant-fold the stored value (sw $zeroinstead ofsw $a0), and §193-C records hoist-vs-dup residuals of 0, +1 and −1 with frame-size and.maskchanges. Read the delay slots and the frame, never the count. -
Not verified: that no earlier pass can lift a store out of an arm to above the branch. I checked the plausible ones by argument, not by probe — 2.7.2 has no gcse and no if-conversion, sched1/sched2 are per-basic-block, combine never spans blocks (cookbook L2489), and loop.c does not hoist memory writes. A
volatilestore, aswitchdispatch, and more than two arms are all untested here.
SECOND INSTANCE. func_80182744 (ov_SC03_029, 69 ins, banked MATCH) — different overlay, different function, opposite branch polarity (bnez not beqz), and the guarded arm is an early-return arm rather than a body block. Banked C at /home/musashi/bfm-decomp/src/ov_SC03_029/ov_SC03_029_jr_8017DC70.c:4117; target at /home/musashi/bfm-decomp/.run/waveU_asm_snapshot/ov_SC03_029/func_80182744.s, which shows
/* 8018275C */ bnez $a0, .L80182774
/* 80182760 */ sw $a0, 0x20($s0)
against the banked source shape ret = func_8012C1B8(); *(s32*)(a0+0x20) = ret; if (ret == 0) { func_8012CAE4(a0); return; } — the store written above the if, sitting in the guarding branch's slot. I extracted it standalone to /home/musashi/bfm-decomp/.run/harvest_u/v_2744_base.c and re-verified MATCH (69 ins), then ran three relocations I wrote myself (the submitter tested only one direction, on one function):
| variant | spelling | result | the slot |
|---|---|---|---|
v_2744_base.c |
store above the if |
MATCH 69 | sw $a0,0x20($s0) |
v_2744_A_inarm.c |
store moved INSIDE the ret==0 arm |
DIFF, 70 ins (+1), 60 mismatched, LENGTH-DRIFT |
nop; store reappears as sw $zero,0x20($s0) after the branch (cse folded the value, since ret==0 in that arm) |
v_2744_B_below.c |
store moved BELOW the whole if (fall-through thread) |
DIFF, 69 ins, 3 mismatched, DELAY-SLOT |
nop; store reappears at idx 13 |
v_2744_D_dup.c |
store duplicated into BOTH paths (semantics-preserving) | DIFF, 70 ins (+1), 60 mismatched | nop; both copies live (sw $zero in the arm, sw $a0 at idx 14) |
B_below is the direction the exemplar never tested and it is the cleanest single result in the set: the store is in the fall-through thread, not the taken one, and reorg still refuses it — which is what opposite_needed.memory == 1 predicts and what a "speculation" story would not. D_dup is the composition probe: even with the store present on both paths it does not come back into the slot (reorg refuses each copy) and it is not merged (§193-C, no prefix merge) — the two laws stack to +1 rather than cancelling.
Population: .run/harvest_u/scan_slots.py over all 14,899 asm/**/*.s → 2,482 conditional-branch-slot store sites.
Addendum (P31 S63 t5b-t5d, func_80182420): THIRD INSTANCE, and a new residual face for the triage note — this class also presents as plain LENGTH-DRIFT +2, not only as the equal-length ADDRESSING/move!=sw profile=cse of the triage note or the equal-length DELAY-SLOT of bound 3's B_below. func_80182420 (ov_SC04_011, 132 ins, banked src/ov_SC04_011/ov_SC04_011_jr_8017D494.c:5618): a u16 counter RMW (cnt = a0->unkF2 + 1; a0->unkF2 = cnt;) whose sh $v1,0xF2($s0) sits in the slot of the neighbouring clamp branch bgtz $v0,.L80182530 (lim = a0->unkF6 - 1; if (lim <= 0) lim = 1;). Written BELOW the clamp if — bound 3's B_below direction — the draft is LENGTH-DRIFT/2, close 62/58, 134 vs 132 ins, delta at idx 15; hoisting the two RMW statements ABOVE the if, the only edit made, gives MATCH 132/132, close 0 on the very next match_one. So a length drift does not exonerate statement placement: bound 6 already says the cost direction is unbounded, and this is its first measured +2 and the first time match_one stamps the class with a count class instead of a schedule/addressing one — exactly the misdirection the triage note exists to head off.
Two claims from this card NOT banked. (a) Its reading that the statement must sit immediately before the branch in the C — bound 4 already states the invariant is dominance, not lexical adjacency, and nothing here re-opens that. (b) Its proposed tell that "the same sll $v0,$a0,16 appearing TWICE, straddling the branch, is the fingerprint of an unfilled slot patched by rematerialization" — the card traced no RTL for it (self-flagged as unconfirmed), and both byte-read duplicated-shift sightings in this file read a duplicated sll the OTHER way, as a slot that got filled: the cross-BB combine law (L2489, fill_slots_from_thread steals the top-BB sll into the bnez slot and redirects the label) and the §74 counter/clamp addendum (L27920, dbr duplicates an idempotent promotion head into a mutually-exclusive arm's slot). Read the slot itself, per §194-M's own reading rule; do not read the duplicate.
§194-N — §193-D's C dial is misstated: the lever is a SURVIVING CODE_LABEL (a label with a real incoming edge), not "a label between the block and the call" — a bare label, or a goto L; L: pair whose target is the next active insn, is deleted by jump1 (jump.c:663-669 → delete_insn → jump.c:3458-3461, and jump.c:243 for the bare case) long before sched1/local-alloc, and costs exactly zero bytes
§193-D CORRECTION (replaces the "THE C DIAL" paragraph at L18546 and precondition 1 at L18549).
THE C DIAL IS AN INCOMING CONTROL-FLOW EDGE, NOT A LABEL. What disqualifies optimize_reg_copy_1 is a CODE_LABEL that is still alive when sched1 runs — i.e. a block the call is entered into from somewhere else. A C label: is not that. jump_optimize runs at toplev.c:2827, before cse1 (:2865) and far before sched1/local-alloc, and it removes any label that has no real edge:
- Bare
tail:—LABEL_NUSES == 0atjump.c:243(/* Delete all labels already not referenced. */,:237) →delete_insn. goto tail; tail:(target is the next active insn) —jump.c:664if (reallabelprev == insn && condjump_p (insn)) delete_jump (insn);fires, because gcc-2.7.2'scondjump_p(jump.c:2946-2955) returns 1 for an unconditional(set (pc) (label_ref));prev_active_insn(emit-rtl.c:1878-1891) skips the BARRIER and the CODE_LABEL, soreallabelprev == insn.delete_jump(:3261) →delete_insn(:3393), whose:3458-3461decrementsLABEL_NUSESto 0 and deletes the label too.
Because a C user label has LABEL_NAME != 0, delete_insn:3408-3416 rewrites it as NOTE_INSN_DELETED_LABEL instead of unlinking it. A NOTE is not a CODE_LABEL and is not a basic-block boundary, so sched1 hoists the move $aN,$sN to the top of the block exactly as if you had written nothing, optimize_reg_copy_1 fires, and the whole block re-bases onto $aN.
Corrected precondition 1: the call must not be entered by a control-flow edge from elsewhere. Operationally: the disarming spelling is a non-adjacent goto tail; from another arm (§165-24's shared-tail join), giving the label ≥1 predecessor besides fall-through. Writing tail: — or goto tail; immediately above tail: — to "insert a boundary" is a zero-byte no-op and will hand you a false negative against an otherwise-correct law.
Byte cost of the two null spellings: exactly 0. On func_80181F38 the fake-label forms are assembly-identical to the no-label form (only the $L counter advances), and the emitted count is 98, not 99 — proof the j was deleted rather than emitted.
(This corrects only the DIAL wording. §193-D's mechanism — sched1 hoist → local-alloc.c:1004-1015 → optimize_reg_copy_1 at :700 — and its other eight bounds are untouched and were re-confirmed here: the real join does flip all six/thirteen operands back to $sN and puts the copy in the jal's delay slot.)
BOUND. Where the correction does NOT apply / where it must not be over-read:
-
It does not touch §193-D's mechanism or its other bounds. -O2-only (
flag_expensive_optimizations), pointer-in-a-callee-saved-reg only, argument-must-be-the-bare-variable, etc. all still hold verbatim. This is a wording fix to precondition 1 and the "THE C DIAL" paragraph only. -
Do NOT generalize to "labels are inert in C". A label with a real incoming edge is a first-class dial all over the cookbook (§165-24, §136g-1, §167-27's
LABEL_NUSES(label1)==1, §46-L3). The null is specific to a label with no surviving reference. -
Address-taken labels are an untested escape (R14).
jump.c:234-235bumpsLABEL_NUSESfor everything onforced_labels("Keep track of labels used from static data; they cannot ever be deleted"), andLABEL_PRESERVE_Pis honoured at:158. So a gcc computed-goto label (void *pp = &&tail;) plausibly survives jump1 with nogotoat all and WOULD create the boundary. I did not compile this. It is a place to look, not a claim — and it is irrelevant to real decomp C. -
"Adjacent" means next active insn.
prev_active_insnskips NOTEs, BARRIERs and CODE_LABELs but nothing else. Put any real insn between thegotoand its label and thegotosurvives, the label survives, and you are back in the boundary regime — which is just the ordinary non-adjacent join. -
A label with two references loses only one. If
tail:already has a realgotofrom elsewhere, adding a redundant adjacentgoto tail;deletes only the adjacent one (NUSES 2→1); the label lives and §193-D's join behaviour is unchanged. The correction says a sole adjacent/absent reference is worth nothing, not that adjacent gotos are harmful. -
Backward gotos / loops are a different regime.
NOTE_INSN_LOOP_BEG/ENDare separateoptimize_reg_copy_1scan-breakers (local-alloc.c:722-727, §193-D bound 5) andduplicate_loop_exit_test(jump.c:594-604) can rewrite the shape. Nothing here was measured on a loop. -
-O0/-O1 is out of scope —
delete_insn's patch-out is guarded onoptimize(:3437), and §193-D itself evaporates below -O2 anyway. -
Not a prescription to prefer the join. §193-D bound 9 stands: both poles are attested in real target bytes across five overlays. This correction only fixes how you reach the join pole.
SECOND INSTANCE. Two, one of them independent of the exemplar and DISCRIMINATING (it shows both poles).
(a) The submitter's, reproduced but WEAK on its own. func_801874E8 / ov_SC02_017, gate --asm-subdir .run/waveU_asm_snapshot/ov_SC02_017:
.run/harvest_u/A_dup.c→MATCH (113 ins).run/harvest_u/C_redundant_label.c(= A_dup +goto tail;/tail:immediately before the terminalfunc_80143970(a0);) →MATCH (113 ins)Confirmed. But I checked.run/waveU_asm_snapshot/ov_SC02_017/func_801874E8.s:8018768Cand the copyaddu $a0,$s0,$zeroalready sits in thejal func_80143970delay slot with no re-base, i.e. the §193-D mechanism is not engaged in this function. So this cell proves "the label is inert" but cannot show the contrast. I did not accept it as sufficient.
**(b) MINE — a synthetic that shares nothing with func_80181F38, compiled with the pinned cc1 (-O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker), four one-axis variants of
void f(int *p, int c) { int v = h();
if (c == 0) { g(p); <X> }
p[0]=v; p[1]=2; p[2]=3; p[3]=4; p[4]=5; p[5]=6;
<Y> g(p); }
| variant | <X> / <Y> |
emitted |
|---|---|---|
e_inline |
return; / — |
move $4,$17 hoisted to block top, all six sw on $4 ($a0) — the re-base fires |
f_barelabel |
return; / tail: |
byte-identical to e_inline modulo the $L counter; still all six on $4 |
g_adjgoto |
return; / goto tail; tail: |
byte-identical to e_inline modulo the $L counter; still all six on $4 |
h_realjoin |
goto tail; / tail: |
all six sw on $17 ($s1), and move $4,$17 lands in the jal g delay slot — copy_1 disqualified |
That is the whole law in one table on a function with a different arity, different callee, different offsets, different store count and a different base register from the exemplar: fake label = 0 bytes (twice), real incoming edge = the register flip. Files: <scratch>/syn/{e_inline,f_barelabel,g_adjgoto,h_realjoin}.{c,s}.
(A fifth/sixth null: an earlier synthetic pair where p stayed the incoming $a0 — §193-D bound 2, mechanism not engaged — also gave bare-label and adjacent-goto output identical to inline.)
§194-REJECTED — what this harvest did NOT bank (recorded so it is not re-derived)
- A store in a
jal's delay slot proves the arg setup was emitted above it — read it backward to place the pre-call statement (with the C lever being the ternary split, not the argument local) — ATTACK 1 LANDED (re-derivation) — and ATTACK 2 landed independently on the evidence→law link.
Attack 1 — ALREADY IN THE COOKBOOK: §176-A (docs/matching-cookbook.md:17613-17627). The candidate's headline half is §176-A restated. §176-A's "The law it rests on" paragraph is the identical mechanism, already read out of the same pass: *"fill_simple_delay_slots backward-scans from the slot owner. It **never hois
- The atlas
seed_ref"can never name a same-binary sibling" — REJECTED: same-binary refs exist (43 card-eligible / 339 atlas-wide), and the h_seq dedup explains only 3 of 51 wave-U cards — TWO attacks landed, and BOTH of them are the submitter's own pre-committed falsifiers.
ATTACK 3 (misattributed mechanism) — the decisive kill. The citations are all real and correctly read: tools/atlas.py:507-517 does build pool[h_seq] = (b,a,nins) first-write-wins over registry() with the ov_SC01_077 override; registry() IS sorted (r == sorted(r), 213 entries — I ran it); K_SEED=6 at :48, `SEED_
- Write-only global whose address also reaches a neighbouring symbol needs a pointer local (
T *p = &SYM; *p = v; f(p - K)) — the$attell — ATTACK 1 LANDED — this is a re-derivation of §167-45 (docs/matching-cookbook.md L16108-16131), "A GLOBAL THAT IS STORED TO AND HAS ITS ADDRESS PASSED TO A CALL IN THE SAME BLOCK NEEDS A POINTER LOCAL — AND ANaddiu $rD,$rB,-KOFF THAT BASE NAMES WHICH SYMBOL THE SOURCE ANCHORED ON." The correspondence is one-for-one, not thematic:
- Candidate's target shape (`lui/addiu SYM into $a1; sw 0($a1); addiu $a1,$a1,
- match_one's
-Iinclude-only CPPFLAGS is a one-flag DEFECT; adding-Isrc/sharedlets drafts#include "engine_types.h"and match — Attack 1 LANDED (already in the cookbook, three places), and attack 3 landed behind it (the prescription is unsafe for the reason the existing § gives).
Attack 1 — REJECT: re-derivation of §77's subsection "The CANDIDATE gate and the REAL gate need DIFFERENT preambles — keep the difference out of the bank" (docs/matching-cookbook.md:6124, indexed in docs/cookbook-index.md:571 / :1144). In my own words the me
- §165-24's copy-count oracle is only readable when the merged
jal's slot is anop: when it holds a realaddu $aN,$sN,$zerothe count is unrecoverable and duplicate-vs-join is byte-FREE — THREE of the five attacks landed. The submitter's own four compiles reproduce exactly — that part is honest — but the two load-bearing generalizations do not survive.
Attack 2 LANDED (decisive) — the byte-FREE half is refuted by the candidate's own named falsifier, which is sitting in this tree. The falsifier reads: "A target function whose merged jal carries a real argument setup in its OWN delay slot and no
§195 — THE WAVE-V HARVEST (P31 S54): 67 index_gap reports -> 14 laws, 9 rejected, 76 already-covered
Third harvest in one session, same mill (cluster readers -> one adversarial verifier per candidate, defaulting to REJECT), seeded with §193 AND §194 so neither could be re-derived. The already-covered count keeps climbing as a SHARE — 61/71 (T), 44/64 (U), 76 of 67 reports (several cite more than one section) — which is the flywheel working: an agent's unknowns are increasingly the cookbook's known.
The most valuable entry here is not a compiler law but a defect in our own verifier (§195-D):
masked_diff.mask_for was dropping EVERY j/jal word from the comparison on the opcode alone, so
an internal j to the wrong local label — the difference between break and return, i.e. which
calls execute — was invisible to match_one, the permuter's scorer, family_cousins.tok and the
atlas tiers simultaneously. Fixed the same session (mask the 26-bit field only when the reloc
says the LINKER fills it), with the already-banked population re-verified 35/35 as the control.
§195-A — §167-08's "an $aN READ before the jal is scratch" has a byte-proven FALSE-NEGATIVE class: an argument that DIES at the call is allocated straight into $aN, so its only def is a plain load far above the jal and every intervening use reads $aN — there is no positive tell in either direction, only the two-arity A/B
§NNN — AN $aN THAT IS READ BEFORE THE jal CAN STILL BE THAT CALL'S ARGUMENT: when the value DIES AT THE CALL, the arg copy (set (reg $aN) P) degenerates to a self-move and disappears, so the argument's only def is an ordinary lw/lh/li far above the jal and every use in between reads $aN. §167-08's def/use walk is NOT a decision procedure — in EITHER direction. A/B both arities. (BOUNDS §167-08 (L15185/L15206), whose DIAGNOSTIC TELL — "USE (source operand of a store/compare/ALU op) ⇒ scratch" — is stated unconditionally; §167-08 remains byte-true on its own exemplar, which is why this is a bound and not a refutation. Reuses §194-F's (L19163) two-allocator copy-preference chain and its BOUND 5, applied to a new consequence: callee ARITY. Orthogonal to §164-61/§16Xd (L13164) and §167-23 (L15542), which bound only the nop delay slot and say nothing about a $aN that is read.)
Target shape — an $aN defined by a plain load, read as an ADDRESS or a COMPARE OPERAND, never redefined, and the jal's delay slot setting only a different $aN (or a bare nop):
lw $a1,0xCC($s1) <- ordinary load, no `move`
lh $v1,0x6($a1) <- $a1 READ as an ADDRESS
beq $v1,$s0,.L… <- a block boundary may sit here
jal func_80013478
addiu $a0,$s1,0x4 <- delay slot sets $a0 ONLY
THE LAW. expand_call emits (set (reg:SI $aN) (reg P)). That is a reg-reg copy, so it is a COALESCING HINT, and both allocators honour it. If P is block-local, combine_regs (local-alloc.c:1791-1819) refuses to tie a hard reg to a pseudo and records $aN in qty_phys_copy_sugg[reg_qty[P]]; block_alloc satisfies it in a dedicated FIRST pass (find_free_reg (…, 0, 1, …), local-alloc.c:1466-1477) ahead of the decreasing-life-length pass. If P is cross-block it falls to global-alloc, set_preference (global.c:1535, :1579-1592) records $aN in hard_reg_copy_preferences[allocno], and find_reg (global.c:990-1015) overrides its first-fit pick with the copy-preferred reg when the class matches. Either way P is BORN in $aN, the copy is a self-move, and it is deleted. The discriminator is LIVENESS AT THE CALL, not the def/use direction: a value live AFTER the call must go callee-saved and the move $aN,$sN §167-08 predicts really does appear.
AND THE ABSENCE OF A COALESCE PROVES NOTHING EITHER. find_reg's copy-preference override is a preference; it is abandoned under AND_COMPL_HARD_REG_SET (…, used), under allocno_calls_crossed > 0 stripping caller-saved prefs (global.c:906), or when a competitor outranks it in allocno_compare. §167-08's own exemplar func_8017EEEC satisfies the dies-at-the-call precondition and its 2-arg build still emits move $a0,$a2 ; move $a1,$v1 — two values competing for two arg registers, no coalesce, and the byte-true arity is 0. So there is no positive tell in either direction.
THE PROCEDURE. When an $aN's last pre-jal event is a READ, do NOT read arity off the walk. Compile both arities against the target (two minutes, one-hunk edit). Take the destination-TU definition first when one exists (§166a still outranks any register read). The fix is a prototype + call-site edit — family-safe; do NOT reach for a register __asm__("$5") pin (§37/§162p rung 3), which forfeits the h_seq family for a result the arity edit gets for free.
THE DIAGNOSTIC TELL FOR THE RESIDUAL. You drafted the LOWER arity and the gate returns REGALLOC-PERM at exactly 2 mismatched instructions, zero length drift, with the signature $vN>$aM — your load and its consumer both sitting in $v0/$v1 where the target has them in $aM. That pair is the deleted arg copy. Add the argument.
BYTE EVIDENCE — two functions, two overlays, two callees, two kinds of read, both A/Bs re-run by the verifier.
| fn | binary | ins | byte-true spelling | counterfactual |
|---|---|---|---|---|
func_8017FF60 |
ov_SC05_003 | 152 | 2-arg MATCH | 1-arg: 152/152, 2 mism, REGALLOC-PERM/$v1>$a1 (idx 84/86) |
func_80180584 |
ov_SC03_101 | 77 | 1-arg MATCH | 0-arg: 77/77, 2 mism, REGALLOC-PERM/$v0>$a0 (idx 28/30) |
ALLOCATOR GROUND TRUTH — read the .greg CONFLICT SET, not just the preference line. func_80180584 is the clean proof: with the argument, pseudo 101 is a global allocno whose conflicts are 73 101 29 — no caller-saved hard reg conflicts at all, so ascending first-fit would hand it $v0; ;; 101 preferences: 4 flips it to $a0 (101 in 4). Drop the argument and the allocno vanishes, and the same pseudo lands 101 in 2 = $v0 — first-fit's answer, which is exactly the two mismatched instructions. On func_8017FF60 the disposition is the same (;; 104 preferences: 5, 104 in 5) but ;; 104 conflicts: … 2 3 4 29 means first-fit reaches $a1 unaided, so that dump is consistent with the mechanism without proving it. Cite func_80180584 for the mechanism. (;; N preferences: R prints hard_reg_preferences, global.c:1685 — not the copy set; the two coincide for a reg-reg copy.)
(SHARPENS — bounds §167-08 (L15185); reuses §194-F's (L19163) allocator chain and BOUND 5 for a new consequence; evidence: byte-probed, 2 tree functions in 2 overlays + a 5-arm synthetic; from func_8017FF60 / func_80180584.)
BOUND. Six conditions, three of them measured by me at vet time.
-
THE VALUE MUST DIE AT THE CALL — measured, in BOTH allocator paths. My own probe
/home/musashi/bfm-decomp/.run/harvest_v/vet/vet_probe.c, five arms, one compile. Same-block, dies (vA):lw $5,64($16)/lhu $2,10($5)/jal g2— no copy, and $a1 IS the argument. Same body withxlive after the call (vB):lw $17,64($16)/move $5,$17/jal g2— exactly §167-08's predicted copy, +1 callee-saved reg, frame 24→32. Cross-block, dies (vC):lw $5,64($16)/lh $3,10($5)/beq/jal— no copy. Cross-block, live after (vD):lw $16,…/move $5,$16. The bound holds identically on the local (qty_phys_copy_sugg) and the global (hard_reg_copy_preferences) paths. -
THE COALESCE IS A PREFERENCE, SO ITS ABSENCE IS NOT EVIDENCE OF LOWER ARITY — this is the half the submitter under-stated and it is the bound that matters most. §167-08's own exemplar
func_8017EEECmeets the dies-at-the-call precondition and its 2-arg build still emitsmove $a0,$a2 ; move $a1,$v1: two values competing for two argument registers, the preference loses to conflicts, and the byte-true arity is 0.find_regabandons the preference underAND_COMPL_HARD_REG_SET (…, used)(global.c:1001), under caller-saved stripping whenallocno_calls_crossed > 0(global.c:906), and under anallocno_compareloss. Expect the law to fire on a SINGLE dying value with a free $aN; expect it to fail when several values contend. Never write "nomove⇒ higher arity" as a rule. -
THE COPY PREFERENCE IS ONLY PROVABLY LOAD-BEARING WHEN FIRST-FIT WOULD PICK SOMETHING ELSE. Check the
.gregCONFLICT line before crediting it. If the allocno conflicts with $v0/$v1/$a0 (regnos 2/3/4) asfunc_8017FF60's 104 does, ascending first-fit lands on $a1 anyway and the dump proves nothing about the mechanism — the OUTCOME is unaffected, the STORY is. Do not repeat "the copy preference is printed by the compiler itself":global.c:1685printshard_reg_preferences. -
The delay slot must not redefine the register. Three of the four shape-matches my scanner found across the wave-V snapshot are ordinary §167-08 scratch:
func_80181544andfunc_8017E92Credefine the read $aN in thejaldelay slot (addu $a0,$s0,$zero/addu $a3,$zero,$zero), andfunc_8017E640reads$a1into anandibeforejal rand, a genuinely 0-arg callee. §167-08 is right far more often than it is wrong — this bound describes a minority class, not the common case. -
The destination-TU definition still outranks everything (§166a). Both arms of this law are register-level inference; a real prototype in the TU or a sibling overlay ends the question without a compile.
-
Scope of the byte record. Two banked functions, two overlays, two callees, two kinds of read (address, compare operand), plus one 5-arm synthetic I wrote. The mechanism is source-general (two allocator passes, not a peephole). The frequency is not measured: 2 confirmed positives against 3 confirmed §167-08-correct negatives in one 9-overlay snapshot scan is a sample, not a rate. Do not quote a ratio.
SECOND INSTANCE. FOUND BY ME, IN THE BANKED TREE, AND IT IS A BETTER EXEMPLAR THAN THE SUBMITTER'S.
func_80180584, ov_SC03_101 (different overlay), 77 ins, banked at /home/musashi/bfm-decomp/src/ov_SC03_101/ov_SC03_101_jr_8017CA80.c:4807. Found by scanning .run/waveV_asm_snapshot/**/*.s for "load into $aN → intervening read → jal with no redefinition", then reading the banked C.
Target /home/musashi/bfm-decomp/.run/waveV_asm_snapshot/ov_SC03_101/func_80180584.s:
/* 801805F4 */ lh $a0, %lo(D_8019A9F8)($at) <- ordinary load
/* 801805F8 */ nop
/* 801805FC */ bltz $a0, .L8018061C <- $a0 READ as a COMPARE OPERAND
/* 80180600 */ nop
/* 80180604 */ jal func_801806B8
/* 80180608 */ nop <- BARE NOP delay slot
§167-08 gives a 0-arg verdict twice over here — the read is a compare operand (its own enumerated case) and the delay slot is a bare nop. The byte-true banked C passes ONE argument: func_801806B8((void *)(s32)val); with extern void func_801806B8(void *a0);.
A/B run by me (extracted verbatim to /home/musashi/bfm-decomp/.run/harvest_v/vet/S2_1arg.c, one-hunk sed to S2_0arg.c):
- 1-arg →
MATCH (77 ins) - 0-arg →
DIFF mine=77 target=77, 2 mismatched, REGALLOC-PERM/$v0>$a0, idx 28lh v0,0(at)vslh $a0,%lo(D_8019A9F8)($at), idx 30bltz v0vsbltz $a0. The same 2-instruction, zero-drift signature as the primary, one register file over.
Three ways this instance is independent and stronger:
- It is the FIRST-
CODE_LABELcontrol:.L801805CCsits at801805CC, well above thejalat80180604, so §164-61/§167-23'snopexemption does not apply — this is a position those laws call arity-INFORMATIVE, and it still reads wrong under §167-08. - Its
.gregis the clean mechanism proof the primary lacks:;; 101 conflicts: 73 101 29(no caller-saved conflicts at all ⇒ first-fit would say$v0),;; 101 preferences: 4, disposition101 in 4; drop the argument and 101 is not a global allocno and lands101 in 2=$v0— the first-fit answer, and precisely the diff. - The read is a COMPARE OPERAND, not an address — a second row of §167-08's enumerated scratch list.
Third, out-of-family: my own 5-arm synthetic vet_probe.c (different callees g2/g1, different offsets, no engine structs) reproduces the positive in three shapes — address-read same-block (vA), address-read cross-block (vC), and compare-operand (vE: lw $5,64($16) / beq $5,$2,$L12 / jal g2 / addu $4,$16,8) — and the bound in two (vB, vD).
§195-B — A CALL_INSN does not start a basic block in gcc-2.7.2 — so a call-crossing temp can be a LOCAL-alloc quantity (the missing precondition under §48-A2 / §52 / regalloc.md K8)
§X — A jal DOES NOT START A BASIC BLOCK. "Block-local" and "crosses a call" are not in tension, and that is why a call-crossing prologue temp is allocated by local-alloc, before global-alloc runs. (SHARPENER — supplies the missing precondition under §48-A2 (L3433), §52's wall discussion (L3946), §76 (L6039) and docs/gcc-2.7.2-map/regalloc.md K8, all four of which state or quote REG_BASIC_BLOCK >= 0 without ever defining what ends a block. Widens §48-A2's "$s0" and §52's "s0/s1" to the whole $s0-$s7 pool.)
THE FACT (flow.c:446-452, verbatim).
/* A basic block starts at label, or after something that can jump. */
else if (code == CODE_LABEL
|| (GET_RTX_CLASS (code) == 'i'
&& (prev_code == JUMP_INSN
|| (prev_code == CALL_INSN
&& nonlocal_label_list != 0)
|| prev_code == BARRIER)))
A block starts at a CODE_LABEL, or after a JUMP_INSN/BARRIER — and after a CALL_INSN only if the function has a nonlocal label (nonlocal_label_rtx_list(), flow.c:323 → function.c:3029, walking nonlocal_labels: nested-function gotos only, which no BFM overlay has). The identical predicate is duplicated in the block-counting loop at flow.c:352-359, so both passes agree. An ordinary jal is transparent to block formation.
THE CONSEQUENCE. local-alloc.c:472's gate reg_basic_block[i] >= 0 && reg_n_deaths[i] == 1 is therefore satisfied by a pseudo spanning any number of calls, so long as all of its references sit between two labels/jumps. local-alloc runs BEFORE global-alloc; local-alloc.c:2103-2106 (qty_n_calls_crossed[qty] == 0 ? fixed_reg_set : call_used_reg_set — the set of FORBIDDEN regs) denies a call-crosser every caller-saved register, so find_free_reg hands it $s0, then $s1, … up to $s7; and global.c:669-670 re-marks each such placement as a live HARD register before global-alloc's first conflict scan. A whole call-containing prologue is one basic block, and its temps are already in $s0.. before the highest-priority global allocno is even considered.
HOW TO READ THE -dg DUMP. A function with no ;; N regs to allocate: line at all but a populated ;; Register dispositions: line has zero global allocnos — every register in it was chosen by local-alloc. Any pseudo in the dispositions line that is absent from the "regs to allocate" list is a local placement; if it sits in 16-23 it is (almost certainly) call-crossing. Read that line before reasoning about global.c:594 priorities — the priorities may not be deciding anything.
BYTE-PROBE (pinned cc1 -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -dg). Five values across FIVE jals, zero branches ⇒ no "regs to allocate" line, 72 in 16 73 in 17 74 in 18 75 in 19 76 in 20. The same body with ONE if wrapped round the calls ⇒ ;; 5 regs to allocate: 72 73 74 75 76. Three added calls flip nothing; one added branch flips everything.
CORRECTS A LIVE MIS-INFERENCE. §164-61/§16Xd's "ask which block the jal is in" reads as though the jal moved a boundary. It never does. (§164-61's own correction is about cse_end_of_basic_block, cse.c:8039 — a different pass with a different, longer block. Do not conflate the two: for cse a JUMP_INSN is transparent and a LABEL is the terminator; for flow a JUMP_INSN terminates and a CALL_INSN is transparent.)
BOUND. Five bounds, all measured or source-cited:
-
The nonlocal-label escape hatch. If the function ever acquires a nonlocal label (
nonlocal_labelsnon-empty — a nested function'sgotoout; C with no nested functions never does), every CALL_INSN DOES start a block, and separatelylocal-alloc.c:2095-2098refuses a hard reg outright to any call-crossing qty (if (current_function_has_nonlocal_label && qty_n_calls_crossed[qty] > 0) return -1;). Both halves of the law die at once. Not reachable from BFM's C, but it is the exact condition, so state it. -
A NORETURN callee is not transparent.
calls.c:1972-1973—if (is_volatile || is_longjmp) emit_barrier ();— so a call to a__attribute__((noreturn))/volatilefunction ends the block viaprev_code == BARRIER, not via CALL_INSN. In practice such a call must sit behind a conditional anyway, so the branch has already split the block; measured (v_noret) it makes no independent difference. -
Eligibility is NOT placement — the local pool is exactly 8 deep. MEASURED: nine simultaneous block-local call-crossing values give
72..79 in 16..23(all of $s0-$s7 consumed by local-alloc) and the ninth pseudo falls through to global-alloc, whose dump reads;; 1 regs to allocate: 80/;; 80 conflicts: 80 16 17 18 19 20 21 22 23 29— i.e. it is a global allocno colliding with eight hard regs that local-alloc already took (global.c:669-670in action), and it lands in$fp. So "block-local + call-crossing" predicts candidacy, not a callee-saved register. This also widens §48-A2's "$s0" and refines §52's "nothing can pre-occupy s0/s1 for a call-crossing qty": within one function, other block-local call-crossing qtys pre-occupy $s0-$s7 routinely. -
The OTHER half of local-alloc.c:472 still binds, twice. (a)
reg_n_deaths[i] == 1— a call-crossing value referenced in one block but dying twice is global regardless (this is §48-A3/§76's lever, unchanged). (b) The third conjunct at:473-474,reg_alternate_class (i) == NO_REGS || ! CLASS_LIKELY_SPILLED_P (reg_preferred_class (i))withCLASS_LIKELY_SPILLED_Pdefaulting toreg_class_size == 1(local-alloc.c:76-77) — so a pseudo whose preferred class is a one-register class (LO_REG/HI_REG: §164-42'smultproducts) is excluded from local-alloc even when it is block-local, 1-death and call-crossing. -
"Block-local" is about REFERENCES, not the live range, and the class flip is not automatically a REGISTER flip.
reg_basic_blockrecords where the pseudo is mentioned; and MEASURED, myv_straight(5 locals) andv_branch(5 globals) produce the identical allocation $s0-$s4. So do not use "make it global / make it local" as a register-moving lever on its own evidence — the class change only matters when it changes the ORDER in which registers are claimed (which is §48-A2's actual claim: the local occupant entersregs_used_so_farfirst and pushes the top-priority global allocno off $s0).
SECOND INSTANCE. func_8017E1E8, real banked decompiled code in /home/musashi/bfm-decomp/src/ov_SC05_003/ov_SC05_003_jr_8017BEBC.c (its dump: /home/musashi/bfm-decomp/.run/harvest_v/vdump/tu.i.greg, asm tu.s). ELEVEN jals, ZERO branches, ZERO labels in the entire function; both parameters live end-to-end across all eleven calls in $s0/$s1. Its greg block has no ;; N regs to allocate: line at all — zero global allocnos — and ;; Register dispositions: 72 in 16 73 in 17 …. Both call-crossing parameter pseudos were placed by local-alloc. This is the submitted law in production code, not a probe.
And it is not a singleton: a machine scan of every function in that one banked TU (parse dispositions, subtract the global-allocno list, keep pseudos in hard regs 16-23) finds 15 functions with local-alloc callee-saved placements — func_8017BEBC(72→$s1, 108→$s0), func_8017CD9C, func_8017DA3C, func_8017DE14, func_8017DE7C, func_8017E058, func_8017E1E8, func_8017F1AC, func_8017F880, func_8017FF60, func_801803B8, func_80180B04 (four of them, zero globals), func_801818B8, func_80181AAC, func_80181F78 (five). Note several of these — func_8017BEBC with 122 global allocnos, func_8017F1AC with 14 — are large, heavily-branched functions, so the local/callee-saved placement coexists with a full global allocation; the block-locality only has to hold for that one pseudo, not for the function.
I also independently reproduced the submitter's corroboration on .run/harvest_v/dump/A_2arg.i.greg (func_8017FF60): globals are 72 132 73 104 84, dispositions include 74 in 16 — 74 is absent from the global list, so $s0 there is a local placement in a function whose every call site is a jal.
§195-C — A call-argument %hi/%lo pair sitting at the block head, far above its jal, is a load-delay-gap filler chosen by SOURCE STATEMENT ORDER — swap the two independent statements nearest the call; a single-use void *p = &SYM; call-arg temp is OUTPUT-inert against it (but NOT expand-stream-inert)
§NNN — A CALL-ARG %hi/%lo PAIR AT THE BLOCK HEAD, FAR ABOVE ITS jal, IS A LOAD-DELAY-GAP FILLER PICKED BY SOURCE STATEMENT ORDER — SWAP THE TWO INDEPENDENT STATEMENTS NEAREST THE CALL. THE void *p = &SYM; CALL-ARG TEMP IS OUTPUT-INERT AGAINST IT (AND NOT FOR THE REASON YOU THINK).
(BOUNDS §167-34 (L15868) by exact polarity inversion: §167-34's law is "a named temp is the load-order dial and NO statement permutation substitutes" — true for a symbol's LOADED VALUE; for a symbol's ADDRESS passed as a call argument the polarity flips, the statement permutation is the dial and the named temp is a no-op. BOUNDS §190-C (L18285): that is a call-arg CONSTANT wedged into a delay slot by sched2; this is a call-arg ADDRESS moved by sched1, and -fno-schedule-insns vs -fno-schedule-insns2 is what tells them apart. INSTANTIATES §165-14 / §2-T2 / §167-17's INSN_LUID law on a new object. Does NOT contradict §194-K (L19411) — that temp is a MEM BASE and is a live sched1 alias lever; this one is a passed VALUE. Evidence: byte-probed, func_80181E58 (ov_SC03_118, 113 ins), two independent call sites.)
TARGET SHAPE. A block head that materialises one or two call-argument symbol addresses, then ~13 instructions of unrelated stores/loads, then the jal that consumes them:
/* 44 */ lui $a1,%hi(D_801D42F8) <- block head, 15 insns above the jal
/* 45 */ addiu $a1,$a1,%lo(D_801D42F8)
/* 46 */ lui $a2,%hi(D_8018DE20) <- 13 insns above the jal
/* 47 */ addiu $a2,$a2,%lo(D_8018DE20)
... 2200/1600 stores, the |= block, the 0x12 copy ...
/* 60 */ jal func_80128EA8
THE LAW. The la $aN,SYM pair is dependence-free — it reads nothing and is read only by the call. sched1 therefore parks it wherever a gap is left over after the block's dependent chains are placed, and which gaps exist is decided by which insns are available, which is rank_for_schedule's all-ties fallback INSN_LUID (tmp) - INSN_LUID (tmp2) (sched.c:2425-2428) = expand-stream position = source statement order. So the two adjacent, mutually independent statements nearest the call are the dial: one order leaves the load-delay slot of a dependent load filled by a real insn and the la stays at the block head; the other order empties that slot and the la sinks into it, adjacent to the call. Direction is not predictable a priori — A/B both orders.
BYTE EVIDENCE — count-neutral throughout (113/113), all via tools/match_one.py --asm-subdir .run/waveV_asm_snapshot/ov_SC03_118:
- site 1,
func_80128EA8(obj,&D_801D42F8,&D_8018DE20): banked order (*(u16*)(obj+0x12) = …;BEFORE*(u32*)(obj+4) |= 0x51000000;) → MATCH. Those two statements swapped, everything else byte-identical → NEAR 12, filedWIDTH/li!=lui,la $6,D_8018DE20sunk to 4 insns above thejal. - site 2,
func_8012A828(self,&D_8018DF4C)in the same function: target hoistsla $a1,D_8018DF4Cinto the delay gap oflw $a2,0x20($s0), 9 insns above thejal. Swapping the|= 0x71000000statement against the two following0xFFFD0000/0xFFFE0000stores → NEAR 17, the pair driven to the top of the block. - A zero-byte §194-A fence is NOT a substitute. Three
__asm__ __volatile__("")placements around the swapped statements: 15, 15, and a +2LENGTH-DRIFT. Nothing rescues the wrong order but the order.
THE NEGATIVE CONTROL — the temp is inert in OUTPUT, not in the RTL. Deleting void *p1 = &D_801D42F8; void *p2 = &D_8018DE20; and writing the addresses inline at the call is MATCH (site 1); adding void *q = &D_8018DF4C; adjacent to the call, and again ~10 statements earlier at the head of the branch, is MATCH both times (site 2). But the reason is not "the temp has no expand-stream presence" — it does. cc1 -dr shows the two (set (reg) (symbol_ref)) insns at 108/111 (block head) with the temps and at 138/140 (in the call's own arg setup) without them — a ~30-position LUID shift that sched1 simply undoes, because the insn is dependence-free either way. A REG_EQUIV cannot be the explanation at all: update_equiv_regs runs inside local_alloc (toplev.c:3052), AFTER sched1 (:3033). So: an address temp IS a LUID dial (§49/§167-34/§194-K stand); it just has no OUTPUT authority over a dependence-free call-arg address, which is exactly the thing you are tempted to reach for when you see this tell.
THE REGISTER FLIP RIDES ALONG, AND IT IS local-alloc, NOT global-alloc. The 0x51000000 constant lands in $a3 in the matching build and $v1 in the swapped one. Attribution, byte-measured: rebuild both with -fno-schedule-insns -fno-schedule-insns2 → the two outputs differ ONLY by the OR-store block's source position and both emit li $3,0x51000000. So the flip is a scheduler consequence, not an allocator preference. But the deciding pass is local_alloc (toplev.c:3052), not global_alloc (:3080): cc1 -dl reports the constant as a LOCAL allocno in both builds — "Register 95 used 2 times across 10 insns in block 5" (matching) vs "Register 93 used 2 times across 4 insns in block 5" (swapped). sched1 stretched the live range across the lw/lhu $v1 group, $v1 conflicts, and block_alloc's first-fit walks past $a1/$a2 (holding the two live addresses) to $a3. Do not chase this with pins or §148-C priority sliders — fix the statement order and the register follows.
DIAGNOSTIC TELL. Target has a call-arg %hi/%lo pair ≥5 insns above its jal at a block head, your draft emits it adjacent to the call, residual is small and count-neutral, and it is misfiled WIDTH/li!=lui or OPCODE-MIXED because the pair is diffed against whatever store it displaced. Run the §167-17 statement sweep on the 2-3 independent statements nearest the call FIRST; do not introduce an address temp, and do not open the permuter.
Honest scope: n=1 function, 2 independent call sites, both halves A/B'd in both polarities. The shape (call-arg %hi/%lo ≥5 insns above its jal) occurs 14× across 6 overlays in the waveV snapshot and ov_SC01_080/func_8017DEB0 is a clean unbanked confirmation of the gap-filling mechanism, but no second FUNCTION has been A/B'd.
BOUND. Where it does NOT apply:
- Single use only. The address must reach sched1 as a dependence-free one-use call argument. Used ≥2× inside one CSE basic block,
cseunifies the pseudos,local-alloc.c:1080's remat gate (reg_n_refs == 2 && reg_basic_block < 0) never fires andglobal.c:388hands it a callee-saved register — that is §153 (L10463), and there the temp is anything but inert. - Passed as a VALUE, not used as a MEM BASE. If the temp is dereferenced (
p->x,*(T*)p), §194-K (L19411) governs: the single-setp = &SYM;carries a REG_EQUAL note,init_alias_analysis(sched.c:399-438) canonicalises the MEM to the symbol, an anti-dependence is dropped, and the temp changes the schedule. The "inert" half is scoped strictly to the passed-value case. - Intra-block only. sched1 schedules one basic block at a time. Statements separated by a label, branch or call are in different blocks and swapping across the boundary cannot move the pair; a
goto-join (§165-24) or the presence of a call between them voids the lever. - Count-neutral only. If the reordering changes CSE opportunities, remat, or the number of loads (a length drift, not a permutation), you are not looking at this class — go read the count first.
- sched1, not sched2. If the pair only differs by which insn occupies the
jal's delay slot, it is §190-C / dbr territory. Discriminate before spending the sweep:-fno-schedule-insns2alone reproducing the target order means sched1 was already right. - Direction is unpredictable. At site 1 the swap sank the pair; at site 2 it raised it. The law says the order moves it, never which way. Compile the natural order first (§190-B) and only then sweep.
- Per-block rigidity is asymmetric (§190-B). A block of independent same-base writes whose values die locally may be order-insensitive — a swap there is a no-op and is NOT a refutation of this law; the swap has to change what is available to fill a dependent load's delay slot.
- The mechanism half about the register flip is local-alloc's. Any function where the flipped value is live across a call is a global allocno and this reasoning does not transfer — go to §5a/§76/global.c density instead.
SECOND INSTANCE. A second, independent call site inside the same function, and it was A/B'd in both polarities by me (not by the submitter, who only ever probed site 1):
Site 2 — func_8012A828(self, &D_8018DF4C), the else-branch of func_80181E58. Target .run/waveV_asm_snapshot/ov_SC03_118/func_80181E58.s lines 91-93 and 109: lw $a2,0x20($s0) / lui $a1,%hi(D_8018DF4C) / addiu $a1,$a1,%lo(D_8018DF4C) — the address pair sitting exactly in the load-delay gap of lw $a2, 9 instructions above jal func_8012A828. Different symbol, different callee, different basic block, different argument register from site 1.
.run/harvest_v/adv_e_swap2.c— the two independent statements nearest the call swapped (*(u32*)(o+4) |= 0x71000000;moved below theself+0x10 = 0xFFFD0000; self+0x14 = 0xFFFE0000;pair): DIFF 17 mismatched, 113 vs 113 (count-neutral),WIDTH/lui!=lw, withlui/addiu $a1driven from idx 92-93 up to idx 85-86 at the top of the block. The dial half replicates..run/harvest_v/adv_f_temp2.c—{ void *q = &D_8018DF4C; func_8012A828(self, q); }in natural order: MATCH 113/113..run/harvest_v/adv_g_temp2early.c— the samevoid *q = &D_8018DF4C;hoisted ~10 statements up to the head of the else-branch: MATCH 113/113. The inertness survives a deliberately large expand-stream displacement — a stronger negative control than the one submitted.
Shape-level generality beyond the function: a scan of all 7 overlays in .run/waveV_asm_snapshot/ found 14 instances of "call-arg %hi/%lo pair ≥5 insns above its jal, register not redefined in between", across 6 overlays. I hand-verified two: ov_SC01_080/func_8017DEB0 .s lines 69-70 is a clean independent instance of the exact mechanism (la $a1,D_80183884 occupying the load-delay gap of lhu $v0,0x2C($v1), 7 insns above jal func_8012A828); ov_SC02_039/func_8017E024 is a scanner false positive (handwritten GTE function — the pair is a lwl/lwr copy base, and $a1 is overwritten in the jal delay slot). Neither of those functions is banked, so no causal A/B was possible there. Honest bottom line: causal evidence is n=1 function / 2 independent sites; shape evidence is n=6 overlays.
§195-D — §195 — masked_diff.mask_for returns 0 for EVERY j/jal word, so an internal j destination is invisible to match_one, the permuter scorer AND every similarity tier: a control-flow semantic error (which calls execute) surfaces as a 1-instruction residual mislabelled DELAY-SLOT / profile=schedule, or as an outright false MATCH
An internal j destination is invisible to match_one and to every similarity tier, so a control-flow semantic error reads as a scheduling residual (or as MATCH)
The mechanism, one line. tools/masked_diff.py:127-129 opens mask_for with
if (word >> 26) in (2, 3): # jal / j — the 26-bit target is a link-time value
return 0
— unconditional, and ahead of the reloc dispatch. The comment's justification ("a link-time value") is right for jal and wrong for a j to a local label inside the same function, which the assembler resolves with no reloc at all. So every j .L… is dropped from the comparison entirely. match_one, the permuter's MaskedScorer (both go through diff_object_s/diff_object_object), family_cousins.tok (tools/family_cousins.py:55-66 → (2,) for every j), atlas.py:174 ratio, and atlas_features.py:150-167 li_norm_toks are all blind to it. Nothing between the draft and the whole-binary gate can see which block an unconditional jump targets.
Why that is a semantic hole, not a cosmetic one. For a loop/switch arm, which label the j targets is the entire difference between break (fall into the shared post-loop tail) and return (skip past it) — i.e. which calls execute. Byte-proven on ov_SC03_118:func_801825EC (70 ins, banked src/ov_SC03_118/ov_SC03_118_jr_8017FB84.c:4031):
| C written in the matched arm | semantics | match_one verdict |
|---|---|---|
break; (banked) |
calls func_8002D4C8(0x426,0) |
MATCH (70 ins) |
return; |
skips that call | DIFF, 1 mismatched, class: DELAY-SLOT [permuter] sig=DELAY-SLOT/1 profile=schedule |
*(u16*)(a0+0x5E)=0; return; |
skips that call | MATCH (70 ins) — FALSE MATCH |
Row 3 is not contrived: it is verbatim the idiom two banked siblings of this very family already use (ov_SC03_024:func_80182534 and ov_SC02_011:func_80186430 both write an explicit *(…+0x5E)=0; before jumping). The two objects for row 1 and row 3 differ in exactly one word — 0800003e (j +0xF8, the addiu $a0,0x426 tail) vs 08000041 (j +0x104, the epilogue).
THE ASYMMETRY THAT BOUNDS §176-F. §176-F row 3 tells you to trace branch targets after you get an IMM-OFFSET, closeness 1 verdict. You get that verdict only for a conditional branch: beq/bne to a local label carries no reloc, so mask_for falls through to 0xFFFFFFFF and the displacement is compared full-word. The identical control-flow error is loud in a bne and silent in a j. §176-F's recipe therefore never fires on the class that most needs it.
THE MISDIAGNOSIS, which is the expensive part. When the delay slot does differ, the residual is a lone nop-vs-store at the j's slot, and match_one stamps it DELAY-SLOT / profile=schedule and routes it to the permuter. An agent then spends a round on scheduling barriers (§164-29 / §194-A / §194-H) for a bug whose fix is one keyword. A DELAY-SLOT/1 residual whose differing index sits immediately after an unconditional j is a control-flow verdict until proven otherwise — resolve the j target before touching the scheduler.
PRESCRIPTION (do this, it is cheap).
- When drafting from a near-1.0 twin (§193-A/§136c workflow), resolve the twin's and the target's
jdestinations to their actual instructions before copying the body.seed_simcannot distinguish them; only reading the two.stails can. A branch landing on the label that beginslw $ra,K($sp)is a RETURN (§136b); one landing before it is a JOIN. - On any
DELAY-SLOT/1residual, check whether index-1 is aj. If so, diff thejwords unmasked (objdump -drzon your.ovs the target.s) —match_onewill not do it for you. - Run
tools/reloc_verify.pybefore banking any body containing an internalj. This is §88f's promoted tool; it resolves internaljdestinations and is the only pre-gate rung that closes this class.
(EXTENDS the L6649 blindness ladder from FOUR classes to FIVE — and this is the only one of the five whose failure is a wrong semantic, not a link/rodata/staleness problem. SHARPENS §88f, which recorded the j mask as a relocation issue and never noted the semantic or the misdiagnosis. BOUNDS §176-F row 3 to conditional branches. Complements §168, which named the cousin stream imm-blind without drawing this consequence. Evidence: ov_SC03_118:func_801825EC, ov_SC03_014:func_80188E00, ov_SC03_015:func_80188E00; reproduced 2026-08-17.)
BOUND. Where the law does NOT apply — five real limits, three of which cut hard.
-
j/jalonly.mask_forshort-circuits onword >> 26 ∈ {2,3}. Conditional branches (beq/bne/bgez…) to local labels carry no reloc, get mask0xFFFFFFFF, and their displacement is compared — that is precisely why §176-F row 3 exists and reportsIMM-OFFSET. If your target's arm exit is a conditional branch, the oracle sees it fine and none of this applies. (R_MIPS_PC16— 211 instances tree-wide per §88f's audit — is masked0xFFFF0000, but that is a cross-function/unresolved case, not an intra-function arm exit.) -
The function must actually contain an internal
j. Measured scope: 4,063 of 14,552 unmatched.sfiles (27.9%) contain at least onej .L. The other 72% have no exposure at all. gcc-2.7.2 emits the unconditional form for abreak/goto/arm-exit out of a block; small straight-line and single-conditional bodies never hit it. -
breakandreturnmust be semantically DISTINCT — and usually they are not. This is the strongest limit and it refutes the naive alarm. In my sweep of 14 bankedbreak-bearing waveV functions, 4 of 5 keyword flips produced identical correct bytes (func_8018178C,func_80180230,func_801823D0,func_801825A4) because the loop/switch is the last statement of avoidfunction, sobreakandreturnmean the same thing. The hazard requires a post-loop tail that does work (here, afunc_8002D4C8(0x426,0)call). Do not raise this alarm on a trailing switch. -
The whole-binary gate still catches it — nothing wrong ever banks if G3/P9 is honoured. The linked bytes genuinely differ by one word, so the gate refuses. The cost of this law is wasted agent rounds, a residual routed to the wrong lane, and (worst case) a
match_oneMATCH that gets ledgered as "done" and then blows up one gate cycle later reading as one-word noise in aj. It is a diagnosis hazard, not a correctness hazard — provided the gate is actually run.tools/reloc_verify.pycloses it earlier and cheaper. -
Not a compiler law. This is a defect in our oracles, not a statement about gcc-2.7.2 — the submitter was right to file no gcc citation, and no future session should go looking for a pass that "decides" this. Corollary: it can be fixed in tooling rather than memorised. The minimal fix is to narrow
mask_for's op-2/3 short-circuit to words that actually carry anR_MIPS_26reloc, letting reloc-free (intra-function)jwords compare full-word; that would move this class from "law you must remember" to "the oracle tells you". I did not attempt the change (hard rule: no repo edits), and it needs the §88f-style decisive test — re-rundiff_object_sover all 60,740 stubs and require it still returns 0 — before anyone trusts it.
SECOND INSTANCE. Found — and it is a genuinely independent target, not a re-run of the first.
ov_SC03_015:func_80188E00, whose target asm is an unbanked, separate .s at asm/ov_SC03_015/nonmatchings/ov_SC03_015_jr_801848E4/func_80188E00.s (a third overlay copy of the twin skeleton, different binary from both functions in the submission). I A/B'd the twin's banked C against it in the opposite direction from instance 1:
return;(the correct keyword for this skeleton) → MATCH (70 ins)break;(semantically different — it adds afunc_8002D4C8(0x426,0)call on that path) → DIFF, 1 mismatched,class: DELAY-SLOT [permuter] sig=DELAY-SLOT/1 profile=schedule, sole diff at idx 57,sh zero,94(s0)vsnop
Perfectly mirrored: same 1-instruction residual, same wrong schedule label, j again reported identical. Two independent targets in two different binaries, opposite keyword errors, same misdiagnosis.
Supporting real-world spread (the transplant hazard is not hypothetical). This one h_seq family has four banked members across four binaries, carrying three different control-flow spellings, and they genuinely disagree on semantics:
| function | binary | spelling | calls func_8002D4C8(0x426,0)? |
|---|---|---|---|
func_801825EC @ ov_SC03_118_jr_8017FB84.c:4031 |
ov_SC03_118 | break; |
yes |
func_80188E00 @ ov_SC03_014_jr_801848E4.c:4433 |
ov_SC03_014 | return; |
no |
func_80182534 @ ov_SC03_024_jr_8017DF84.c:5289 |
ov_SC03_024 | *(…+0x5E)=0; goto end; |
yes |
func_80186430 @ ov_SC02_011_jr_8017AE2C.c:8051 |
ov_SC02_011 | *(…+0x5E)=0; goto end; |
yes |
Three of four call it, one does not — at a pairwise tok-similarity of 0.9857. This is exactly the population a §193-A seed_ref transplant draws from, and the two goto end members are the source of the false-match spelling I used in instance 1.
Negative control, reported honestly. Four other functions (ov_SC03_007:func_8018178C, ov_SC05_003:func_80180230, ov_SC06_024:func_801823D0, ov_SC06_024:func_801825A4) return MATCH/MATCH under the same flip, and are not instances of the law — their switches are terminal, so the two keywords are synonymous. Recorded so the next session does not count them as evidence.
§195-E — A 0/1 materialised at a JOIN immediately before the controlling beqz/bnez proves the source NAMED the condition — a truth expression in an if's controlling position reaches do_jump, which has no value path
§19x — A 0/1 MATERIALISED AT A JOIN LABEL IMMEDIATELY BEFORE THE CONTROLLING beqz/bnez PROVES THE SOURCE NAMED THE CONDITION. A TRUTH EXPRESSION LEFT IN AN if's CONTROLLING POSITION CAN NEVER EMIT IT — do_jump HAS NO VALUE PATH. (SHARPENS §167-09 — extends its COND_EXPR reading to TRUTH_ANDIF_EXPR, supplies the reverse-reading tell for the j JOIN + constant-arm shape §167-09 does not cover, and BYTE-REFUTES its "ternary-into-a-local, if/else and conditional overwrite are all byte-identical" when one arm is a constant. Supplies the ENTRY CRITERION §164-19 presupposes. Not §164-56/§165-22 — those are fold_truthop paths and cannot fire on two independent calls.)
Target shape:
bne $v0,$v1,.L64 <- arm-1 test
addiu $a0,$zero,0x9
j .LJOIN <- arm A: the CONSTANT arm
addu $v0,$zero,$zero
.L64: jal func_801848AC
addiu $a1,$zero,0x11
sltu $v0,$zero,$v0 <- arm B: the `!= 0` normalisation, ONE instruction
.LJOIN: beqz $v0,… <- the single consumer test
THE LAW. gcc-2.7.2 splits truth expressions across two expanders. In a controlling position the tree reaches do_jump (expr.c:8970-8990, TRUTH_ANDIF_EXPR: two start_sequence/do_jump/end_sequence blocks aimed at if_false_label/if_true_label), which returns no rtx — branches only, no addu rD,$zero,$zero, no sltu, no join. A value therefore requires the source to have named the condition and carried it across a control-flow join. The value-producing expander is a different function (expr.c:5678, expand_expr: "If no set-flag instruction, must generate a conditional store into a temporary variable" → emit_clr_insn (target); jumpifnot (exp, op1); emit_0_to_1_insn (target); emit_label (op1)).
READING RULE — three named forms, and they are NOT interchangeable. With arm A a constant and arm B a runtime test:
| source | ins | vs target |
|---|---|---|
s32 ok; if (A) ok=0; else ok=(B!=0); if (ok) — constant arm written FIRST |
123 | MATCH |
s32 ok; if (!A) ok=(B!=0); else ok=0; if (ok) |
123 | 4 diffs, arms swapped |
s32 ok = A ? 0 : (B!=0); if (ok) (either polarity) |
123 | 4 diffs, arms swapped — the ?: ignores written arm order and always lands call-arm-first |
s32 ok = 0; if (!A) ok=(B!=0); if (ok) (conditional overwrite) |
121 | −2 |
s32 ok = (!A) && (B!=0); if (ok) (value-position &&) |
121 | −2, and shape differs (below) |
if (!A && B!=0) / if (A ? 0 : (B!=0)) / if ((!A && B!=0) != 0) / De Morgan if (!(A || !B)) |
120 | −3, all four byte-identical: pure short-circuit branching |
So: the value/no-value axis is POSITION (controlling vs named); the arm-layout axis is FORM (if/else honours source arm order, ?: does not); the length axis separates conditional-overwrite and value-&& from both. Write the if/else with the arm the target falls through to written FIRST.
THE SUB-DISCRIMINATOR — a 0/1 in a register is not by itself proof of naming. A value-position && (s32 ok = A && B;) also materialises, via expr.c:5678. Tell them apart by the clear:
j JOINwhose delay slot holdsaddu rD,$zero,$zero,rD=$v0/a caller-saved temp, other arm ends insltu/sltiuwriting the samerD⇒ named flag across a join (if/else,?:-into-a-local, or agotoladder).move rD,$zerositting before/at the firstjal(typically in its delay slot),rDcallee-saved ($s0…), and nojto a join ⇒ that isemit_clr_insn⇒ value-position&&/||, a different source.
THE != 0 IS A ONE-INSTRUCTION DIAL. Dropping it (ok = g(…) instead of ok = (g(…) != 0)) deletes exactly the sltu $v0,$zero,$v0 and nothing else: 122 ins, −1. If your draft is one short at the join, the normalisation is missing.
BYTE EVIDENCE. func_8017EB30 (ov_SC05_017, 123 ins, banked src/ov_SC05_017/ov_SC05_017_jr_8017AE2C.c:4986-4995; target .run/waveV_asm_snapshot/ov_SC05_017/func_8017EB30.s:8017EC48-8017EC70). Eleven single-axis spellings, pinned triple, files in .run/harvest_v/. INTERNAL CONTROL, same body: the other conjunction 30 lines up (f()==3 && sel<0x492) IS a plain && in the banked C and the target emits pure short-circuit branching for it (8017EB98/8017EBA0, two branches to one label). Both spellings coexist in one function — this is a per-site read, never a whole-function style. SECOND INSTANCE: func_80180DE0 (same TU, target 60 ins, .s:80180E54-80180E6C), whose banked C at :6009-6028 names the flag with a goto ladder — proof the law is about naming across a join, not about if/else syntax.
BOUND. Where the law does NOT apply, or does not decide:
-
It says "named", not "
if/else". Any construct that carries the value across a join qualifies:if/else,?:-into-a-local, agotoladder (the second instance uses one). Do not read it as anif/elseprescription — you will mis-shape the arms. -
It decides value-vs-no-value only. It does NOT decide arm layout or length. Those are separate dials:
?:-into-a-local ignores written arm order (three ternary spellings all give call-arm-fallthrough, 4 diffs); conditional overwrite (ok=0; if(A) ok=…;) is −2; value-position&&is −2. Reaching MATCH needs all three axes, and this law only fixes the first. -
A 0/1 in a register is NOT the tell — the
j JOIN+addu rD,$zero,$zeroPAIR is. Value-position&&/||materialises too, viaexpand_expr(expr.c:5678). Use the clear-site sub-discriminator above. This is the single most likely way to misapply the entry. -
Foldable conjuncts never reach
do_jumpas a chain at all. Two compares on the SAME operand go throughfold_truthop:range_test(§164-56) or the bit-mask field-merge (§165-22), and there is no short-circuit code to read. The law's evidence is two independent CALLS, which cannot fold. Do not run this reading on(f&1) && (f&2)orx>=LO && x<=HI. -
do_jumpcan materialise a flag — under cleanups.expr.c:8990+:cleanups = defer_cleanups_to (old_cleanups); if (cleanups) { rtx flag = gen_reg_rtx (word_mode); … emit_move_insn (flag, const0_rtx); … }.cleanups_this_callis only ever non-empty for C++ destructors, so this is dead on BFM — but the law's warrant is "C has no cleanups", not "do_jumpstructurally cannot", and the entry should say so. -
The join value must be 0/1 constants. If the join carries a general value (two non-constant arms), you are in §167-09's select, not here; §167-09's "no
jto a join, one-instruction arm in the branch's delay slot" tell fires instead. -
n = 2 banked, one TU, one overlay, one binary. The 47 same-shape sites I swept out of
asm/(12 overlays) are all unbanked — predictions, not confirmations. Re-measure the arm-order and conditional-overwrite rows on the first out-of-TU instance before treating the table as exhaustive.
SECOND INSTANCE. func_80180DE0 — .run/waveV_asm_snapshot/ov_SC05_017/func_80180DE0.s:80180E54-80180E6C shows the identical join shape (bnez $v0,.L80180E5C / .L80180E54: j .L80180E6C ; addu $v0,$zero,$zero / lhu $v0,2($v0) ; xori $v0,$v0,3 ; sltiu $v0,$v0,1 / .L80180E6C: bnez $v0,.L80180E9C). Its banked C at src/ov_SC05_017/ov_SC05_017_jr_8017AE2C.c:6009-6028 names the flag v — but with a goto ladder, not the if/else the candidate insisted on. That is simultaneously the second instance AND the refutation of the law's "specifically an if/else STATEMENT" half.
Independence is weak: same overlay, same TU, same jr_ template as the primary. I also swept the entire asm/ tree with a scanner (.run/harvest_v/vwork/scan.py) for j <L> ; addu $rD,$zero,$zero … <L>: b(eq|ne)z $rD and found 47 sites across 12 overlays (ov_SC04_018/019, ov_SC02_000/003/011, ov_SC03_124, ov_SC05_017, …) — all in nonmatchings/, i.e. unbanked, so they corroborate that the SHAPE is common but confirm nothing about the source side. Sweeping the other two wave snapshots (waveT, waveU) returned 0 hits, so there are exactly two banked confirmations project-wide today.
§195-F — fold-const.c:4825 canonicalises a ?: whose THEN arm is zero (or constant against a non-constant ELSE) by inverting the condition and swapping the arms — so a ternary's written arm order is byte-inert there, and c ? 0 : X is unspellable: it always compiles as !c ? X : 0
§XXX — fold CANONICALISES A ?: WHOSE THEN ARM IS ZERO (OR IS CONSTANT WHILE THE ELSE ARM IS NOT) BY INVERTING THE CONDITION AND SWAPPING THE ARMS. THE WRITTEN ARM ORDER IS BYTE-INERT THERE — c ? 0 : X IS UNSPELLABLE, IT ALWAYS COMPILES AS !c ? X : 0. (BOUNDS §167-09, whose "ternary-into-a-local, if/else and conditional-overwrite are all byte-identical" is measured on a NON-constant THEN arm — the one shape this fold cannot touch. Distinct from §172b-2, which is about two non-constant min/max arms, and from §76's two-constant select, both of which the predicate excludes.)
THE LAW. c-typeck.c:3393/3526/3553 build every C ternary through fold, and fold-const.c:4825 fires when integer_zerop (arg1), or TREE_CONSTANT (arg1) && ! TREE_CONSTANT (op2). It then calls invert_truthvalue (arg0) and rebuilds the node with operands 2 and 1 EXCHANGED. c ? 0 : X and !c ? X : 0 are therefore the SAME TREE before expansion — the ternary has no spelling for a constant fall-through arm. This is a FRONT-END fold: it fires at -O0 as well as -O2, and no register pin, permuter run or declaration-order edit can reach it.
An if/else STATEMENT is not a COND_EXPR at all (the front end goes to stmt.c's expand_start_cond/expand_end_cond), so its arm order survives the tree. Whether it survives to the BYTES is a separate question decided later by the RTL conditional-overwrite collapse (§136d-2 / §164-74): sometimes that pass erases the if/else's arm order too and all three forms re-converge (that is §167-09's case, still valid). The asymmetry that is always true: the if/else can express BOTH arm orders and the ternary can express only one. When arm order is byte-observable, only the if/else can reach the losing order.
THE PREDICATE, byte-measured cell by cell (pinned triple, s32 r, f()/g() calls, label-normalised):
| spelling A | spelling B (arms + condition flipped) | result |
|---|---|---|
r = (f()==1) ? 0 : g(); |
r = (f()!=1) ? g() : 0; |
IDENTICAL — integer_zerop |
r = (f()==1) ? 5 : g(); |
r = (f()!=1) ? g() : 5; |
IDENTICAL — const/non-const |
r = V ? 0 : g(); |
r = !V ? g() : 0; |
IDENTICAL — truthvalue_conversion makes V!=0, invertible |
r = (V==0) ? 0 : 2; |
r = (V!=0) ? 2 : 0; |
IDENTICAL — zero THEN beats both-const |
r = (f()==1) ? 5 : 7; |
r = (f()!=1) ? 7 : 5; |
REAL DIFF — both const, neither zero ⇒ NO swap |
r = (f()==1) ? h() : g(); |
r = (f()!=1) ? g() : h(); |
REAL DIFF — both non-const ⇒ NO swap |
if (V) r=0; else r=g(); |
if (!V) r=g(); else r=0; |
REAL DIFF — statement, never folded |
DIAGNOSTIC TELL. A BRANCH-POLARITY / beq!=bne residual at IDENTICAL instruction count, on a select that lands in a register and feeds a branch, where flipping the ternary's arms changes NOTHING. Length never drifts, so nothing warns you. Do not permute the ternary — respell it as an if/else statement and put the constant arm in the THEN. Conversely, reading a target: a ternary's written arm order carries no information whenever one arm is a constant, so never infer source arm order from the branch polarity of such a select — infer it only from an if/else.
(BOUNDS §167-09; corrects nothing in §3-T4 / §32.2 / §165-23, which are readback rules for if/else statements; evidence: byte-probed, 2 in-tree instances + a 9-cell isolated predicate sweep; from func_8017EB30 (ov_SC05_017) and func_8017C59C (ov_SC03_007))
BOUND. Five bounds, four of them byte-measured:
-
Both arms constant and the THEN arm nonzero ⇒ NO swap; arm order IS observable.
r = (f()==1) ? 5 : 7;vs(f()!=1) ? 7 : 5;compile to a REAL difference (bne/li 7vsbeq/li 5, blocks exchanged). fold-const.c:4826 needs! TREE_CONSTANT (op2), and :4995's second copy needs the same. This is the case §76 andsrc/800.c:14542(((uVar2 & 0x80) == 0) ? 3 : 0) andsrc/resident/resident.c:2657((i != n-1) ? 10 : 0) live in — a two-constant ternary is still a dial. A ZERO THEN arm overrides this (first disjunct is unconditional on op2 — measured:(V==0) ? 0 : 2≡(V!=0) ? 2 : 0). -
Both arms non-constant ⇒ NO swap. Measured REAL DIFF. This is exactly §167-09's own ladder (
(r & 0x2000) ? (r & 0x8000) : h(a0)), which is why §167-09's three-form equivalence was correctly measured and stays valid inside this bound. -
The submitted consequence "the if/else always carries arm order, so ternary ≠ if/else" is FALSIFIED as a general rule. In my isolated probe
if (f()==1) r=0; else r=g();and its swap compile identically up to label numbering (identical bytes) — the RTL conditional-overwrite collapse the submitter "ruled out" DOES fire there, and the ternary agrees with both. Whether an if/else's arm order reaches the bytes is decided later and locally (register pressure, what consumes the value,-Olevel: at-O0the if/else arm order IS observable while the ternary swap still fires). The half that survives unconditionally is one-directional: the ternary can never reach the constant-THEN order; the if/else can reach both. -
The condition must invert to something other than a
TRUTH_NOT_EXPR, orfoldskips the rebuild (fold-const.c:4837). For integer conditions this always holds (comparisons invert directly; a bare operand becomesx != 0via truthvalue_conversion; TRUTH_ANDIF/ORIF de-Morgan). It fails for a non-equality FLOAT comparison (fold-const.c:2019-2021) and for a SAVE_EXPR condition — theoretical on this soft-float target, untested here. -
c ? 1 : 0is a different rule (fold-const.c:5015 "Convert A ? 1 : 0 to simply A") and collapses to the bare condition rather than swapping — do not diagnose those with this law.src/800.c:972,src/ov_SC04_011/...:6483are that shape. -
Scope: the select must be a VALUE (assigned to a local / used as an operand). This law says nothing about a
?:sitting in anif's controlling position — that is §167-09's POSITION law and is untouched.
SECOND INSTANCE. func_8017C59C in src/ov_SC03_007/ov_SC03_007_jr_8017AE2C.c:3515 — a BANKED, byte-matching function (the same clamp idiom is replicated across at least 6 overlays: ov_SC02_000, ov_SC02_004, ov_SC03_003, ov_SC03_007, ov_SC04_021, …). Lines 3561-3566 are four clamps:
cx0 = (cx0 < 0) ? 0 : ((cx0 > 0x3F) ? 0x3F : cx0); /* and cx1, cy0, cy1 */
Zero THEN arm ⇒ the law predicts the written order is inert. I copied the whole TU to .run/harvest_v/adv/TU_orig.c, made TU_swap.c with all four outer ternaries flipped to (cx0 >= 0) ? ((cx0 > 0x3F) ? 0x3F : cx0) : 0;, and compiled both through the pinned cpp | cc1:
TU_orig.s / TU_swap.s: 9111 lines each
diff (whole TU, .file line excluded): RAW IDENTICAL — not even label renumbering
func_8017C59C body extracted (1656 lines): identical
An independent second instance, in a different binary, in a different idiom (a clamp, not a guard), on a comparison condition rather than a call-result equality — and the swap is so total that even the label counter does not move. That is stronger than the primary instance, where the swap is only visible because it collides with the target's own polarity.
Counter-instances that BOUND rather than confirm, also found in the tree: src/800.c:14542 D_800A4F2C = ((uVar2 & 0x80) == 0) ? 3 : 0; and src/resident/resident.c:2657 idx = (i != n - 1) ? 10 : 0; — both two-constant, nonzero THEN, so the fold does NOT fire and their arm order is still a live dial (bound 1).
§195-G — §NEW — TWO ARMS CALLING THE SAME CALLEE MERGE INTO ONE jal UNLESS EACH ARM'S OWN CODE CONSUMES THE RESULT: the C dial that keeps two call sites apart is WHERE the consumer test lives, and the branch SENSE you spell it with is byte-inert (jump.c:1737 canonicalises it)
§NEW — TWO ARMS THAT CALL THE SAME CALLEE MERGE INTO ONE jal UNLESS EACH ARM'S OWN CODE CONSUMES THE RESULT. THE C DIAL IS WHERE THE CONSUMER TEST LIVES, NOT HOW YOU SPELL ITS BRANCH — the two spellings are provably the same RTL.
(the actionable inverse of §8 / L1893-1904 (duplicate-the-call-into-both-arms MERGES), and a third dial in §165-10 / §46-L5's "make the two arms' RTL streams unequal in pure C" family — §165-10 uses the global's address spelling, §46-L5 uses the register, this uses the consumer's placement. Rests on §193-C's scheduled-suffix rule and §162h/§50-B's minimum=1 fall-through path. Refutes the wave-V self-report's "asymmetry is the lever".)
TARGET SHAPE — one conditional, two jal <same callee> sites, differing only in a constant argument (or not even that), each followed by its own test of $v0:
bne $v1,$v0,.L2
nop
jal f ; addiu $a0,$zero,K1 <- arm 1
beqz $v0,.Lzero <- arm 1's own consumer test
j .Lchk
.L2: jal f ; addiu $a0,$zero,K2 <- arm 2 bnez $v0,.Lchk <- arm 2's own consumer test (fall-through to .Lzero)
THE LAW. Write the consumer INSIDE each arm and both call sites survive; hoist it below the join and cross_jump eats one. find_cross_jump (jump.c:2371) anchors on the arms' terminating JUMP_INSNs and walks strictly backward. With the test hoisted, arm 1 is li $a0,K1 ; jal f ending in a simplejump to the join and arm 2 falls through to it: the :1978 minimum=1 path finds the common jal f, stops at the differing li, and merges — the target's two arms collapse to two bare li $a0,K feeding ONE shared jal, with the first li stolen into the bne delay slot. With the test in each arm the two anchors are a conditional branch and a simplejump, the very first backward step mismatches (length 0 < any minimum), and the :1941 condjump path never even runs because jump_back_p (jump.c:1929-1933) is false. The mechanism is that the arms' SCHEDULED SUFFIXES DIFFER; "each arm consumes the result" is the C-level way to force that in this shape.
⚠ THE BRANCH SENSE IS FREE — DO NOT SPEND A PROBE ON IT. if (v != 0) goto chk; (fall through to zero:) and if (v == 0) goto zero; goto chk; compile to byte-identical .text: jump.c:1737's invert-a-cond-jump-that-skips-an-uncond-jump rewrites the symmetric spelling into the asymmetric one before cross_jump ever runs. Write whichever reads better. The wave-V prescription "TO KEEP TWO ARMS' BODIES SEPARATE, MAKE THEIR TERMINATING BRANCHES DIFFER … asymmetric on purpose" is byte-false as a C-level lever — the asymmetry you see in the target is jump1's output, not the source's (§3-T4 / §165-23 / §165-49, read-direction).
DIAGNOSTIC TELL. LENGTH-DRIFT −N where your draft shows TWO consecutive li $aN,K (one of them in a bne/beq delay slot) feeding a single jal, and the target shows two jal <same callee> blocks each followed by a beqz/bnez $v0. Do not reach for a §5a __asm__ barrier, a register pin, or a §165-10 respelling: move the null/value test back INTO the arms. Costed here at −5 instructions when merged (isolated); a cruder variant that also reused the same variable reads −8 because the merged shape additionally lets cse collapse the 0/1 flag — quote −5, never −8, and never as a length law (§193-C bound 4).
BYTE EVIDENCE. func_80180DE0 (ov_SC05_017, 60 ins, banked src/ov_SC05_017/ov_SC05_017_jr_8017AE2C.c:6009-6027), re-run by the adversary against .run/waveV_asm_snapshot/ov_SC05_017/func_80180DE0.s: test-in-arms → MATCH 60; symmetric respelling of arm 2 → MATCH 60, .text byte-identical; test hoisted below the join onto a fresh variable → 55 ins, 38 mismatched, LENGTH-DRIFT/−5, objdump -drz showing ONE R_MIPS_26 func_8012E544 with li a0,867 in the bne delay slot and li a0,869 falling into it. Second function, byte-probed: func_80180AEC (same TU, different signature, target asm/ov_SC05_017/nonmatchings/ov_SC05_017_jr_8017AE2C/func_80180AEC.s) — test-in-arms = 2 jal func_8012E544, instruction-exact against the target through idx 24; test hoisted = 1 jal, LENGTH-DRIFT/−3. Class breadth: 69 target-side instances of the shape across ~30 overlays.
BOUND. BOUNDS — the law does not apply when:
-
The consumer is IDENTICAL in both arms and both arms reach the same continuation the same way. The dial is not "a consumer exists", it is "the scheduled suffixes differ". Here the difference is forced by layout (arm 2 is adjacent to
zero:and falls through; arm 1 is not and needsbeqz ; j). In a shape where both arms' post-call streams and terminators come out identical, the merge still fires and this prescription buys nothing — fall back to §165-10 (respell the global) or §46-L5 (split the registers). -
You try to price it. Measured −5 here, −8 for the variant that reuses the variable (cse folds the flag on top), −3 on the second function. §193-C bound 4 already records +1/0/−1 residuals for the sibling lever. Read the delay slots, the frame size and the
.mask, never the count. -
sched2 relocates the arm's insns. jump2 runs after sched2 (§186), so the invariant is the SCHEDULED suffix. §193-C's own control (
func_801810E4, tail-dup = +4) is the case where a source-level tail duplication FAILED to merge because sched2 moved one copy into the middle. The same hazard runs the other way: a consumer written in an arm can be sunk out of it. -
More than two arms / a switch dispatch is UNSCOPED — inherited verbatim from §193-C. The :1978 path additionally walks
jump_chainfor every other jump to the same label atminimum=2, so an N-arm shape has more merge opportunities than the 2-arm case measured here. (asm/ov_SC06_006/…/func_8017ED24.sshows the shape at FIVErandsites and is the obvious probe, unrun.) -
The branch-sense-is-inert half holds only under jump.c:1737's preconditions: the conditional jump must be IMMEDIATELY followed by the unconditional one (
prev_active_insn (reallabelprev) == insn), withno_labels_between_p, andsimplejump_p (reallabelprev). Put a survivingCODE_LABELbetween them (§194-N) and the transform cannot fire, at which point the two spellings are no longer guaranteed equal. -
CITATION PRECONDITION MISSING FROM §193-C, ADD IT HERE: the :1941 conditional-jump cross-jump path is gated on
! jump_back_p (x, insn)(jump.c:1929-1933) — the label's predecessor must be an OPPOSING jump back. §193-C lists :1941 as "cond jump, minimum=2" with no guard, which over-states how often two conditional arm-terminators are even candidates for merging. -
-O2 / any
optimize > 0.cross_jumpis hardcoded on and-fno-crossjumpingdoes not exist before gcc 3.3 (§5a).
SECOND INSTANCE. Byte-probed second function: func_80180AEC (ov_SC05_017, 47 ins, still unmatched, target asm/ov_SC05_017/nonmatchings/ov_SC05_017_jr_8017AE2C/func_80180AEC.s — two jal func_8012E544 at 80180B0C and 80180B24). I wrote both cells myself (.run/harvest_v/adv/AEC_A.c, AEC_C2.c), single-axis apart (consumer test in the arms vs hoisted below the join onto a fresh variable), different function signature and a different tail from func_80180DE0:
- AEC_A →
objdump | grep -c 'R_MIPS_26 func_8012E544'= 2, andmatch_onereports the first mismatch at idx 25 — the entire two-jalhead region (idx 0-24) is instruction-exact against the target; the residual is amove v0,a0in the unrelated tail. - AEC_C2 → 1
jal, diverging at idx 6 with the same merged fingerprint (li a0,867in thebnedelay slot,li a0,869falling into one sharedjal), LENGTH-DRIFT/−3.
Third, and it SHARPENS the law: func_8018858C (asm/ov_SC03_014/nonmatchings/ov_SC03_014_jr_801848E4/func_8018858C.s, .L801885C8) — two jal func_80184028 sites taking NO argument at all, so the arms are byte-identical up to and including the call; the ONLY thing keeping them apart is each arm's own consumer test on $v0 (addiu $v1,0x18 ; bne vs addiu $v1,0xF ; bne). That proves the split is caused by the consumer and not by the differing li $a0,K the func_80180DE0 exemplar happens to also have.
Class breadth (corpus scan, mine): a scan of all 14,552 asm/**/*.s for "label whose next insn is jal X, with an earlier jal X in the same 16-line window reached by a conditional branch to that label" returns 154 hits, 69 of which have a conditional branch (the consumer test) between the first jal and the label — spread across ov_SC03_001 (×7), ov_SC04_018/019 (×20), ov_SC05_017 (×8), ov_SC06_018/020/022/024/025/032/033, ov_SC02_004/011, ov_SC03_006/007/014/015/029/105, ov_SC05_010/018, md_SC03_078/137/138, md_SC04_029, md_SC05_028. This is a recurring engine idiom, not one function.
§195-H — §165-27g — ON A FIXED-SYMBOL GLOBAL, extern T D[]; + D[0] IS THE CSE RELOAD DIAL, AT ZERO ADDRESSING COST, AND THE ELEMENT COUNT IS INERT (§165-27's T v[2] CAVEAT IS A LOCAL-FRAME FACT AND DOES NOT TRANSFER)
§165-27g — ON A FIXED-SYMBOL GLOBAL THE CSE RELOAD DIAL IS THE DECLARATION extern T D[]; + D[0], IT COSTS ZERO BYTES OF ADDRESSING, AND THE ELEMENT COUNT IS INERT. (SHARPENS §165-27 — which states the dial but whose example, prescription and trap are all $sp-slot, and whose "scalar → T v[2] to make it reload (§162i1: one element collapses)" caveat is a LOCAL-frame fact that does NOT transfer to a global; FILLS IN §193-E BOUND 5 (L18626), which asserts the toolkit applies to a fixed symbol but supplies no spelling and no bytes; CORRECTS the expr.c:4855 line number carried at cookbook L12449 → expr.c:4888.)
Target shape. One global symbol re-loaded with a fresh per-use lui %hi(SYM) / lw %lo(SYM)($x) immediately before each store that goes through the loaded value — no held base register, no la, and interleaved stores to OTHER fixed symbols causing no reload at all.
THE LAW. For a load at a NON-VARYING address, cse's two-level kill (cse.c:1711-1716: p->in_memory && (all || (nonscalar && p->in_struct) || cse_rtx_addr_varies_p (p->exp))) can only fire through the middle disjunct, so /s is the entire dial and the declaration is how you set it:
extern T D[];+D[0]→ constant-index ARRAY_REF → falls intocase COMPONENT_REF:(expr.c:4748, comment at:4746) → the index is folded byplus_constantinto the MEM's own address and thenMEM_IN_STRUCT_P (op0) = 1(expr.c:4888, unconditional) →(mem/s:SI (symbol_ref "D"))→ dies at every varying-address store, one reload per store.extern T D;+D→ plain scalar VAR_DECL →(mem:SI (symbol_ref "D")), no/s, non-varying → survives the whole run of stores; the reloads collapse to one.
Zero cost. RTL-proven, not inferred: cc1 -dr on the two spellings produces byte-identical dumps except the /s bit on the MEMs — same count, same (symbol_ref) address, no la, no extra pseudo. The array spelling buys the reload and pays nothing.
THE ELEMENT COUNT IS WORTH NOTHING. [], [1], [2], [4] are interchangeable — byte-verified on two independent functions. §165-27's T v[2] prescription encodes §162i1/L11380 (layout_type + MIPS_STACK_ALIGN(get_frame_size()): a 1-element LOCAL array collapses to its element's mode and never becomes a frame MEM). A file-scope symbol is always a MEM, so the size is irrelevant. Use [1] freely on a shared symbol — this matters, because T v[2] on a global would be a lie about the symbol's size and can collide with the other TUs' declarations of it (§161c).
DIAGNOSTIC TELL. Your draft emits ONE lui/lw %lo(SYM) where the target emits N, N == the number of pointer stores fed by that value, and the addressing is per-use %lo in both. Change ONE line — the extern — and index [0]. Do not reach for volatile, do not touch the stores, do not respell the load through a struct cast (that emits la and destroys the regime).
THE INVERSE. Target loads once, you emit N ⇒ the symbol is over-declared as an array somewhere in the TU. Flip it to extern T D; and drop the [0].
(evidence: byte-probed + RTL-isolated; from func_8017EA48 (ov_SC03_007, 146 ins) and func_80145CEC (×7 overlays, 127 ins))
BOUND. Five conditions under which it does NOT apply — the first two are corrections to the submitted mechanism text, the third kills the naive generalization.
-
The killing store must be at a VARYING address.
nonscalaris set ONLY insidecse.c:7564'selse if (cse_rtx_addr_varies_p (written))arm — the candidate's "unconditionally" (and §165-27's) is wrong. A store to another FIXED symbol setsvaralone (:7577), leavingall=0, nonscalar=0, and the /s fixed-symbol load SURVIVES it. Empirically corroborated in instance 2's target: ~10 interleavedD_80126BC8/BC0/BB8 = 0x1000fixed-symbol stores cause zero reloads; exactly the 4 pointer stores before thejalcause exactly 4 reloads. So the dial is worth nothing in a block whose only stores are to fixed symbols. -
A QImode store through a pointer, or a store whose address RTL is a bare REG, sets
all=1(cse.c:7574) and flushes EVERYTHING — the scalar spelling reloads too and there is no lever. §165-27's trap, unchanged and still live here. -
The dial is the ARRAY DECLARATION, not "any /s spelling". Verified counter-cell (
.run/harvest_v/ea48_S.c, scalar decl +((struct{s32 w;}*)&D_801270C8)->w): that also grants /s, but it force_regs the address — onela $16,D_801270C8for the whole function, DIFF mine=142 target=146, -4. Once the address lives in a register the load's address VARIES, the third disjunct fires unconditionally, and you are in §193-E/§193-H territory with a different (and worse) shape. Only the array-with-constant-index route keeps the MEM at a bare%lo(sym). -
Constant index only. A variable index (
D[i]) rebuilds the ref as*(&D + i*size), which is a varying address ⇒ dies to every store regardless of declaration, and brings the §48-B/§164-52/L12445 stride-and-held-base regime with it. This law isD[0](or any compile-time-constant index). -
A
jaloutranks the dial.cse_insncallsinvalidate_memory (&everything)on any non-const CALL_INSN (cse.c:7245-7246), so a call reloads both spellings. The dial only discriminates WITHIN a call-free run — infunc_8017EA48the 3 loads that survive the scalar spelling are the call-partitioned ones; only the 3 inside the post-call store run are the dial's.
Not a bound but worth carrying: changing the declaration also flips the same /s bit that drives all three sched.c dependence predicates (§37/§164-70/§16Xy) and §82-2's aliasing. Both instances here happened to be schedule-neutral; re-verify the whole function, not just the load count.
SECOND INSTANCE. func_80145CEC — found by grepping banked C for the shape, not supplied by the submitter. Present in src/ov_SC05_003/ov_SC05_003_after.c:122-233 and byte-identically in ov_SC02_027, ov_SC03_006, ov_SC03_099, ov_SC03_103, ov_SC03_119 (+ ov_SC03_107 / ov_MAIN_012 / ov_SC02_037 / ov_SC07_006/007/010/011 targets).
Target (asm/ov_SC03_107/nonmatchings/ov_SC03_107_jr_8013F350/func_80145CEC.s): FIVE per-use lui %hi(D_80126B78) / lw %lo(D_80126B78)($x) at 80145D40, 80145D50, 80145D70, 80145D94, 80145E08 — one per store routed through the loaded pointer (+0x28, +0x18, +0x1a, +0x1c, +0x2c |=), with the ~10 interleaved fixed-symbol stores causing none.
The banked C declares it extern s32 * D_80126B78[1]; (function-scope) and reads (s32)D_80126B78[0] — a ONE-ELEMENT global array, in production matching source, which independently corroborates the half of the candidate that contradicts §165-27. The very same symbol is declared extern s32 *D_80126B78; (scalar pointer) in ~10 other TUs of the same overlay where it is read once — the dial being used in both directions across one binary.
My A/B on it (.run/harvest_v/inst2_A.c / inst2_B_scalar.c, single axis: the decl plus every [0]):
extern s32 * D_80126B78[1]; + [0] -> MATCH (127 ins)
extern s32 * D_80126B78; + bare -> DIFF mine=122 target=127, 80 mismatched, LENGTH-DRIFT/-5
[] -> MATCH 127 ; [2] -> MATCH 127 ; [4] -> MATCH 127
Same sign, same magnitude (one instruction per collapsed reload), same size-independence, on a function in a different overlay with a different symbol and a different store mix. Two independent instances ⇒ not WEAK.
§195-I — §145(b) AMENDED — the pointer-bump addiu is saved by cse, not combine: ANY second SET of a pseudo in the address's equivalence chain (in-place p += K, §145(b)'s p = r; copy, or an asm re-tie) kills the fold; the pass is cse and the gate is invalidate's reg_tick++
§145(b) — AMENDED AND CORRECTED (adversarial verify, S54). The pass is CSE, not COMBINE, and the cure is one predicate with THREE spellings, not one barrier copy.
(This is an AMENDMENT to §145(b) at docs/matching-cookbook.md L9944-9951 — do NOT bank it as a new §. It corrects §145(b)'s stated mechanism the way §186/§188 corrected theirs: the advice worked, the reason was wrong, and the wrong reason sent later sessions to combine.)
TARGET SHAPE. A base loaded at RUNTIME (lw $rD,K($rS)), then an explicit addiu $rD,$rD,K2 the target KEEPS, then ≥2 MEMs at SMALL displacements off $rD.
THE TELL. LENGTH-DRIFT −1; the missing insn is exactly that addiu; and every one of your MEM displacements is the target's plus K2.
THE LAW. gcc-2.7.2's cse enters the bump's result in the address base's equivalence class, and find_best_addr (cse.c:2622, called on every MEM address from cse.c:5034) + fold_rtx substitute that equivalent into each (plus base disp), producing a legal MIPS K2+disp displacement; the base goes dead and the addiu is deleted. The one thing that stops it is a SECOND SET, before the MEM uses, of ANY pseudo the base's cse equivalence depends on. invalidate (cse.c:1508, REG arm) runs delete_reg_equiv (regno); reg_tick[regno]++; and removes the pseudo's entries; cse_insn's invalidate (dest) at :7269 then leaves sets[i].src_elt == 0 at the :7328 guard (and/or exp_equiv_p's reg_in_table != reg_tick test at :2079 rejects the stale entry, remove_invalid_refs at :947-950). cse holds nothing to substitute, the addiu survives, and every MEM keeps its small displacement.
THREE EQUIVALENT CURES — all byte-verified on TWO functions:
- In-place bump (simplest, zero extra text):
p = *(T**)(q+0x20); p = (void*)((s32)p + K);orp += K;— the ADD's dest is its own src, so the ADD insn is itself the second set. - §145(b)'s copy barrier (the original entry, and it still works):
p = load; r = p + K; p = r;with the MEMs offr— the second set is onp, the pseudo insider's equivalent(plus p K), which is enough to make that entry stale. - An asm re-tie on the base (NEW, §194-K's one-liner, byte-free):
p = (T*)(load + K); __asm__("" : "=r"(p) : "0"(p));.
TWO SPELLINGS THAT LOOK RIGHT AND BYTE-FAIL — do not re-buy:
- §164-13's named temp —
r = load; p = r + K;. Everything is single-set; cse folds. −1 on BOTH instances. §164-13 is anexpand_exprADDRESSING law (expr.c:5359-5375) about(v+C)*Kindices; it does not reach a pointer bump. - The reversed copy —
r = load + K; p = r;. Also all single-set; cse tiespandrinto one class and folds. −1 on BOTH instances. This is NOT §145(b) — §145(b) needs the copy to land on a variable that already had a value.
READ THE PREDICATE, NOT THE SPELLING. Count SETS of the pseudos in the address's dependency chain between the add and the first MEM use. Zero ⇒ cse folds and you lose the addiu. One or more ⇒ it survives. That is the whole law.
FIFTH CONSUMER OF THE "EXTRA SET" KNOB. Add to the family list in §194-K: sched1 birthing boost (§30#3), update_equiv_regs remat (§52b RC-7), cse qty_const operand order (§164-02), sched1's alias oracle (§194-K), local-alloc reg_n_deaths (L3304) — and now cse's MEM-address fold. One source edit, six downstream consumers; expect collateral.
BYTE EVIDENCE (13 variants, 2 functions, all re-run by the verifier). func_80182AC0 (ov_SC05_010, 68 ins) and func_80185A0C (ov_SC03_014, 78 ins) — see reproduction.
RTL PROOF, not inference. cc1 -O2 -dr -dj -ds -dc, dumps in .run/harvest_v/vfy/. All variants carry the standalone (plus … (const_int 96)) at .rtl; only the second-set forms still carry 3 {addsi3_internal} at .cse, and the single-set forms already show const_int 96 inside a (mem …) in a 163 {movhi_internal2} at .cse. The fold completes before combine runs — never route this residual to a combine-side cure.
BOUND. 1. RUNTIME-loaded base ONLY. A &SYM base is the OPPOSITE verdict. When the base's qty carries a constant (a la of a SYMBOL_REF), cse re-derives the folded constant address across the second set via qty_const, so bp -= 0xA in place folds anyway — one of §162l/§48-B's nine banked byte-refutations on func_80189540, together with a register u8 *bp __asm__("$5") pin. (Banked evidence, not re-run here.) The two entries must be read as a PAIR: runtime base ⇒ second set saves the addiu; symbol base ⇒ nothing short of an EBB split saves it.
2. STRAIGHT-LINE ONLY. Inside a loop the bump is a biv and strength_reduce owns it — §164-34 (LUID emission order), §163f2/§164-14 (giv benefit), §145a (anchor choice). Do not reach for this on a loop-tail addiu.
3. The second set must sit BETWEEN the add and the first MEM use, and must survive to cse. A dead self-copy is deleted (§194-K's dead-asm-elision warning applies: grep -c '#APP' the .s; baseline is 1 from common.h). A second set placed AFTER the uses buys nothing.
4. Needs ≥2 MEM uses off the bumped base for the tell to be readable. With one use you still get −1 but the residual is too small to classify confidently.
5. INERT when the pointer VALUE (not just its dereferences) is live. A store of the pointer, a compare, or passing it as an argument has no absolute address form to fold into and forces the addiu regardless — e.g. src/ov_SC02_027/ov_SC02_027_jr_8017D898.c:6243 (p = *(s32*)(a0+0xCC); … p += 8; … *(s32*)(a0+0xCC) = p;). There is nothing to fix there, and the += 8 is carrying no weight. (Same bound §164-52 LAW 1a already states from the other side.)
6. Register pins are byte-INERT on this axis. AC0_base.c (with a $16 pin) and AC0_nopin.c (pin deleted) are both MATCH 68; AC0_pin_merged.c and AC0_nopin_merged.c are both 67/−1/40. §17/§42e do not touch it — do not spend a pin here, and do not read a pin's failure as evidence against this law.
7. Read it as a cse fact, so it CANNOT be undone downstream. Once the constant is inside the MEM at .cse, no combine-, sched- or regalloc-side edit restores the addiu. Conversely, a residual whose -ds dump still shows a live addsi3_internal is NOT this law.
8. Collateral risk (untested by me). Because a second SET also trips reg_n_sets for update_equiv_regs and sched1's alias oracle, applying cure 1 or 3 can move unrelated instructions or change the count. §194-K measured −6 instructions from the identical one-liner on a different base. A length drift after applying this is expected, not a broken draft.
SECOND INSTANCE. func_80185A0C (ov_SC03_014, 78 ins), banked at src/ov_SC03_014/ov_SC03_014_jr_801848E4.c:3079, target .run/waveT_asm_snapshot/ov_SC03_014/func_80185A0C.s. Independently authored by a different agent in an earlier wave; the submitter never mentions it.
Target: lw $s0,0x20($v0) → and $s0,$s0,$v1 (0xFEFFFFFF) → addiu $s0,$s0,0x70 at 0x80185A4C → MEMs at 0x6/0x0/0x1/0x0/0x2/0x3/0x4/0x5($s0) — eight uses, exactly the law's shape. Banked C spells the bump in place: p = (u8*)(*(s32*)(*(s32*)(arg0+0x20)+0x20) & 0xFEFFFFFF); p = p + 0x70;.
I extracted the banked block to .run/harvest_v/B_banked.c and ran the full matrix against the snapshot:
| variant | file | result |
|---|---|---|
| banked (in-place bump + asm re-tie) | B_banked.c |
MATCH 78 |
| in-place bump, asm re-tie DELETED | B_noasm.c |
MATCH 78 |
compound p += 0x70 |
B_pluseq.c |
MATCH 78 |
| merged into one statement (single set) | B_merged.c |
77 ins, −1, 55 mismatched |
| merged single set + asm re-tie | B_merged_asm.c |
MATCH 78 ← the new datum |
§164-13's named temp (r = load; p = r + 0x70;) |
B_tempr.c |
77 ins, −1, 55 mismatched — FAILS |
reversed copy (r = load + 0x70; p = r;) |
B_copybar.c |
77 ins, −1, 55 mismatched — FAILS |
The B_merged.c diff is the predicted tell verbatim: the addiu $s0,$s0,0x70 is gone and the loads read 118(s0) / 112(s0) / 113(s0) / 112(s0) / 114(s0) where the target reads 0x6 / 0x0 / 0x1 / 0x0 / 0x2($s0) — every displacement exactly +0x70.
Two functions, two overlays, two authors, 13 variants, one law. Generality established. The B_merged_asm row is what forced the law to be restated as a predicate on SETS rather than on the p += K spelling.
§195-J — GTE / PsyQ op CALL-vs-INLINE is a PER-SITE SOURCE FACT, not a TU style — both forms coexist in one TU (measured in 2 TUs), and the tell is the target's own opcodes (lwc2/sqr/swc2 vs jal), not the sibling. Costs -8 ins on func_8018505C. Corollary: the game's inline sqr macro emits TWO hazard nops, the SDK's Square0 body emits ONE — so the inline form is provably not Square0 inlined (a second instance of §187's SDK-vs-game GTE nop divergence).
GTE / PsyQ-library op CALL-vs-INLINE IS A PER-SITE SOURCE FACT. A same-TU banked sibling is authoritative for symbol SPELLING, TYPES and FRAME IDIOM (§193-A step 2) — it is NOT authoritative for whether the op is a jal. The original source used both forms of the same operation inside one translation unit, and no -O2 transformation converts either into the other, so the form must be read off the target's own bytes every time.
THE TELL IS ONE BIT, READ IT OFF THE TARGET .s: the target body contains a cop2 opcode (lwc2/swc2/sqr/rtps/…) ⇒ INLINE (__asm__ __volatile__ block); the target body contains addiu $aN,$sp,K + move $a1,$a0 + jal <sym> ⇒ CALL. Do not use the finer "one base register, no jal between the lwc2 group and the swc2 group" test — the single shared base is a cse artifact you get for free (byte-proven: .run/harvest_v/S05C_noshare.c writes "r"(&d.vx) independently in each of the two asm blocks and still MATCHes 139), so it is neither a required reader-side check nor a required C-side prescription.
COST OF GETTING IT WRONG (byte-measured, my own re-run). func_8018505C (ov_SC05_010, 139 ins). Base with the inline block = MATCH 139. Same file with only that block replaced by the sibling's Square0(&d.vx, &d.vx); = 131 ins, LENGTH-DRIFT/-8, 120 mismatched. Arithmetic (coherent, not hand-waved): the inline group is 10 instructions (addiu $v0,$sp,0x20 + 3×lwc2 + 2 hazard nop + sqr 0 + 3×swc2) against the call's 4 (addiu $a0,$sp,K + move $a1,$a0 + jal + delay slot) = -6, plus -2 more because the call's two setup instructions get scheduled into load-delay slots the inline form leaves as nops (target idx 12 and 19). Direction is forced: inline ≥ call, never the reverse.
COROLLARY I DERIVED (not in the submission) — THE NOP COUNT SEPARATES THE TWO FORMS, AND PROVES THE INLINE FORM IS NOT Square0 INLINED. The real library body at asm/nonmatchings/800b_2/Square0.s carries ONE hazard nop between the last lwc2 and sqr 0; every banked game-side inline expansion carries TWO (src/ov_SC01_077/ov_SC01_077_jr_8012ACE0.c:3062-3067, src/ov_SC07_007/ov_SC07_007_jr_80131340.c:1748-1751, src/shared/engine_core.h:87738, :88903, and the 20+ _jr_8012ACE0.c siblings). So the game's inline macro and the SDK's function are different code, not one inlined into the other — cc1-2.7.2 cannot inline across the TU anyway. This is a second, independent instance of §187 (SDK build vs game build disagree on GTE hazard nops), on a different op than §187's GsSortBg. Practical use: a target cop2 stream with 2 nops is game-inline; 1 nop is SDK-compiled library code.
(Fifth construct instance of the cookbook's established per-site meta-law — L12282 guard re-read, L13944 global RMW "read the form off EACH arm separately", L16130 pointer-local, §164-40 macro-vs-static inline. File it there, not as a new mechanism.)
BOUND. 1. It is NOT a codegen law and explains NO residual on a correctly-formed draft. No compiler pass decides it; there is no C dial that converts one form to the other. If your draft already has the target's form and still drifts, this law is silent — go elsewhere.
2. It only bites an agent that never read the target's own .s. Unlike §12282 (guard re-read) or §13944 (global RMW), where BOTH spellings emit plausible-looking integer asm and you must diff carefully, here the wrong form is visible at a glance: the target literally contains sqr 0. The failure mode is sibling-templating without reading the target, and that is the only thing this law prevents. Its value is proportional to how much of the mass lane runs off §193-A same-TU retrieval — not to any codegen subtlety.
3. The -8 magnitude is op-specific and n=1. It is Square0/sqr 0 on a 3-word stack vector only. Other GTE ops have different inline body lengths and different nop counts, and the delay-slot half of the -8 depends on this function's neighbouring lh loads. What is invariant across ops is (a) the SIGN — inline is never shorter than the call — and (b) the residual class: LENGTH-DRIFT with the cop2 group sitting at the drift point. Do not quote "-8" for any other op.
4. The coexistence premise is n=2 TUs, not a survey. Measured by sweeping every .c that contains an inline "sqr 0 for Square0( calls: only src/ov_SC05_010/ov_SC05_010_jr_80181CDC.c and src/ov_SC01_077/ov_SC01_077_jr_8012ACE0.c carry BOTH forms; the seven _jr_80131340.c files carry the inline form plus a declaration only, and ~20 _jr_8012ACE0.c files are inline-only. So mixed TUs are the minority, and "TU-uniform" is in fact the common case — the law is that uniformity is not guaranteed, not that mixing is typical. A same-TU sibling's form is a decent PRIOR; it is just not evidence.
5. It does not devalue same-TU retrieval. §193-A step 2 (same-TU banked body by exact symbol join, 13/21 = 62%) stands unchanged for names, types, struct layout, frame idiom and callee spellings. Exactly one field is excluded: call/inline FORM. Do not let this law be cited as a reason to skip the sibling lookup.
6. The 2-nop-vs-1-nop corollary is verified for sqr only. I did not check rtps/rtpt/ncs/gte_ldv* families. Do not generalise the count to other ops without re-reading each SDK body under asm/nonmatchings/.
7. Not a main claim. Everything measured here is overlay code.
SECOND INSTANCE. FOUND — src/ov_SC01_077/ov_SC01_077_jr_8012ACE0.c, a different overlay and a different TU, both sites BANKED AND MATCHED. I located it by sweeping every .c carrying an inline "sqr 0 for Square0( call sites:
for f in $(grep -rln '"sqr 0' src/ --include=*.c); do echo "$(grep -c 'Square0(' $f) $f"; done | sort -rn
8 src/ov_SC05_010/ov_SC05_010_jr_80181CDC.c <- the submitter's TU
2 src/ov_SC01_077/ov_SC01_077_jr_8012ACE0.c <- INDEPENDENT SECOND INSTANCE
1 src/ov_SC07_011/... (x7, decl-only)
0 src/ov_SC0*/..._jr_8012ACE0.c (x20+, inline-only)
In ov_SC01_077_jr_8012ACE0.c the two forms sit ~600 lines apart in one TU:
- CALL form —
func_80132E6Cat:2469-2478:s32 in[3]; s32 out[3]; … Square0(in, out); return out[0] + out[2];(extern at:2465). - INLINE form — inside the
func_80133CD4region at:3060-3075: the full__asm__ __volatile__("lwc2 $9,0(%0)…nop nop…sqr 0")+swc2block, with different in/out pointers (D_801870C0→D_801870C4) — i.e. a site whereSquare0(D_801870C0, D_801870C4)would have been a perfectly legal spelling and the target chose inline anyway.
This directly answers the submitter's own falsifier ("show a same-TU pair where copying the sibling's form is byte-correct at BOTH sites"): here it provably is not — the two banked-and-matched sites in one TU disagree on form, so no TU-uniform rule can be right. Note also that this second TU's inline site carries the SAME two hazard nops, which is where my Square0.s-vs-game nop-count corollary comes from.
What I could NOT get a second instance of: the COST. func_80132E6C and func_80133CD4 are banked, so their stubs are gone and no target .s exists anywhere under asm/ or .run/ — there is nothing for match_one to diff against, and I will not run a gate. So the -8 measurement remains n=1 function; the coexistence premise is n=2 TUs.
§195-K — At -O2 the §18 array-of-struct lever is a FRAME lever, not a length lever — but only when the N reads carry DISTINCT index expressions; with a SHARED index §18's +2-instruction residual is still alive at -O2 (the submitted "memory-loaded narrow index" precondition and unconditional length-neutrality are both falsified)
§19x — AT -O2 THE §18 ARRAY-OF-STRUCT LEVER IS NOT A LENGTH LEVER — IT IS A FRAME LEVER, AND WHICH ONE IT IS DEPENDS ENTIRELY ON WHETHER THE N READS SHARE ONE INDEX EXPRESSION. (bounds §18 (L1557-1572) to -O0 and to the shared-index regime; adds a SECOND orphan producer that §165-09's recipe table has no row for, and CORRECTS §165-09's "a register-sourced value can never orphan" bound.)
The dichotomy. Reading a global table N times, SYM spelled as an ADDRESS (*(u16*)((u8*)SYM + i*8), SYM[i*4] on a bare u16 SYM[], *(u16*)&SYM[i], a pointer local) vs. as a REFERENCE TREE whose element type supplies the stride (SYMS[i].h, SYM2D[i][0]):
| the N index expressions are… | address spelling costs | reference-tree spelling |
|---|---|---|
| DISTINCT (recomputed per read: memory reload, call return, different params, mutated index) | 8·(N−1) bytes of orphan frame, body BYTE-IDENTICAL | 0 |
| SHARED (one cse-able pseudo) | +2 INSTRUCTIONS (la SYM + addu, then 0($base) ×N), vars unchanged |
0 |
| N = 1 | nothing — the two spellings are byte-identical | 0 |
THE MECHANISM. SYMS[i].h expands with the SYMBOL_REF still inside the MEM — (mem/s:HI (plus (symbol_ref) (reg))) — so %lo folds and no address pseudo is ever minted. The address spelling expands (set r80 (symbol_ref)) ; (set r83 (plus r82 r80)) ; (mem (reg 83)). cse1 collapses the N duplicate (set r (symbol_ref)) to one. If the indices are distinct, combine folds the symbol back into each MEM independently and the shared symbol pseudo is left in no insn — but reg_n_refs was computed by life_analysis (toplev.c:2983) before combine (:3004) and is never recomputed, so alter_reg's reg_n_refs[i] > 0 gate (reload1.c:2309) still fires; regclass converges the cost-free pseudo to ST_REGS with alt NO_REGS (regclass.c:931-949 + reg_class_subunion :229-256), find_reg (global.c:918) cannot hold SImode there, reg_renumber stays −1, and assign_stack_local (mode, total_size, -1) (reload1.c:2349 → function.c:681-685, BIGGEST_ALIGNMENT/BITS_PER_UNIT = 8 via mips.h:1080) hands it 8 bytes nothing addresses. If the indices are SHARED, the address is one pseudo with N real MEM uses, combine cannot fold it away, the base register survives — that is §18's length residual, alive at -O2.
THE ONE-GREP DISCRIMINATOR. cc1 -dl; the orphan line ends ; ST_REGS or none; pointer. The ; pointer suffix separates THIS producer (address pseudo) from §165-09's (SImode extension of a narrow memory load). Count them: N−1 per twice-or-more-read symbol.
THE DIAGNOSTIC TELL. match_one says mine=N ins, target=N ins with the mismatch set consisting ONLY of addiu $sp and $sp displacements off by a multiple of 8, and you are indexing a global. Do not route it to the permuter (residual_class scores it IMM-VALUE [permuter] profile=cse, wrong — same trap §193-I flags). Fix: find the symbol read more than once with a varying index and give it a stride-sized struct/2-D element type. It is a declaration edit.
DISAMBIGUATE FROM §193-I. §193-I's tell is the same uniform $sp shift, but its producer is a DECLARED AGGREGATE LOCAL. If there is no aggregate local in the function, it is this law, and no amount of struct-collapsing locals will fix it.
EXTENSIONS (byte-probed). It applies to STORES as well as reads (SYM[i*4] = k twice → vars=8; SYMS[i].h = k twice → vars=0); to reads of DIFFERENT elements of the same symbol; and with or without an intervening call. It is PER SYMBOL — two different symbols with the same index cost nothing.
BYTE EVIDENCE. func_8018367C (ov_SC06_024, 85 ins, target frame 0x18): banked struct spelling MATCH 85; D_801AC5B2 (read twice, index reloaded from 0x70($s0) each time) respelled *(u16*)((u8*)D_801AC5B2 + i*8) → DIFF 85/85, 6 mismatched, and the 6 are exactly addiu sp,-32 vs -0x18 plus four $sp displacements — zero body change. Second target func_80182868 (ov_SC02_027, banked, 94 ins): symbol read ONCE → the two spellings are byte-identical, vars=0 both. Isolated reducers on the pinned cc1 (.run/harvest_v/verif/v1.c v2.c v3.c, and the submitter's red/r4.c): distinct-index address spelling → vars = 8·(N−1) with exactly N−1 ; pointer orphan lines at N = 2, 3, 4; reference-tree spelling → vars=0 at every N; shared-index address spelling → vars=0 but +2 ins.
BOUND. Where it does NOT apply.
-
Shared index ⇒ the law inverts into §18. If all N reads use ONE index expression that cse can collapse to one pseudo, the pointer-arith spelling keeps a base register (
la SYM+addu) and pays +2 INSTRUCTIONS with vars=0 instead of 8·(N−1) frame bytes.v3.c p5(14 ins, vars=0) vsp6(12 ins, vars=0). Read the target's$spfirst: uniform displacement shift ⇒ this law; an extrala/addupair and0($reg)addressing ⇒ §18's length residual, still live at -O2. -
Naming the index in a local moves you into regime 1, it is NOT a free fix.
v1.c h2(s32 i = *(s16*)(p+0x70); snk(SYM[i*4]); snk(SYM[i*4]);) → vars=0, 0 orphans, but a different body (la+addubase form) thana2/e2. Only the reference-tree spelling is both frame-free and body-identical. -
N = 1 ⇒ no cost at all, either spelling. Byte-proven on a second real target:
func_80182868, ov_SC02_027 — pointer-arith rewrite is byte-identical to the banked struct spelling, vars=0 both. -
Two DIFFERENT symbols ⇒ no cost.
v2/v1 g2x(SYM then SYM2, two memory-loaded indices) → vars=0. The orphan is one shared SYMBOL_REF pseudo, so it is per-symbol, and the bankedfunc_801825A4(five distinct bare u16 symbols,SYM[i*4], frame 0x18 vars=0) is the in-tree witness. -
Not length-neutral in a frameless leaf.
v2.c n1vsn2— if the function would otherwise need no stack frame, the orphan addssubu $sp,8/addu $sp,8, +2 ins. -
The stride must be the element type. The reference-tree escape only works when
sizeof(element)equals the byte stride the target'ssllimplies (§18's construction rule).SYM[i*4]on a bareu16 SYM[]names an address and is in the costed class;u16 SYM[][4]withSYM[i][0]is in the free class. -
-O0 is out of scope, and there the arrow reverses: at -O0 the pointer-arith spelling is +1 INSTRUCTION (no fold, §18's original evidence on
func_8013B7AC) and frame is not the tell. -
Not tested / out of scope: volatile-qualified tables, symbols also written through a pointer alias, and any case where the extra 8 bytes also flips a callee-save decision (then the instruction count moves and you are in §161b/§162n2 territory, per §193-I bound 6).
SECOND INSTANCE. Yes — three, one of them a fresh byte-check I ran myself.
-
func_80182868, ov_SC02_027 (src/ov_SC02_027/ov_SC02_027_jr_8017D898.c:4387-4436, banked, 94 ins). Uses the identicaltypedef struct { u16 v; u16 pad[3]; }stride-8 spelling onD_801AB670/672/674, each read ONCE. I rewroteD_801AB670[i].vto*(u16*)((u8*)D_801AB670 + i*8)and compiled both through match_one's exact flag set: the two.sfiles differ only in the.filedirective — vars=0, 0 orphans, 94 ins on both sides. This is an independent, in-tree byte-proof of the N=1 row of the table and of the claim that the -O2 fold happens in both spellings. -
func_8018367C_801811A4, ov_SC06_022 (src/ov_SC06_022/ov_SC06_022_jr_8017BEBC.c:4396-4421). Family sibling of the candidate's function against a different overlay's symbols (D_801AD46Eread twice at :4415 and :4421), with the sameRow8struct fix already banked and the sameframe 0x20 → 0x18note in its comment. Corroborating, but not fully independent — it is a template remap of the same function. -
func_801825A4(same TU as the candidate's function), the negative witness the submitter cited:SYM[i*4]on five DISTINCT bare u16 symbols, banked at frame 0x18 / vars=0. I did not re-gate it, but myg2xreducer reproduces exactly that shape (two different symbols → vars=0) from first principles.
Plus 11 independent reducer instances I authored (v1.c a2/c2/d2/f2x/i2, v2.c n1/n3/n5/n6/n8/n9, v3.c p1/p3) covering N=2,3,4, loads and stores, four different index provenances, and both the frameless-leaf and the non-leaf cases.
§195-L — The cse store-re-seed does not cross a JOIN LABEL: per-arm stores + a join read keep the reload that one join store deletes (bounds §193-E BOUND 1/BOUND 3 with §48-B's EBB boundary)
§193-E BOUND 1's store-re-seed is CSE-BLOCK-SCOPED: a store inside the arms of an if/else never re-seeds the table for a load at the JOIN. Collapsing per-arm stores into one store after the if/else DELETES the target's reload.
cse_end_of_basic_block scans while (p && GET_CODE (p) != CODE_LABEL) (cse.c:8039). The follow-jumps extension (cse.c:8100-8117) requires LABEL_NUSES (JUMP_LABEL (p)) == 1 plus a preceding BARRIER, so cse can walk INTO an else-arm but a join label (NUSES >= 2) always ends the block. The arms' stores therefore never reach the join's load, and the load is emitted. Long after cse, jump2's cross_jump (jump.c:1978) merges the arms' identical sh suffixes into ONE store sitting at the join — so the emitted store COUNT is 1 either way and carries no information about the C. The reload is the only surviving fingerprint.
THE C DIAL (byte-proven, both directions). Per-arm store + a read at the join => <store> then <load> of the same address. One store after the if/else => cse substitutes the stored register and the load is GONE (the extension the compare/argument needs is done in-register with sll/sll+sra instead).
THE TELL — and its mandatory precondition. Only a store that is the FIRST INSN AFTER A JOIN LABEL and is followed by a same-address, SAME-MODE load is evidence of a per-arm store. Corpus census (all 10137 target .s): 186 store-then-same-address-load adjacencies are NOT label-preceded, 6 are. Adjacency alone proves nothing — sched2 manufactures it. Verified non-join producers of the identical adjacency: an __asm__ __volatile__("" ::: "memory") in the C (func_80184920, ov_SC04_002, sh/lh 0x100); an intervening store to a DIFFERENT address that sched2 relocated into a delay slot (func_80187044, ov_SC02_017, sh/lhu 0x86; and func_8017E8F8's own 0x76 site); a jal between store and read (func_8017DECC, ov_SC01_084, sh 0x44($sp) in the jal delay slot).
CORRECTS §193-E BOUND 3. "A conditional branch does NOT end an interval — cse runs over extended basic blocks" is true for a load sitting BEFORE an if and false for a load at a JOIN, and BOUND 3 as written distinguishes neither. Read it as: the block extends through a not-taken side and (with -fcse-follow-jumps, on at -O2) into a single-use, barrier-preceded taken side — but it stops dead at any label two paths reach.
REPAIR RULE (use this, not the asm oracle). If your draft collapses per-arm stores into one store after the if/else and the target's post-join reload vanishes from your output, put the store back into every arm. cross_jump refunds the duplicated stores; the reload comes back. The converse repair also holds: if the target has NO reload where you emit one, hoist the arms' stores into a single post-join store.
BOUND. Does NOT apply / does not fire:
- No label => no information. The tell requires the store to be the first insn after a join label. 186 of the 192 corpus adjacencies are non-join and are produced by a call, a
"memory"clobber, or an intervening different-address store — all §193-E BOUND 2/BOUND 1-at-a-different-address effects. Never read source order out of post-sched2 adjacency. - Mode must match.
sw K(b)followed bylh/lhu K(b)(func_80182ECC ov_SC06_024 0xE0; func_800D2C0C md_MAIN_003 0x0) is a narrow read of a wide store — cse's table is keyed by mode, so the join spelling would reload too. Those two of the six join-shaped hits carry no information. - Anything between store and read in SOURCE order re-arms the reload even at the join — another store at any address (cse.c:7577 -> :1701, the varying-address disjunct fires unconditionally on a pointer base), a non-const call (cse.c:7246), a
"memory"clobber, orvolatile. I measured the volatile route (.run/harvest_v/e8_probeB_volread.c): a join store +*(volatile s16*)read does emit a reload — but aslhu+sll+sra(124 ins), NOT the target's barelh, so it is distinguishable and is not a way to fake the per-arm shape. - Not length-neutral, and the sign of the drift is not fixed. In func_8017E8F8 the collapse costs +2 (
sll+srasign-extension for the argument). In my micro-probe the collapse is 1 word SHORTER (sh+sllvssh+lh+nop, since only the sign bit is needed forbgtz). The invariant is the LOAD's presence, never the instruction count — do not use this as a length lever. - Only for a store whose address is the one being read. Nothing here touches §193-H (a pointer-derived base is uncacheable across any store/call — that is about the base pointer, not the stored value).
- Untested for jump-table (
jr) case bodies. Their case labels are also NUSES>=1 CODE_LABELs so the same boundary should hold, but cse can never follow a computed jump at all, which is a different (stronger) argument — do not claim it without a probe.
SECOND INSTANCE. YES — and its asm is reproduced from scratch, not just observed.
func_801845A4, ov_SC03_028 (asm/ov_SC03_028/nonmatchings/ov_SC03_028_jr_8017DF98/func_801845A4.s:16-24, unmatched target, so shape-evidence not banked-C evidence):
/* 801845C0 */ bgez $v0, .L801845D4
/* 801845C8 */ lhu $v0, 0xFE($s0) <- arm A
/* 801845CC */ j .L801845E0
/* 801845D0 */ addu $v0, $v0, $v1
.L801845D4: /* 801845D4 / lhu $v0, 0xFE($s0) <- arm B / 801845DC / subu $v0, $v0, $v1 .L801845E0: <- the JOIN / 801845E0 / sh $v0, 0xFE($s0) <- ONE cross_jump-merged store / 801845E4 / lh $v0, 0xFE($s0) <- the surviving reload / 801845E8 / nop / 801845EC */ blez $v0, .L8018460C
I then compiled the two spellings standalone under the pinned triple (.run/harvest_v/vfy2/p_arms.c / p_join.c, cpp + tools/bin/gcc-2.7.2-psx/cc1 -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float), a function that shares nothing with func_8017E8F8:
per-arm store -> $L5: sh $2,254($4) ; lh $2,254($4) ; #nop ; bgtz $2,$L4 (the target shape, insn for insn)
one join store -> $L3: sh $2,254($4) ; sll $2,$2,16 ; bgtz $2,$L4 (reload GONE, folded to the register)
Plus 3 further credible join-shaped corpus hits of the same shape (func_8018214C ov_SC04_011 sh/lh 0xE2; func_80181A7C ov_SC01_084 sh/lh 0xDC($s3); func_80182564 ov_SC04_000 sh/lhu 0x3E($s1)), and 2 rejected by bound 2 (mode-mismatched sw/lh).
Addendum (P31 S63 t5e-t5i, func_8017FA2C): a ?: expression assigned to the field belongs to this section's one-store-at-the-join class — §164-73's 4-spelling ladder already byte-measures "?: inline into the store" and "if/else into a shared local, then one store" as the SAME losing spelling — and §195-L is what that costs when the field is read again in the next statement: the reload dies. Second instance, and the first in QImode at a fixed $sp address (§195-L's own evidence and §193-E BOUND 1's probe are both HImode/SImode through a pointer base). p.c[1].b = (D_800B99DA & 1) ? 0x58 : 0x48; followed by p.c[1].r = p.c[1].g = p.c[1].b >> 2; sends the constant to srl register-to-register (li $v1,0x58 / srl $v0,$v1,2) where the target executes sb $v0,0x56($sp) / lbu $v0,0x56($sp) / srl $v0,$v0,2; re-spelling the single ternary as an if/else with a full store statement in each arm restores the pair. match_one: DIFF mine=198 / target=200, 188 mismatched, class LENGTH-DRIFT/−2 → MATCH (200 ins); whole-binary byte-gate accepted the body, law-1c reloc identity 9/9.
RETRIEVAL NOTE — this is why the addendum exists. The residual does not present as a store/load mismatch; every instruction after the divergence cascades, so match_one classes it LENGTH-DRIFT [structural] and an agent chases schedule/length levers instead of arriving here. Route to §195-L on: mine is 1–2 instructions short AND the target shows a store immediately followed by a same-address, same-mode reload. Per BOUND 4 the count is still not the law (−2 here, +2 in func_8017E8F8) — the LOAD's presence is.
MECHANISM CORRECTION (do not propagate the drafting agent's story). The transcript induces "an if-conversion/CSE-across-memory effect keeps the destination register live and forwards it". That is not a pass. It is cse's same-address store re-seed (cse.c:7577 → :1701) reaching the immediately following read, bounded by cse_end_of_basic_block's CODE_LABEL stop (cse.c:8039) once the stores sit inside the arms — already cited in §195-L/§193-E. Nothing new about ?: is involved beyond which class it lands in.
§195-M — Frame vars is a SEQUENTIAL bump-allocation, not a flat sum: §193-I's CEIL(aggregate,8) term and §165-03/§167-06's 8×orphan term are the SAME frame_offset walk at two different compiler stages, and each stage re-CEILs frame_offset to 8 before it allocates
CORRECTION TO §165-03 (L13734) AND §167-06 (L15163) — vars IS A SEQUENTIAL BUMP ALLOCATION, NOT A SUM. WRITE IT AS A WALK. (merges §193-I's aggregate term into the §165-03/§167-06 equation — §193-I already flags the repair at L18806 but mis-names its target as "§165-34's L15155"; L15155 is §167-06. Fix that pointer too.)
The existing line vars = Σ(declared locals) + 8×(orphans + real spills) is right only when every declared local is already an 8-multiple — which is the case in its own worked example (func_8017C294, 32+32+32+8+8=112), which is why the defect has survived. Two independent frame producers write into ONE frame_offset, at two different stages, and BOTH take assign_stack_local's align == -1 arm (function.c:681-685), which 8-rounds the offset (:697, FRAME_GROWS_DOWNWARD is off — mips.h:1643) and 8-rounds the size. Compute it as a walk, in this order:
off = 0
# stage 1 — expand, DECLARATION order (stmt.c:3411 -> function.c:879, align = BLKmode ? -1 : 0)
for each declared BLKmode local (array >=2 elts, or multi-member struct):
off = CEIL8(off); off += CEIL8(sizeof) # <- CEIL8 of the SIZE: this is the term the old line drops
for each addressable SCALAR, at its FIRST '&' (align == 0 arm, function.c:675-679):
off = CEIL(off, its own align); off += sizeof # NOT rounded up
# stage 2 — reload1.c:657-658, ascending pseudo REGNO, alter_reg(i,-1) -> assign_stack_local(..., -1)
for each orphan pseudo and each real spill:
off = CEIL8(off); off += 8 # <- the CEIL8(off) is what makes a flat sum wrong
vars = CEIL8(off)
WHY A SUM IS NOT ENOUGH. Because stage 2 re-aligns, any 4-byte slack left by stage 1's scalars is swallowed rather than carried. Measured on func_80182ECC (ov_SC06_024, banked, 175 ins): base s32 vec[3]; s32 out[2]; + 1 orphan = 16+8+8 = 32, exact, and the orphan owns sp+0x28..0x2F, addressed by nothing. Add ONE address-taken s32: the sum says 36, the walk and the compiler both say 40 (vec 0x10, out 0x20, scalar 0x28, dead pad 0x2C, orphan 0x30). Add a SECOND address-taken s32: still 40 — it lands free in that pad.
COUNT, DON'T GUESS, BOTH TERMS. Aggregates from the C, CEIL8 each. Orphans in one grep: cc1 -dl then grep -c 'ST_REGS or none' t.i.lreg (§165-03). Real spills from .greg (§167-06).
THE TWO DIALS ARE INDEPENDENT — DO NOT CONFLATE THEM. Aggregate SIZE moves vars; declaration ORDER moves only the displacements. func_80182ECC: out[2]→out[3] gives vars 32→40 and DIFF 175/175, 8 mismatched; swapping the two declarations leaves vars=32 and gives DIFF 175/175, 6 mismatched, all six being $sp displacements. The orphan count is 1 in every variant — the declaration edit never perturbs stage 2. Same result on three more banked bodies (func_8018788C 32→40, func_80181328 24→32, func_8018871C 32→40, orphan fixed at 1 throughout). So: vars wrong ⇒ size/orphan-count problem (§193-I + this); vars right and displacements wrong ⇒ order problem (§163e / §193-I BOUND 7). Never sweep the array size as a blind dial — the walk is closed-form and costs one -dl compile.
BOUND. 1. THE FLAT-SUM SPELLING IS DEAD — the submitter's own falsifier clause is the falsified half. Σ CEIL8(aggregates) + Σ(scalars at own alignment) + 8×orphans under-counts whenever the addressable-scalar sub-total is not an 8-multiple: measured 36 predicted vs 40 actual on func_80182ECC + one address-taken s32. Only the sequential walk is correct. (When there are no addressable scalars, sum and walk coincide — which is why the three submitted variants all fit the flat form.)
-
The
(last-declared absorbed by the total 8-round)parenthetical is redundant and its reason is wrong. Every aggregate, last included, isCEIL_ROUNDed atassign_stack_local. The whole-frame round (reload1.c:660+) is a separate later step that is degenerate here because an 8-aligned orphan or callee-save always follows. Keep §193-I BOUND 2's observation, drop it from this equation. -
BLKmode only. A 1-element array or 1-member struct collapses to the element's mode (§193-I BOUND 4), is not BLKmode, takes the
align == 0arm, and is register-eligible — it contributes 0, notCEIL8(4). Two elements minimum. -
Non-addressable scalars contribute 0. They never reach
assign_stack_local. Only&-taken (or otherwiseTREE_ADDRESSABLE) scalars appear, and they are slotted LAZILY at first use, so they can land after an aggregate declared below them. -
grep -c 'ST_REGS or none'counts orphans only. Real reload spills need.greg. In all ten banked functions I measured the spill term was 0, so the additivity of the spill half is inherited from §167-06'sfunc_8017C294measurement, not re-verified here. -
The conjunction is rare — do not expect to meet it. Across ~1400 extracted top-level functions in five overlay families, exactly ONE banked function (
func_80182ECC) has both a sub-8-granular aggregate and a nonzero orphan. The value of the merged line is that it is correct when you do meet it, not that it fires often. The CEIL term alone fires far more (anys16 v[3],s32 pos[3], odd struct); the orphan term alone fires more often still. -
-O2 MIPS only, and not -O0.
FRAME_GROWS_DOWNWARDoff,BIGGEST_ALIGNMENT 64. -O0 subsegments have a frame-pointer prologue (§116) and are out of scope. Not tested: functions taking aggregates by value, VLAs (stmt.c:3393requiresINTEGER_CSTDECL_SIZE). -
Bank as an EDIT, not a new §. The mechanism is 100% already on disk. What is missing is one repaired formula line in §165-03/§167-06 plus a fixed cross-reference in §193-I ("§165-34's L15155" → "§167-06's L15155"). Giving this a fresh § number would create a fourth place the frame equation is written, which is the retrieval defect it is trying to cure.
SECOND INSTANCE. Found, in banked matching in-tree source, for each half plus three controlled joint deltas.
CEIL half — func_80187ECC (src/ov_SC06_024/ov_SC06_024_jr_80186F00.c:3421, banked, MATCHing — no entry under asm/ov_SC06_024/nonmatchings/). Locals s16 buf[16] (32), s16 rv[3] (6), s32 w[3] (12). Packed sum 50; a flat 8-round of 50 gives 56 by luck, but the placement discriminates: measured $sp slots are 16 / 48 / 56, i.e. rv at 0x30 occupies 0x30-0x35 and w starts at 0x38, so the 6-byte array really does take an 8-byte stride with the pad TRAILING. .frame $sp,88 # vars= 56, orphans 0 = 32+8+16. The function's own in-tree comments already record /* sp+0x30 */ and /* sp+0x38 */ — the law is visible in banked source written before it was stated.
Additivity half — nine more banked functions, formula exact in every one: func_8018788C (8+8+8, orph 1 → 32), func_8018871C (24-byte struct, orph 1 → 32), func_80181328 and func_80188968 (16, orph 1 → 24), func_80163EC8 (8+8, orph 1 → 24), func_80191070 (8+8, orph 1 → 24), func_80136334 (8, orph 2 → 24), func_8013EE10 (8, orph 1 → 16), func_8015094C (48-byte struct + 16, orph 1 → 72).
Joint (CEIL × orphan) — one in-tree instance plus three controlled deltas. In-tree: only func_80182ECC. Deltas on other banked bodies, each a single declaration edit with the orphan count pinned at 1: func_8018788C s32 sp20[2]→[3] gives vars 32→40 (12 CEILs to 16); func_80181328 s32 sp10[4]→[5] gives 24→32 (20 CEILs to 24); func_8018871C + s16 tail[1] inside the frame struct gives 32→40 (26 CEILs to 32).
One near-falsifier, explained and dismissed. My batch first reported func_8015094C as vars=72 against a predicted 88. Cause was my own extractor, not the law: S16 (a 16-byte struct typedef in src/shared/engine_types.h:547) is declared outside the function body, so the standalone compile emitted `S16' undeclared and dropped the local entirely. With that local genuinely absent, 48+16+8 = 72, exact. Recorded because it is the shape a future scan will hit again — always check cc1 stderr for undeclared before treating a frame mismatch as evidence.
§195-N — In a call-bearing chain of N≥2 if (f(...)) return 1; tests closed by return 0;, the LAST test must stay in STATEMENT form — the value form (return f() != 0; / ? 1 : 0 / !!f()) costs +1 j and empties the other N−1 delay slots. The cause is REORG block placement, not jump.c's delete_jump.
§ — IN A CHAIN OF N≥2 if (f(…)) return 1; TESTS CLOSED IMMEDIATELY BY return 0;, WRITE THE LAST TEST AS A STATEMENT. A VALUE RETURN (return f() != 0;, return f() ? 1 : 0;, return !!f();, or a named temp r = f(); return r != 0;) COSTS +1 j AND STRIPS THE li $v0,1 OUT OF THE OTHER N−1 BRANCH DELAY SLOTS. (bounds §167-27, which tests only N=1 and only statement spellings; distinct from "Residual B" L867, same reorg family, opposite direction.)
Target shape — N bnez straight to the epilogue with the constant in each delay slot, and the last test's result normalised with no j:
jal f
…
bnez $v0,.Lepi
addiu $v0,$zero,0x1 <- one per surviving test, IN THE SLOT
…
jal f
…
sltu $v0,$zero,$v0 <- falls straight into the epilogue, NO `j`
.Lepi: lw $ra,…
THE LAW. expand_return funnels return K; through the DECL_RESULT pseudo, so if (t) return 1; return 0; reaches jump.c as x = 0; if (t != 0) x = 1; — case 1 of jump.c's store-flag transform (tools/reference/gcc-2.7.2/jump.c:1139 "That didn't work, try a store-flag insn"; normalizep = 1 at :1193-1197 because cval == const0_rtx and uval == const1_rtx; emit_store_flag at :1210; delete_jump (insn) at :1299). Verified with cc1 -dr -dj: (gtu:SI (reg) (const_int 0)) is absent from the .rtl dump and present in the .jump dump. Written as a value, != 0 is expanded by expr.c's do_store_flag at expand time — the gtu is already in the .rtl dump — and jump.c never sees a conditional jump to transform.
⚠ THE +1 IS reorg, NOT delete_jump. Under -fno-delayed-branch the two forms are the SAME length (24 vs 24 on the N=5 probe) and the shared li $v0,1 block is alive in BOTH. What jump.c's transform actually buys is the block's PLACEMENT: statement form leaves li $v0,1 ; j <epi> mid-function, terminated by an unconditional jump, so reorg steals the li into each bnez delay slot, redirects every branch straight to the epilogue and deletes the block; value form leaves li $v0,1 last, falling through into the epilogue, which reorg cannot consume — the j over it survives and each earlier bnez slot takes its fall-through insn (move $a0,$sX) instead. Read the residual as a delay-slot/reorg residual, not as a comparison residual.
THE DIAGNOSTIC TELL. LENGTH-DRIFT +1; your extra instruction is a j immediately before a sltu $v0,$zero,$v0; the target's bnezes carry addiu $v0,$zero,0x1 in their delay slots where yours carry argument setup. Do not invert the condition (§3-T4/§32.2 are inert here — both forms have identical polarity) and do not respell the comparison: if (f(…) != 0) { return 1; } return 0; and else { return 0; } are BOTH byte-free. Move the return back inside an if statement.
PRECONDITIONS (all byte-probed; outside them the dial is dead and the two spellings are byte-identical):
- N ≥ 2. With one test,
if (f()) return 1; return 0;andreturn f() != 0;are byte-identical — the penalty is paid by the SURVIVING siblingreturn 1s that share the block. - The last returned non-zero constant must be exactly 1.
if (g(a)) return 1; if (g(a+1)) return 2; return 0;vs… return g(a+1) ? 2 : 0;→ identical, 15 insns each.normalizep == 1(uval == const1_rtx) is what splits the two expansions. - The chain's tests must be call-bearing. With plain-variable tests (
if (b) return 1; return c != 0;), leaf OR with a frame, both forms are identical — the earlierbnezslots need a competing fall-through fill for reorg's choice to diverge. - The last
ifmust be immediately followed byreturn 0;. Any statement in between (… if (g()) return 1; h(a); return 0;, e.g.func_801539F8) removes the value form as an option entirely. - The "both forms emit the same
sltu" framing is NOT universal: onfunc_8017EFACthe statement form emits nosltuat all (the outerif+__asm__slider blocks the store-flag transform) and the value form still costs +1 — the +1 is about block placement, not about the compare.
BYTE EVIDENCE. func_8017E2F4 (ov_SC02_039, 77 ins, banked src/ov_SC02_039/ov_SC02_039_jr_8017BEBC.c:3992), 5 calls to func_8012DEB8: statement / != 0-inside-the-if / else { return 0; } → MATCH 77 ×3; return f() != 0; / ? 1 : 0 / !!f() / named-temp → mine=78, target=77, 18 mismatched, LENGTH-DRIFT ×4. Second instance func_8017EFAC (ov_SC03_112, banked): 79 → 80. Generic synthetic (extern s32 g(s32)): N=1 identical, N=2 14→15, N=3 18→19, N=5 26→27; also +1 for a relational last test, mixed sibling constants, and the inverted return 0-chain-closed-by-return 1. Probes preserved under .run/harvest_v/adv/.
BOUND. The law is DEAD (both spellings byte-identical) whenever any of these holds — each byte-probed at .run/harvest_v/adv/probe/:
- N = 1. Single test:
n1_stmt.sandn1_val.sare identical modulo the.filedirective (7 insns each). The submitter's own falsifier (c) — tested and passed. - The last returned non-zero constant is not 1.
ret2_stmt.c/ret2_val.c(return 2vs? 2 : 0) → identical modulo label numbers, 15 insns each; both getsltu ; sll $2,$2,1and both get the optimal delay-slot shape.normalizep == 1requiresuval == const1_rtx(jump.c:1194). - The tests are not call-bearing.
var_stmt.c/var_val.c(leaf, plain-parameter tests) → identical, 4 insns;vf_stmt.c/vf_val.c(a frame from an unrelatedh(a)call, but plain-variable tests in the chain) → identical, 15 insns each, and the VALUE form also reaches the optimalbne ; li 1(slot) +sltufall-through shape. The dial needs a competing fall-through delay-slot candidate at each earlierbnez. - A statement sits between the last
ifand thereturn 0;. Then the value form is not an equivalent rewrite at all (func_801539F8, banked in three overlays, is this shape). -fno-delayed-branch. Both forms are 24 insns on the N=5 probe. This is the bound that proves the cause isreorg, not jump.c — do not go looking for thejin a jump-pass dump.
Also bounded in scope of CLAIM: the mechanism's second half ("delete_jump kills the li block") is FALSE and must not be carried forward; and "both forms emit the same sltu" is false in general (func_8017EFAC).
SECOND INSTANCE. func_8017EFAC — ov_SC03_112, banked/matched at src/ov_SC03_112/ov_SC03_112_jr_8017C294.c:4296, 79 cc1 instructions. Same 5-test func_8012DEB8 chain but a genuinely different enclosing structure: the whole chain sits inside if (*(u8 *)(a0 + 0x74) != 0) { … } with a zero-byte __asm__("" :: "r"(avg)) allocno slider before the trailing return 0;. A/B at .run/harvest_v/adv/efac_stmt.c vs efac_val.c: stmt = 79 insns, 0 j $L, 0 sltu; val = 80 insns, 2 j $L, 1 sltu. This instance is important twice over — it is a second real target function, and it shows the statement form winning even where the store-flag transform never fires (no sltu in the matching form at all), which is what pinned the cause to block placement + reorg rather than to jump.c's store-flag.
Generic (non-family) instances: .run/harvest_v/adv/probe/n{2,3,5}_{stmt,val}.c — extern s32 g(s32), no structs, no register __asm__ pins, no shared callee with the exemplar. 14→15, 18→19, 26→27 with the extra j $L and the li $2,1 leaving the bne delay slot in every case. Three further shape variants (rel_* relational last test, sib2_* mixed sibling constants, inv_* the inverted return 0-chain closed by return 1) each reproduce the +1.
Forward reach in unmatched targets (statement-form tail already visible, so the law tells you what to write): asm/ov_SC03_119/nonmatchings/ov_SC03_119_jr_8017FB84/func_801827A0.s (bnez @801827F8 with addiu $v0,$zero,0x1 in the slot, sltu $v0,$zero,$v0 @8018281C falling into lw $ra @80182820) and asm/ov_SC03_014/nonmatchings/ov_SC03_014_jr_801848E4/func_80188FB4.s (same shape @8018900C / @80189030 / @80189034). Neither shows the value-form j <epi> ; sltu over a live li $v0,1, so the preference is not backwards.
§195-REJECTED — what this harvest did NOT bank (recorded so it is not re-derived)
- §193-E's "the /s toolkit is INERT on a pointer load" is a COUNT-only claim; /s remains the sched1 placement dial for the same varying load — ATTACK 1 (already in the cookbook) LANDED — twice, on both halves of the candidate.
HALF A ("/s is the decisive sched1 PLACEMENT dial that hoists a varying pointer load above a fixed-SYMBOL sw/sh %lo(D_x) store") is §30 verbatim, on the identical shape. docs/matching-cookbook.md L2385: "…zero-offset / bare-deref loads never get /s -> they carry a hard true-dependence on an
- The
((struct{T f;}*)&D_sym)->fload-base address materialization — re-derivation of §42d lever 2, with a false COMPONENT_REF//smechanism — Attack 1 LANDED (re-derivation) and Attack 3 LANDED (misattributed mechanism). Attacks 2, 4, 5 did not land — the observable is real and I reproduced it byte-for-byte.
Attack 1 — already in the cookbook, twice. The submitter's "ruled out" list names §30a-1/L2396, L9119, L2515 and §165-27/§193-E, but misses the § that already owns this exact observable:
- **§42d lever
- "Shipped
register __asm__pins in banked C are byte-inert 3/3; the co-introduced statement split is the lever" (ov_SC05_010, wave V) — ATTACK 1 LANDED (already in the cookbook — every component, including the half the submitter calls novel). Secondary landings on ATTACK 2 (the headline is false as written) and on the mechanism (my own extra A/B shows "inert" is base-relative, which is a banked law).
ATTACK 1 — re-derivation. Named sections, mechanism restated in my own words:
- *"A
register __asm__pi
- Union
{s32 plain; struct{s32 idx:N;} bf;}as an arbitrary-width narrowing dial (sll 32-N ; sra 32-N-log2(elemsize)) — ATTACK 2 (evidence does not say what it claims) and ATTACK 4 (one instance, and even that one does not require the mechanism) both land. The submitter's OWN falsifier condition is met.
The kill. The target pair is sll $v0,$s0,17 ; sra $v0,$v0,13. The submitter reads this as N=15, elemsize 2^(17-13)=16, and concludes only a :15 bitfield can produce it. But the shift ari
- An ordinary reassigned pointer local is an ACCIDENTAL §194-K: one pointer pseudo holding N global addresses is multi-set, and the FIRST
lui/addiumaterialization then emits ~12 slots too early — wri — ATTACK 3 LANDED (misattributed mechanism); ATTACK 1 lands as a consequence.
ATTACK 1 (already in the cookbook?) — PARTIAL HIT that becomes a full hit once the mechanism is corrected. The real mechanism is §30#3 (docs/matching-cookbook.md L2390), "THE BIRTHING-BOOST PROLOGUE-ORDER LEVER": adjust_priority/birthing_insn_p boosts to max_priority any insn whose dest reg is set
- On a fixed-address global,
extern T G[]; G[0] = vis the free/sgrant (store-side substitute for §135-2's anon-struct cast) — ATTACK 1 LANDS (first success ⇒ REJECT) — §176f (docs/matching-cookbook.md:16895-16918, "THE DECLARATION FORM IS A MATCHING LEVER, SO RECONCILE TOWARD THE FORM THE MATCH NEEDS", P31 S52) already states this lever in prescriptive form: "the declaration form is load-bearing codegen, not style … **array is the stronger form — it constrains gcc more, so scalar users can ado - The preheader "hoisted-invariant fence" as a counter for declared pointer locals (ov_SC02_039 / func_8017D484) — ATTACK 4/5 LANDED (decisively), with ATTACK 1 landing on the mechanism half. Attack 3 did NOT land — every gcc citation is real and correctly read.
Attack 1 (already banked) — PARTIAL HIT. The entire causal mechanism is §190-A verbatim (docs/matching-cookbook.md:18257): the three strata, move_movables inserting at loop_start from scan_loop:966, strength_reduce at
- REJECTED — "a $sp+K address passed to N calls is declaration-inert" is falsified inside its own exemplar (the OTHER frame address is a live existence dial, §164-35) — ATTACK 2 LANDED (evidence does not say what it claims), and it landed using the candidate's own stated falsifier.
The falsifier text reads: "Find any function where two $sp+K addresses are passed to >=2 calls and moving one between 'named in a local' and 'inlined at each call site' changes a single byte. One instance kills the bound." That instance is inside the submitted ev
- Loop-only constant: "promote it to a pre-loop local — that stratum choice is the ONLY dial over its preheader position and register" — ATTACK 2 LANDED — via the candidate's OWN falsifier (2). It also has a partial ATTACK-1 hit. Details, in order:
Attack 1 (already in the cookbook?) — PARTIAL HIT, not fatal on its own. The positive half is already banked twice, in two places the candidate did not list under "ruled out":
- §34, L2465 ("Statement-position / type levers"): *"an explicit
u32 pv = uVar1;
🔴 ITS
defROW WAS ADDRESS-KEYED AND WRONG IN THE OVERLAY WINDOW — corrected by §201-A, same session, before wave Z launched. Overlay functions are named by VRAM address and 134 overlays load at the same window, so the index mixed N unrelated functions under one name and §196 ranked that row ABOVE the destination TU. Measured: 3,911 of 9,861 symbols with a definition are defined in >1 binary, 1,219 disagree on ARITY, and 818 of those were a TIE thatmost_commonbroke by sorted-file order (lowest-numbered overlay silently won). On wave Y's binaries, 26 of 65 overlay-window DEF rows (40%) were another overlay's function. Byte cost: applying one row's arity tofunc_8017E83Ctook it from MATCH (114 ins) to 113 / 83 mismatched.tools/decl_prior.pynow keys defs by BINARY and withholds the row with a stated reason when the target's own binary does not define the symbol. Resident/shared/main symbols are fleet-unique and were always fine (0 of 43 wrong).
§196 — PUT ON THE CARD WHAT THE TREE ALREADY KNOWS: the fleet's declaration consensus (P31 S54)
The measurement that chose this lever. Wave V's output tokens, attributed by phase:
| phase | agents | output tokens | share |
|---|---|---|---|
| draft | 70 | 2,748,778 | 88.3% |
| repair | 6 | 262,938 | 8.4% |
| reconcile | 9 | 99,986 | 3.2% |
So the plumbing is already cheap — eliminating declaration conflicts outright would recover ~3%.
The leverage is in what a drafter must GUESS before its first compile, and the wave averaged
9.3 match_one compiles per agent (37 of 85 agents needed 10 or more). Every guess that can be
answered from the tree instead of from a compile is a direct saving.
Two guesses are answered somewhere in the tree and were on nobody's card:
- CALLEE ARITY AND RETURN TYPE. §195-A proved there is no positive tell in the asm — an
argument that dies at the call is allocated straight into
$aN, so its only def is a plain load and every intervening use reads$aN; the def/use walk cannot decide, in either direction. The prescribed procedure is a two-arity A/B, i.e. an extra compile per ambiguous callee. But some other TU has usually banked a caller already:func_8012BEE8is declared('s32', ('s32',))in 4,674 places fleet-wide. - GLOBAL TYPE. Every CONFLICTING-EXTERN drop and the whole Reconcile phase exist because N
drafters independently invent a spelling for one
D_symbol. The fleet has usually settled it.
tools/decl_prior.py builds the index deterministically (4,162 files → 67,094 symbols, 9,739
with a banked DEFINITION) and build_wave_atlas puts the rows for each target's own symbols on its
card, strongest evidence first:
- DEF — a banked definition's own signature. The function exists; this is its real shape.
- TU — the destination TU's own declaration. Authoritative for this draft by wave law 2 whatever the fleet says, because that is the file it has to compile in.
- FLEET — the modal
externspelling with its count, plus its rivals and their counts.
Measured on a control draw: 63 rows over 10 cards, 83% already settled by the destination TU — which is not wasted, because the agent was grepping the TU for each of them by hand. The other 17% is the part with no local answer at all, and that is precisely the §195-A arity guess.
THE GENERAL LAW, third instance in one session. §193-A (seed_ref, the atlas's banked twin),
§194-E (tu_ref, the banked neighbour in the destination TU) and now §196 are the same finding:
the answer was already computed and the card did not carry it. Before adding an agent, an
attempt, or a prompt paragraph, ask what the tree already knows — the card is the cheapest place in
the whole pipeline to put a fact, and every field costs zero tokens per wave forever.
What this is NOT. It is not a struct model, and it does not attempt one. docs/actor-struct.md
already measured that: feeding the recovered ~154-field actor struct to m2c as --context scored
0 better / 10 same / 2 worse on a 12-function sample — type recovery is comprehension, not a
match-rate lever. And p->field is not always byte-equal to *(T *)(p + K) (§183.3: a spelling that
forces &D_x materialises one shared base register and perturbs the whole body). Declarations are
the part that is both cheap to derive and load-bearing for the GATE; the struct model is a
readability project for later, introduced per-adoption behind the byte gate.
§197 — THE WAVE-W HARVEST (P31 S54): 68 index_gap reports -> 4 laws, 3 rejected, 41 already-covered
Fourth harvest of the session, and the first on a wave drawn from the UNKNOWN lever lane (see §198). Yield fell to 4 confirmed from 14 — expected and healthy: the readers were seeded with §193, §194 AND §195, so three sessions of laws were off the table before they started.
⚠ TWO VERIFIERS CONFIRMED THE SAME PHENOMENON WITH CONTRADICTORY PASS ATTRIBUTIONS. Both looked
at lhu ; sll 16 ; sra 16+K vs the target's lh ; sra K; one attributes the merge to cse
(fold_rtx's associative block), the other to combine (preferring the count-merge over forming
lh). They cannot both be right, and R34 forbids banking agreement between disagreeing oracles as if
it were corroboration. What they agree on — the tell, the cure, and the refutation of §136-9's stated
cure — is banked below as ONE entry with the discrepancy named. The C lever is identical either way,
so the open question is attribution only.
§197-A — A NARROW SIGNED MEMORY READ FEEDING A CONSTANT >> LOSES ITS lh, AND THE CURE IS AN ASM RE-TIE (attribution CONTESTED: cse vs combine)
TARGET SHAPE / DIAGNOSTIC TELL (both verifiers agree, byte-probed twice). The target has
lh $r,K(base) followed — with no intervening write of $r — by sra $r,$r,N with 0 < N < 16.
You have lhu $r,K(base) ; sll $r,$r,16 ; sra $r,$r,16+N: LENGTH-DRIFT +1, and your sra count
is exactly the target's plus 16. Do not chase it with casts, widths, or a u16 spelling.
THE CURE (both agree, and both byte-proved it). A zero-byte __asm__ SET that takes the user
shift's operand out of the extension's equivalence class — e.g.
__asm__("" : "=r"(u) : "0"(t)); return u >> 7; — restores lh ; sra 7 (2 ins). One verifier proved
a fresh temp works with no second SET of the original pseudo at all, which refutes the "second SET
of the shifted pseudo" framing; plain-C control flow that makes the operand multi-def does it too.
Controls that are INERT: "" ::: "memory", a bare __asm__ __volatile__(""), an output-only asm on
an unrelated pseudo. One control is WORSE than inert: the input-only anchor
__asm__ __volatile__("" :: "r"(t)) costs +1 by forcing the extension result live.
⚠ THE ATTRIBUTION IS UNSETTLED — DO NOT CITE EITHER PASS AS SETTLED.
- cse (
cse.c:5577-5666,fold_rtx'sfrom_plus:associative block, reached fromcase ASHIFTRTat:5581;lookup_as_function(op0, ASHIFTRT)at:5592). Evidence offered:-dadumps in which the middlesra 16insn is already gone inx.i.csewith itsREG_EQUAL sign_extendnote, andx.i.combineis unchanged on those insns. The source comment at:5582-5588names the case ("the similar optimization done by combine.c only works if the intermediate operation's result has only one reference") and:5641-5656describes this exact sign-extend-then-shift pair. An independent prediction confirmed: the ASHIFTRT clamp makest >> 16andt >> 20both emitsra 31. - combine (
mips.md:2340extendhisi2, whose:2346force_not_memat -O2 means the one-insnextendhisi2_internalis never produced at expand, so combine faces a choice between forminglhand merging the counts). Evidence offered: the expansion order plus the single-def/multi-def fork. Whoever is right, the observable and the lever are the same. Settle it, if it ever matters, by re-running-daon the two spellings and reading which dump first loses the middle insn.
BOTH VERIFIERS BYTE-REFUTE §136 TYPE-FORM RULE 9's CURE. Rule 9 (L8892-8895) says a scratch
vector read back sign-extended-then-shifted "must be u16 v[4], NOT SVECTOR". Built both spellings
in rule 9's own stated context (a stack local filled by an out-param call): u16 v[4] and
SVECTOR v compile byte-identically — lhu 16(sp) ; sll 16 ; sra 23 in both. The declared type
is inert; strike that half of rule 9 and use the re-tie.
§197-B — A REPEATED COMPARE OF ONE VALUE AGAINST ONE CONSTANT IS DELETED BY cse's qty_comparison_code CHANNEL (the non-EQ complement of §165-03) — and a front-end-opaque mask on EITHER compare is a pure-C dial that keeps the target's second branch
§NEW — A REPEATED COMPARE OF ONE VALUE AGAINST ONE CONSTANT IS DELETED BY cse's qty_comparison_code CHANNEL — the non-EQ complement of §165-03, and the case §164-46's "never across a statement boundary" mispredicts. The dial is a front-end-opaque mask on EITHER compare. (NEW channel. §165-03 (L13767) is the code == EQ EQUIVALENCE arm of the SAME function — cse.c:5951 splits the two arms — and §164-46 (L12828) is the RANGE channel; neither covers this, and §164-46's headline predicts the opposite outcome here. Extends and CORRECTS docs/gcc-2.7.2-map/cse_expr.md:389, whose antidote is asm-only and whose recognition tell is byte-refuted.)
Target shape. The same value tested against the same constant TWICE on one straight path — classically an early-out beqz rX and, later, a loop-entry guard beqz rX on the same register. The natural C spelling emits only ONE of them.
THE LAW. On a conditional branch, cse_basic_block calls record_jump_equiv — fall-through at cse.c:7511, taken at :8448. For a non-EQ code (including the NE that a beqz's fall-through implies, and the NE that the fall-through of an if (n == K) implies) record_jump_cond takes the if (code != EQ || FLOAT_MODE_P (GET_MODE (op0))) arm at cse.c:5951, returns at :5961 unless GET_CODE (op0) == REG, and otherwise stores qty_comparison_code[reg_qty[REGNO (op0)]] = code; at :5985. The consumer is fold_rtx at cse.c:5413-5431: it requires GET_CODE (folded_arg0) == REG, looks up reg_qty[REGNO (folded_arg0)], and when comparison_dominates_p (qty_comparison_code[qty], code) holds it returns true/false — the second branch is proved and deleted. The knowledge is keyed on the QUANTITY of the compare's operand register, so the dial is not "hide the test from cse" but "make the two compares reference DIFFERENT quantities."
THE DIAL (pure C, no asm). Wrap EITHER compare's operand in a mask the front end cannot fold to identity: if ((n & 0xFFFF) == 0) or if ((n & 0xFFFF) != 0). The compare's op0 is then the AND-result pseudo, whose quantity either carries no recorded comparison (masking the FIRST test) or is not the quantity that was recorded (masking the SECOND). The dial is symmetric — masking either side works — and that symmetry is what proves the mechanism is the LOOKUP, not the recording.
COST — read this as a gate, not a footnote. The mask emits nothing iff combine can prove the value's nonzero_bits already fit the mask, i.e. the value's single set is an lhu/lbu (or equivalent). Then the mask is byte-free. On an argument, a call return, or an lw, the same dial still works but costs a real andi. Check the provenance before you spend the mask.
THE DIAGNOSTIC TELL. LENGTH-DRIFT/-1 where the missing instruction is a conditional branch whose register is ALREADY tested earlier on the same path, and the residual then cascades far beyond the branch — into the callee-save prologue and the loop preheader — because the surviving guard is what pins the preheader statements below it and fixes callee-saved letter order by definition order. Do not chase that cascade with register pins; restore the branch and the cascade dissolves.
PREFER THE MASK OVER §165-03's ASM. The identity re-tie __asm__ ("" : "=r"(n) : "0"(n)) also works and is codegen-identical on a straight body (byte-checked), but it emits an #APP block — which per the maspsx note (cookbook L2497) blocks the ASPSX delay-slot hop — and it adds a SECOND SET to the pseudo, forfeiting update_equiv_regs' single-set live_length doubling (local-alloc.c:1064, cse_expr.md item 2) and rotating the callee-saved bank. The mask does neither. Reach for the mask first; keep the asm for values you cannot mask.
BOUND. Four bounds, all byte-probed by me under .run/harvest_w/vet/:
-
PROVENANCE GATE (falsifies the "zero bytes" headline). The mask is free only when combine can prove
nonzero_bits(value) ⊆ mask— in practice a single set fromlhu/lbu.b1_arg.c(identical body,nan incomingunsigned intparameter) keeps both branches but pays a realandi v0,s0,0xffff= one instruction. On anlw, a call return, or a computed value the dial costs bytes and stops being a free lever. -
THE MASK MUST BE FRONT-END-OPAQUE.
b2_full.cwith(n & 0xFFFFFFFF)→ ONE branch: fold-const collapsesx & -1toxbefore cse ever runs, so the dial is dead. The mask must be a no-op the FRONT END cannot see but COMBINE can. Same reasoning kills+ 0and| 0. -
ONE cse PATH ONLY (§164-52 lifetime).
e2_label.cinterposes a balancedif (C) {…} else {…}between the two tests; its join label has ≥2 jump references, the cse table resets, and the secondbeqzsurvives with no dial at all (3 conditional branches). If a multiply-referenced label separates the two tests, the guard is already free and adding a mask is a §3-perturbation trap. Same bound as §165-03(a): two sibling tests at the same level are separate PATHS and neither folds the other. -
NOT A ZERO-TEST LAW — but also not an EQ-equivalence law. It widens to any repeated compare against the same constant (
== 5/!= 5reproduced), but the channel isqty_comparison_code, whichcse.c:5951reaches ONLY forcode != EQ. When you are on the TAKEN side of anif (x == K), you are in §165-03's equivalence channel instead — different structure, different antidote (that one merges the operands into one class and reaches stores' SET_SRC too, per §165-43/§167-41). Diagnose which side of the branch you are on before picking the dial.
Not tested, so out of scope: float modes (FLOAT_MODE_P short-circuits the whole arm at :5951), and whether a mask narrower than the value's nonzero_bits (a genuinely semantic mask) behaves the same — that changes program meaning and is not a dial.
§197-C — Fix A1 (operand order) cannot move a commutative destination whose .greg conflict set already contains BOTH operand hard registers — split the accumulate so the destination IS the load's pseudo
§NNN — FIX A1 (OPERAND ORDER) CANNOT MOVE A COMMUTATIVE DESTINATION WHOSE .greg CONFLICT SET ALREADY CONTAINS BOTH OPERAND HARD REGISTERS. SPLIT THE ACCUMULATE SO THE DESTINATION IS THE LOAD'S PSEUDO. (BOUNDS §10 Residual A / Fix A1 (L856-864) on a new axis — the previous bounds are §164-01 (L11876, pointer canonicalisation), §164-02/§165-33 (L11897/L15823, cse const-equivalence) and §189-B (L18217, self-accumulate identity); none covers a fresh destination fed by two ordinary operands. The LEVER is NOT new: it is §164-64's (L13256) "write the product IN PLACE — n = A; n = n * B;", here supplying that entry's missing second instance and a second residual class for it.)
THE SHAPE. t = x + y; (or |, &) where t survives its block. Expand mints three pseudos: (set P1 …x), (set P2 …y), (set P0 (plus P1 P2)). Both operand pseudos are live at the add, local-alloc places them in $v1/$v0, and global.c:668-671 re-marks those placements as live HARD registers before the global conflict scan — so P0's conflict set contains BOTH. Whatever preference it has is then stripped by prune_preferences (global.c:849-862) and find_reg hands it a third register.
WHY OPERAND ORDER IS INERT ON THE DESTINATION. Two independent reasons, and you need both:
combine_regsis out of scope.local-alloc.c:472-477gives a pseudoreg_qty == -2only whenreg_basic_block >= 0 && reg_n_deaths == 1; anything else gets-1.combine_regsbails onreg_qty[sreg] == -1(local-alloc.c:1774) and only ties when it is-2(:1843). The;; N regs to allocate:line in the-dgdump is the free printed test for whether your destination is in scope.- global-alloc's order-sensitive path exists but is symmetric-and-pruned.
set_preferenceis called for EVERY set (global.c:1348), and for a non-copy src it descends into operand 0 (global.c:1546-1547,src = XEXP (src, 0), copy = 0) — so a global destination CAN get a hard-reg preference off(plus P1 P2)'s first addend. It just cannot be granted here, because that register is in the destination's own conflict set. Swapping the addends swaps which operand is in$v0/$v1in lockstep, so the conflict set{2,3}is unchanged. ⚠ Do NOT cite "global.c has no coalescing for a non-copy insn" — that is false and it will send you past the line that actually decides. Read the CONFLICT SET, not the preference line (the §193 lesson at L19701, again).
THE LEVER. t = x; t += y; — the accumulator and the load become ONE pseudo at expand, so the destination's conflict set loses that operand's register and it can take it. t = x; t = t + y; is byte-identical (§189-B: the compound operator is not the ingredient; the SPLIT is).
BYTE EVIDENCE (func_8017F074, ov_SC06_006, 121 ins, banked at src/ov_SC06_006/ov_SC06_006_jr_8017BEBC.c:4067; three r/g/b clamp blocks; all five re-run independently at vet time against .run/waveW_asm_snapshot/ov_SC06_006, files .run/harvest_w/vfy_{A..E}.c):
| variant | spelling | result |
|---|---|---|
| A | tr = a.r; tr += b.r; ×3 (banked) |
MATCH 121 |
| B | tr = a.r + b.r; |
120 ins, 69 mismatched, LENGTH-DRIFT −1 |
| C | tr = b.r + a.r; (Fix A1) |
120 ins, 72 mismatched, same −1 drift |
| D | tr = a.r; tr = tr + b.r; |
MATCH 121 (§189-B: += is not the ingredient) |
| E | B + register s32 tr __asm__("$3") |
121 ins, 3 mismatched, REGALLOC-PERM |
diff vfyB.s vfyC.s is exactly three hunks, all of them the two lbus swapping $3/$2 — the adds are addu $4,$3,$2 in both spellings, and addu $3,$3,$2 in the split. Fix A1 is inert on the DESTINATION, which is the only thing it promises to move; it does permute the operand loads. .greg proof, one number: B/C ;; 77 conflicts: 73 74 77 2 3 29 → 77 in 4; A ;; 77 conflicts: 73 74 77 78 2 29 → 77 in 3. All three read ;; 6 regs to allocate: 79 77 78 93 74 73, so tr is a global allocno in every spelling.
THE DIAGNOSTIC TELL (the expensive half — this is what the entry is worth). Failing the tie makes consecutive accumulate/clamp temps share one hard register (mine $a0/$a0/$v1, target $v1/$a0/$v1). The resulting anti-dependence pins each sb <prev> ahead of the next block's addu, where it fills a load-delay slot the assembler otherwise emits as nop (§188). The residual therefore presents as LENGTH-DRIFT −1/−2 with per-block nops MISSING, which match_one stamps [structural] and which sends an agent hunting for source that does not exist. Concretely, B idx 54: mine sb a0,16(sp) where the target has nop, with the target's sb $v1,0x10($sp) displaced three slots later. When a LENGTH-DRIFT −N sits on top of RGB/clamp-style accumulators, count the accumulators' destination registers before you look for missing statements.
THE PIN IS THE WRONG TOOL, BUT IT IS NOT "WORSE". register s32 tr __asm__("$3") fixes the LENGTH (121 ins) — corroborating the anti-dependence story from the other side — but a pinned pseudo can never be the load's destination, so it converts the drift into a permanent lbu-destination swap ($v0↔$v1) it cannot close: 3 mismatched, stuck. Far closer than the 69 of the unpinned one-expression form, and still not bankable. Split first; if you have already pinned and are stuck at ~3 REGALLOC-PERM on two lbus, un-pin and split.
BOUND. Where it does NOT apply, measured or reasoned:
-
"Global allocno" is SUFFICIENT, NOT the discriminator — do not read it as an if-and-only-if. My probes
.run/harvest_w/vfy2/pL1.cvspL2.cput the commutative destination in a BLOCK-LOCAL pseudo (no;; regs to allocateline at all) and Fix A1 was equally inert:addu $3,$3,$2in both spellings, only the twolbus swapping. Same inqL1/qL2with pre-named operands. The candidate's own falsifier — "a block-local destination where the split fails and A1 succeeds" — is therefore already half-answered in the negative: A1 does not move the destination in the block-local case either. The operative condition is the CONFLICT SET, and a block-local destination born with both operands live has the same symmetric{$v0,$v1}problem. State the law by the conflict set, not by the allocno class. -
It requires BOTH operands live at the insn. If one operand is a constant, a
$zero, an in-place accumulator, or a value already dead, the conflict set is asymmetric and operand order can genuinely reach the destination — that is §10 Residual A's own territory and Fix A1 stays live there.qG1shows the granted case:;; 78 preferences: 3recorded off(plus 77 79)'s operand 0 and honoured. -
It says nothing about pointer adds or symbol bases.
pointer_int_sumcanonicalisation (§164-01, L11876) and cse's const-equivalence swap (§164-02/§165-33, L11897/L15823) each defeat Fix A1 BEFORE allocation, for different reasons; on those shapes this entry's.gregtest will look consistent while the real decider ran two passes earlier. Twozero_extendedu8operands cannot reach either path, which is why they are disjoint bounds. -
It does not apply to a SELF-accumulating statement (
x = b + x) — that is §189-B (L18217), whereoptabs.c:399-421swaps by rtx-pointer identity and the addends are already in the right order. -
The split is not free elsewhere. §NNNb/§164-43 (L12775) is the standing warning: how many named temps an expression is split into is a non-monotone global-alloc pricing dial. Splitting an accumulate you did NOT need to split can move a whole function's allocation. Ablate the split per accumulator, do not sweep it globally.
-
The
[structural]tell is not exclusive. A LENGTH-DRIFT −N with missing load-delaynops can still be genuine missing source, a strength-reduced giv (see theang += 0x100family,src/ov_SC03_014/…:5211), or §78. This entry only earns its place when the drift co-occurs with ≥2 same-shape accumulators sharing one destination register. Check that first; it is one grep of the draft.s. -
Scope of the byte evidence: one banked function. The bound half is verified on four controlled synthetic shapes; the LENGTH-DRIFT/missing-
nopdiagnostic half has exactly one banked instance. Treat the diagnostic as a strong hypothesis at n=1 until a second function confirms it.
§197-REJECTED
- Inside a loop gcc-2.7.2 clears
subtarget, so a compound assignment can never write its own destination in place — different inside a loop, byte-identical outside one — Attacks 1, 2 AND 3 all land. Any one is fatal.
1 — ALREADY IN THE COOKBOOK (§164-80 LAW 1, L13565). The submitter grepped for subtarget and preserve_subexpressions (0 hits, correct) but never grepped for the phenomenon. §164-80 LAW 1 states it verbatim: "t = (t & M) | x; evaluates the inner AND into an ano
- THE ARG-COPY ARITY EXEMPTION DIES AT THE FIRST CALL, NOT AT THE FIRST LABEL — ATTACK 3 LANDED (misattributed mechanism — the pass that actually decides it runs much later), with ATTACK 2 landing on the control half.
- COOKBOOK — did NOT land.
grepfor invalidate_for_call / find_equiv_reg / "first call" turns up §164-61/§16Xd (L13164), §167-23 (L15542), §195-B (L19745), and the L13339 entry (
- Statement order inside a §194-A fence's clean region as a byte-live ordering dial, inert without the fence — ATTACK 4 LANDED — decisively, and it is the candidate's OWN NAMED FALSIFIER ("a case where the fence-free source is already order-sensitive, which would refute the 'inert without the fence' half and reduce this to plain cookbook-index L25"). ATTACK 1 then lands on the residue.
ATTACK 1 — ALREADY IN THE COOKBOOK (LANDS
§199 — THE WAVE-X HARVEST (P31 S54/S55): 63 index_gap reports -> 7 laws, 2 rejected, 56 already-covered
Fifth harvest of the session. Two of the seven correct laws banked EARLIER TODAY (§189-A twice over, from two independent readers), which is the adversarial-verifier design earning its cost: a law written from one wave's evidence is a first draft, and the next wave is its review.
(Process note: this run was killed mid-flight by the session usage limit with 3 verifiers
outstanding. Workflow({scriptPath, resumeFromRunId, args}) replayed the 10 finished agents from
cache and re-ran only the 3 — 398k tokens against the original 1.31M. Third time this recovery has
been used; it is reliable.)
§199-A — §189-A's asm→source inference is byte-FALSE: an interloper between a split constant's lui/ori is a SCHEDULE fact, not a source fact — and the separator is the BIRTHING BOOST, not a "priority floor" (§189-A's split-timing half survives; its "no statement order / no pin" absolute and the candidate's own forward-scheduler narrative both fall)
§NNN — A SPLIT CONSTANT'S lui/ori ARE LUID-ADJACENT BUT FREELY SEPARABLE: an interloper between them is a SCHEDULE fact, never a source fact. (REFUTES the inference direction and the "no statement order / no pin" absolute of §189-A (L18199); §189-A's split-TIMING half survives untouched. Composes §30#3 / §49-variant onto the split pair.)
What survives of §189-A. mips.md:3208's large_int define_split does fire inside sched1's per-block try_split (sched.c:4830, reload_completed == 0), before sched_analyze hands out LUIDs (:2175), so the two halves really are chain-adjacent with consecutive LUIDs. Byte-proof: the same preprocessed input compiled with -fno-schedule-insns emits a single unsplit li $2,0x00101010 and no pair at all. sched1 both CREATES and SEPARATES the pair.
What is FALSE. LUID adjacency does not imply emission adjacency, and an interloper (including a second constant) licenses no conclusion about the source spelling. rank_for_schedule tests INSN_PRIORITY FIRST (sched.c:2395), then the 3-class dependence test (:2399-2422), and reaches the INSN_LUID tie-break only at :2428. §189-A's derivation is scoped to equal-priority insns and its conclusion drops that scope. Worse, the LUID tie-break can never adjudicate the pair at all: they are data-dependent, so they are never on the ready list simultaneously.
The real mechanism (read off -dS, not modelled). gcc-2.7.2's sched1 is a BACKWARD list scheduler (sched.c:3771-3772 "we are traversing the instructions backwards"; :3748 "the first insn scheduled becomes the new tail"), so picked-first = placed-last. The ori is picked early and placed LATE; the lui only enters the ready list after the ori is picked, and then competes at plain priority 1 against everything already waiting. The usual thing that beats it is the birthing boost: adjust_priority (:2507) → birthing_insn_p (:2469) raises to max_priority any SET (REG,…) whose dest has reg_n_sets[i] == 1 (:2490). A split constant can never be birthing — try_split gave its pseudo TWO sets — while an ordinary one-instruction constant next to it is single-set and boosts to 0x7f000001. The boosted neighbour wins the priority test outright and is placed later, i.e. between the halves. (Same reg_n_sets==2 fact as L2503, spent on the scheduler instead of local-alloc.c:1021.)
Ground truth (func_801A5674, md_SC07_003, 123 ins, banked MATCH; -dS bb 16): every insn in the contest is priority = 1 except insn 232 = lui $v0,0x50000000 (pseudo 118, one set) at (7f000001). Pick order T-5 ori(301) → T-8 lui $v0(232) → T-13 lui $v1(300). Emitted: lui v1,0x10 · lui v0,0x5000 · 4× sh zero · sw v0 · lhu v0 · ori v1,v1,0x1010 — from the single statement prim.col[1] = 0x101010; (src/md_SC07_003/md_SC07_003.c:1988).
Corollary — the tell is non-discriminating, and statement order is the actual dial. On this function the one-statement spelling and the two-step spelling (s32 c1 = 0x100000; c1 |= 0x1010;) both return MATCH (123 ins) — identical bytes, so the interloper cannot distinguish them in either direction. What does move the interloper is statement order: hoisting/sinking the neighbouring constant's own statement moves it out of the gap (12 mismatches, lui $v0,0x5000 migrating from idx 91 to idx 98).
Independent instances (2 other binaries): func_801800E0 (ov_SC06_016) — one plain … & 0x7FFFFFFF; statement, halves split by sh+lhu; and, decisively, func_801812FC (ov_SC06_025) — the split constant is 0x88888889, gcc's synthesized magic for the / 0x3C at :4617, a constant no source can spell, halves split by lhu.
Practical rule. Never read a lui/ori gap as evidence about the C. When you actually need the halves adjacent (or apart), reach for the priority dial, not the spelling: kill the interloper's birthing boost (§30#3 re-tie / multi-set / arg-reg pin → reg_n_sets > 1), or move the interloper's defining statement.
BOUND. Where the corrected law does NOT apply, and what it does not claim:
-
It does not touch §189-A's split-timing half.
try_splitatsched.c:4830genuinely precedes LUID assignment at:2175; the halves genuinely are LUID-adjacent. Keep that clause; delete only the inference and the "no statement order / no pin" absolute. -
Adjacency is still the DEFAULT, and that is why §189-A looked right. Separation requires (a) other insns ready in the SAME basic block, and (b) at least one of them beating priority 1 — via the birthing boost, a longer dependence chain, or the 3-class hazard/
potential_hazardreorder. In a block with nothing else to schedule, or where every rival is also multi-set/priority-1, the pair stays adjacent and the LUID tie-break does keep them together. Across the wholewaveXsnapshot corpus only 8 separated pairs exist in 5 functions; most gaps are 1-2 insns. So "adjacent" is weak positive evidence of a quiet block, and "separated" is evidence of a busy one — neither is evidence about the C. -
Pre-reload only.
birthing_insn_preturns 0 whenreload_completed == 1(sched.c:2474), so the boost is dead in sched2. A gap that first appears after reload (sched2/jump2/dbr) is a different mechanism; do not attribute it here. Likewise theswin a branch delay slot is dbr's doing and is irrelevant at sched1 — the candidate's "delay-slot priority floor" story is the falsified half. -
Not about
%hi/%losymbol pairs. Alui/addiu(orlui/ori) pair that resolves to a relocated symbol is thelamacro handled by maspsx/as, not thelarge_intdefine_split; nothing here governs it. The law is scoped to plain-literal splits (splat renders these as(0xNNNNNN >> 16)/(0xNNNNNN & 0xFFFF)). -
It does not say §189-A's prescribed FIX never works. Two source steps + a §30#3 re-tie is still a legitimate dial (it changes
reg_n_setsand cse's remat behaviour), and §189-A reports it moving bytes onfunc_8001BBBC. The claim is narrower and sharper: the asm tell does not license reaching for that dial, and onfunc_801A5674the fix is byte-inert (both spellings MATCH). Treat the two-step spelling as a scheduling lever to TRY, never as a diagnosis to READ. -
Not a claim about which half moves in general. In
func_801A5674theluiis the sunk half because theorifeeds a store scheduled early in the backward pass. A different consumer structure can invert that. The invariant is only: the pair is never simultaneously ready, so LUID never decides between them, and whichever half enters the ready list second competes on priority/hazard against every independent insn waiting. -
Single-compiler scope. All of this is gcc-2.7.2
sched.cas shipped intools/reference/gcc-2.7.2/(verified byte-identical to vanilla per the cookbook's pass-order note). No claim about aspsx/maspsx, which the-fno-schedule-insnscontrol rules out as the separator here.
SECOND INSTANCE. Two independent second instances, in two different binaries from the candidate's, both banked C (I checked there is no INCLUDE_ASM stub for either in its own TU — the only INCLUDE_ASM(func_801812FC) lives in a different overlay, src/ov_SC02_028/ov_SC02_028_jr_8017D898.c:4442):
(a) func_801800E0 — ov_SC06_016. Definition: /home/musashi/bfm-decomp/src/ov_SC06_016/ov_SC06_016_jr_8017C8D0.c:4165. The source writes ONE plain statement at :4208:
*(s32 *)((s32)p + 0x4) = *(s32 *)((s32)p + 0x4) & 0x7FFFFFFF;
Target /home/musashi/bfm-decomp/.run/waveX_asm_snapshot/ov_SC06_016/func_801800E0.s:80-83:
lui $a0,(0x7FFFFFFF >> 16) @80180204 · sh $v0,0x8($s0) · lhu $v0,0x12($sp) · ori $a0,$a0,(0x7FFFFFFF & 0xFFFF) @80180210.
Two interlopers from one plain masking statement.
(b) func_801812FC — ov_SC06_025 (the decisive one). Definition: /home/musashi/bfm-decomp/src/ov_SC06_025/ov_SC06_025_jr_8017BEBC.c:4592. The split constant is 0x88888889 — gcc's synthesized reciprocal magic for the / 0x3C in the statement at :4617 (*(s16 *)(*(s32 *)(a0 + 0x20) + 0x1A) = (0x3C - *(s32 *)(a0 + 0x1C)) * 0x1000 / 0x3C;). There is no source constant 0x88888889 anywhere, so §189-A's conclusion ("the target wrote two source steps") is not just false, it is unsatisfiable. Target /home/musashi/bfm-decomp/.run/waveX_asm_snapshot/ov_SC06_025/func_801812FC.s:51-53:
lui $a0,(0x88888889 >> 16) @801813AC · lhu $v0,0x2C($v1) · ori $a0,$a0,(0x88888889 & 0xFFFF) @801813B4.
(c) Three further separated pairs found by the same sweep, unexamined but same shape: func_801839D0 (ov_SC06_016, 0x60000040, gap 1), func_8018048C (ov_SC06_016, 0x7FFFFFFF, gap 1), func_8017F694 (ov_SC06_016, 0x7FFFFFFF, gap 1).
Sweep script (mine): /home/musashi/bfm-decomp/.run/harvest_x/vscan.py — 8 separated lui/ori literal-split pairs across the whole .run/waveX_asm_snapshot/ corpus.
§199-B — A permutation sweep that holds ANY statement fixed is not a sweep: the statement an agent pins as "obviously load-bearing" is the one carrying the signal, and the partial sweep returns a FLAT residual that reads as proof of order-invariance
§NNN — A PERMUTATION SWEEP THAT HOLDS ANY STATEMENT FIXED IS NOT A SWEEP. The statement an agent pins because it "looks load-bearing" is the one whose position carries the signal, and pinning it at the wrong end returns a residual so FLAT it reads as proof that sched1 normalizes the block. (BOUNDS docs/cookbook-index.md:25, which prescribes the N! brute force without a completeness requirement, and BOUNDS §178-G (L17298), whose "statement-order sweeps are frequently worthless — proven by exhaustion" is exactly the false conclusion an incomplete sweep manufactures. Upstream of §194-A: run the COMPLETE sweep before spending the zero-byte fence.)
THE RULE (one line): when the instruction count already matches and the residual is intra-block store order plus the register assignment falling out of it, the permutation set must include EVERY independent statement in the block — no exceptions, and specifically not the one with memory operands.
THE DIAGNOSTIC TELL (this is the payload). Your sweep comes back with a narrow residual band and no MATCH — a floor of 18 with a spread of 0–2 across 120 orders. That is not order-invariance; it is the signature of a pinned degree of freedom. A complete sweep of the same block spreads 0–23. Flatness is evidence about your sweep, not about the compiler. Measured, two functions:
| function | sweep | result |
|---|---|---|
func_801A2014 (md_SC07_003, 154 ins) |
full 6! | 3 exact orders, closeness spread 0–23 |
| " | mem-stmt pinned LAST (120) | 60×18 + 60×19, spread 1, 0 matches — 18 diffs from a reachable MATCH |
| " | mem-stmt pinned FIRST (120) | 20/21/22 only, spread 2, 0 matches |
func_80182A24 (ov_SC01_077, 158 ins) |
full 6! | 10 exact orders, spread 0–18 |
| " | mem-stmt pinned FIRST (120) | every one of 120 = exactly 18, spread 0, 0 matches |
| " | mem-stmt pinned SECOND (120) | every one of 120 = exactly 18, spread 0, 0 matches |
THE FALSIFIED HALF — do not file it. "Pinning the memory statement makes the answer unreachable" is byte-wrong. On func_80182A24 the answer needs that statement LAST (all 10 exact orders have it there), so a last-pinned sweep would have found it. The two functions want it in opposite places — position 3-of-6 on the exemplar, position 6-of-6 on the second instance. You cannot know which position is right in advance; that is the whole reason the sweep must be complete. Equally, do not file "the memory statement is the only load-bearing one": on the exemplar, swapping the two constant stores f10=0 ↔ f0C=0x7FFF costs 8 diffs, and pinning f08 = i last also hides all three answers (min 18).
MECHANISM (honest state). rank_for_schedule decides on INSN_PRIORITY first (sched.c:2395), then on a dependence class against last_scheduled_insn (:2400-2422), and only then on INSN_LUID — literal source order (:2428); priority is computed at sched.c:1423-1470. The statement with memory operands carries the largest dependence height and therefore the largest reach over the ready-list contest, which is consistent with the measured landscape. This has not been instrumented — no INSN_PRIORITY values were dumped. The rule above stands on the 1,440 measured compiles alone and does not need the mechanism. Pass attribution IS verified: -fno-schedule-insns moves the exemplar's natural order from 154 to 156 insns, so sched1 owns this residual.
COST. 6! = 720 compiles ≈ 4 min at 16–24-way; 7! = 5,040 ≈ 30 min. Past 7 independent statements the exhaustive sweep stops being affordable — that is where §3/§3b's permuter takes over, not before.
(Evidence: func_801A2014 md_SC07_003 154 ins banked, src/md_SC07_003/md_SC07_003.c:784; func_80182A24 ov_SC01_077 158 ins banked, src/ov_SC01_077/ov_SC01_077_jr_80182268.c:3404. 1,440 compiles, independently reproduced 2026-08-18: .run/harvest_x/vsweep.py|vsweep.json, vsweep2.py|vsweep2.json, vattr.py.)
BOUND. Where it does NOT apply.
- Count-exact only. Both sweeps held instruction count constant (154/154 across all 720; 158/158 across all 720). If orders change the count, the residual is not a schedule and a permutation sweep is the wrong tool.
- Straight-line, mutually independent statements only. Field stores to one local aggregate with no data dependence between them. A dependence chain collapses the reachable order set and the sweep degenerates.
- sched1 must own the residual. Verify with
-fno-schedule-insnsfirst (§194-A's attribution step). If-fno-schedule-insns2alone reproduces the target you are in §190-C's sched2 case and the block-order dial is different. - This does not resurrect §178-G's dead sweeps. A block that is genuinely fully DAG-determined stays fully DAG-determined; the claim is only that a flat result from an incomplete sweep is not evidence of that. A flat result from a complete sweep is real evidence — believe it and go looking for an alias or set-count property, exactly as §178-G says.
- N ≤ 7. 8! = 40,320 compiles is not a lever.
- Not a "middle position" law. The two verified instances put the memory statement at opposite positions. Any rule of the form "put the memory statement at position k" is refuted by the pair.
- Not "the memory statement is the unique degree of freedom." At least two other statements in the exemplar block move the residual materially (8 diffs; and an end-pin that hides all matches).
- One idiom family. Both instances are the
func_8012C51Cprim-submit block. The law is not yet tested outside contiguous aggregate-field-store blocks.
SECOND INSTANCE. The submitter's second instance does not exist. Its "wave-X agent self-report ... at a different N" is the SAME function: I read the source gap report in /home/musashi/bfm-decomp/.run/wave_x_gaps.json, and it names prim.f0E / f0A / f10 / f0C — func_801A2014 itself, swept at N=7 (5,040 orders, 12 MATCHes) instead of the submitter's N=6 (720, 3 MATCHes). Presenting the source agent's own report as independent corroboration is the one integrity failure in the submission. (The agent's own conclusion — "the permutation set must include EVERY statement in the block" — is correct and is the half that survives.)
A genuine second instance, found and measured by me: func_80182A24, /home/musashi/bfm-decomp/src/ov_SC01_077/ov_SC01_077_jr_80182268.c:3404 (banked, 158 ins, different overlay, different TU, same func_8012C51C prim-submit idiom). Block: sp.f10=0; sp.fE=0; sp.f0=buf[0]; sp.f2=buf[1]; sp.f4=buf[2]; sp.fC=*(u16*)(*(s32*)(arg0+0x20)+0x12); — five constant/stack-load stores plus exactly one double-deref memory statement (fC). All 720 orders compiled, referenced against the banked natural order's own object via masked_diff.diff_object_object (a banked function's natural-order object IS the target bytes; no snapshot .s exists because banked functions are removed from asm/*/nonmatchings/).
Result — it splits the law:
- Confirms the flatness half, harder than the exemplar.
fCpinned FIRST: all 120 orders return exactly 18, spread 0, zero matches. Pinned SECOND: all 120 return exactly 18, spread 0, zero matches. A perfectly constant residual with the answer 18 diffs away. - Refutes the unreachability half.
fCpinned LAST: contains all 10 exact orders (ABCDEM,ACBDEM,ACDBEM,ACDEBM,CABDEM,CADBEM,CADEBM,CDABEM,CDAEBM,CDEABM). The submitter's own falsifier condition (a) fires. - Confirms the position-dominance half. Min closeness by memory-statement position:
18 / 18 / 16 / 9 / 8 / **0**— monotone, a 18-diff swing, and the mirror image of the exemplar's20 / 19 / 11 / **0** / 17 / 18.
§199-C — A NEGATIVE CONSTANT MULTIPLY ALWAYS TAKES expmed's negate_variant — but whether you ever SEE the neg is decided by COMBINE, and for an EVEN |K| it never disappears
§NNN — A NEGATIVE CONSTANT MULTIPLY ALWAYS TAKES expmed's negate_variant; WHETHER YOU EVER SEE THE neg IS DECIDED BY COMBINE, AND FOR AN EVEN |K| IT NEVER DISAPPEARS. (sharpens §136-7 (L8881) and the §1 swarm bullet at L1047, both of which are sign-blind: §136-7 tells you a literal x*K goes through synth_mult but never that the chain SHAPE differs by sign; L1047's "hand-write the explicit x*2 + x*8 form" is spelling-blind advice that misfires on positive constants.)
Target shape — a shift/subtract chain whose subtract reads the SOURCE register first, then a shift, then a plain addu:
sll $v0, $v1, 3 # v1 = D
subu $v1, $v1, $v0 # D - 8D <-- source reg is operand 1
sll $v1, $v1, 4 # * -112
addu $v0, $v0, $v1
THE LAW. For a CONST_INT multiplier, expand_mult (expmed.c:2136) unconditionally tries synth_mult (&alg2, - val, …) at :2186-2193 and takes negate_variant whenever alg2.cost + negate_cost < alg.cost. For any negative literal the magnitude's chain is the cheap one, so gcc always builds the POSITIVE chain for |K| and emits a neg at :2314. What you finally read in the .s is then decided by combine, not by expmed, and there are exactly three outcomes:
- synth_mult's chain for |K| ends in a SUBTRACT (
alg_sub_t_m2/alg_sub_t2_m/alg_sub_factor) ⇒ combine rewritesneg(A − B)asB − A, the neg vanishes into the chain's own subtract with its operands swapped, and the plain literal already emits the direct negative chain.x * -7→sll 3 ; subu $2,$4,$2. Write the literal. Do not rewrite. - The chain ends in a SHIFT — i.e. |K| is EVEN — or in an ADD ⇒ the neg cannot pass through and survives. If the product is immediately
+=/-=-ed, combine folds it into that operator: the trailingaddubecomessubuand the chain keeps the POSITIVE operand order (8D − D), count-neutral. This is unreachable from any negative literal spelling, and-= x*Kgives the identical result — permuting the two natural spellings proves nothing. Write the shift arithmetic explicitly:(D - D*8) * 16, neverD * -112. - Same as 2 but the product is NOT adjacent to an add/sub (standalone, or feeding a
mult) ⇒ the neg materialises as its ownsubu $rD,$zero,$rSand the negative literal is one instruction longer, not count-neutral.
Measured discriminant (399 cc1 compiles of a + x * -K, K = 2..400): 0 / 200 even K absorb the neg; 99 / 199 odd K do (K ≡ 3 mod 4, plus 45, 93, 189, 213, 345, 381). So the operational rule is: |K| even ⇒ the literal can never produce the direct chain, spell the shifts. |K| odd ⇒ compile the literal FIRST; half the time it is already right, and rewriting it by hand breaks it.
No mirrored rule for POSITIVE constants — but not because "spellings converge". They converge only when your explicit spelling IS synth_mult's own chain: x*240 and (x*16 - x)*16 are byte-identical, while x*10 (sll 2 ; addu ; sll 1) and the hand-written x*2 + x*8 (sll 1 ; sll 3 ; addu) are NOT, and x*256 - x*16 is not x*240. For a positive constant the literal is always the safe spelling; L1047's hand-written form is a lever for forcing a NON-canonical decomposition, not a general cleanup.
DIAGNOSTIC TELL. match_one reports equal instruction counts, a handful of mismatches, and the class ADDRESSING [structural] sig=ADDRESSING/subu!=addu profile=cse — a mislabel that routes you to CSE and addressing levers. Ignore it. Read the two subu operand orders instead: yours subu rD, rSHIFTED, rSRC (= 8x − x, the positive chain) against the target's subu rD, rSRC, rSHIFTED (= x − 8x). Compute the net multiplier; if it is EVEN, the target cannot come from a literal and one character of source (* -112 → (x - x*8) * 16) is the whole fix. The register-role swap on the preceding lui/lh/sll triple is a consequence, not a regalloc permute — do not reach for pins or the permuter.
BOUND. Does NOT apply when:
- |K| is ODD and synth_mult's chain for |K| ends in a subtract — 99 of the 199 odd K in 2..400, i.e. K ≡ 3 mod 4 plus the factor cases {45, 93, 189, 213, 345, 381}. There the plain literal already emits the direct negative chain and hand-writing the shifts is a needless (and often wrong-shaped) edit. Two of the three instances of this asm shape in the whole
asm/tree (func_80183DA0@ 80183E3C,func_8017F4DC@ 8017F5B4) are exactly this case, both ×-127. - The product is not adjacent to an add/sub. Then outcome 3 applies: the negative literal emits a visible extra
subu $rD,$zero,$rSand the counts differ, so the residual is a LENGTH drift, not a same-count operand permute. (func_801844D8is this case — the product feeds amult.) - The multiplier is a non-const local, not a literal — §136-7 owns that (a real
mult $rX,$rY);expand_mult's CONST_INT arm is never entered. - The multiply is DImode, or the mode is wider than
HOST_BITS_PER_INT— expmed.c:2157-2159 setsconst_op1 = 0and :2187's guardHOST_BITS_PER_INT >= GET_MODE_BITSIZE (mode)skips the negate_variant entirely. Not reachable on our SImode/HImode targets, but it is the source-level boundary of the "always". add_variantwins (expmed.c:2196-2199,synth_mult (&alg2, val - 1, …)) — mostly a division-by-constant path; a chain ending in+ op0with a REG_EQUAL note forval-1is that, not this.- The expression is narrowed to HImode before expansion — §136-8's territory; the constant becomes its 16-bit unsigned image and the sign question is moot.
- This is a DIVISION magic multiply (
expand_divmod/choose_multiplier, cookbook L19319+, §167-24) — a different function with its own sign correction; amultagainst alui/orimagic is never this law.
SECOND INSTANCE. Found — asm/ov_SC07_002/nonmatchings/ov_SC07_002_jr_8017C8D0/func_801844D8.s:28-30 (VRAM 80184534-8018453C), a DIFFERENT overlay from the submitter's md_SC07_003:
/* 5C3D4 8018452C 00140600 */ sll $v0, $a2, 16 # sign-extend s16
/* 5C3D8 80184530 03140200 */ sra $v0, $v0, 16
/* 5C3DC 80184534 00190200 */ sll $v1, $v0, 4
/* 5C3E0 80184538 23104300 */ subu $v0, $v0, $v1 # x - 16x <-- source reg first
/* 5C3E4 8018453C 00110200 */ sll $v0, $v0, 4 # * -240
/* 5C3E8 80184540 18004400 */ mult $v0, $a0
Net multiplier −240, EVEN ⇒ case 2/3 of the law. I verified both halves with cc1: int f(int x){return (x-x*16)*16;} emits exactly sll $2,$4,4 ; subu $2,$4,$2 ; sll $2,$2,4 — the target's three instructions — while int f(int x){return x*-240;} emits sll $2,$4,4 ; subu $2,$2,$4 ; sll $2,$2,4 ; subu $2,$0,$2, four instructions with the naked NEG. This function is still unmatched, so it is a live prediction, not a banked proof.
I scanned all 14423 files under asm/ for the shape (sll rD,rS,k immediately followed by subu rX,rS,rD) — exactly 3 hits. The other two (func_80183DA0 @ 80183E3C, func_8017F4DC @ 8017F5B4) are both ×-127, ODD, and are instances of the FALSIFIED half: x * -127 emits sll 7 ; subu $2,$4,$2 from the plain literal. Banked C contains exactly one occurrence of the explicit spelling — src/md_SC07_003/md_SC07_003.c:371, the submitter's own function. So: one banked instance, one predicted instance in another overlay, and two counter-instances that define the bound.
§199-D — A narrow UNSIGNED value compared in an ordered if emits sltiu/sltu; the only dial is the WIDTH of a real object, and a widening (s32) cast is inert — AMENDS §35 (whose stated "only CSE-reuse canonicalizes" mechanism is byte-wrong) and does NOT apply inside a switch
AMENDS §35 (L2483) — the "separate signed-int copy" prescription is right, its stated reason is byte-WRONG, and it has a scope limit §35 never named.
In an ordered comparison expression (< > <= >= in an if/?:/&&), gcc-2.7.2's C front end runs shorten_compare on both operands (c-typeck.c:2456-2470, reached whenever short_compare = 1 is set at :2205/:2284). shorten_compare calls get_narrower, which throws away every widening conversion already present in the operand (fold-const.c:1753-1760, whose comment says so in as many words), redoes the comparison in the narrower type, and takes the UNSIGNED variant if the stripped operand was unsigned. That unsignedp picks the LTU/GEU row over the LT/GE row of cmp_info in config/mips/mips.c:1734-1764.
Therefore:
if (*(u16*)p >= K)/if (*(u8*)p < K)→sltiu/sltu.lhu/lbufeeding a SIGNEDslti/sltis UNREACHABLE from a direct compare of the loaded value.- The dial is the WIDTH OF A REAL OBJECT, in both directions.
s32 t = *(u16*)p; if (t >= K)→slti.s32 t = …; if ((u16)t >= K)→ back tosltiu. A NARROWING cast is a live dial. - A widening
(s32)cast is INERT.if ((s32)*(u16*)p >= K)is byte-identical to the uncast form —get_narrowerstrips it. This is the half a reader will get wrong, and it is the half §35's phrase "a separate signed-int copy" invites you to get wrong. - Width, not naming.
u16 t = *(u16*)p; if (t >= K)is byte-identical to the raw rvalue. A named local buys nothing; a WIDE named local buys everything. - Narrow-unsigned is the trigger, not narrow.
s16 t = …keepsslti— but changes the load tolh, so it is only usable when the target actually haslh/lb. - The mask is a separate, independently-visible axis. Dropping a source
& 0xFFrestores nothing about signedness and costs a target-visibleandi(H_nomask: −1 instruction, LENGTH-DRIFT).
Falsified half of §35, stated plainly: §35 L2483's reason — "since only CSE-reuse canonicalizes signed→unsigned" — is byte-wrong. The canonicalisation is in the C front end, happens with a SINGLE use and no second read (one_use_raw below), needs no CSE, and survives an explicit (s32) cast. Do not go looking for a second use to suppress; there is nothing there to suppress.
Falsified half of the submission: the wide-local prescription is not new (cookbook-index L20 → §35), and "the ONLY C dial is a real assignment into a wide local" is false twice over — a narrowing cast is also a dial, and inside a switch there is no C dial at all.
Reader's rule: target has lhu/lbu then SIGNED slti/slt? First check whether it is a switch dispatch (an addiu $x,$zero,K ; beq staircase around it, or a jtbl). If yes, the signed compare is free — write the natural switch and chase nothing. If it is a real if, the source parked the value in an int/s32 object first; write the assignment, not a cast.
BOUND. HARD BOUND (found by me, not in the submission — and it is where the submitter's own falsifier #2 misfires): the law applies ONLY to ordered comparison EXPRESSIONS, never to switch dispatch.
expand_end_case synthesises its own range/bisection compares in the back end; they never pass through c-typeck.c's short_compare path, so shorten_compare never sees them and the operand's narrow unsignedness is irrelevant. Both rows are then unconditionally SIGNED, and the wide-local dial is completely inert.
Micro-probe (/home/musashi/bfm-decomp/.run/harvest_x/vrf/micro.c), four functions, identical *(u16*)(p+0x34) operand, cases 0/1/2:
m_switch_u16: lhu $3,52($4) ; slt $2,$3,2 <- bare u16 in a SWITCH -> SIGNED
m_if_u16: lhu $3,52($4) ; sltu $2,$3,2 <- bare u16 in an IF -> unsigned
m_switch_s32: lhu $3,52($4) ; slt $2,$3,2 <- wide local, SWITCH -> dial INERT
m_if_s32: lhu $3,52($4) ; slt $2,$3,2 <- wide local, IF -> dial WORKS
Corroborated by banked, byte-matching source: func_801812FC (ov_SC06_025) is switch (*(u16 *)(a0 + 0x34)) — a bare u16 rvalue, no local, no cast — and its target is slti $v0,$v1,0x2. func_801831C8 (ov_SC05_017) goes further and uses a u16 st; NAMED local in the switch, still slti $v0,$v1,0x6.
Why this bound is load-bearing rather than pedantic. I swept every .run/*asm_snapshot/*/*.s for "lhu/lbu whose destination reaches a SIGNED slt/slti within 3 instructions": 30 sites in the snapshots (212 counting asm/). Classifying them, ~24 of 30 are switch dispatch, not law instances. A reader who applies this law to the majority population will hunt for a wide local that the source does not contain and cannot contain. Naming the bound is most of the value.
Two further limits:
==/!=are outside the law.short_compareis set only on the ordered arms; equality compares go theXORrows ofcmp_infoand have no signed/unsigned distinction to pick.func_801828A0's own laterif (*(u16 *)(s1 + 2) == 5)is a bare rvalue and is untouched.- The
s16/*(s16*)escape changes the LOAD. It restoressltibut turnslhu→lh(V5_s16local: 1 mismatched,sig=WIDTH/lh!=lhu). It is only a fix when the target genuinely haslh/lb.
SECOND INSTANCE. Three beyond the submitted pair, two of them outside ov_SC06_016.
1. func_80182E2C — ov_SC03_028, a DIFFERENT overlay, banked and byte-matching, the UNSIGNED row from target-true code. Source at /home/musashi/bfm-decomp/src/ov_SC03_028/ov_SC03_028_jr_8017DF98.c:4425 is a bare u16 rvalue in an ordered if with no local and no mask:
void func_80182E2C(void *a0) {
if (*(u16 *)((s32)a0 + 0x2) < 0x21) {
Compiling that banked TU through the pinned triple gives lhu $2,2($16) ; sltu $2,$2,33. Because the function is banked, that output IS the target bytes — an independent, cross-overlay instance of the exact row the law predicts. (func_80180120 in the same file, if (*(u16 *)(s1 + 0x2) < 0x1B), and func_80183814 in ov_SC07_006, if (*(u16 *)((s32)a0 + 0x34) < 0x11), are two more of the same shape.)
2. func_8017F3B8 — the u8 width, reproduced and IMPROVED. The submitter's I_raw.c changes three things at once. I built the one-token version instead: on the banked file, if (cc < 0xFF) → if ((u8)cc < 0xFF) (W4_castcmp.c). Result: 112 ins vs 112, exactly 1 mismatched, sltiu $v0,$a0,255 where the target has slti $v0,$a0,0xFF. Nothing else moves. Companion W5_s16cc.c (s16 cc) keeps slti — proving the trigger is narrow-AND-UNSIGNED, not narrowness.
3. CSE-free micro-probes (micro2.c) — the airtight refutation of §35's stated reason. Every probe below has ONE load and ONE use, so no CSE and no reuse is possible:
one_use_raw: lhu $2,2($4) ; sltu $2,$2,7 if (*(u16*)(p+2) >= 7)
one_use_cast: lhu $2,2($4) ; sltu $2,$2,7 if ((s32)*(u16*)(p+2) >= 7) <- cast INERT
one_use_wide: lhu $2,2($4) ; slt $2,$2,7 s32 t = *(u16*)(p+2); if (t >= 7)
one_use_u8raw: lbu $2,2($4) ; sltu $2,$2,7 (same law at byte width)
one_use_u8wide: lbu $2,2($4) ; slt $2,$2,7
wide_then_narrow: lhu $2,2($4) ; sltu $2,$2,7 s32 t = …; if ((u16)t >= 7) <- REVERSE dial
§199-E — §189-A BOUNDED AND CORRECTED — the discriminator is INSN_PRIORITY, not "is the interloper a constant": an insn between a lui/ori pair proves NOTHING about the source spelling unless it TIES the ori on priority, and on the pinned -mcpu=3000 triple a dependent load never does
§189-A BOUNDED — AN INTERLOPER BETWEEN lui/ori IS A PRIORITY FACT, NOT A SOURCE-SPELLING FACT. It proves two source steps ONLY if it TIES the ori on INSN_PRIORITY; on the pinned -mcpu=3000 triple a dependent load never ties, so lui K>>16 / lw / ori K&0xFFFF carries ZERO information about the C.
Falsifies §189-A's headline ("an interloper … proves the target wrote TWO source steps"), its inference rule ("if the target shows one there, the target did not write one constant"), and its prescribed fix. §189-A's MECHANISM SENTENCE is the only part that survives, and only when read with its own qualifier doing all the work: "rank_for_schedule's only live discriminator among equal-priority ALU constants is the INSN_LUID tie-break".
Why. rank_for_schedule (sched.c:2385) returns INSN_PRIORITY difference FIRST (:2394), the class-vs-last_scheduled_insn rule second (:2420), and INSN_LUID only as the stable-sort fallback (:2427-2428). mips.md:3208's large_int split does make the halves LUID-adjacent (still true), but LUID adjacency is only enforceable across a priority TIE. priority() is dep-chain depth weighted by function-unit ready-delay: mips.md:153-163 gives a load ready-delay 2 on r3000 (3 otherwise), a store 1, ALU 1, with mips.h:2944 ADJUST_COST zeroing only anti/output deps. For the ubiquitous mask RMW *p &= K:
| insn | chain | priority |
|---|---|---|
lui K>>16 |
→ ori → and → sw | P(and)+2 |
lw |
→ and → sw | P(and)+2 |
ori K&0xFFFF |
→ and → sw | P(and)+1 |
lui and lw tie ⇒ LUID picks lui; then lw outranks ori ⇒ lw is placed next. Result lui / lw / ori from ONE C statement. Recompiling the identical C at -mcpu=4000 (ready-delay 3) makes the lw outrank both halves and hoist ABOVE the lui, leaving them adjacent — proof that ready-delay, not spelling, is the dial.
And "constant" is not the predicate either. q[0] = 4; p[1] &= 0x7FFFFFFF; (ONE source constant) emits li 0x7fff0000 / li 4 / sw / lw / ori 0xffff — a plain li 4 between the halves, put there by statement order alone, because it feeds a store and so does not tie the ori. The same li 4 feeding a call argument (a real tie) stays out in either statement order.
PRESCRIPTION.
- Never read an interloper as a source tell unless you have checked it ties the
orion priority — i.e. it is a single-cycle ALU insn whose dependence chain to the block's tail is exactly as deep as theori's. In practice on this frontier that is almost never: a census ofasm/finds 376lui/X/orisightings, 284 of them loads and 73 delay-slotjal/jfills; ~0 are the second constant §189-A describes. - Write
*p &= K;/*p |= K;/*p -= K;as ONE statement and expect the load to land between the halves. That is the default shape, not a residual. - Do NOT apply §189-A's fix at a load interloper. The split alone is inert (byte-identical stream), and the §30#3 zero-byte re-tie between the halves is an active regression: it perturbs local-alloc and swaps the whole
lui/lw/ori/and/swgroup's registers.
BYTE EVIDENCE (re-run by the verifier, not inherited). func_8017F694 (ov_SC06_016, 60 ins, banked at src/ov_SC06_016/ov_SC06_016_jr_8017C8D0.c:3969), target lui $v1,0x7FFF / lw $v0,0x4($a3) / ori $v1,$v1,0xFFFF at 8017F6D8-E0:
| probe | source form | result |
|---|---|---|
J_694_base.c |
*(s32*)(ptr2+4) &= 0x7FFFFFFF; — ONE statement |
MATCH 60/60 (§189-A predicts impossible) |
L_split_nortie.c |
m = 0x7FFF0000; m |= 0xFFFF; *p &= m; — TWO statements |
MATCH 60/60, byte-identical to J |
K_189A_split.c |
§189-A's full recipe: split + the §30#3 re-tie between the halves | DIFF 60/60, 5 mismatched, REGALLOC-PERM/$v0>$v1>$v0 |
| One-statement and two-statement are INDISTINGUISHABLE in the emitted stream; the re-tie alone (L vs K) is what costs the match. |
MINIMAL STANDALONE REPRO (pinned cc1 only, no BFM headers — .run/harvest_x/minimal/m1.c): f(){p[1] &= 0x7FFFFFFF;} and g(){s32 m=0x7FFF0000; m|=0xFFFF; p[1]&=m;} compile to the same six insns, both with the lw between the halves.
FURTHER INSTANCES (banked, byte-matching). func_8018048C (ov_SC06_016, a->flags &= 0x7FFFFFFF; → lui $a0 / lw $v0,0x4($s1) / ori $a0 at 8018053C); func_80186C4C (ov_SC03_014, 273/273, *(s32*)(r+0x48) = *(s32*)(r+0x48) - 0xC000; → lui v1,0xffff / lw v0,72(s1) / ori v1,v1,0x4000 — already printed inside §16Xb's own byte evidence); func_801839D0 (ov_SC06_016, |= 0x60000040).
WHAT §189-A'S ORIGINAL A/B ACTUALLY SHOWED. Its four-form ladder on func_8001BBBC moved a li 4 — an insn that did tie — so the observation is real; the generalization from it is not. Keep §189-A's ladder as an equal-priority-tie recipe; delete its unconditional headline, inference rule and fix.
BOUND. The corrected law is itself scoped. It does NOT apply, and §189-A's original claim DOES hold, when the candidate interloper ties the ori on INSN_PRIORITY and on class: a single-cycle ALU insn (a li/addiu constant, a move, a shift) whose dependence chain to the block tail is exactly as deep as the ori's. Verified positively: k1/k2 (li 4 feeding a call argument, i.e. a true tie) refuse to enter the pair in EITHER statement order. That is the cell §189-A's func_8001BBBC ladder lives in, and inside it the LUID argument is sound.
Further conditions:
-mcpusensitive. Thelui / lw / orishape is specific to r3000's load ready-delay of 2 (mips.md:157-159). At the generic ready-delay 3 the load hoists ABOVE theluiand the halves go adjacent again. Our pinned triple is-mcpu=3000, so on THIS project the interloping form is the default — but do not carry the shape to a project on another-mcpu.- Only for a load the split constant's consumer also consumes. The
lwmust be an operand of the sameand/or/addutheorifeeds. An unrelated load in the block is ranked by its own chain and may land anywhere. - Delay-slot fills are a different pass. The 73
jal/jinterlopers in the census arereorgartifacts (theoriwas pulled into the delay slot); the pair was adjacent when sched1 finished. Never read those as a scheduling fact at all. - The harm half is site-specific. "The §189-A re-tie costs 5 instructions of drift" is proven at
func_8017F694; the general claim is only that the re-tie is UNNECESSARY at a load interloper (L matches without it) and is an allocation perturbation whose sign must be measured. I did not search for a re-tie placement that recovers K's match. - Says nothing about
%hi/%losymbol pairs — those areLO_SUM, notlarge_int, and are excluded from the census. - Silent on §189-B through §189-E, which were not tested.
SECOND INSTANCE. Four independent instances beyond the submitted one, all banked and byte-matching, spanning three overlays, three different constants and three different operations:
-
func_8018048C(ov_SC06_016, banked atsrc/ov_SC06_016/ov_SC06_016_jr_8017C8D0.c:4303). Source is the single statementa->flags &= 0x7FFFFFFF;. Target.run/waveX_asm_snapshot/ov_SC06_016/func_8018048C.s:51-55—8018053C lui $a0,(0x7FFFFFFF>>16) / 80180540 lw $v0,0x4($s1) / 80180544 ori $a0,$a0,(0x7FFFFFFF&0xFFFF) / and / sw. Same shape, different function, in the SAME overlay, with no re-tie anywhere in the banked C. -
func_80186C4C(ov_SC03_014, 273/273,src/ov_SC03_014/ov_SC03_014_jr_801848E4.c). Source is the single statement*(s32 *)(r + 0x48) = *(s32 *)(r + 0x48) - 0xC000;. Its target stream is quoted verbatim inside the cookbook's own §16Xb at L13145:lui v1,0xffff ; lw v0,72(s1) ; ori v1,v1,0x4000 ; addu v0,v0,v1 ; sw v0,72(s1), explicitly annotated there as "the wholer->0x48 -= 0xC000statement". Different overlay, different constant (0xFFFF4000), different operator (subtract, not mask) — and it has been sitting in the cookbook as a standing counterexample to §189-A since before §189-A was written. -
func_801839D0(ov_SC06_016,src/ov_SC06_016/ov_SC06_016_jr_801839D0.c:2841): single statement*(s32*)(*(s32*)((s32)a0+0x20)+0x4) |= 0x60000040;→.run/waveX_asm_snapshot/ov_SC06_016/func_801839D0.s,80183B1C lui $v1,(0x60000040>>16) / 80183B20 lw $v0,0x4($a0) / 80183B24 ori $v1,.... -
A standalone minimal repro that needs no BFM code at all (
.run/harvest_x/minimal/m1.c, pinned cc1): the one-statementfand the two-statementgproduce byte-identical streams, both interloped.
Target-side census (shape only, unbanked): 376 sightings across asm/, 284 of them loads. func_8017CC44/func_8017CCC4 (ov_SC03_015), func_8017EBD0/func_8017F01C (ov_SC03_092), func_8017E30C (ov_SC06_013), func_80187660 (ov_SC06_029) and many more are the identical lui 0x7FFFFFFF / lw 0x4(base) / ori fingerprint awaiting the same one-statement spelling.
§199-F — §164-36b — THE TARGET-HEAD FENCE IS A DELAY-SLOT THREAD SELECTOR, AND IT ONLY FIRES WHEN mostly_true_jump > 0 (amendment to §164-36; its "−1 instruction" tell is falsified)
§164-36b — AMENDMENT TO §164-36: THE TARGET-HEAD FENCE DOES NOT LEAVE A nop; IT HANDS THE SLOT TO THE FALL-THROUGH THREAD. THE FENCE IS A THREAD SELECTOR, AND IT ONLY FIRES ON A BRANCH mostly_true_jump PREDICTS TAKEN. (amends §164-36 / L12620, which states only the denial half and misnames the pass; the placement warnings at §165-40/L14720 and §194-A/L18943 inherit both fixes.)
THE PASS, corrected. For a CONDITIONAL branch the slot is not filled by fill_simple_delay_slots — its forward scan is fenced off by if (target == 0) break; (reorg.c:3041-3042), false for any JUMP_INSN. It is filled by fill_eager_delay_slots → fill_slots_from_thread, whose scan loop is for (trial = thread; ! stop_search_p (trial, ! thread_if_true) && (! lose || own_thread); …) (reorg.c:3315-3317). stop_search_p (:674-704) returns 1 on ASM_INPUT / asm_noperands (…) >= 0, so an asm as the FIRST insn of the thread ends the loop before its first iteration and the call returns 0 — §164-36's predicate is right, its function name is not.
THE UNSTATED CONSEQUENCE — the actionable half. fill_eager_delay_slots does not stop there:
if (prediction > 0)
{ delay_list = fill_slots_from_thread (…, insn_at_target, …); /* reorg.c:3692 */
if (delay_list == 0 && own_fallthrough) /* reorg.c:3698 */
delay_list = fill_slots_from_thread (…, fallthrough_insn, …); } /* reorg.c:3706 */
So on a two-armed if/else the fence is a THREAD SELECTOR: fence the head of the arm whose insn you do NOT want in the slot, and the OTHER arm's insn appears there.
READ THE TARGET FROM THIS END. The delay slot of a conditional branch holds an insn from the FALL-THROUGH arm while the branch-TARGET block still owns its own first insn ⇒ the original had something at the head of the target block that reorg refused. Add a bare __asm__ __volatile__(""); as the FIRST statement of the target arm. Your draft's tell is the mirror: the slot holds the TARGET block's head insn and that block now opens one insn later.
BYTE EVIDENCE (independently re-run, two functions / three branch sites / two overlays).
func_8017F694, ov_SC06_016, bankedsrc/ov_SC06_016/ov_SC06_016_jr_8017C8D0.c:3945. Fence present → MATCH 60 ins,bgez $v0,.L8017F6D8 ; lui $v1,0x8000(the fall-through|= 0x80000000arm's insn). One line deleted → 59 ins, 45 mismatched, slot holdslui $v1,0x7fffand.L8017F6D8opens onlw.func_8017ED80, ov_SC06_008, bankedsrc/ov_SC06_008/ov_SC06_008_jr_8017C294.c:3853,3873, both sites, same flip — at ZERO length drift.
THE FENCE IS PER-THREAD, NOT A BARRIER. Moving the identical fence to the head of the FALL-THROUGH arm instead is INERT: byte-identical to no fence (59 ins / 45 mismatched). It denies the thread it sits in, nothing else.
⛔ DO NOT USE LENGTH AS THE TELL. §164-36's "+1 instruction" and this exemplar's "−1" are both INCIDENTAL: the displaced insn may or may not land in a load-delay slot the assembler would otherwise fill with nop (§188). func_8017ED80 flips thread at 117 → 117. The invariant is WHICH ARM owns the insn in the slot, never the count.
Also correct the in-source comment at ov_SC06_016_jr_8017C8D0.c:3958-3967: it labels the lever §194-A and credits it a second job, "pins the arm's first sched1 group". Byte-checked false — the else-arm's internal order (lw / ori / sh / and / sw) is identical with and without the fence; the only change is the hoist. This is reorg, one mechanism, one axis. (Its "drop it and the diff is 7" is also stale; the measured residual is 45.)
BOUND. 1. THE HARD GATE — it only works on a branch mostly_true_jump predicts TAKEN, and this is byte-proven. fill_eager_delay_slots only tries the target thread first (and therefore only falls back to the fall-through) when prediction > 0 (reorg.c:3690). When prediction <= 0 the fall-through is tried FIRST (:3714-3722) and a fence at the target head changes nothing. I inverted func_8017F694's test so the same two arms compile to bltz instead of bgez (.run/harvest_x/ZZ_inv_fenced.c vs ZZ_inv_nofence.c): the .s is character-identical with and without the fence — the lever is completely inert. From reorg.c:1397-1418: NE ⇒ 1, GE/GT-vs-0 ⇒ 1, unconditional ⇒ 1, loop-top ⇒ 2 (:1360), loop-test ⇒ 1; EQ ⇒ 0, LE/LT-vs-0 ⇒ 0. In MIPS terms: bgez / bgtz / bne — lever LIVE. beq / beqz / blez / bltz — lever DEAD. (Plus two overrides ahead of that table: rare_fallthrough - rare_dest at :1377-1391, and LABEL_OUTSIDE_LOOP_P ⇒ -1 at :1343-1350.)
2. THE FALLBACK IS CONDITIONAL ON own_fallthrough — denial with NO guaranteed substitute. reorg.c:3698 is if (delay_list == 0 && own_fallthrough). If the fall-through block is entered by a second edge (own_thread_p false, LABEL_NUSES != 1, :2151-2170) there is no second try and you get a bare nop. This is the submitter's own suspected weak point; I did not construct the case, so treat "the slot gets filled from the other arm" as conditional on a singly-entered fall-through.
3. The substitute is not guaranteed to be the fall-through's FIRST insn. The scan continues past ineligible insns while own_thread holds; in func_8017F694 the winner (li $3,0x80000000) is the fall-through arm's SECOND insn — the first (lw $2,4($7)) sets the branch's own condition register and is refused. Predict "some insn from that arm", not "that arm's head".
4. Zero length authority. See the amendment — do not route by ±1.
5. Not a sched1 lever. The arm's internal statement order is untouched. If your residual is an order problem inside the arm, this is the wrong section (§194-A / §165-40).
6. Placement is strictly the block HEAD. stop_search_p halts at the FIRST asm; a fence lower in the target block leaves everything above it stealable.
7. Target-scoped: gcc-2.7.2 -O2 -mips1 -mcpu=3000, the pinned triple. -fno-delayed-branch erases the whole effect.
SECOND INSTANCE. func_8017ED80 — ov_SC06_008, banked at src/ov_SC06_008/ov_SC06_008_jr_8017C294.c (declared aF8017ED80 via __asm__("func_8017ED80") at :3830), with the fence at :3853 and :3873. Two independent branch sites in one body, same if (*(s32*)(*(s32*)(a0+0x20)+0x4) < 0) { … |= 0x80000000; } else { fence; … &= 0x7FFFFFFF; } shape as the exemplar but in a different overlay, different TU, different surrounding code, and with a register u32 val __asm__("$2") pin in the other arm.
Reproduction (no target .s exists — the function is banked, so its asm is out of asm/nonmatchings/; the banked C IS the target by the whole-binary gate, and the A/B is therefore against the banked form):
cp src/ov_SC06_008/ov_SC06_008_jr_8017C294.c .run/harvest_x/ZZ_ed80_full.c
sed '3853d;3873d' .run/harvest_x/ZZ_ed80_full.c > .run/harvest_x/ZZ_ed80_nofence.c
bash .run/harvest_x/cc1s.sh <each> | awk '/\.ent[ \t]+func_8017ED80/,/\.end[ \t]+func_8017ED80/'
Banked: bgez $2,$L233 ; li $3,-2147483648 and $L233: → #APP / #NO_APP / li $3,0x7fff0000 / lw $2,4($5) / ori / and.
Fences deleted: bgez $2,$L233 ; li $3,0x7fff0000 and $L233: → lw $2,4($5) / ori / and, with li $3,-2147483648 pushed back inline into the fall-through arm.
Instruction count (#nop included): 117 both ways — the second instance is where the submitted "−1" tell dies.
Two further shapes in the tree are the fence-as-sched1-barrier placement, NOT this law, and should not be counted as instances: src/ov_SC03_099/ov_SC03_099_jr_80135D20.c:1169-1178 (fence mid-arm, after two statements) and its ov_SC03_006 / ov_SC02_027 / ov_SC05_003 family copies.
§199-G — At TWO case nodes the switch-vs-if oracle is not blind — but the tell is ALL tests positive + a trailing j default, NOT the first test's polarity
§X — AT TWO CASE NODES THE DISPATCH ORACLE IS NOT BLIND: A switch EMITS A CONTIGUOUS HEADER OF ALL-POSITIVE EQUALITY TESTS THAT FALLS OUT TO j <default>; AN if-CONSTRUCT ALWAYS SPENDS ONE INVERTED TEST AND PUTS A BODY INSIDE THE HEADER. (fills the hole §193-G leaves open — "Below 3 nodes the oracle is genuinely blind and §164-54's original warning stands" (L18703 BOUND) — and retires §164-54's "at 2-3 arms a switch and an if-chain can both emit a bne staircase" (L13030) for the 2-node half as well as the 3-node half.)
THE MECHANISM (both halves read out of the pinned source).
switch: at 2 case nodesbalance_case_nodes' split gateif (i > 2)(stmt.c:5361) never fires, so no median and no ordering test — this is why §193-G calls it blind. Butexpand_end_casetakesbefore_case = get_last_insn()after the bodies are already in the stream (stmt.c:4749), emitsemit_case_nodes+emit_jump_if_reachable (default_label)(stmt.c:4909), thenreorder_insns (before_case, get_last_insn(), thiscase->data.case_stmt.start)(stmt.c:5054-5056) hoists that whole block in front of every body.emit_case_nodestests each node with a branch TO its arm, so every test is positive and the header falls out to the default jump.if/else if:expand_start_condcallsdo_jump (cond, next_label, NULL_RTX)(stmt.c:2009) — branch to the FALSE label — andexpand_start_elseif(stmt.c:2016-2024) emitsemit_jump (endif_label)and then the nextdo_jump. There is no reorder. Body 0 is therefore physically interposed between test 0 and test 1, and at least one test must point at a non-arm.- NOT
jump_optimize. jump.c runs long after both; it never sees a choice here.
READ IT BACKWARDS — THE TELL (read the WHOLE header, never just the first branch). A 2-arm dispatch on one value is a switch iff (a) both equality tests are positive (beq/beqz) and each branches to a distinct arm body, (b) nothing but the compare-constant li sits between them, and (c) the header falls out to a j at the default/join. Any if spelling breaks at least one of (a)/(b)/(c):
| C construct | header |
|---|---|
switch (x) { case A: … case B: … } |
beq→armA ; li ; beq→armB ; j default |
if (x==A){…} else if (x==B){…} |
bne→L ; bodyA ; j end ; L: bne→… — body inside the header, first test inverted |
if (x!=A){ if (x==B){…} } else {bodyA} |
beq→armA ; li ; bne→default ; j armB — first test positive, LAST test inverted and the j points at an arm |
PRESCRIPTION. Target shows the all-positive contiguous header + j default ⇒ write a bare switch. §164-54's prescribed cached local (s16 st = x;) is inert here — byte-identical output with and without it; the construct is the only lever, exactly as §193-G found at 3 nodes for arm order.
THE SIGN TRAP — worth banking with it. The if-chain is SHORTER (the switch pays j default + nop), so the residual presents as LENGTH-DRIFT / −2 with a whole-function mismatch cascade (61 of 76 on the exemplar). That signature invites a structural rewrite; the fix is a one-word construct swap. Symptom line: "LENGTH-DRIFT −2 on a small dispatch function, drift starting at the very first conditional branch, first branch bnez where the target has beqz" ⇒ swap the if-chain for a switch before touching anything else.
BYTE EVIDENCE. func_801817A8 (ov_SC06_025, 76 ins, banked) — switch = MATCH (76); same file, construct-only change to if/else if = 74 ins, 61 mismatched, LENGTH-DRIFT/−2; with the §164-54 cached local = byte-identical to the uncached if-chain (74/61); nested-inverted spelling = 74 ins, 63 mismatched. Second instance, ROM bytes: func_80018A20 / src/800.c:3611 in the main binary (143dbb89 = config/check.us.sha), beqz s0 ; li v0,1 ; beq s0,v0 ; j default.
(SHARPENS — fills §193-G's 2-node blind spot (L18703) and §164-54's ⚠ bound (L13030); orthogonal to §3-T4 / §32.2, which pick an ARM inside one if/else, not a construct; evidence: byte-probed in-tree + ROM-verified second instance + cc1-probed on the pinned triple; from func_801817A8, func_80018A20.)
BOUND. FALSIFIED HALF — DELETE IT: "read the FIRST test's POLARITY." Byte-refuted twice, once in-tree.
if (x != A) { if (x == B) {…} } else { bodyA } — a nested inverted if, not a flat chain — compiles to a contiguous header with a POSITIVE first test. In-tree: .run/harvest_x/a8_nested_inv.c vs the real target gives idx 3 beqz which agrees with the target's beqz $v1,.L801817D0, while idx 6 is bne $v1,$v0 against the target's beq (74 ins, 63 mismatched, LENGTH-DRIFT/−2). Controlled cc1 probe h_ifneg (.run/harvest_x/vprobe.c) reproduces it in isolation: beq $4,$0,$L45 ; li $2,1 ; bne $4,$2,$L47 ; j $L48 — the same instruction count as the 2-arm switch, differing only in the second opcode and the two jump targets. A drafter reading only branch #1 would have declared the nested-if correct. The discriminator is the LAST test plus where the fall-out j points.
FALSIFIERS THE SUBMITTER NAMED THAT DO NOT LAND (tested, refuted):
- "the tell inverts on u32" — NO. Probe
d_swu(switch (unsigned int)over {0,1}) emitsbeq $4,$0 ; beq $4,$2 ; j $L19— the same positive grouped header. §193-G's u32 caveat is about the ordering test, which does not exist at 2 nodes, so it has no purchase here. Also holds foru8(q_u8),short,int. - "reversing the if-chain's arm order flips the polarity" — NO. Probe
c_ifrev(if(x==1)… else if(x==0)…) still leads withbne $4,$2. Arm order is not the lever, matching §193-G's 3-node finding.
WHERE THE LAW DOES NOT APPLY:
- Exactly 2 case NODES. At 1 node the switch itself emits the inverted branch (
j_sw1:bne $4,$0,$L56and noj default) — indistinguishable from anif, genuinely blind. At ≥3 nodes §193-G's median/sltitell takes over and the header leads with the medianbeq, not case 0. Node counting follows §193-G:case 0: case 2:is 2 nodes; adjacent labels collapse. - Every arm must have a non-empty body. Probe
l_empty(case 0: break;): the empty arm's label folds onto the join, and the SECOND test comes out inverted (beq $4,$0,$L2 ; … bne $4,$2,$L2) with noj default. Clause (a)/(c) both fail on a real switch. - Arms that all
returnlose clause (c). Probem_ret: both tests stay positive but the default falls straight into the epilogue and the label-collapse eats thej. Read (a)+(b) only; do not require thej. - Unchanged from §164-54/§55a/§163c: this is entirely inside the sub-
CASE_VALUES_THRESHOLDtree route. At 2 casescount = 2 < 5always, so the jump-table route is unreachable and that caveat is automatically satisfied — one fewer thing to check than at 3-4 nodes. - Values may be gapped (
g_swg{3,7}), far apart (o_far{100, 90000}), negative (p_neg{-5,3}), come from a call (n_call), carry an explicitdefault:(f_swd), or fall through between arms (k_swft) — all still emit the all-positive header +j default.
SECOND INSTANCE. func_80018A20 — banked C at /home/musashi/bfm-decomp/src/800.c:3611-3624, main binary, ROM-byte verified. Different binary from the exemplar (main vs ov_SC06_025), different discriminant class (an s32 parameter arg0 vs an s16 global), different TU. Its C is a bare 2-case switch with no default:
switch (arg0) { case 0: chan = 0; break; case 1: chan = 16; break; }
sha1sum build/us/SLUS_007.26 = 143dbb89f34491258bbc27810d0a12ec8b43a8dd, which equals config/check.us.sha, so the disassembly below is the shipped ROM's bytes, not merely our compiler's opinion (mipsel-linux-gnu-objdump -d --start-address=0x80018A20 build/us/SLUS_007.26.elf):
80018a54 12000006 beqz s0,80018a70 <- positive, to arm 0
80018a5c 24020001 li v0,1
80018a60 12020005 beq s0,v0,80018a78 <- positive, to arm 1
80018a68 0800629f j 80018a7c <- j default (the join)
Same shape as the exemplar's beqz $v1,.L801817D0 ; addiu $v0,$zero,0x1 ; beq $v1,$v0,.L80181880 ; j .L801818C8, where .L801818C8 is byte-confirmed to be the epilogue (lw $ra,0x18($sp) ; addiu $sp ; jr $ra), i.e. the implicit default.
Population check. A structural scan of banked src/**/*.c for single-level switches with exactly 2 top-level case labels returns 510 sites across the main binary and ~130 overlays (src/800.c ×4, src/ov_SC06_008/ov_SC06_008_jr_8017C294.c:4364, src/ov_SC03_014/ov_SC03_014_jr_801848E4.c:2838, src/ov_SC06_032/ov_SC06_032_jr_80182890.c:7805, …). Every one of those is a shape §193-G's "genuinely blind below 3 nodes" told a reader to give up on.
Independent corroboration noted by the submitter: a second wave-X agent reported the adjacent 2-vs-3-node cliff on func_801805BC. I did not verify that one.
§199-REJECTED
- A $v0-setting insn in an ordinary conditional branch's delay slot proves the function returns void — ATTACK 4/5 LANDED HARD (the inference is false), and ATTACK 1 landed partially (the mechanism is a re-derivation of
docs/gcc-2.7.2-map/sched.md§D3).
Attack 1 — already in the knowledge base: PARTIAL HIT, and it is the damning one. The candidate's "ruled out" list checks §162f1, §162g, §194-M, L3096, L14761 — al
register T x __asm__("$N") = INIT;silently emits no instruction when a call clobbers $N before the first use — ATTACK 1 LANDED — this is a re-derivation of TWO existing sections, not one.
§74 (docs/matching-cookbook.md L5822, "Auditing a pinned draft: the §72 hazard is CALLER-SAVED pins spanning a call"), failure mode 1, states the mechanism verbatim: *"gcc-2.7.2 does not save/restore an explicit-register variable across
§200 — THE ALIAS IS THE UNIVERSAL DECLARATION ESCAPE: stop negotiating with the TU's spelling (P31 S55)
The situation this replaces. A byte-verified draft is refused because its declaration of a symbol
disagrees with the destination TU's, or with a slate-mate's. §183's playbook answers this by
NEGOTIATING — adopt the TU's spelling and narrow at the use site, cast at the call, view-cast the
pointer — and that works 18 times in 20. The other 2 are §183.3's hard limit: when the TU's spelling
forces an &D_x your spelling does not, every cast escape loses the match from instruction #1,
and the entry says to report IMMOVABLE with a TU edit.
There is a third option, and it always works: don't share the C identifier at all. The link name is the only thing that has to agree. gcc's asm-label binding (§37/§124) lets a draft declare its OWN identifier, with its OWN type, bound to the same symbol:
extern u8 aD8018A800[] __asm__("D_8018A800"); /* TU spells it `extern s32 D_8018A800;` */
extern void *aD801EF9FC __asm__("D_801EF9FC"); /* TU spells it `extern s32 D_801EF9FC;` */
s32 aF80185B48(s32 a0) __asm__("func_80185B48"); /* TU prototypes it `void func_…(s32);` */
Same symbol, same relocation, same bytes — and nothing left for the slate to disagree about. It works on the DEF side too, which matters because a definition's own signature is the one place a cast cannot reach (§183.3, §20's DEF-side wall): give the definition a private C name and bind it.
Measured, wave Y's recovery lane (P31 S55). Five gate drops, five different refusal classes:
array-vs-scalar where reconcile_slate's automatic array fix had broken the match; a slate-mate's
private struct type; a void * vs s32 global; and two DEF-side return conflicts, one of them a
function whose only in-TU use TAKES ITS ADDRESS as a callback. All five aliased, all five still
MATCH, all four re-gated banked (the fifth was already banked by then). Total agent cost: zero —
the API was down with 529s and the whole lane was done by hand in three edits.
WHEN NOT TO REACH FOR IT. This is the escape hatch, not the first move, and the ordering matters:
- Adopt the TU's spelling if it costs no bytes — that is what wave law 2 is for, it keeps the file readable, and it is right 18 times in 20.
- Cast at the use site (§183's playbook) when the types differ but the access does not.
- Alias when 1 and 2 cost bytes, or when the conflict is DEF-side and has no cast form.
An alias is a small readability debt: a reader sees
aD8018A800and must follow the label to learn it isD_8018A800. Pay it deliberately, with the comment explaining WHICH spelling you could not use and why — every alias in the tree carries one.
The tooling had to learn this idiom the same day. sym_of was returning __asm__ as the symbol
for every aliased declaration (an identifier followed by (, matched before the real one), so
aliased declarations all collided under that name — 1 byte-verified draft dropped and 2 phantom
CONFLICTING-EXTERNs on the very slate this lane was recovering. Fixed with a 0-regression control
over 1,210 changed verdicts (899 of them one symbol: the idiom is fleet-wide). A project idiom the
tools cannot parse is an idiom that silently costs work — third instance of the §192 class.
§201 — THE WAVE-Y HARVEST (P31 S55): 67 gap reports -> 5 laws, 8 rejected, 53 already-covered
Sixth harvest. The rejection count (8) is the highest of the session and the confirmation count the lowest — the readers were seeded with §193/§194/§195/§197/§199/§200, six passes of prior art, and the verifiers killed most of what got past them. That is the flywheel converging, not failing.
§201-A is a defect in decl_prior — the card field shipped EARLIER THE SAME SESSION as §196 —
and it is the fourth same-session self-correction (§194-E→§193-A, §199-A→§189-A, §197-A→§136-9,
now §201-A→§196). Fixed before wave Z launched; see the entry for the byte evidence and the fix.
§201-A — §150-B applies to decl_prior's DEF row: for an overlay-window symbol the banked "definition" is usually another overlay's function, and the card ranks it ABOVE the destination TU
(signature-index analogue) — decl_prior's DEF row is ADDRESS-keyed, so in the overlay window it is usually a DIFFERENT function's signature — and §196 ranks it ABOVE the destination TU
(corrects the evidence hierarchy of §196/L20492 and tools/decl_prior.py's docstring L21-25; this is §150-B (L10267, functions) and §164-77 (L13523, data — "a fleet PLURALITY … carries zero authority") reaching the one artifact they had not been applied to. Not a new mechanism.)
THE LAW. build() globs src/**/*.c with no binary awareness (decl_prior.py:59) and accumulates every definition of a name into ONE counter (:71). Overlay functions are named by VRAM address and 134 overlays load at the same window, so for a func_8017xxxx–func_801Axxxx symbol defs[name] mixes N unrelated functions. :77 takes most_common(1) — and :123 throws the count away, so the card cannot show that the "consensus" was 1-vs-1. A DEF row for an overlay-window symbol is evidence only if the target's OWN binary is among the definers. Otherwise it is §164-77's zero-authority declaration wearing the label "the function exists; this is its real shape."
MEASURED (whole tree, 4,165 files, re-derived with the tool's own _DEF regex + cdecl._mask): 9,861 symbols carry a banked DEF. 3,911 (39.7%) are defined in more than one binary; 1,219 have DEFs that disagree on arity, 1,204 of them in the overlay window, and 818 of those are a top-two TIE that most_common breaks by sorted-file order — i.e. the lowest-numbered defining overlay wins, systematically. On the five wave-Y binaries the card would print 65 DEF rows for overlay-window symbols; 26 (40%) are not this overlay's function (18 with no definition in the target binary at all, 8 printing a foreign signature even though the target binary defines it). Resident/shared rows are clean: 0 of 43 wrong.
BYTE EVIDENCE (mine, this session). func_8017E83C (ov_SC03_007, 114 ins). Card says def=('void',('s32',)) for func_8017EC90, from ov_SC04_011 — a different overlay. The target's own .s sets $a0 and puts addiu $a1,$zero,0x2 in the jal's delay slot. Banked 2-arg form → MATCH (114); the identical file with only the DEF-row arity applied (extern void func_8017EC90(s32 a0); + drop the second argument at both call sites, 4 diff lines) → 113 ins, 83 mismatched, LENGTH-DRIFT. .run/harvest_y/e83c_base.c vs e83c_defrow.c. Three unrelated bodies confirmed under func_801806B8 (ov_SC03_007:5236 s32 f(s32,s32,s32,s32), ov_SC02_028:4144 void f(s32*), ov_SC03_110:2909 void f(s32)), and func_8017BEBC has 8 distinct bodies over 95 TUs (23 to 7,036 normalized chars).
THE TELL, and the ordering fix. Read the address before reading the row. < 0x80170000 ⇒ one function fleet-wide, DEF is exactly what §196 claims. >= 0x80170000 ⇒ DEF outranks nothing; the correct hierarchy in the overlay window is TU > this-binary DEF > the callee's own .s (§176-F row 1) > nothing, and a foreign DEF is worth less than a coin flip because it is confidently wrong 40% of the time.
TOOL PRESCRIPTION. Key defs by (binary, symbol) for overlay-window names; emit the target binary's own DEF and suppress the rest, or label them def(foreign: ov_SC02_028) so the row cannot read as ground truth. Restore the count that :123 strips — the docstring already promises it ("each row is emitted with its count so the agent can weigh") and a 1-vs-1-vs-1 tie must never render like a 95× consensus. The same scoping bug is in the FLEET row; §164-77 already condemned it for data and the argument is identical for code.
BOUND. Does NOT apply to resident/engine addresses (< 0x80170000). Only 15 of the 1,219 arity disagreements live there, and on the wave-Y cards 0 of 43 resident DEF rows were wrong — 26 of them came from another binary and were still correct, because that IS the same code. For resident symbols §196's hierarchy is exactly right and this correction must not be applied.
Does NOT apply when the target's own binary is among the definers — 47 of the 65 overlay-window rows on the wave-Y cards, 39 of them printing the right signature. The failure is silent selection, not universal wrongness.
The "1,204 = aliasing" number is a ceiling, not a floor. The candidate's falsifier branch 2 has partial bite: some arity disagreements are genuine declaration latitude among h_norm family siblings (the same body at the same address, spelled by different agents), which §22 already covers — extra unused params sit in $a0-$a3 and are free at -O2. Measured: 4 of the 1,219 have byte-identical normalized bodies across all defs and 93 (7.6%) have all defs within 15% normalized body length, i.e. plausibly one function. The other 1,126 (92%) have grossly different body lengths and are different functions. State the blast radius as "≥92% of 1,204", never as all of them.
Unaffected: the TU row (by construction the target's own file), §193-A seed_ref and §194-E tu_ref (same-family / same-TU pointers), and any binary-scoped consumer. The fix is scoping and ordering, not deleting the row.
Cost shape: for a callee whose true arity is HIGHER than the DEF row, gcc-2.7.2 rejects the extra argument outright, so a stubborn drafter finds out on compile #1; the expensive path is the one measured above — the drafter believes the row, writes the shorter call, and burns compiles on an 83-mismatch LENGTH-DRIFT with no obvious cause. That is precisely the compile budget §196 was built to save.
SECOND INSTANCE. Byte-proven by me, in a different target function and a different callee from the submitter's headline func_80180598/func_801806B8 case:
func_8017E83C (ov_SC03_007, 114 ins) calling func_8017EC90. The DEF row is ('void',('s32',)) and comes from src/ov_SC04_011/ov_SC04_011_jr_8017D494.c — a foreign overlay (its rival, tied at 1, is ov_SC04_016's void f(void*); neither is ov_SC03_007's function). The target's own asm proves 2 arguments (addu $a0,$s3,$zero at 8017E8AC, addiu $a1,$zero,0x2 in the jal delay slot at 8017E8B4). A/B under match_one against .run/waveY_asm_snapshot/ov_SC03_007: banked 2-arg = MATCH (114 ins), DEF-row 1-arg = 113 ins, 83 mismatched (LENGTH-DRIFT). The banked TU's own header comment at ov_SC03_007_jr_8017AE2C.c:4477-4479 records the agent working around it by hand.
Third instance (population-scale, no byte test): func_8017BEBC is defined in 95 TUs with 8 distinct normalized bodies, from 23 to 7,036 characters — at minimum eight different functions collapsed into one defs[name] counter. func_8017C180 (8 defs, 3 bodies) and func_8017BF34 (8 defs, 3 bodies) are the same shape.
§201-B — In a narrowed PLUS/MINUS/AND/IOR/XOR expression the destination pointee is INERT — the sign of the materialized constant is decided by an OR over the UNWIDENED operands (convert.c trunc1), which bounds §1841 to direct constant stores
§ — WHEN A CONSTANT MUST BE MATERIALIZED INSIDE A NARROWED + - & | ^ EXPRESSION, THE DESTINATION POINTEE IS INERT: ori vs addiu IS DECIDED BY AN OR OVER THE UNWIDENED OPERANDS. THIS BOUNDS §1841 / §136-rule-3 (L8788) / §2928-5 TO THE DIRECT CONSTANT STORE SHAPE THEY WERE MEASURED ON.
(Bounds §1841 L1839-1846, §136 type-form rule 3 L8788, §2928-5, and unifies them with cookbook-index L28's narrow-local tell under one RTL-level statement. First cookbook citation of convert.c.)
THE LAW. Every member of this family reduces to one fact: the opcode is the SIGN OF THE const_int THAT THE C FRONT END PUTS INTO THE INITIAL RTL. -130 → addiu (0x24); 65406 → ori (0x34). objdump prints both as li (§1841) — read the opcode byte. Three different front-end routes choose that sign, and they do not agree:
- Direct constant store —
*(T *)p = K;— the POINTEETdecides. This is §1841/§8788-3/§2928-5, and it remains correct only here. - Named local —
T k = K;— the LOCAL'S OWN declared type decides, independent of everything downstream.s16 k = -0x82→(reg/v:HI) (const_int -130)→addiu;u16 k = -0x82→(reg/v:HI) (const_int 65406)→ori;s32 k→(reg/v:SI) (const_int -130)→addiu. Both narrow spellings are HImode — the pseudo's MODE is not the discriminator, the stored value is. (This is index L28, given its RTL.) - Bare literal inside a store-/assignment-narrowed expression — the DESTINATION HAS NO VOTE.
convert_to_integer'strunc1(tools/reference/gcc-2.7.2/convert.c:270-316) redoes the arithmetic intypex, whose signedness isTREE_UNSIGNED(TREE_TYPE(expr)) || TREE_UNSIGNED(TREE_TYPE(arg0)) || TREE_UNSIGNED(TREE_TYPE(arg1))(:306-309), wherearg0/arg1come fromget_unwidened(:277-278). Any one unsigned unwidened operand ⇒ unsignedtypex⇒ the INTEGER_CST is converted tounsigned short⇒ori.
THE TRUTH TABLE (byte-verified, func_8017F234, all four cells, one line changed each):
| destination | operand | opcode | verdict |
|---|---|---|---|
*(s16 *) |
*(u16 *) |
3402ff7e ori |
DIFF |
*(s16 *) |
*(s16 *) |
2402ff7e addiu |
MATCH 80 |
*(u16 *) |
*(u16 *) |
3402ff7e ori |
DIFF |
*(u16 *) |
*(s16 *) |
2402ff7e addiu |
MATCH 80 |
The destination column is constant-free. Do not reach for the pointee.
get_unwidened STRIPS CASTS — the C-visible operand type is not the decider. -0x82 - (s32)*(u16 *)(p+0x8A) still emits ori. Only a cast to a NARROW signed type ((s16)) works, because that is what survives unwidening.
DIAGNOSTIC TELL. Exactly one mismatched instruction, zero length drift, class OPCODE-MIXED, 34xx where the target has 24xx (or the reverse), on a standalone li feeding a subu whose result goes to sh. Three one-token fixes, in this order of preference:
- flip the OPERAND's cast
*(u16 *)⇄*(s16 *)(free — see the load note below); - cast the operand
(s16)/(u16); - bind the constant to a local of the matching signedness (
s16 kforaddiu,u16 kforori). Reading asm→source: a bareori $rX,$zero,0x8000+feeding asubuinto anshsays at least one operand was written unsigned — it says nothing about the store's pointee.
FREE-EDIT NOTE (and a bound on §194-D's "load width is a separate axis"). In this shape *(s16 *) ⇄ *(u16 *) on the OPERAND is byte-visible only through the constant's opcode — the load stays lhu either way, because a HImode load feeding HImode arithmetic into an sh carries no signedness. f234_C_signedoperand.c flipped the operand to *(s16 *) and matched 80/0 with no lh/lhu residual. So this fix costs nothing; §194-D's "changing the source cast selects lh vs lhu" does not apply when the value's only consumer is narrow arithmetic + sh. (Same statement as index L28's "it does NOT cost you the lhu on readback", now with its precondition named.)
FALSIFIED HALVES OF THE SUBMISSION — DO NOT BANK THESE:
- ❌ "binding it to a named local of ANY type restores the
addiu" —u16 k = -0x82;emitsorion both exemplars. - ❌ "the lever is NAMING, not the type" — the operand's cast is the primary lever; naming is the third-choice one.
- ❌ "a MIPS local scalar is promoted to SImode, so it emits
addiu" — REFUTED by-dr:s16 kandu16 kare BOTH(reg/v:HI 105); only the const_int differs. - ❌ The submitted falsifier (d) ("find the constant in SImode ⇒ mechanism dead") — the bare-literal constant IS SImode (
(set (reg:SI 110) (const_int 65406))) and the mechanism is right. The correct falsifier is the VALUE: if the bare-literal build showsconst_int -130rather than65406in the initial RTL,trunc1did not fire.
BOUND. All measured, not asserted.
-
BIT 15 OF THE 16-BIT CONSTANT MUST BE SET (
[-0x8000,-1]or[0x8000,0xFFFF]). At0x82the lever is completely inert:f234_D_bit15clear_bare.candf234_E_bit15clear_named.cproduce the identical24020082 li v0,130. Byte-witnessed in the ORIGINAL too —func_80185D24carries its own negative control four instructions above the firing site:addiu $v0,$zero,0x10 ; subu $v0,$v0,$v1(lhu) ; sh(same shape, bit-15 clear,addiu). (This half agrees with §1841's own bound, though §1841 says "both emitori" for K<0x8000 while the measured answer here is that both emitaddiu— §1841's low-K clause is loose and should not be quoted.) -
THE CONSTANT MUST ACTUALLY NEED MATERIALIZATION.
x - Kfolds K into the operand's ownaddiuimmediate (no standaloneli, nothing to flip).-0x82 & *(u16 *)pfolds toandi $v0,$v0,0xff7e— noliemitted at all (f234_M_andop.c).D_80126CB4 - *(u16 *)(p+0x88)— the sibling line one above the exemplar — has no constant and is unaffected. In practice the firing shape is the constant as the LEFT operand of-. -
THE EXPRESSION MUST BE NARROWED BY THE ASSIGNMENT.
s32 ang = -6 - *(u16 *)(a0+0x104);(banked,src/ov_SC03_007/ov_SC03_007_jr_8017AE2C.c:7061, plus ~30-3 - *(u16*)siblings across the_jr_8017AE2Cfamily) never enterstrunc1— SImode arithmetic, alwaysaddiu. A wide destination kills the law. Confirmed by probe:*(s16 *)dest = -0x82 - *(s32 *)src→2402ff7e addiu(f234_P_s32op.c). -
ONLY
trunc1's OPCODES: PLUS, MINUS, BIT_AND, BIT_IOR, BIT_XOR, BIT_ANDTC (convert.c:270-276). MULT is a different rule — it takes the:253-264path whose gate isTREE_UNSIGNED(arg0) == TREE_UNSIGNED(arg1)(equality, not an OR);f234_N_multop.creads 82 vs 80 ins, a different class entirely. Division and shifts never reachtrunc1. -
WIDTH-AGNOSTIC ON THE OPERAND.
*(u8 *)fires (f234_O_u8op.c→ori, 2 mismatched).*(u32 *)also fires (f234_L_u32op.c→ori), but via the first disjunctTREE_UNSIGNED(TREE_TYPE(expr))— the whole expression becameunsigned int— not viaget_unwidened. All-signed operand sets at any width giveaddiu. -
UNTESTED, do not assume: -O0 (this is a front-end fact so it should hold, but no probe was run); QImode destinations (
*(s8 *)); an unsigned named local used as the OPERAND rather than a memory deref; and whether the load-width freeness in the "FREE-EDIT NOTE" survives when the value has a second, SImode consumer (§194-D's territory — change one axis at a time).
SECOND INSTANCE. func_80185D24, ov_SC06_000 (22 ins, still nonmatching, asm/ov_SC06_000/nonmatchings/ov_SC06_000_jr_8017AE2C/func_80185D24.s). Found by scripted scan of all 14,352 asm/**/*.s for ori $rX,$zero,0x8000+ followed within 7 insns by a subu/addu on that register and an sh (.run/harvest_y/scan_ori.py, 24 hits; this and ov_SC02_003:func_8018483C @80184934 — ori $v0,$zero,0xFE60 ; andi $v1,$v1,0x1C ; subu $v0,$v0,$v1 ; sh $v0,0($s2) — are the two clean ones).
Target, verbatim:
80185D38 addiu $v0, $zero, 0x10 <- bit-15 CLEAR sibling: addiu
80185D40 subu $v0, $v0, $v1 <- $v1 = lhu %lo(D_800B9ABA)
80185D44 sh $v0, 0x0($a1)
80185D48 ori $v0, $zero, 0xFFB0 <- bit-15 SET: ori (= -0x50)
80185D50 lhu $a0, %lo(D_800B9AB8)($a0)
80185D58 subu $v0, $v0, $a0
80185D60 sh $v0, %lo(D_801B2150)($at)
So the ORIGINAL SOURCE wrote the firing spelling — this is not merely "the compiler can do this".
I drafted it (.run/harvest_y/g5d24_v1.c, D_801B2150/D_801B2152 declared s16, D_800B9AB8/D_800B9ABA declared u16) and ran the same four-way A/B against the real .s. The function does not fully match (17 mismatched, LENGTH-DRIFT/1 — an unrelated shared-%lo-address-register residual on the two stores to D_801B2152), but the constant insn is decisive and independent:
| variant | constant insn |
|---|---|
v1 bare literal, u16 operand |
3402ffb0 ori ✅ = target |
v2_signedop bare literal, s16 operand |
2402ffb0 addiu ❌ |
v3_named s16 kk = -0x50 |
2402ffb0 addiu ❌ |
v4_u16named u16 kk = -0x50 |
3402ffb0 ori ✅ |
Identical four-way table to func_8017F234, in a different overlay, with a different constant (−0x50 vs −0x82), a different operand kind (%hi/%lo global vs $s0 struct field), and a different destination kind (global vs struct field). n = 2 functions / 2 overlays / 8 builds, plus the in-target bit-15-clear negative control at 80185D38.
Two further corroborating (untested) sites from the same scan sharing the signature: ov_SC02_003:func_8018483C @80184934 (0xFE60 minus an andi-masked, therefore unsigned, value → sh) and the 0xFFFF/sra 16/negu/sh 0xFE($s0) family that recurs in ov_SC03_006, ov_SC03_007, ov_SC02_016, ov_SC02_017.
§201-C — §X — A CALL'S OWN DELAY SLOT AND THE UPSTREAM CONDITIONAL BRANCH'S SLOT COMPETE FOR ONE INSN (the call's argument copy), AND ONE STATEMENT'S POSITION RELATIVE TO THE CALL DECIDES BOTH — the residual is visible at the BRANCH, not at the call
ONE STATEMENT'S POSITION RELATIVE TO A CALL DECIDES TWO DELAY SLOTS: the call's backward fill and the UPSTREAM CONDITIONAL BRANCH's eager fill contend for the same insn — the call's own argument copy
(the diagnostic §176-A/L17616 is missing: §176-A's prescription is identical ("move the store above the call") but all three of its tells — A1 an arg-register-addressed store in the jal slot, A2 a −1 drift with a nop in a following unconditional jump's slot, A3 a call result in the jal slot — are sited AT the call. This entry is the case where the call looks fine and the defect is upstream. Composes docs/gcc-2.7.2-map/sched.md D1 (nearest-eligible backward fill; arg-reg setups eligible for their own call's slot) + D3 (pass order; the "provide a backward candidate" lever, stated there for the branch's OWN slot, not for a downstream call's). Distinct from §194-M (a STORE in a conditional slot ⇒ dominance — §194-M BOUND 2 explicitly says non-store insns ARE freely stolen, which is this case), from §165-24 (a nop in the call's OWN slot + the copy duplicated per-predecessor ⇒ the source wrote the call ≥2 times), and from §193-D (the sched1 hoist that PUTS the copy at the block head in the first place — that hoist is this law's precondition).)
THE LAW. dbr_schedule runs fill_simple_delay_slots (first, 1) — non-jump slot owners, i.e. CALL_INSNs — before fill_simple_delay_slots (first, 0) and before fill_eager_delay_slots (reorg.c:4329-4332, same order at :4170-4171). A call's backward scan (reorg.c:2907, for (trial = prev_nonnote_insn (insn); ! stop_search_p (trial, 1); …), break on the first eligible trial at :2945) is almost unconstrained: the slot owner's resources are computed with include_delayed_effects == 0 (reorg.c:2904-2905), so mark_referenced_resources' case CALL_INSN: arm (:373-374) is skipped entirely and the call contributes no needed — not even its own argument registers. It therefore takes the literal nearest eligible preceding insn.
sched1 has already hoisted the compiler-generated addu $aN,$sN,$zero to the head of the fall-through block (§193-D). So there are exactly two claimants on it:
- Store written ABOVE the call in C ⇒ the store is nearest the
jal; the call takes the store; the arg copy survives at the block head, andfill_eager_delay_slots(running later) hands it to the preceding conditional branch. Both slots filled. - Store written BELOW the call ⇒ the arg copy is the nearest eligible insn (a
la $aN,SYMbetween them is a 2-insn macro and ineligible per sched.md D2); the call eats its own copy; by the timefill_eagerruns there is nothing left at the block head, and the branch's slot changes owner.
THE READING RULE (asm → source). A conditional branch whose slot holds addu $aN,$sN,$zero for an argument of a call that appears several instructions further down the same block, where that call's own slot holds a store ⇒ that store's C statement is written ABOVE the call. Write it there. Do not route the branch-slot defect to §5a/§34 (asm fence), §194-H (WAR fence) or §194-M (dominance) — all three are wrong here, and §194-M BOUND 1 explicitly excludes jal slots from its reading so it cannot cover it.
THE DIAGNOSTIC TELL (the whole point of the entry). Your draft's defect is at the conditional branch, one or more instructions UPSTREAM of the call you actually have to edit. The branch slot no longer holds the arg copy; the arg copy has reappeared in the jal's slot below. The residual class is unstable and carries no information — it was LENGTH-DRIFT/+1 with a real nop on one exemplar and ADDRESSING/li!=addu profile=cse at zero drift with a wrong-arm insn on the other. Route on WHO OWNS THE BRANCH SLOT, never on the class or the count.
⛔ TWO HALVES OF THE ORIGINAL CLAIM ARE FALSIFIED — do not bank them.
- "
mostly_true_jumpreturns 0 forbeqz, so the fall-through is tried first" is NOT the cause. True of the code path (reorg.c:1405,:3713) but not load-bearing. Rewriting the guard so it compiles tobnez(prediction = 1, TARGET thread tried first,reorg.c:3690) leaves the arg copy in the branch slot anyway — it arrives via theif (delay_list == 0 && own_fallthrough)fallback atreorg.c:3698. Byte-checked object:bnez v0,110 / move a0,s0. The lever is branch-polarity-independent — which also means §199-F'sbgez/bgtz/bneLIVE vsbeq/beqz/blez/bltzDEAD gate does NOT apply to this law. - "the visible residual is a
nop" / "+1 drift" is NOT an invariant. See the second instance: zero drift, and the slot is taken by an insn stolen from the other arm.
BOUND. 1. A single-insn candidate must actually exist between the branch and the call. If everything between them is la $aN,SYM / large-li (2-insn macros at -G0), or a load / mfhi / mflo (dslot=yes, sched.md D2), the call has nothing else to take, it eats the arg copy regardless of where you put the store, and the store-order lever is inert. This is why func_801803D8's FIRST beqz (@801803F4) legitimately keeps a real nop while its second one is filled — a la $a0,D_801D5210 is the only thing in that block head.
-
The freed copy is NOT guaranteed to reach the branch slot (this is the honest weak point).
fill_eager's fall-through fallback is gated onown_fallthrough(reorg.c:3698, andown_thread_pat:2151-2170requiresLABEL_NUSES == 1+ a preceding BARRIER). If the fall-through block has a second incoming edge you get a barenopeven with the store correctly placed. Untested here; inherited from §199-F BOUND 2. So the law reads asm→source as a strong tell, and source→asm only as a necessary condition (same one-way shape as §194-M BOUND 3). -
The substitute is not necessarily a
nop, and not necessarily from the fall-through. Onfunc_801803D8the vacated slot was taken byli $a0,0xFfrom the target thread (the else-arm's first insn). Predict "the slot changes owner", never "the slot goesnop". -
CONDITIONAL branches with a fall-through body only. For a
j/jalthe eager machinery is not involved (condition == const_true_rtx,reorg.c:3289-3290), and §176-A/A2 already owns the following-unconditional-jump case. -
The store must be genuinely independent of the arg copy and of the branch's condition register. The backward scan accumulates
set/neededfrom every trial it skips (reorg.c:2951-2952); a store that reads the register the copy writes, or writes the branch's condition register, is refused and the whole contention dissolves. -
§193-D's hoist is a precondition. If sched1 did not put the copy at the block head — e.g. the argument is not the bare variable, the pointer is not in a callee-saved register, or a surviving CODE_LABEL splits the block (§194-N) — there is no copy at the head for
fill_eagerto steal and the second slot never existed. -
Not a length law. Both directions cost 0 or ±1 depending on whether the displaced insn lands somewhere the assembler would otherwise
nop-pad (§188). Zero routing authority from the count. -
Target-scoped: gcc-2.7.2 -O2 -G0 -mips1 -mcpu=3000 + maspsx/ASPSX 2.56, the pinned triple.
-fno-delayed-brancherases the whole effect (I used it deliberately to read the pre-reorg stream).
SECOND INSTANCE. func_801803D8 (ov_SC05_017, 70 ins, banked MATCH) — different overlay, different TU, different callee, and it contains BOTH polarities of the shape inside one body, which makes it the better teaching exemplar of the two.
Banked C: /home/musashi/bfm-decomp/src/ov_SC05_017/ov_SC05_017_jr_8017AE2C.c:5588 — *(s16 *)((char *)a0 + 0x2) = 3; written above func_8012E8E0((s32)a0, (s32)&D_80191434);.
Target: /home/musashi/bfm-decomp/.run/waveV_asm_snapshot/ov_SC05_017/func_801803D8.s, which shows the paired slots
/* 80180428 */ beqz $v0, .L8018047C
/* 8018042C */ addu $a0, $s0, $zero <- the arg copy, in the BRANCH's slot
/* 8018043C */ jal func_8012E8E0
/* 80180440 */ sh $v0, 0x2($s0) <- the store, in the CALL's slot
I extracted it standalone to /home/musashi/bfm-decomp/.run/harvest_y/v3D8_base.c → MATCH (70 ins), then moved that one store below the call (/home/musashi/bfm-decomp/.run/harvest_y/v3D8_A_storebelow.c) → DIFF, 70 ins vs 70, 15 mismatched, ADDRESSING [structural] sig=ADDRESSING/li!=addu profile=cse. The -fno-delayed-branch-free cc1 output shows the mechanism directly: jal func_8012E8E0 / move $4,$16 (the call ate its own copy) and beq $2,$0,$L3 / li $4,0x0000000f (the branch slot backfilled from the ELSE arm). This is the instance that kills the submitted "+1 / nop upstream" tell — zero length drift, no nop, and the substitute came from the target thread rather than the fall-through.
Population of the shape (my scan, /home/musashi/bfm-decomp/.run/harvest_y/vscan.py): 514 sites across asm/**/*.s + all .run/wave*_asm_snapshot/, 41 of them in snapshotted waves.
§201-D — THE SIGNEDNESS OF A div/mod MAGIC IS DECIDED BY THE STATIC TYPE OF THE DIVIDEND TREE AFTER get_narrower STRIPS WIDENING CONVERSIONS — NEVER BY A PROVABLE RANGE. AN & 0xFF IS NOT A CONVERSION, SO IT NEVER FLIPS THE MAGIC; A DECLARED-UNSIGNED LOCAL OR A NARROWING CAST WRITTEN AT THE DIVIDE BOTH DO.
§X — THE SIGNEDNESS OF A div/mod MAGIC IS THE SIGNEDNESS OF THE DIVIDEND TREE AFTER get_narrower STRIPS WIDENING CONVERSIONS. A PROVABLE RANGE IS IRRELEVANT, AN & 0xFF IS NOT A CONVERSION AND NEVER FLIPS IT, AND A NARROWING CAST AT THE DIVIDE IS AS STRONG A DIAL AS THE DECLARATION. (sharpens §167-03 (L15039), which owns the same shorten/get_narrower predicate for the HImode sll 16 ; sra 16 position and whose "the cast is INERT" lever is measured on a WIDENING cast only — the narrowing direction is live and is the half a reader will get wrong; and §167-39 (L15974), which gives the signed SHAPE mult ; sra 31 ; mfhi ; sra k ; subu but no C-side dial. Divisor arithmetic stays §194-I/§16N+3; powers of two stay §164-04; variable divisors stay §1-I4.)
THE MECHANISM. In build_binary_op's TRUNC_DIV/TRUNC_MOD arm, shorten = (TREE_UNSIGNED (TREE_TYPE (orig_op0)) || divisor is an INTEGER_CST != -1) (c-typeck.c:2029-2032, comment at :2027 — "We shorten only if unsigned or if dividing by something we know != -1"). Under if (shorten && none_complex) (:2352) gcc calls get_narrower (op0, &unsigned0) (:2355), then at :2406-2413 the TREE_CODE (arg1) == INTEGER_CST arm sets result_type = signed_or_unsigned_type (unsigned0, TREE_TYPE (arg0)). get_narrower walks conversions only (NOP_EXPR chains). Therefore:
| dividend as written (divisor a non-2^k constant) | emitted |
|---|---|
s32 c = f() & 0xFF; c % 3 |
mult 0x55555556 + sra 31 — the mask is a BIT_AND_EXPR, not a conversion |
(f() & 0xFF) % 3 (no local at all) |
mult — identical; the range 0..255 is invisible to the front end |
s32 c; (s32)c % 3 |
mult — widening cast inert (§167-03) |
s8 c; c % 3 |
mult (operand sra 24) — narrow but SIGNED |
u8 c; c % 3 |
multu 0xAAAAAAAB + srl |
u16 c; c / 5 |
multu 0xCCCCCCCD |
u32 c = f() & 0xFF; c % 3 |
multu — no narrowing involved at all; result_type is simply unsigned |
s32 c = f() & 0xFF; (u8)c % 3 |
multu — a NARROWING cast IS the dial |
u16 f; (s16)f / 15 |
mult 0x88888889 + sra 31 — and the reverse dial |
THE C-SIDE RULE. Read mult+sra 31 ⇒ write the dividend so the stripped tree is SIGNED (a plain s32/s16/s8 local, or an explicit narrowing signed cast on an unsigned object). Read multu+srl ⇒ make it unsigned (declared u8/u16/u32, or a narrowing unsigned cast). Do NOT reach for a mask, a pin, or the permuter — the mask is a different dial entirely.
THE SECOND, ORTHOGONAL DIAL — WHERE THE andi SITS. The source mask and the magic's signedness are independent. On func_80182F3C, deleting & 0xFF from a signed local costs exactly one instruction and leaves the magic untouched (139 ins, 1 mismatch — move $a1,$v0 where the target has andi $a1,$v0,0xFF). Re-spelling the same operand u8 costs 56 of 140 words: the truncation migrates off the definition site (move $a1,$v0) and reappears one instruction later on the consumer (andi $a0,$a1,0xff), and the count grows by one. So: mask present/absent = one andi at the DEF; declared/cast unsignedness = the whole ladder plus the andi's POSITION.
BLAST-RADIUS TELL. A single mult↔multu residual never appears alone. Getting the signedness wrong on a 139-ins function moved 56 words and 1 instruction of length, because the ladder length differs (sra 31 + subu vs srl 1 + subu + trailing andi) and every branch offset behind it shifts. A LENGTH-DRIFT/1 residual whose first real divergence is a lui 0xaaaa against a lui 0x5555 is this bug and nothing else.
BOUND. Conditions under which it does NOT apply — several of these falsify the candidate as submitted:
- "Declared type" is NOT the dial — signedness of the stripped tree is.
get_narrowerstrips conversions, so a narrowing cast written at the divide overrides the declaration in BOTH directions.s32 c; (u8)c % 3-> multu;u16 f; (s16)f / 15-> mult. The submitted title's "declared C type ... via get_narrower" is false as an exclusive claim. - "Narrow ⇒ multu" is FALSE. An
s8local takes the SIGNED path (probe t8:sra $2,$4,24 ; mult ; sra 31). Width is not the dial. - "get_narrower narrowing" is not the only route. A
u32/unsigned intoperand flips to multu with no narrowing at all —TREE_UNSIGNED (result_type)alone does it. The candidate's QImode story is correct only for the u8/u16 case. - "Cannot strip a VAR_DECL" is the wrong framing (inherited from §167-03). The real statement is "a mask is not a conversion": with NO local at all —
(f() & 0xFF) % 3written inline — gcc still emits the signed magic. The VAR_DECL case is a special case of the general rule, and stating it the narrow way invites the false inference that hoisting the expression into a temp is what matters. - Constant divisors only.
shortenneedsTREE_CODE (op1) == INTEGER_CST; a runtime divisor takes thediv/divu+ zero-checkbreakpath (§1-I4) and no magic exists to be signed (probe t9 confirms: nomultat all). - Divisor == -1 kills
shortenfor a signed operand (the!= -1clause), so the whole shortening block is skipped. - Powers of two are a different regime (§164-04/§167-40): signedness still shows (the
bgez/biasaddiu 2^k-1), but there is no magic and none of the above shapes apply. - Unsigned
mh != 0divisors (d = 7, 127, 455 …) emit the add-correctmultu ; mfhi ; subu ; srl 1 ; addu ; srl kform — the unsigned side's own exception, already owned by §194-I; do not read those through the plain multu shape. - Everything AFTER the ladder is the source's own arithmetic (§167-39 half 2) — unchanged by this entry.
SECOND INSTANCE. func_80176218, src/ov_SC03_099/ov_SC03_099_jr_801734BC.c — BANKED (no func_80176218.s anywhere under asm/, no INCLUDE_ASM; i.e. byte-matching under the whole-binary gate). Different overlay, different function, different divisors, and it carries BOTH directions of the dial in one body:
- line 3208 —
u16 cc; ... *(u8 *)(pp + 0x5D) = D_80186BBC[cc / 5];→ the declared-unsigned side. My isolated probe of that exact spelling:andi $4,$4,0xffff ; li $2,0xcccccccd ; multu $4,$2 ; srl $2,$3,2. - line 3274-3275 —
u16 f = *(u16 *)(cach + 0x1A); ... s32 t = (((s16)f / 15) & 3) * 15;→ the SAME u16 object, with an explicit(s16)narrowing cast written at the divide, and it flips to the signed ladder:sra $2,$4,16 ; li $3,0x88888889 ; mult $2,$3 ; sra $4,$4,31 ; sra $2,$2,3. Dropping just the cast (f / 15) givesmultu $4, 0x88888889 ; srl— control run, same probe file.
This is the strongest possible corroboration AND the reason the law had to be narrowed: a previous session banking this function had to write (s16)f on a u16 variable to get mult. Under the submitted headline ("declared C type decides it") that banked line is the submitter's own named falsifier and refutes the read; under the corrected statement ("the type after get_narrower strips conversions") it is a clean confirmation.
Third, target-side corroboration of the signed shape at other sites is already banked in §167-39 / §194-I (0x2AAAAAAB in 69 files, 0x55555556 in 43).
(Probes: .run/harvest_y/verify_f3c/p1.c, p2.c, driven by .run/harvest_y/verify_f3c/probe.sh — the pinned cpp→cc1 -O2→maspsx triple copied out of tools/match_one.py.)
§201-E — §194-J-2 — The last_mem_set deletion window is measured in SOURCE/expand order, not in the printed stream; a foreign store moved between the pair in C source is a real lever, and volatile is not always the better one
§194-J's deletion window is measured in SOURCE (expand) order, not in the printed stream — so statement order IS a lever, and volatile is not always the better one
AMENDS §194-J (L19363). Falsifies two of its sentences: L19373 ("Statement order is a no-op too — moving a real foreign store between them saves the store but costs you a SCHEDULE-REORDER/2") and L19382 ("only volatile is [reliable]"). The mechanism and all five of §194-J's BOUNDs stand unchanged.
THE CORRECTION. propagate_block's last_mem_set DSE runs inside flow_analysis (toplev.c:2983), which is BEFORE sched1 (schedule_insns, toplev.c:3033, gated by flag_schedule_insns at :3028) and long before sched2 (:3117); both flags are set at -O2 by :3397-3398. (The pass-order fact itself is not new — it is already the opening of §164-43/§NNNb at L12778, for a different consequence.) The RTL flow inspects is therefore still in expand order — i.e. C statement order — while the .s you are diffing is post-sched2. Consequence, and it cuts both ways:
- (a) A memory reference that PRINTS between the two identical stores does not save the first one if the source did not put it there. §194-J's BOUND 1 test ("ANY memory reference falls between them", L19381) must be applied to the C source, never to the emitted stream.
- (b) The store that actually saves the pair can print arbitrarily far AFTER both, because the scheduler is free to sink it — including into a
jaldelay slot.
BYTE PROOF (func_8017F818, ov_SC05_018, 94 ins, banked at src/ov_SC05_018/ov_SC05_018_jr_8017D604.c:3479). The target's printed stream is sh $t1,0x12($sp) / sh $t3,0x10($sp) / sh $t1,0x12($sp) — the printed separator is the 0x10 store, and that is not what saves the pair. One line moved, nothing else changed:
source order of the four shs |
result |
|---|---|
A: 0x10, 0x12, 0x14, 0x12 (local.z = … written before local.y = local.y + a3;) |
MATCH (94 ins) |
| B: 0x10, 0x12, 0x12, 0x14 | DIFF 93 vs 94, 80 mismatched, LENGTH-DRIFT/-1 |
Bvol: B + *(volatile s16 *)&local.y = … |
94/94 but 49 mismatched, OPCODE-MIXED/addressing,strength |
Since A is byte-identical to the target, the saving store — sh $t2,0x14($sp) — provably prints at 8017F904, in the jal func_80015954 delay slot, after both 0x12 stores. The local.y = local.y + a3 read of local.y is present in both arms and saves nothing (§194-J BOUND 2: cse folds it, no MEM survives to flow). On this exemplar volatile is a strictly worse lever than statement order.
THE SEPARATOR CHOICE IS LOAD-BEARING — SWEEP IT (this is why §194-J concluded the opposite). On §194-J's own second instance func_8018392C (ov_SC01_084), moving a foreign store between buf1[1] = v0dead; and buf1[1] = angle2; saves the store in both trials (82 -> 84 ins), but the two separators do not cost the same:
| separator moved between the pair | result |
|---|---|
*((s16 *)&in3[0] + 1) = (s16)sinVal; |
84/84, 2 mismatched, SCHEDULE-REORDER/2 — exactly §194-J's measurement |
buf1[2] = (s16)cosVal; |
MATCH (84 ins) — zero cost, no volatile |
§194-J's "statement order costs you a SCHEDULE-REORDER/2" was one unswept sample, not a property of the lever.
PRESCRIPTION. At a §194-J site, both levers are candidates and neither dominates — try both, and when trying statement order, sweep every legally-movable foreign store in the block as the separator, not just the nearest one. The statement-order fix costs zero bytes and zero declarations.
SECONDARY DIAGNOSTIC. A §194-J hit in straight-line code does NOT present as a local -1. The single deleted store cascaded to 80 of 93 instructions mismatched on f818 and 45 of 82 on 392C. The tell is LENGTH-DRIFT/-1 (or /-2, when the producing insn dies too) paired with a mismatch count near the whole function.
BOUND. The amendment does NOT apply, or does not help, when:
- There is no foreign store available to move. The separator must be a REAL store (or a load that survives cse — §194-J BOUND 2 is unchanged and still the trap). If the block contains only the two identical stores,
volatileremains the only lever. Statement order is an ADDITIONAL lever, not a replacement. - The intervening statement is data-dependent on the pair. Both f818 (
local.zis independent oflocal.y) and 392C (buf1[2] = cosValis independent) are free reorders. If moving the separator changes semantics you cannot use it, and the sweep space may be empty. - The separator choice is wrong. Measured, not asserted: on 392C one legal separator MATCHES and another costs
SCHEDULE-REORDER/2. Statement order is zero-cost only if you sweep; a single trial reproduces §194-J's pessimistic reading. Corollary: a loneSCHEDULE-REORDER/2result is NOT evidence that the lever is broken — it is evidence you picked the wrong separator. volatileis not uniformly worse. On 392CvolatileMATCHES (84 ins) and so does the right statement order; the "volatileis strictly worse" finding is f818-specific (49 mismatched there). Neither lever dominates the other across functions.- All of §194-J's original five BOUNDs still hold unchanged and gate this amendment too: an intervening load that cse folds does not count (BOUND 2); the two stores must be in the SAME basic block (BOUND 3, flow.c:1391) or nothing is deleted in the first place; at -O0
stupid_life_analysis(toplev.c:2974) runs instead and there is no DSE at all (BOUND 5), so neither lever is needed. - Frequency, inherited from §194-J: 31 adjacent same-width/same-offset/same-base store pairs across all 14,647 files in
asm/. This is a rare shape — do not over-invest in the sweep tooling. - Credit bound: the pass-order half ("flow at :2983 precedes sched1 at :3033, the .s is sched2's order") is NOT new — it is already stated at L12778 in §164-43/§NNNb. Cite that section for the pass order; this section is only for the DSE consequence.
SECOND INSTANCE. func_8018392C (ov_SC01_084, jr_8017CA80, 84 ins) — §194-J's OWN second instance, re-run by me with the new lever. Different overlay, different function, different separator, and it is the instance that both confirms the amendment and explains §194-J's error.
Baselines first, reproduced from §194-J's own files against asm/ov_SC01_084/nonmatchings/ov_SC01_084_jr_8017CA80/:
.run/harvest_u/vfy_392C_vol.c-> MATCH (84 ins).run/harvest_u/vfy_392C_novol.c-> DIFF 82 vs 84, 45 mismatched,LENGTH-DRIFT/-2
Then two statement-order variants I built myself from the novol arm (one line moved between buf1[1] = (s16)v0dead; and buf1[1] = (s16)angle2;, no volatile anywhere):
/home/musashi/bfm-decomp/.run/harvest_y/f392C_SO1.c— separator*((s16 *)&in3[0] + 1) = (s16)sinVal;-> DIFF 84 vs 84, 2 mismatched,SCHEDULE-REORDER/2/home/musashi/bfm-decomp/.run/harvest_y/f392C_SO2.c— separatorbuf1[2] = (s16)cosVal;-> MATCH (84 ins)
Both SO arms restore 82 -> 84 instructions, so the store is saved by source-order separation in both — the candidate's core mechanism holds on a second function. SO1 reproduces §194-J's exact measured penalty (SCHEDULE-REORDER/2), which pins down why §194-J demoted the lever: it tried one separator. SO2 shows a zero-cost separator exists on that same function. The amendment is general; the "zero-cost" adjective is conditional on sweeping.
§201-REJECTED — eight, the session's highest
- A goto-target block is emitted at its SOURCE position, so it can land between the two arms of an earlier if/else — residual is equal-count OPCODE-MIXED/structural — ATTACK 1 — LANDS. The mechanism is a re-derivation of §164-55 (L13048). Its LAW paragraph is not scoped to arms; it is stated in full generality and in the candidate's own words: *"gcc-2.7.2 has no basic-block reordering pass (bb-reorder is gcc-3.x); RTL block order is the order
expand_stmtem - Halfword store of a bit-15-set literal: destination pointee signedness picks addiu vs ori — ATTACK 1 LANDED — the mechanism is already in the cookbook, three times over.
§21 — wave-distilled idioms (Phase 21), docs/matching-cookbook.md L1841-1846, states the candidate's headline law essentially verbatim:
"**store a high-bit (≥0x8000) 16-bit constant to a halfword → `unsigned
- Narrow store-to-load forwarding into
sll 16; sra 16— fix at the store end, not the §21 read end (ov_SC03_121 / func_8017F374) — ATTACK 1 LANDED — this is a re-derivation of §193-E, with §195-L BOUND 3 as a second owner. Both halves of the candidate are already indocs/matching-cookbook.md, with the SAME gcc-2.7.2 citations.
(a) THE MECHANISM AND THE STORE-END LEVER — §193-E (L18623, "A varying-address (pointer) load is re
- **A constant-valued local passed as a call argument is const-propagated by cse1 into the argument-load — but "the fence family is structurally powerless, only a second SET ** — ATTACK 2 LANDED (decisively), and ATTACK 1 landed alongside it. ATTACK 3 and the mechanism half survived; ATTACK 4 would have capped it at WEAK anyway.
ATTACK 1 — ALREADY IN THE COOKBOOK. §190-C (docs/matching-cookbook.md L18302, "A CALL-ARG CONSTANT WEDGED INTO A DEPENDENT LOAD'S DELAY SLOT IS A *
- Sharing one C local across two repeated sub-blocks kills sched1's birthing boost (reg_n_sets 1→2) — a 2-insn transposition — ATTACK 1 LANDED — re-derivation of banked prior art, and not of one section but of four independent ones. The submitter's novelty claim ("none states the inverse — that ordinary variable reuse kills it by accident — and none gives the diagnostic tell") is false against the text of the cookbook:
(a)
- A narrow (u16/u8) MEMORY LOAD separated from its first widening use by ANY memory-writing insn keeps a real
andi 0xFFFFat zero frame change — the decider is combine.c: — ATTACK 1 (already in the cookbook) — did NOT land. I grepped §16Xy (L12925), §165-02/§165-09 (L13913-13927), §162b1 (L11072), §193-B (L18480), §197-A. §16Xy's own isolated A/B recordsu16 s(from memory) as emitting NEITHER an andi NOR frame, and §165-02's table has no row for a widening use separ - Zero-drift
andioperand swap (raw vs masked) onc = f() & 0xFF; … c & 1— submitted as combineforce_to_mode, actually csefold_rtxfrom_plus:; "subset" discrim — ATTACK 3 (misattributed mechanism) LANDED, and ATTACK 4's falsifier (a) — the submitter's OWN pre-registered falsifier — landed too.
- COOKBOOK — does not land, but the candidate missed its two nearest neighbours. It is not a re-derivation of §165-41 (that is the mask DELETION / length change). BU
- N-ARM CONSTANT STORE AT A JOIN — re-derivation of §164-73/§164-74, and its "NOT a shared local" half is byte-refuted — TWO attacks landed; either alone is fatal.
ATTACK 1 — ALREADY IN THE COOKBOOK (§164-73 L13425, §164-74 L13452, bounded by §165-21 L14198). The submitter's "ruled out" list names §193-C, §194-M, §8/§48-A1/§50-B, §186c, §3-T4 — and omits the entire family that owns this shape. §164-73's title *is
§202 — THE ALIAS CARRIES A DEFINITION, NOT JUST A DECLARATION: the DEF-SIDE-RETURN wall (P31 S56)
§200 escapes a use-side clash. This escapes a definition-side one, and it is the same move.
func_8018280C (ov_SC05_001, wave Z) drafted MATCH and its reconciler correctly refused to slate it,
reporting IMMOVABLE with the §183.3 DEF-SIDE-RETURN verdict:
- the asm PROVES an
s32return — every exit path writes$v0(addu $v0,$zero,$zeroat0x80182828/0x8018283C/0x80182858,addiu $v0,$zero,0x1at0x801828E8); - the destination TU already declares the symbol
voidat three sites, two of them file-scope (:5735,:5847) plus one block-scope (:5822), with live callers at:5753,:5825,:5873— one of them already casting through((void (*)(s32))func_8018280C).
Adopting the TU's void loses the return; changing the TU's decls touches three sites and every
caller. Both are the negotiation §200 says to stop having.
The fix — put the alias on the DEFINITION:
s32 aF8018280C(s32 a0) __asm__("func_8018280C");
s32 aF8018280C(s32 a0) { ... } /* the byte-verified body, unchanged */
The TU's three void decls stay exactly as they are and keep serving their callers; the compiler
never sees a conflict because the C identifiers differ; the linker resolves both to one symbol.
Banked first try (commit commit:2559), body untouched from the drafted MATCH.
What is genuinely new. The tree already used this idiom, but only ever on declarations of
externs — extern void aF80137030(s32 x, s32 y) __asm__("func_80137030"); in ov_SC03_099 /
ov_SC06_008. Applying it to a definition is what makes it a DEF-side escape, and it means
§183.3's "report IMMOVABLE with a TU edit" is no longer the end of that road: a def-side return-type
or arity wall is now a one-line, zero-token, zero-TU-edit recovery.
Ordering (extends §200's). adopt the TU's spelling -> cast at the use site -> alias the use -> alias the definition. Reach for it the moment a wall is DEF-side, before writing IMMOVABLE.
§203 — A DEDUPED TYPEDEF MUST PRECEDE EVERY SPLICE POINT, NOT JUST ITS OWN (P31 S56)
The defect. strip_provided_typedefs (harvest_verify:225) removes from a draft every typedef the
destination TU already provides. That is right, and it is address-blind — which is wrong when a
slate banks two functions that share a type.
Wave Z's md_MAIN_034 group banked 6 of 7. The drop, func_800CC310, failed with:
PLUMBING: src/md_MAIN_034/md_MAIN_034.c:547: parse error before `D_800CCAD0'
func_800CC4E8 — same slate, same batch — banked a Quad4_800CCB14 typedef into the TU. The
stripper then correctly deleted func_800CC310's duplicate copy as "already provided". But
func_800CC310 splices EARLIER in address order (0x800CC310 < 0x800CC4E8), so the surviving
definition sat ~20 lines BELOW the externs that needed it. The type was provided, just not yet.
The wrong fix, and why the gate caught it. Renaming the draft's typedef so the stripper spares it
(Quad4_800CCB14 -> Quad4_800CCAD0) moves the error rather than removing it: the draft then
declares extern Quad4_800CCAD0 D_800CCB14; while the TU declares extern Quad4_800CCB14 D_800CCB14;
— one symbol, two types, and the failure simply walks to the second line. A dedup fix that
introduces a second name for one type is not a fix.
The fix. Hoist the shared typedef to the top of the TU, above every splice point, and let the stripper delete the draft's copy as designed:
#include "common.h"
/* HOISTED: defined here rather than beside its first banker, because an
* EARLIER-addressed function's draft declares externs of this type. */
typedef struct { u8 f0; u8 f1; u8 f2; u8 f3; } Quad4_800CCB14;
Banked on the next gate (commit commit:2558).
Correction, recorded because it bit the writer of this section (P31 S56). The text that banked
is the RENAMED variant, not the original draft — gate_stage calls backlog.save_draft() on
failure, so the failed rename attempt OVERWROTE .run/backlog_drafts/func_800CC310.c, and copying
"the original" back copied the rename. The BYTES are correct (whole-binary gate + R22 213/213), but
the TU now carries two names for one 4-byte shape, and a symbol-rewriting transform mangled prose
inside a comment (not (*(Quad4_800CCAD0 *)&D_800CCB14)). Two lessons: a backlog draft path is
not a stable original — snapshot the text you mean to re-gate; and a transform that rewrites
symbols must skip comments (H5).
The general law. A type shared by two functions in one TU belongs at the TOP of that TU, not
beside whichever of them happened to bank first. pregate_check already hoists for main's
gate_main driver (it hoisted one for ov_SC03_094 in this very wave) — it does not for
harvest_verify's module/overlay driver, which is the third-driver gap the S55 checkpoint warned
about. Until it does, a slate with two functions sharing a typedef needs the hoist by hand.
Diagnostic order for this class (R38, learned the expensive way). The verdict was already on
disk in .run/harvest_failed.<binary>.classified.txt before any investigation started. Reading it
first would have cost one command. Instead: reloc_identity (AGREE, 13 relocs — correct and
irrelevant), then a disassembly of the built .elf showing 69/69 instructions identical — a
clean-looking result that was pure artifact, because the draft never compiled and the .elf
therefore still held the original INCLUDE_ASM bytes. A byte-diff against a build that failed to
include your draft is a diff against the target and itself: it always reads MATCH. Check the
classified ledger before any oracle, and confirm a build actually consumed your source before
believing any diff taken from it.
§204 — THE WAVE-Z HARVEST (P31 S56): 82 gap reports -> 5 laws, 16 rejected, 30 already-covered
Seventh harvest, and the biggest batch yet (82 reports, 75 targets, 74 banked). The rejection count
doubled again — 16, twice wave Y's record 8 — and it is the headline number. The readers were seeded
with seven passes of prior art (§193/§194/§195/§197/§199/§200/§201) plus §202/§203 from earlier the same
session, and the verifiers killed sixteen submissions: eight as re-derivations that one grep -n would
have found, five as records that explicitly contained no claim at all ("gap: none"), two as byte-false
mechanisms, one as a confirmed dead end. 30 already-covered on top of that is the index WORKING.
A harvest that comes back mostly-new means the readers did not grep; a harvest that comes back
mostly-covered means the knowledge base is doing the job it was built to do and the marginal cost of
the next function is falling. Only 5 of 82 reports contained something the tree did not already hold.
§204-E is the THIRD tool defect found in the §196 declaration-card this session — §201-A killed
the DEF row's overlay scoping, §203 killed strip_provided_typedefs' ordering, and §204-E finds that
the card's second headline lever — the GLOBAL TYPE row — has never emitted a single row in four
shipped waves. Same shape all three times: the field shipped, the evidence never arrived. That is
§176h.A ("a batch-integration tool must be audited for what it DOESN'T look at") collecting its fourth
instance, and it is the strongest argument in the file for auditing a tool's OUTPUT rather than its code.
§204-A — A COMPARE THAT APPEARS BOTH IN A BRANCH'S DELAY SLOT AND AGAIN ON THE FALL-THROUGH IS A JOIN WITH TWO INCOMING EDGES: THE TWO GUARDS ARE SEQUENTIAL ifs, NEVER if/else if
(reads §165-24's own_thread_p law (L14317) BACKWARDS, as a source-shape oracle on a guard pair
rather than on a call's argument move. BOUNDS §164-55 (L13048), whose "the spelling is
irrelevant — a guard clause and } else if … compile to the same bytes" holds only because its arms
END IN return; and bounds the func_80184944 refutation at L13677-13681 the same way. Cousin of
§167-40 (L16001), the same bgez-slot / fall-through duplication for the signed-division bias.
Disjoint from §164-47 / §164-54 / §193-G, which choose a DISPATCH construct, not a guard's
exclusivity.)
THE TRIGGER (readable in the target before you write a line). The same compare instruction
appears twice, a few insns apart: once in a conditional branch's delay slot, once immediately before
the label that branch targets. func_80182E14 (ov_SC01_084, 95 ins, banked;
.run/waveZ_asm_snapshot/ov_SC01_084/func_80182E14.s) does it twice in one function:
80182EA0 bgez $a1, .L80182EB0
80182EA4 slti $v0, $a1, 0x301 <- the upper-bound compare, in the slot
80182EA8 addu $a1, $zero, $zero <- the lower-bound reset
80182EAC slti $v0, $a1, 0x301 <- THE SAME COMPARE AGAIN
.L80182EB0:
80182EB0 bnez $v0, .L80182EBC
and identically at 80182EE4-80182EF0 with slti $v0,$a1,0x80.
THE LAW. cc1 expands each compare exactly once; the second copy is reorg's doing.
if (v<lo) v=lo; if (v>hi) v=hi; makes the upper-bound test the head of a join reached by two
edges — the bgez-taken edge and the reset's fall-through — so own_thread_p is false and
fill_slots_from_thread may not delete what it takes: it puts copy_rtx (trial) in the slot and
reorg_redirect_jumps the branch past the original (reorg.c:3423-3433, :3593-3616; §165-24).
Writing the same clamp as if (v<lo) v=lo; else if (v>hi) v=hi; gives the upper-bound test one
predecessor, own_thread_p is true, and the compare is MOVED into the slot — it can then never
appear twice. A compare on both continuations of an earlier related branch is therefore positive
proof that the two tests are independent statements, and it is unreachable from any else-chain
spelling. Do not read the duplicate as a missing statement, as a lost CSE, or as scheduling noise.
BYTE EVIDENCE (A/B re-run at vet time, pinned triple: cpp -P → tools/bin/gcc-2.7.2-psx/cc1 -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float; the two sources one else apart, both clamps).
Banked C: src/ov_SC01_084/ov_SC01_084_jr_8017CA80.c:4411-4423. Probes
.run/harvest_z/seq.c / .run/harvest_z/els.c.
| spelling | cc1 .s, one clamp |
|---|---|
if (v<0) v=0; if (v>0x300) v=0x300; |
bgez $3,$L6 ; slt $2,$3,769 / move $3,$0 / slt $2,$3,769 / $L6: — the target, duplicate present |
if (v<0) v=0; else if (v>0x300) v=0x300; |
bgez $3,$L2 ; slt $2,$3,769 / j $L3 ; move $3,$0 / $L2: — no duplicate compare anywhere, in either clamp |
⚠ NARROWED AT VET TIME — the length delta is NOT the tell; the DUPLICATED COMPARE is. The
submission claimed the else-if form costs "+2 ins per clamp, +4 across this function's two". Measured
on the harness above: 32 insns sequential vs 33 else-if — +1 across BOTH clamps, not +4. Per clamp
the two forms are length-neutral (the sequential form's extra slt is paid for by the else form's
extra j); the lone +1 comes from elsewhere — the else form's join label lands inside the chain and
reorg duplicates the next statement's constant load (li $4,0x2aaa0000) into the $L3 path
instead. So do not route on a ±N drift. Route on the literal duplicated compare, which is
categorical and which the else form cannot produce at any length.
BOUND.
- The two guards must have FALL-THROUGH arms. §164-55 measured guard-vs-
else ifas byte-identical, and it is right — for arms that end inreturn. An arm ending inreturnterminates its block, there is no join to have two predecessors, and the spelling really is free. The moment both arms fall through to a common continuation, §164-55's "spelling is irrelevant" stops applying and this section takes over. Read the two sections as one law with a fall-through discriminator, not as a contradiction. - A single duplicated compare is not enough on its own — you need the branch/slot/fall-through
triple: branch B, compare C in B's delay slot, C again immediately before B's target label. A
compare that merely appears twice in a function is ordinary (
reorgis not the only way to get there; cse can fail to unify two genuinely separate source tests). reorgruns after everything, so no C-level fence,volatile, or statement reorder can add or remove the duplicate — §186's "cross-jumping/reorg runs AFTER scheduling, so no C-level barrier can steer it" applies verbatim. The ONLY lever is the guard construct.- Untested: three-or-more-way clamp chains, and the case where the reset arm is more than one
insn (here it is a single
addu $a1,$zero,$zero; a multi-insn arm may exceed the slot budget and suppress the duplication without changing the source construct).
§204-B — A LOOP COUNT THAT ARRIVES ON THE STACK IS DECREMENTED IN PLACE: a fresh counter local can cost a real move AND permute the whole callee-saved file
(NEW for scalars. The scalar mirror of §52 lever 1 ("declare the walking pointer AS the mutated
parameter"), and the OPPOSITE prescription from §164-41 ("a ++-ed POINTER parameter MUST be aliased
into a local"). Distinct from §161b/§162n2 (unconditional pointer ALIAS — frame +8 and one extra
sw $sN) and from §167-21 (a REASSIGNED parameter — count-neutral). Evidence: byte-probed at vet
time; from func_800CBA18.)
TRIGGER. A 5th-or-later integer parameter — it arrives in the caller's outgoing-args area, so the
prologue reads it with lw rX,K($sp) — and the function loops down from it.
TARGET SHAPE / THE TELL (readable before you compile).
lw $s4, 0x58($sp) <- the stack arg loads STRAIGHT into a callee-saved reg,
with NO `move` after it
...
addiu $s4, $s4, -0x1 <- and that same register is the loop's counter
bnez $s4, .Lbody
Your draft's failing signature: LENGTH-DRIFT +1, an lw $v0,K($sp) … move $sN,$v0 pair in the
prologue, and one or more callee-saved registers trading roles with the counter.
THE MOVE. Write the loop against the PARAMETER — for (; count != 0; count--, …) or
while (count--) — never s32 i = count; … i--. When the copy survives it is not merely +1
instruction: the extra live pseudo re-ranks the allocnos and the whole callee-saved assignment shifts
behind it.
BYTE EVIDENCE (re-run at vet time, pinned triple). func_800CBA18 (md_MAIN_034, 133 ins, banked
src/md_MAIN_034/md_MAIN_034.c:169; target .run/waveZ_asm_snapshot/md_MAIN_034/func_800CBA18.s,
whose lw $s4,0x58($sp) at 800CBA38 feeds addiu $s4,$s4,-0x1 at 800CBBE0 with no intervening
move). Baseline for (; count != 0; count--, f++, prim += 2) → MATCH (133 ins). Single edit —
wrap the loop in { s32 i = count; for (; i != 0; i--, f++, prim += 2) … }, nothing else —
→ mine=134, target=133, 124 mismatched, LENGTH-DRIFT. The extra insns are lw $v0,88($sp) +
move $s3,$v0; $s3 and $s4 then trade places (target $s3=verts / $s4=count; draft
$s4=verts / $s3=count) and the permutation ripples through the body. Probes:
.run/verify_b3/base.c vs .run/verify_b3/cnt.c.
⚠ BOUND — the copy is NOT unconditionally un-coalescable; do not restate this as a compiler rule. Two counter-probes on the same cc1:
.run/verify_b3/a.cvsb.c— the identical 7-arg, stack-passed-count shape in an isolated 69-insn function — compile byte-identical, thelw+movefolding to onelw $19,80($sp)..run/verify_b3/ra.cvsrb.c— the same body withcountmoved to$a0— differ only in wheremove $19,$4is scheduled, same instruction count.
The stack-passed qualifier is necessary but not sufficient: the copy only survives when the counter is a multi-block / call-crossing global allocno, which local-alloc cannot tie and global-alloc will not coalesce (§45 Lever A, K8). Treat the edit as free and tell-driven — it never cost instructions in any probe run here, and it is the first thing to try when the tell fires.
DISCRIMINATOR against the neighbours (this is the part that keeps the three sections apart).
| shape | prescription | section |
|---|---|---|
POINTER parameter that is ++-ed in a loop |
alias it into a local | §164-41 |
| POINTER parameter, unconditional alias | costs frame +8 and an sw $sN — only when forced |
§161b / §162n2 |
| SCALAR parameter reassigned (not counted down) | give the recomputed value its own local; count-neutral | §167-21 |
| SCALAR loop COUNT arriving on the stack | decrement the parameter in place | this section |
The pointer and scalar cases point in opposite directions and both are real. Read the parameter's kind first, then the section.
§204-C — WHEN A LOCAL BUFFER'S ADDRESS IS PASSED TO A CALL, ITS SIZE IS A FACT ABOUT THE CALLEE'S BODY, NOT ABOUT THE CALL SITE: grep the callee's proven definition and count the stores through the pointer parameter before you declare the local
(EXTENDS §196/§201-A one level down: the DEF row hands you the callee's SIGNATURE, which for
T *param_1 is silent about how much of *param_1 is written. The arity analogue is already banked —
§176-F row 1 (L17760), "open the callee's target .s and count the argument registers it saves/uses";
this is the same move for the caller's FRAME. COMPLEMENTS — and points the opposite way from —
§136's "partially-read out-param region → ONE stack struct, not separate scalars" (L1775), which cures
a frame that is too SMALL and will mislead you when the frame is too BIG.)
THE TELL. A frame-size mismatch (here 0x50 vs the target's 0x40) on a caller whose only
unexplained locals are stack addresses handed to calls. Nothing else in the body drifts.
THE PROCEDURE — one grep, no rebuild. For each $sp-relative address passed to a jal, look for
a proven body of that callee, in this order:
grep -n 'DEFINE_func_<addr>()' src/shared/engine_core.h— 1,377 such macros exist (§28/L447) and no col-0 text scan of a.ccan see them;- an in-tree definition (
grep -rn '<callee>(' src/); - the callee's own
.s, readingsw/sh/sbthrough$a0.
Count the stores through the pointer parameter. That count — not the call site — sizes the local.
THE NEGATIVE RULE (the part that costs waves). An immediate sitting beside the pointer argument is
NOT a size, however plausible its magnitude. 0x18 next to a void * reads as 24 bytes / 6 words and
is wrong here: in DEFINE_func_8012B0B4() that argument is param_3, a multiplier
(prod = iVar1 * param_3). Argument-shape inference about a buffer is a guess; the callee body is
ground truth.
DO NOT REACH FOR THE SCOPE LEVERS FIRST. Block/nested-scope declaration tricks and §136-6/§79 declaration-order reordering are the wrong tool for a frame that is simply over-sized — the drafter tried them first here with ZERO byte effect. Check the callee's write footprint before spending any allocator lever.
BYTE EVIDENCE. func_801846B4 (ov_SC01_084, 81 ins, fresh crack, banked MATCH). Target
.run/waveZ_asm_snapshot/ov_SC01_084/func_801846B4.s: frame addiu $sp,$sp,-0x40;
addiu $a0,$sp,0x10 / addiu $a2,$zero,0x18 feeding jal func_8012B0B4 at 801846C8-801846D4; and
the ONLY access to that region anywhere in the function is lw $v1,0x10($sp) at 801846E4 — one
word (the frame's other slots, 0x18-0x28 and 0x30-0x34, belong to two different calls).
DEFINE_func_8012B0B4() at src/shared/engine_core.h:89027 is
void func_8012B0B4(unsigned int *param_1, int param_2, int param_3) whose sole write through the
pointer is *p = result;. Draft unsigned int buf[6] (sized off the 0x18) → frame 0x50, 16 bytes
over. unsigned int buf → frame 0x40, MATCH.
BOUND.
- Requires a proven callee body. With no
DEFINE_macro and no in-tree definition you fall back to reading the callee's.s, which is sound but slower and gives you a lower bound only (a store the callee makes under a branch you cannot prove taken still needs the slot). - The write footprint is a lower bound on the LOCAL, not on the FRAME. Alignment, other calls' argument areas, and §195-M's orphan/addressable-scalar rounding all still apply — this section fixes the buffer, not the frame arithmetic. If the residual survives at the right buffer size, that is §193-I/§195-M territory.
- Does NOT apply when the callee writes a variable amount (a
memset-shaped callee whose count really is an argument). Then the call-site immediate genuinely IS the size — but you learn that from the body too, not from the argument's magnitude. - Do not generalise from "one word" to "always one word". The law is go read it, not it is usually small.
§204-D — A LOOP-INVARIANT LOAD IS ADMITTED AS A MOVABLE ONLY IF ITS ADDRESS CANNOT TRAP: local.field PASSES, THE SAME READ THROUGH A POINTER LOCAL IS REFUSED
scan_loop's THIRD blocker (loop.c:715), and a one-line bidirectional dial.
(THIRD entry in the movable-blocker set that §162e3 (L11184-11192) closes as "Two independent
gates" — §52-3's n_times_set != 1 and §162e3's own (1)(2)(3) safety filter. All three live in
scan_loop, and two of them are conjuncts of the SAME else if, so do not read them as separate
passes. BOUNDS docs/gcc-2.7.2-map/loop.md:224, which states the gate abstractly — "may_trap_p src
can't move past a conditional/call" — with no C spelling, no tell and no dial. NOT §148-A/§148-A2 (the
threshold × savings × lifetime arithmetic, which runs only on candidates that already got past this)
and NOT §190-A (preheader strata). Evidence: two-sided byte A/B on the pinned triple; from
func_8017D898, ov_SC03_094.)
THE GATE. scan_loop's candidate else if (tools/reference/gcc-2.7.2/loop.c:702-716) ends:
/* If the insn can cause a trap (such as divide by zero),
can't move it unless it's guaranteed to be executed
once loop is entered. Even a function call might
prevent the trap insn from being reached
(since it might exit!) */
&& ! ((maybe_never || call_passed)
&& may_trap_p (src)))
may_trap_p's case MEM: is return rtx_addr_can_trap_p (XEXP (x, 0)); — the comment above it reads
"Memory ref can trap unless it's a static var or a stack slot". rtx_addr_can_trap_p returns 0
for SYMBOL_REF/LABEL_REF; 0 for a REG only when the rtx is frame_pointer_rtx /
hard_frame_pointer_rtx / stack_pointer_rtx / arg_pointer_rtx (an rtx-identity test, not a
regno test); for PLUS, can_trap(op0) || GET_CODE(op1) != CONST_INT; and 1 for everything else.
⚠ rtlanal.c is NOT in tools/reference/gcc-2.7.2/ — read it at
tools/reference/gcc-papermario/rtlanal.c (same function text).
So, for a read that appears twice in a loop body:
| C spelling | rtx after instantiate_virtual_regs |
may_trap_p |
under maybe_never |
|---|---|---|---|
local.field / arr[i] on a stack object |
(mem (plus (reg frame_pointer_rtx) (const_int K))) |
0 | movable → hoisted to the preheader; cse2 then forwards the just-stored register (an sll/sra pair in the preheader instead of an in-loop lh) |
D_SYM[k] fixed global |
(mem (symbol_ref …)) / (mem (plus (symbol_ref) (const_int))) |
0 | same — hoisted |
q[0] where T *q = (T *)&local; |
(mem (reg <pseudo>)) |
1 | refused — stays in the body, reloaded every iteration through an address register |
maybe_never goes to 1 the moment scan_loop passes a conditional jump (loop.c:923-930, already
cited by §162e3), so the gate is armed in any loop whose body branches ahead of the read.
DIAGNOSTIC TELL (read it in both directions — this is the dial). The target holds an
addiu $tN,$sp,K outside the loop and reloads 0($tN) / K2($tN) inside it, off slots the
preheader itself just wrote ⇒ the source read that object through a pointer. Conversely, if your
draft reloads in-loop what the target hoisted, delete the pointer indirection and read the object as
a member/array directly. This is neither a scheduling nor a regalloc residual — do not open §47 /
§158 / §136 or the permuter on it.
BYTE EVIDENCE. func_8017D898 (ov_SC03_094, 149 ins, banked
src/ov_SC03_094/ov_SC03_094_jr_8017BEBC.c:3613). Same body, one spelling apart; the target's
addiu $a2,$sp,0x18 / addiu $a1,$sp,0x10 / addiu $t0,$sp,0x10 (at 8017D930, 8017D948,
8017D960) are the pointer-held stack addresses the law predicts, and the in-loop reads go through
them rather than off $sp directly.
BOUND.
- The loop body must branch (or call) ahead of the read. With
maybe_never == 0andcall_passed == 0the whole conjunct is false and the trap test never runs — a straight-line loop body hoists the pointer read too, and this section is inert. Check for the conditional first. - This gate runs BEFORE §148-A's cost arithmetic, not instead of it. Passing this gate only makes the load a candidate; §148-A can still decline to hoist it. A refusal at this gate is categorical, a refusal at §148-A is arithmetic — different residuals, different fixes.
- It is an rtx-IDENTITY test on the base register, so a pseudo that merely holds
$spdoes not qualify. This is exactly whyT *q = (T *)&local;fails whilelocal.fieldpasses: the two are semantically identical in C and structurally different in RTL. - Cuts both ways and both directions are levers. Adding an indirection is a legitimate way to prevent a hoist your draft is doing and the target is not. Do not read this section as "pointers are bad".
- Untested: volatile-qualified reads (a different
may_trap_ppath), and whether arestrict- free pointer that provably came from&localin the same block is ever seen through by cse beforescan_loopruns. Change one axis at a time.
§204-E — decl_prior's %hi/%lo ARM HAS NEVER FIRED: the card's promised GLOBAL-TYPE row is 0 of 1,210
(Second defect in the §196 card, same shape as §201-A: the field shipped, the evidence never arrived.
Corrects tools/decl_prior.py:43. Not a new mechanism — it is §176h.A ("a batch-integration tool must
be audited for what it DOESN'T look at") and §192's class, fourth instance.)
THE DEFECT.
_ASM_SYM = re.compile(r'\b(?:jal\s+(\w+)|%[hl][io]\(([\w+]+)\))') # decl_prior.py:43
The leading \b scopes the ENTIRE alternation. A %hi(/%lo( operand is always preceded by
whitespace or a comma — non-word to non-word, so there is no boundary there — and the data arm is
dead code. asm_symbols() has only ever returned jal targets.
MEASURED. .run/waveZ_asm_snapshot/ov_SC05_001/func_8017F3E4.s references six %hi/%lo data
symbols (D_801AEEB0, D_80189738, D_801897B8, D_8018974E, D_80189748, D_80189838);
re-running the tool's own regex over that file at vet time returns eight matches, all of them jal
targets, and None in the data group every time. Across four shipped waves the cards carry 0 D_
rows out of 1,210 (W 233 / X 284 / Y 401 / Z 292). The index is not the problem — it holds
54,572 D_ symbols of 67,558, and for_asm() has no function/data branch. §196's second headline
lever ("GLOBAL TYPE. Every CONFLICTING-EXTERN drop and the whole Reconcile phase exist because N
drafters independently invent a spelling for one D_ symbol") has therefore never been delivered to
a single agent.
THE COST. Recovering the arm on wave Z's 75 targets surfaces 298 data symbols (~4 per function), 193 with a fleet extern row. These are precisely the CONFLICTING-EXTERN drops the reconcile lane exists to repair.
THE FIX, AND ITS BOUND. Drop the \b (or use (?<![\w])). Then apply §164-77/§201-A before
printing: of the 193, 95 are resident (< 0x80170000) — one object fleet-wide, so the fleet modal
is real evidence — and 98 are overlay-window, where a fleet plurality carries ZERO authority (a
per-overlay D_ is a different object in each of 134 overlays) and must be suppressed or labelled
fleet(foreign), exactly as §201-A rules for DEF. Shipping the data rows unscoped would convert one
silent hole into 98 confident false leads — do not fix half of this.
THE READER'S RULE UNTIL IT SHIPS. A card with no D_ row is not evidence that the fleet has no
opinion — it is evidence of nothing. Run python3 tools/decl_prior.py --query D_XXXXXXXX per data
symbol yourself, and discard the answer when the address is >= 0x80170000.
FALSIFIED HALVES OF THE SUBMISSION — DO NOT BANK THESE:
- ❌ "data symbols aren't tracked in
decl_prior/build()only sees banked callees" — the index holds 54,572 of them; the loss is at extraction, not at indexing. - ❌ "the card has no row for an in-flight sibling draft, so grep
.run/wave_<W>/<binary>/before adopting a spelling" — §176b/§176d/§176h.D already state the sibling-agreement law, §176h.C2 assigns it to the pre-gate tool, andreconcile_slate.pyran on this very slate. A parallel wave's sibling file usually does not exist at the moment you would grep it. - ❌ "
def_absentshould print the other binary's signature" — §201-A withholds it on byte evidence, and re-adding it would reintroduce exactly the failure §201-A measured at 40%.
§204-CONFIRMED — 30 reports that the index already answered
Each names the section that already held the law, plus what wave Z added to it. A confirmation is a successful index lookup that the drafter made after the fact instead of before it — the fix is the drafter's grep discipline, not a new section.
- func_8018599C → §194-B, §164-64, §136d-1/§165-47 (RC-5 pin family). Three sub-claims, all
confirmations. The compound-vs-split register-cost lever (
x = (x & c1) << c2;costs a scratch reg the splitx &= c1; x <<= c2;avoids) is §164-64's mechanism on a new exemplar. - func_8018353C → §165-47 (L14852), pin every member of a register relationship or none. New exemplar for "pinning the DEST alone is a trap; a pin is a preference over ONE quantity (§72)".
- func_80183068 → §193-E BOUND 1 (L18639, a same-address store re-seeds the cse interval, so
field = field+1; if (field==4)needs a genuine reload) + §164-66 (volatile blocks combine'szero_extendfold — explains the spuriousandi); gap 2 →reorg.cfill_simple_delay_slotsbackward scan (L868, §21'sjal-slot bullets). - func_80180300 → §30#3 / §37 / §162d1 / §164-50 / §165-19 / §194-A, the birthing-boost family (L2382-2418, L11101-11124, L18921-19010, L14137-14192).
- func_80180300 (second report) → §167-28 (L15696-15704), quoted verbatim by the gap:
schedule_blocktraverses each bb backward, so picked-earlier = placed-later, and a C edit can only ever RAISE a priority. - func_8018BE88 → §186c (L18092) + §194-F (L19169-19240), which explicitly sharpens §186c as "reached by expression nesting rather than by load placement".
- func_800CBE28 → §145(c) (L9958) already word-for-word ("chained assignment emits stores RIGHT-TO-LEFT… writing three separate statements gives ascending order"); §164-34 (L12575, two biv increments emit in LUID/source order) CONFIRMED with a new datapoint.
- func_8017F9CC → §164-52 (L12946) + §20's scalar-global-RMW pointer-variable bullet (L1907). A
store then two branch-arm dereferences of one address had to route through ONE explicit
addr = (s32)&SYMlocal. Near-perfect structural twin ofov_SC01_077:func_8012C354. - func_80181574 → §201-B (L21153) rule 3 — a bare literal inside a store-narrowed expression.
⚠ The submitter cited "§201-D", which does not exist; the mechanism is §201-B's third route. Fix:
hoist
t + 0x80into a nameds32local before storing to theu8/s8field. - func_801807D8 → §193-I (L18820) for the frame-sizing corollary + §194-I (L19320) for the magic-divide grep tip. Two stack aggregates 24 bytes apart, both declared 6-wide even though only 3 elements of each are live — MATCH 123 ins on first compile.
- func_8018FEA0 → §164-52 (L12946) for the plain-C fold, §153 (L10466) for the asm-launder cure,
here generalized beyond §153's call-argument trigger. Guarded triple-init of
D_800A5E88[3];s0bound via__asm__("la %0,D_800A5E88":"=r"(s0)). - func_8017F268 → §164-54 (L13008) / §193-G (L18717) / the unnamed § at L20998-21018: the
dispatch-topology oracle — the construct, not the case count or body density, decides the
compare shape. Written as a full
switch(0,1,2,3)though cases 2 and 3 are emptybreaks. - func_8017F744 → bounds L20048's "branch sense is free,
jump1normalizes it". Case 1's inner conditional needed an explicitif (cond) goto Label;with the then body placed at a label AFTER the case'sbreak;. This is a real narrowing of an existing claim, recorded here rather than as a new section because n = 1. - func_80184460 → §167-27 (L15655-15692,
jump.c:1800-1875every-arm-returns swap, incl. the! firstprecondition at L15670) + §164-38 (compare operand order = load order). - func_8017E65C → §3-T4 (L90-103) / §32.2 (L2426) branch polarity read off the target opcode,
- §36 (the
$0-add opaque copy) + §193-E BOUND 6 (naming changes placement, not just count).
- §36 (the
- func_8017ECD4 → §179-F (L17507-17524): pinning a loop-walked pointer/biv is a total off-switch
for strength reduction —
loop.c:3301requiresREGNO(dest_reg) >= FIRST_PSEUDO_REGISTER. - func_80180830 → §165-24 (L14312-14317, the shared
jal's own delay slot as a copy-count oracle)- §21's combined-assignment and cross-jump-duplicate-the-call bullets (L1872-1881).
- func_8017F5A4 → §193-E BOUND 6 (L18649): count-only, not placement — raw→named also changes register pressure.
- func_801870BC → §18 (L1561-1568), the
%lo-folding array-of-STRUCT idiom for indexed globals: pick the struct so the accessed field's address == the target symbol. - func_80187E18 → §16Xd / §164-61 (L13172-13184): the arg-copy delay-slot arity tell is basic-block scoped and uninformative in-block; take the arity from another call site, a sibling overlay, or the TU's existing decl.
- func_801837FC → §201-A / §150-B / §164-77 (L21121-21135): fleet-consensus rows carry authority proportional to real, binary-scoped agreement; a 1-vs-1 tie is not ground truth.
- func_80183BD0 → §193-B (L18480-18499), §16Xb/§164-59 (L13140-13153), §195-C (L19804/L19823).
L18486 names the mechanism generically as
combine.c:924-929's cross-call guard on ANY(set pseudo hardreg)copy; BOUND 4 (L18499) already says "not a type-recovery tell". - func_801849B4 → §193-I (L18818) + §195-M (L20333-20360). L20348's worked example already shows
the naive flat
CEIL(size,8)sum under-predicting frame size by exactly this shape (sum 36, walk and compiler both 40). - func_8017E3B0 → §167-21 (L15507-15521), stated there for a REASSIGNED PARAMETER; this gap's
D_8018B50C/E/510index temp is a reused LOCAL reloaded three times. Same prescription, wider trigger — noted, not re-sectioned. - func_80180874 → §190-B (L18291-18300, esp. L19839-19840): per-block statement-order rigidity for independent same-base writes is real; direction is unpredictable, do not generalise either way.
- func_8017FC10 → §164-74 (L13452, "the poison is a FRESH allocno, not a temp") / §167-21 (L15507) / §176-B2 pin-the-interloper (L17655).
- func_80180A10 → §199-F / §164-36b (L20946)
fill_eager_delay_slotsthread selection, BOUND 2 ("no guaranteed substitute" when the fall-through block is notown_thread_p), + the dbr insn-duplication law at L14317-14330 (reorg.c:3128-3130). Same mechanism as §204-A above, seen from the call side — the target duplicated a shared merge-block'saddu $a0,$s0,$zerointo the slot because.L80180AB8has ≥2 incoming edges. - func_80182998 → §70 (L5619), walk the PARAMETER, not a copy of it. §70's own byte evidence
(
func_801777BC) is the identical symptom and fix for a narrower precondition:u32 *q = (u32*)a0; … q += 5;makescse.c:make_regs_eqvcanonicalize on the outliving local. Read alongside §204-B — same family, scalar vs pointer. - func_8018FCE4 → §194-K (L19415-19463), blind sched1's alias oracle with a second SET of a
pointer pseudo. The reported lever (an opaque
__asm__("":"=r"(s0):"0"(s0))retie ofs0immediately before the store, to stop cse foldings0[8]=0into a freshlui/addiu) is §194-K's. - func_800CC4E8 → §165-17 / §194-D (L14094, L19088): the declared width of a computed-value local is a sched1 dial. The report itself frames it as adjacent to §165-17.
- func_80185764 → §201-A (L21121) —
decl_prior's overlay-window fleet/DEF rows can be a wrong, address-colliding, same-named-different-function signature — and §174 law 1c (L16844) —match_onemasks relocations and cannot see a wrong callee. The drafter took(s32, void*, void*)from the target's own$a0/$a1/$a2setup and ignored the card's('void',('s32',)). This is §201-A being used correctly by a reader one wave after it was written — the flywheel closing.
§204-REJECTED — sixteen, twice the previous record
Recorded so they are not re-derived. The reason matters more than the claim.
Killed by ATTACK 1 (already in the cookbook) — eight:
- func_8017F27C — REFUTED on all four attacks, three independently decisive. The headline warrant
("RC-9 is a bare label with no elaborating section anywhere") is false: §167-23 (L15545,
arg-copy deletion is label-scoped,
cse.c:8039), §164-49 (L12884, "a pin that fixes the register and leaves a residual is the CAUSE, not the cure"), the §189-C refutation half (L19001, the identical pin +"memory"re-tie ablation onfunc_80183094), anddocs/gcc-2.7.2-map/regalloc.md§RC-9 (L145-149) all exist. - func_8018A968 — REFUTED at step 1 (both triggers already held: §136-3 rule 3 at L8862-8866 as corrected at L19003 for trigger A; §176-B2 "pin the interloper, not the contested value" at L17655-17665 for trigger B) and at step 2 (trigger A's stated mechanism is byte-false). The pins are real and load-bearing — verified, not taken on trust — but the explanation was wrong.
- func_80184460 — REFUTED at steps 1, 2 and 4. The mechanism is not open: it is §167-27's own
cited pass,
jump.c:1799"Look for if (foo) bar; else break;", and the wave's own banked source already derives it in full atsrc/ov_SC01_084/ov_SC01_084_jr_8017CA80.c:5081-5098. A claim whose answer is in the header comment of the file being submitted is the cheapest possible rejection. - func_8018C758 — the evidence is REAL (61-ins target,
lui $v1,0x7FFF / lw $v0,0x4($s0) / oriat8018C770-78; both levers carried in the banked C atsrc/ov_SC02_005/ov_SC02_005_jr_80181D30.c:7254) and every part of it is already banked: §194-A (L18925-18960), §164-36 (L12567, L12620-12628), §31 lever 2 (L2919), §201-C, §42d lever 2 (L3097-3103), §48-B corollary (L11165-11168), §48-C1, §162l (L11485-11520), §176f — plus a standing §195-REJECTED entry at L20434 for the same claim. Rejected twice now. - func_80184B80 — a re-derivation of §167-08's DIAGNOSTIC TELL almost word for word
(
docs/matching-cookbook.md:15188-15211, "an argument register that is READ before thejalis scratch, not an argument — count the DEFs, not the mentions"), reinforced by the giant-crack recipe's arity rule at L2266-2270 and by §166a. Landed on the first grep. ⚠ Note for the reader: §167-08's tell is itself already BOUNDED by §195-B (L19693) — a$aNread before thejalCAN still be that call's argument when the value dies at the call — so the correct move at this shape is to A/B both arities, not to trust either section unilaterally. - func_801829B0 — REFUTED on its own evidence, then again on coverage. The four cited symbols
(
func_8012B0B4/B6D4/BC60/CEB0) are all resident (< 0x80170000), which is precisely the case §201-A's BOUND exempts; the ranking half is §196 + §150-B/§164-77. - func_8018599C (second submission) — no surviving claim after the §194-B/§164-64 confirmation above absorbed it.
- func_80182BAC — no surviving claim; the residual reduced to an existing pin-family entry.
Killed as byte-false mechanisms — two:
- func_800CBC2C — REFUTED at step 2/3: the stated MECHANISM is byte-false, so the "law" is an incidental allocation outcome of one function. I reproduced the baseline and ran the A/B the claim never ran. This is §204-B's near-miss twin — the direction (use the raw parameter, do not alias it into a local) is already §161b's, with §164-41 supplying the pointer exception and §167-21 the reassignment case. §204-B was banked only because the stack-passed-count trigger and the callee-saved permutation cost are new; this submission had neither.
- func_8018A968 — see above; the mechanism half was byte-false independently of the coverage kill.
Killed as a confirmed dead end — one:
- func_8018C758 (second report) — the submitter's own record is a dead end: ~6 C-level nudges
(constant-splitting re-tie,
__asm__fences on both sides, statement reordering) all failed to move the target'slui $v1,0x7FFF / lw $v0,4($s0) / ori $v1,…interleave. A negative result with no mechanism is a worklog entry, not a section.
Killed as process notes, not compiler mechanisms — five:
- func_80186DE8 — "I noticed a TU-neighbor's style and copied it." Consulting a proven sibling/neighbour before deriving from scratch is already the cookbook's standing prescription.
- func_8018A1D8 — the record itself states no derivation beyond standard steps was needed.
- func_8018392C — the record itself states existing structural-twin guidance already covers it.
- func_800CC6C4 — gap field reads "none — this was a clean first-pass mass-lane draft, no residual encountered."
- func_8018B058 / func_8017E890 — both gap fields read "none"; the only work was a
reorder-and-retry (
8018B058) and a same-TU-neighbor + seed-twin lookup (8017E890).
THE PATTERN IN THE FIVE. A "gap: none" record is not a failure of the drafter — it is the index
paying off, and it should be reported as none rather than padded into a claim. The harvest's cost is
dominated by verifying submissions that had no claim in them; the cheapest improvement to the next
wave's harvest is a prompt that says "if nothing was derived, write none and stop."
§205 — THE CHAINED ASSIGNMENT IS ITS OWN SCHEDULING DIAL: *b = *a = v; moves an argument copy that no local, no pin and no statement reorder will move (P31 S56)
Bounds §162d1 ("Statement-vs-expression is not the dial; REG_N_SETS is") — which holds for sched1's
S2 birthing boost and does NOT hold for this shape — and extends §NNNb's finding that expression
granularity is a pricing dial in its own right.
The residual. func_80181714 (ov_SC02_016, 121 ins) drafted to SCHEDULE-REORDER/4: the
addu $a0,$s0,$zero argument copy feeding func_8012BEE8 sat three slots too early, above the
sll 9 / sra 12 / addiu 0x1000 chain the target puts before it. Emitted instructions and register
assignment were otherwise identical.
The fix.
*(s16 *)(p + 0x18) = *(s16 *)(p + 0x1C) = t; /* MATCH */
versus the two-statement form, which does NOT match:
*(s16 *)(p + 0x1C) = t; /* near 4 */
*(s16 *)(p + 0x18) = t;
Both emit the same instructions in the same registers. Only the expression FORM differs.
Why it works. The chained assignment stores to the inner lvalue first and keeps BOTH stores inside one expression's RTL, which shifts the LUIDs that sched2's tie-break reads. The two-statement form gives each store its own expression and its own LUID span; the chained form does not.
The negative controls that make this a law and not a lucky variant — five alternative levers were byte-tested against the SAME residual, and all five left the exact same 4 mismatches:
| lever tried | result |
|---|---|
explicit single-set local t1 (§162d1's own prescription) |
4 mismatches |
| re-assigned pointer local | 4 mismatches |
a second pointer local q |
4 mismatches |
a t2 local for the second shift chain |
4 mismatches |
a dead re-set of r to break the birthing boost (§55a) |
4 mismatches |
*b = *a = v; |
MATCH |
So the dial here is not REG_N_SETS, not statement order, and not the birthing boost —
the three levers §162d1/§55a would send you to first. It is the chained-assignment expression form.
The law. When a same-value pair of stores through one pointer leaves a call's argument copy
scheduled too early, try *b = *a = v; BEFORE reaching for any local, pin, or reorder lever.
§162d1's fresh-single-set-local prescription does not fire on this shape.
Provenance. Found by the Opus arm of the P31 S56 model bake-off, drafting under the standard wave
laws with match_one as its oracle; the six-variant ablation was run in one gated batch. The match
was independently confirmed by reloc_identity (11 relocations, all agreeing) before this entry was
written. §199-D supplied the structural template for the function itself (it names func_801812FC,
ov_SC06_025, as the banked twin of this slti 0x2 switch shape).
§206 — THE JTBL-CARVE DRAFTING IDIOM: the bounds check is the entry count, and an EMPTY case owns a slot (P31 S56)
Scope. 286 open jtbl-carve members, 46,068 instructions, 191 families — of which 245 members
(36,685 ins) are COLD (no sim≥0.99 twin) and therefore need this idiom rather than a remap.
Worked exemplar: func_801F218C (md_SC03_076, 83 ins, jtbl_801EF6D0), MATCH in 5 oracle calls,
reloc_identity AGREE on 13 relocations.
1. THE RECOGNITION TELL — the bounds check IS the entry count.
Find the sltiu $vN,$vM,K feeding the beqz that guards the jr. K is the number of cases:
lui $at, %hi(jtbl_801EF6D0) ; lw $v0, %lo(jtbl_801EF6D0)($at) ; jr $v0
sltiu $v0,$v1,0x5 <- FIVE entries
CONFIRM against the dlabel…enddlabel span, but when they disagree trust the sltiu: a
trailing extra word is §8e alignment pad, not a case (§8a's law, byte-confirmed on func_8015AE2C
at 7-vs-8). Here the span is 5 and 0x801EF6D0 is 8-aligned, so no pad was possible.
2. AN EMPTY CASE OWNS A TABLE SLOT, AND THE SLOT POINTS AT THE EPILOGUE. (new — not in §8a/§8e) Read the table entries as labels first, and only then assign case numbers:
[case0=.L801F21C8, case1=.L801F22C4 (= the EPILOGUE -> EMPTY case), case2=.L801F2238, ...]
An entry aimed at the epilogue is case N: break; — it must be written literally. Omitting it is
not cosmetic:
| variant | result |
|---|---|
case 1: break; present (correct) |
MATCH 83/83 |
| the empty case omitted | 90 ins, 66 mismatched |
Note the direction: dropping it makes the function seven instructions LONGER, because gcc re-derives a denser table and different bounds handling — so a length-drift of +N on a jtbl function is a tell for a missing empty case, not for a missing body.
3. WHEN THE OUT-OF-RANGE TARGET EQUALS A TABLE ENTRY, THERE IS NO default CLAUSE. (new)
One observation settles two source decisions: the beqz (out-of-range) target here is .L801F22C4,
which is also table entry 1. So the source has no default clause AND entry 1 is the empty case.
4. A 3-CASE DISPATCH ON A CALLEE RETURN IS A NESTED switch, NEVER AN IF-CHAIN. (new, ablated)
gcc-2.7.2 compiles switch(t){case 1;case 2;case 3;} to a binary tree — beq t==2 first, then
slti at the middle value, li/beq leaves, explicit j to the join for the fall-through leaf.
The if-chain form does not reproduce it:
| inner form | result |
|---|---|
nested switch (t) |
MATCH 83/83 |
if (t==1) … else if (t==2) … else if (t==3) |
77 ins, 51 mismatched |
5. TAIL-MERGE ACROSS CASES IS SOURCE-ORDER, NOT A LEVER TO FIGHT. Two sites share
jal func_80178CBC; sh $zero,0x34: one arrives by an explicit j, the other FALLS THROUGH. Write
the same two statements in each case and the asymmetry falls out of source order for free.
6. KEEP EXACTLY ONE POINTER LIVE. Type the parameter as the struct pointer itself; a
param + derived copy pair forced $s0+$s1 saves and a 32-byte frame against the target's 24.
BANKING (this is drafting only — the carve is a separate, deterministic step). The draft matches
against the .s, but banking needs the §8a carve first: move jtbl_801EF6D0 (5 words, 8-aligned ⇒
no pad) into the binary's rodata subseg per §8/§8b's ld_interleave sandwich, then replace the
INCLUDE_ASM line. tools/jtbl_family_bank.py does carve → extract → remap → gate per sibling.
PROVENANCE. Cracked and distilled by stealth/ox-alpha (free tier) under the P31 S56 bake-off,
at $0.00 and 5 oracle calls. Both negative results in §4 and §2 were independently re-run and
byte-confirmed before this entry was written (51 and 66 mismatches respectively) — the model
asserted them, the gate proved them.
§207 — THE WAVE ab–ag HARVEST (P31 S58): 278 byte-banked notes → 25 laws, 103 self-reported no-gap
Waves ab,ac,ad,ae,af,ag byte-banked 1,028 verdicts; 1,004 carried a drafting note and 278
were flagged novel-idiom candidates. Every function below passed the whole-binary byte gate — the
bytes are proven. The explanations were the drafting models' own claims and are not, so the
load-bearing ones in §208–§232 were re-checked against the target .s before being written down;
each such claim carries a ✔ byte-checked marker naming what was read.
The headline number is 103. One hundred and three of the 278 notes end with some form of "nothing the cookbook did not already cover" — the knowledge base answered the card outright. Of the remainder, the overwhelming majority restate §193-A (twin-first), §194-E (neighbour-first), §183 (declaration spelling) or §21/§30 (reload regime) in fresh words. What follows is what survived.
Three whole clusters were DISCARDED as already-owned, recorded here so they are not re-mined:
| cluster | notes | already owned by |
|---|---|---|
"a bit-15-set halfword constant needs u16* for ori, s16* for addiu" |
9 (func_80181810, func_8018876C, func_80182FB8, func_801A7CDC, func_8018A0E4, func_80187F18, func_80183750, func_80182AA4, func_80185E3C) | §21 L1841-1846 and §135-x L2928 #5 — verbatim, and already re-rejected once at §201-REJECTED |
"grant MEM_IN_STRUCT_P with an anonymous-struct member ref to move a load past an aliasing store" |
2 (func_801831E4, func_801824D4) | §30 (L2385, L2396, L2515, L9122) — including the "anonymous-struct member-ref is the universal grant" clause. See the §30 addendum below for the one composite that is new. |
"one lw per store-separated run; name it in a local to collapse the run" |
6 (func_8017F920, func_801AD914, func_80183968, func_8018457C, func_801A64DC, func_8018A42C) | §193-E and §193-H, which state the count law and the naming lever exactly. §208 below adds the one dial they do not own. |
§208 — TWO NAMED LOCALS FOR ONE RELOADED EXPRESSION BUY TWO ALLOCNOS — the naming granularity owns the REGISTER SPLIT, not just the load count (P31 S58)
Bounds §193-E / §193-H, which own how MANY lws a pointer-derived base emits (one per CSE-live
interval / per store-separated run) and name the C local as the only lever over that count. Neither
says what happens when you declare two locals for the same expression. Refutes §137's
"source-level levers are a dead end for REGALLOC-PERM" for micro-functions.
THE TELL. The target reloads the same expression twice into two different registers:
lw $v1, 0x20($s0) <- reload #1
sh $v0, 0x12($v1)
lw $v0, 0x20($s0) <- reload #2, DIFFERENT register
sh $zero, 0x14($v0)
(✔ byte-checked: asm/ov_SC03_028/.../func_80185028.s idx 5/9 — $v1 then $v0, same 0x20($s0).)
THE LAW. One named local for both reads gives gcc one pseudo; local-alloc coalesces the two reload sites onto one hard register and you get an unfixable clean swap. Two separate locals mint two pseudos ⇒ two allocnos ⇒ two hard registers, restoring the split. The edit is one extra declaration and it changes the pseudo set, not the schedule — which is precisely why §137's two-compile priority arithmetic cannot see it.
| function | binary | residual before | edit | result |
|---|---|---|---|---|
func_80185028 |
ov_SC03_028 | single pointer CSE'd → lost the $v1/$v0 split and the two zero-stores' delay-slot pairing |
two distinct locals for the two reloads of *(a0+0x20) |
MATCH 29 |
func_801801AC |
ov_SC04_004 | REGALLOC-PERM $v1>$a0>$v1 |
p1/p2 as separate locals (target keeps $a0 for the first — pointer and saved halfword — and $v1 for the reload) |
MATCH 34 |
func_801852C8 |
ov_SC03_006 | 4 tail diffs; one reassigned local let gcc merge both lw 0x20($a0) into one long-lived pseudo |
a second pointer local born only after the halfword dies | MATCH 15 |
func_801A7F84 |
md_SC07_004 | $v1/$a0 split between two identical inline reload expressions |
pin ONE side (register s32 v1 __asm__("$3")) and leave the |= block's temp unpinned |
MATCH 52 |
THE READ-BACK RULE. A target that reloads one expression into two different registers is telling
you the source had TWO locals, not one reassigned local. func_801852C8's note puts it best: the
second lw landing in the halfword's freed register is the diagnostic, and it is in the target
bytes, not in your diff.
BOUND 1 — it only fires when the two reads are separated by a store or a call. With nothing
between them cse merges the two locals back into one and the extra declaration is inert.
BOUND 2 — keeping the FIRST value live across the separator undoes it. func_801852C8
explicitly: a second pin on the first pointer re-merged the loads and cancelled the whole lever.
THE OPPOSITE DIRECTION, for completeness. Where the residual is a value staying live too long
rather than a register split, sink the load INTO the if condition: func_80182960
(ov_SC04_002) hoisted *(p+0xC4) into a local first and gcc kept it live across the store, used li
instead of ori for 0x8A10, reordered sh 0x52 past the andi and duplicated the epilogue — 31
ins. Written inline in the condition: MATCH.
PROVENANCE. Four independent wave-ac/ad/ae/af cards, four different overlays. func_80185028's
two-register reload shape was re-read out of the .s this session.
§209 — THE NARROW LOCAL IS A DIAL IN TWO OPPOSITE DIRECTIONS, AND §194-B's "≥2 sh STORES" BOUND IS BYTE-WRONG (P31 S58)
Direction A — a HImode local KEEPS a truncation copy that an s32 local elides. BOUNDS §194-B.
§194-B reads: "A addu $rA,$rB,$zero copy feeding ≥2 sh stores is a SECOND, 16-BIT-DECLARED
local — the width, not the clamp or the join liveness, is what keeps the copy." Its bound #2
("fewer than two surviving consumers ⇒ no copy") is refuted:
lbu $a1, 0x0($a0)
addiu $v0, $zero, 0xFF
addu $v1, $a1, $zero <- the copy
beq $v1, $v0, .L801A8764 <- consumer #2 is the COMPARE
...
sb $a1, 0x0($a0) <- consumer #1 is ONE sb, not two sh
(✔ byte-checked: asm/md_SC07_004/.../func_801A8738.s, all 13 instructions.)
func_801A8738 (md_SC07_004, wave ac, ~10 drafts): every s32 spelling compiles to 11 ins with the
load landing directly in $v1 and no copy. Declaring the accumulator s16 reproduces the target
exactly — the HImode declaration makes "assigned then tested" emit a deferred-truncation copy before
the compare, which also hands the pseudo $a1 by copy-preference off the incoming argument. Probed
and refuted on the same card: two s32 locals (copy coalesces), an s32+s16 pair (copy appears
but after the increment), u16 (same as s32), a guard-style nested if (identical, per §164-55),
hoisting t = v+2 above the return test (moves the copy below the branch), and opaque asm
barriers (inert — reorg runs after everything).
Three more instances agree, none of which has two sh stores either:
| function | binary | s32 gives |
s16/short gives |
|---|---|---|---|
func_80184E30 |
ov_SC02_005 | +1 ins, an extra nop, no addu $a1,$v0,$zero |
MATCH 36 — the s16 type blocks gcc's dead-copy elimination |
func_8018B830 |
ov_SC04_011 | the copy folds away (it is NOT a CSE duplicate-read) | s16 a2 produces the copy for the compare while the raw load feeds the later add |
func_801815A8 |
ov_SC03_006 | reading straight into the if condition folds it away |
a named short v local produces addu $v1,$v0,$zero |
THE AMENDED LAW. A addu $rA,$rB,$zero immediately before a compare or a narrow store is a
16-BIT-DECLARED local. The consumer count is irrelevant — one sb plus the compare is enough.
Direction B — for a *(s16*) load whose only consumer is a zero/equality test, the RECEIVING
LOCAL'S MODE decides lh vs lhu. This is a SECOND, INDEPENDENT DIAL from §15764.
lh $v1, 0xFE($s0)
...
bnez $v1, .L801A44B0
(✔ byte-checked: asm/md_SC07_004/.../func_801A445C.s idx 4/7 — a bare lh feeding only a bnez.)
§15764's relaxation ("missing sra ⇒ write the lvalue *(s16*)") is necessary but not
sufficient: func_801A445C wrote s16 v1 = *(s16*)(p+0xFE); if (v1 == 0) and still got lhu
(WIDTH/1), because an s16 local is a HImode pseudo (§21189: s16 k and u16 k are both
(reg/v:HI)) and the extension into it is elided by the same relaxation. Landing the load in an
s32 variable makes the sign-extension a genuine SImode requirement fused into the load pattern,
the relaxation cannot fire, and the bare lh survives. func_801A9270 (md_SC07_004) reaches the
same conclusion independently: s16 g ⇒ lhu; s32 g = D_801F8E98 ⇒ lh. func_80183B20
(ov_SC06_000) is the third face — an s16 raw local rematerialised at its second use as
lhu + manual sll/sra; reading the s16 field directly inside the array subscript emitted the
target's plain lh.
A third lever for Direction B, single observation — not yet cross-confirmed. func_8017F300
(ov_SC06_029, 12/12) has
lh $v0, 0x100($a0)
sll $v0, $v0, 2
sh $v0, 0x34($a0)
(✔ byte-checked.) The sign-extension is semantically dead under the later sh, so with the natural
<< 2 spelling extend-propagation weakened the load to lhu. Spelling the scale as a multiply
(* 4) matched. The submitted mechanism — "gcc does not do the dead-high-bits analysis through
MULT, so SIGN_EXTEND stays alive through expand" — is plausible and untested; ship the
precondition (*k vs <<n is a load-width dial when the extension is dead), not the story.
⚠ THE TWO DIRECTIONS WANT OPPOSITE WIDTHS. Direction A wants the narrow local, Direction B wants
the wide one; one local cannot give you both a truncation copy and an lh. Choose by the residual
you actually have: a missing addu ...,$zero ⇒ narrow; an lhu where the target has lh ⇒ widen.
§210 — THE SINGLE-BIT MASK IN A BOOLEAN TAIL: andi K ; sltu $zero,v vs srl n ; andi 1 is a STATEMENT-SHAPE dial, not an operator choice (P31 S58)
THE TARGET SHAPE — byte-identical tails, two different overlays, found independently by two different drafting agents:
jal func_80133784
sh $v0, 0x1A($sp)
andi $v0, $v0, 0x2000
lw $ra, 0x20($sp)
sltu $v0, $zero, $v0 <- the generic normalisation
jr $ra
(✔ byte-checked: asm/ov_SC03_029/.../func_8018723C.s and
asm/ov_SC03_006/.../func_80188C18.s — the two tails are word-for-word identical.)
THE TRAP. In a straight-line value tail with no join and no branches, the fused expression
return (func_80133784(1, pa, pb) & 0x2000) != 0; /* WRONG BYTES */
lets gcc see the single-bit mask inside the comparison and emit the bit-extract form
srl $v0,$v0,13 ; andi $v0,$v0,1. Refuted on the same card: the ternary ? 1 : 0 and the u32-cast
spelling do not fix it.
TWO FIXES, BOTH BYTE-PROVEN, ON THE SAME SHAPE:
| card | fix | result |
|---|---|---|
func_8018723C (ov_SC03_029, wave ag) |
name the masked value: r = call() & 0x2000; return r != 0; — the statement boundary hides the mask from the != 0 expander |
MATCH 30/30 |
func_80188C18 (ov_SC03_006, wave ae) |
spell the test as CONTROL FLOW: if (x & 0x2000) return 1; return 0; |
MATCH |
Same predicate, three statement shapes, two different final instruction sequences.
THE LAW. Mask visibility to the truth-expression expander is controlled by statement
boundaries alone, with no control flow required. When the target shows andi K ; sltu $zero,v and
your draft shows srl n ; andi 1, split the mask out of the comparison — either into a named local
or into an if.
BOUNDARY. This is a SINGLE-BIT-mask law. A multi-bit mask has no bit to extract and already
normalises to andi+sltu from the fused spelling, so the split is inert there. It is also distinct
from §164-56/§165-22 (fold_truthop paths over two comparisons) and from §19x/§167-09 (a 0/1
materialised at a JOIN label, which is about naming a condition, not a mask) — those cannot fire
on one mask of one call result. §195-E's value-vs-no-value axis gains this as a third dial.
§211 — HOIST THE LOOP INIT ABOVE THE DOMINATING GUARD: it fills the guard's delay slot AND flips the counter/pointer register pair (P31 S58)
THE TELL. The guard branch's delay slot holds an induction-variable init:
lbu $v1, 0x10A($a0)
addiu $v0, $zero, 0x1
beq $v1, $v0, .L80184BA4
addiu $a1, $a0, 0xE4 <- the CURSOR init, in the GUARD's slot
addu $v1, $zero, $zero <- the counter, in $v1
(✔ byte-checked: asm/ov_SC04_011/.../func_80184B3C.s idx 0-4.)
THE LAW. An init written INSIDE the guarded block can never be scheduled into the guard's delay slot. Hoisting it above the guard does two things at once: (a) it becomes eligible for that slot, and (b) it lengthens the pseudo's live range across the guard block, flipping the local-alloc density contest between the counter and the pointer.
| function | binary | in-block spelling | hoisted spelling |
|---|---|---|---|
func_80184B3C |
ov_SC04_011 | every variant tried (wp-then-i, i-then-wp, decl-in-block, while vs for) allocated cursor→$v1 / counter→$a1 — the exact swap |
s16 *wp = (s16*)((s32)a0 + 0xE4); before the if ⇒ MATCH 28 |
func_80189EB0 |
ov_SC02_027 | init inside the slow arm: near 7 (li outside the slot + full register swap) |
i = 4 above the +0xDE guard ⇒ addiu $v1,$zero,4 in the beqz slot, $v1=counter / $v0=pointer, MATCH 30 |
func_801861D4 |
ov_SC04_011 | inits swapped: $s0 materialised before $s1 was defined |
i = 0; ptr = ...; as separate statements before the for ⇒ target's sw/lui order |
THE COMPANION NEGATIVE — init PLACEMENT has no default, so A/B both forms. Two cards in the same harvest want opposite answers, and neither is predictable from the shape:
func_8018AA88(ov_SC02_005, 9 ins):for (i = 11, ptr = &D_801E4A84; ...)— comma-init in the header — put counter in$v0and pointer in$v1, reversed vs the target (7/9 mismatched, ADDRESSING class). Hoisting both inits into preceding statements (or equivalentlydo{}while(--i>=0)) gave the target's counter=$v1/pointer=$v0. Local declaration order between the two pseudos was tested both ways and is irrelevant. The hypothesis "the second-defined pseudo claims$v0" was falsified on this card; the determinant is init statement placement.func_801871E4(ov_SC02_005, 39 ins): the opposite —for (v = -1, i = 0; ...)as ONE comma-init is what landsaddiu $a0,$zero,-1+addu $v1,$zero,$zeroadjacently in straight-line code; a separatei = 0;statement gets duplicated by the scheduler into BOTH branch delay slots (move v1,zero×2).
A THIRD PLACEMENT CONSTRAINT — a loop-invariant constant init FLOATS ABOVE an address
materialisation. func_801ACD4C (md_SC07_004): plain source order let sched2 float i = 0 above
the lui/addiu of &D_801F8B18. __asm__ volatile("") over-pinned (it dragged sw $ra below
the setup) and an initializer-list i = 0 did nothing; the fix was to create a real data
dependence on the address between the two statements (first = base[0]; placed before i = 0;).
BOUNDARY. All of the above is local-alloc density and delay-slot eligibility, so it only bites
when the guard/branch actually has a fillable slot and the two induction pseudos actually contend.
On a loop with no dominating guard there is nothing to hoist above.
Addendum (P31 S63 t5b-t5d, func_8017DD80): §211's guard-hoist is a lever for a materialisation, and it INVERTS on a copy. Every card in §211 and in its S58b addendum hoists an insn that computes a fresh value (addiu $a1,$a0,0xE4, addiu $v1,$zero,4, i = 0); when the target's guard slot instead holds a bare addu $rD,$rS,$zero copy of a pseudo defined one statement earlier, hoisting that copy above the guard does not relocate it into the slot — it lands the copy adjacent to its source's own def inside one basic block, where §46-L2/§48-B's EBB rule ("a source-level fp = q; ALWAYS dies unless defined in a guard block and used in the loop") deletes it and folds its destination straight into the producing ALU op. An instruction is eliminated, not moved: LENGTH-DRIFT/-1 with the whole tail shifted one slot early. (Which pass performs the fold was not traced — empirical, not internals-verified; the deletion law it instantiates is byte-proven at §46-L2/§48-B/§162p.)
THE TELL. Target: addu $a1,$a2,$v0 (address calc) ; blez $v1,.L ; slot addu $a0,$a1,$zero. The hoisted draft has no copy at all and the address calc writes the copy's register directly — addu $a0,$a2,$v0 — at nins_mine = nins_tgt - 1. Read the slot insn's SHAPE before applying §211: rD,rS,$zero ⇒ do NOT hoist; a computed operand pair ⇒ §211 applies as written.
THE FIX FOR THIS SHAPE. Leave the copy inside the guarded block and park §164-36a's zero-byte __asm__ __volatile__(""); between the address calc and the guard — reorg.c:675 stop_search_p halts fill_simple_delay_slots' backward scan on any asm, so the addr calc can no longer be stolen into the slot and fill_eager_delay_slots takes the in-block copy instead. The pre-fence residual names itself: the addr calc and the blez appear transposed against the target (mine blez then addu, target addu then blez), which is the backward scan having won.
BYTE EVIDENCE — func_8017DD80 (ov_SC06_025, 55 ins, banked src/ov_SC06_025/ov_SC06_025_jr_8017BEBC.c:3719; target 8017ddc8-8017ddd0). v3 (pins + a $2-pinned integer-space temp for param_1[4]*4, §239) = closeness 2, residual exactly [[18,"blez v1,78","addu $a1,$a2,$v0"],[19,"addu a1,a2,v0","blez $v1,.L8017DDF8"]]. v4/v5 hoisting puVar3 = puVar7; above the if — the §211-prescribed edit — regressed to closeness 39, LENGTH-DRIFT/-1, 54 vs 55. v6/v7 reverted the hoist and added the one fence line ⇒ MATCH, closeness 0, residual []. The fence is solo-proven (§266): it is the only delta between the closeness-2 and the closeness-0 build.
DISCRIMINATE against the two near neighbours before reaching for this. §164-82 has the same surface tell (LENGTH-DRIFT −1, a guard branch whose slot holds addu $sD,$v0,$zero) but its cause is a hoisted LOAD and its fix is the test-then-re-read spelling — that one applies when the slot copy's source is a memory value, this one when it is a just-computed address. And §156's sub = pct bullet prescribes the opposite placement ("survives cse ONLY placed in the LOAD's bb before the branch; in an arm every spelling dies") — that regime has the copy's use two fall-through branches down, so cse's follow-jumps table reset saves it; here the use is in the same block as the def and the guard block is the only surviving boundary.
§212 — THE WALKING CURSOR IS COUNTABLE: *wp++ emits one addiu PER STORE, wp[0..2] emits one (P31 S58)
THE TELL. Three stores at displacement 0 off a base that steps between them:
sh $v0, 0x0($a1)
...
addiu $a1, $a1, 0x2
sh $v0, 0x0($a1)
...
addiu $a1, $a1, 0x2
sh $v0, 0x0($a1)
(✔ byte-checked: asm/ov_SC04_011/.../func_80184B3C.s.)
THE LAW. Three sh 0(reg) with three separate addiu reg,2 is *wp++ written three times.
wp[0] = …; wp[1] = …; wp[2] = …; compiles to sh 0 / sh 2 / sh 4 plus ONE addiu reg,6 at the
loop bottom — a different instruction count and a different delay-slot fill. The two spellings are
semantically identical and byte-distinct; read the displacement column, not the C you would naturally
write.
The same discrimination runs the other way inside one function. func_801861D4 (ov_SC04_011,
wave af) has two loops with two different answers: loop 1 over D_801EFCA8 is a WALKING POINTER
(*ptr++, 16 entries) — the indexed form gave the same 52-ins count but lost the register allocation
— while loop 2 over D_801EFCF8 stays INDEXED (D[i], i+1 as the call argument) exactly as the
asm shows. Do not unify them.
AND THE THIRD FORM — two same-base walkers of DIFFERENT scale must both be INDEXED.
func_80187754 (ov_SC06_000, 17 ins) burned four drafts on this. A naive two-pointer walk gave
LENGTH-DRIFT; a walked-pointer p[5], p++ gave addiu $a0,0x14 where the target holds $v1 as an
addu COPY of $a0; stepping the halfword walker by elements gave the wrong byte step, and moving
the increment earlier let gcc fold it into the store address (losing the standalone addiu). The
crack: spell BOTH walkers as indexed expressions off the single base — ((s16*)a0)[i+2] and
a0[i+5]. gcc-2.7.2's IV elimination then creates the two givs in the target's order ($v1 = word
walker +4, $a0 = halfword walker +2 in the branch delay slot). The residual was giv-CREATION
order between two same-base index expressions of different scale, and it fell out of the
indexed-vs-walked spelling with no pin.
RELATED, and worth reading with §162e2/§164-03: func_80184738 (ov_SC02_011) — gcc hoists
v1 = p + 0x70 out of the loop, so every offset in the body is printed relative to p+0x70;
sh …,0x8C($v1) is really *(s16*)(p+0xFC). And on the same card, writing p += 0x10C in the loop
BODY emits addiu $v1 before addiu $a2; moving it into the for-increment clause
(i++, p += 0x10C) flips the order to match. Pure C-source placement, invisible in any pseudo-C
rendering of the asm.
§213 — INDEPENDENT SAME-BASE STORES: THE EMISSION ORDER IS A PERMUTATION OF SOURCE ORDER, AND THE PERMUTATION IS NOT ALWAYS THE IDENTITY (P31 S58)
§2-T2/§162d and the whole "statement order is the dial" family assume the map is the identity. It is not. Six cards in this harvest measured three different permutations on the same construct — a run of independent stores to one base — and the only reliable procedure is to A/B them.
| function | binary | ASM store order | SOURCE order that produced it | permutation |
|---|---|---|---|---|
func_80181AD4 |
ov_SC02_000 | 0xC,0x14,0x20,0x22,0x30,0x24,0x2E,0x32,0x9C,0xA0 | the same, verbatim | identity (note it is NOT ascending offset) |
func_801853D4 |
ov_SC02_000 | 0x14,0x18,0x10,0x44,0x48,0x4C | the same, verbatim | identity (again non-monotone) |
func_8017F614 |
ov_SC04_011 | 0x10, 0x18, 0x14 | 0x10, 0x14, 0x18 (ascending) | last two swapped |
func_8017F470 |
ov_SC04_002 | 0x10, 0x18, 0x14 | 0x10, 0x14, 0x18 (ascending) | last two swapped |
func_80182FAC |
ov_SC02_005 | 0xE, 6, 0xA | 6, 0xA, 0xE | right-rotate by one |
func_8018A0E4 |
ov_SC02_005 | +0x18 before +0x10 | +0x10 before +0x18 | reversed pair |
(✔ byte-checked for func_80182FAC: asm/ov_SC02_005/.../func_80182FAC.s — loads land in
$v0/$v1/$a0 in ascending-symbol order, stores emit sh $a0,0xE / sh $v0,0x6 /
sh $v1,0xA, the last in the jal delay slot.)
THE PROCEDURE. When a run of independent same-base stores diffs as a pure offset permutation
with identical registers and count, do not reach for pins, fences or /s levers. Try, in order:
(1) the asm order verbatim; (2) ascending offset order; (3) the asm order rotated one
slot left. One of the three has matched on every instance measured here.
A SECOND-ORDER EFFECT WORTH KNOWING (single observation, mechanism unverified).
func_80181960 (ov_SC07_007) reports that the register assignment follows the same order: the
target holds const1 in $t0 but const2 in $a3 because const2's live range is shorter, its store
being scheduled LAST. The submitted mechanism — "sched1 is a backward list scheduler, stores picked
in descending source LUID" — is consistent with sched.c's ready-list construction but was not
re-derived; what is byte-proven is that writing the sh-field assignment after the four constant
stores reproduced the exact register split, and having it second lost $a3 to const1 (near 10).
BOUNDARY. This is about independent stores — no aliasing, no shared value, no call between
them. The moment a call, a /s-relevant load or a shared pseudo enters the run, §30/§193-D/§194-M
own the ordering and the permutation table above is noise.
§214 — THE BANKED TWIN MAY BE A MACRO, A DELETED .s, OR A SEMANTIC INVERSE — six ways a ≥0.9 similarity lies (P31 S58)
Extends §193-A/§194-E. §193-A tells you to read the twin's body; it does not tell you where the body is, and it does not price what similarity hides. Both cost real compiles across waves ab–ag.
1. THE TWIN'S BODY IS OFTEN A DEFINE_ MACRO IN src/shared/engine_core.h, NOT C IN ITS OVERLAY.
Reported independently by seven cards — func_80183848, func_80182964, func_801A9954,
func_8018C308, func_80182FBC, func_8017F6B8, func_80184464 — all of which grepped
src/ov_SC01_077/*.c, found only extern declarations, and concluded "shape-only". The bodies were
in the shared header the whole time. Budget one extra grep: grep -n 'DEFINE_func_<ADDR>' src/shared/engine_core.h BEFORE writing off a twin. The dedup'd body is the matched C, which
makes it strictly better evidence than a sibling .s.
1b. THE CHAIN CAN BE TWO HOPS. func_80181384 (ov_SC07_007, 9 ins): the twin
ov_SC01_077:func_801598BC was itself a DEFINE_ stub; the literal same-family member
DEFINE_func_8016F44C in the shared header carried the exact idiom
(return (func_80029178(base + 0x125) & 0xff) != 0;), with the only delta being the base register.
2. A BANKED TWIN'S .s IS PRUNED. func_8018DEB0 (ov_SC04_011): the twin
ov_SC02_005:func_80187F1C had no .s left — banked functions are removed from
nonmatchings/ — so only its C survives, and the TU-neighbour became the sole shape source.
3. WHAT ≥0.9 SIMILARITY HAS BEEN MEASURED TO HIDE. The skeleton metric is blind to all of these:
| card | sim | what the twin hid |
|---|---|---|
func_8017F66C (ov_SC02_017) |
0.926 | a whole statement — a lone D_801EF9F0 = 0 preamble store, worth 2 instructions; first draft came out 46 vs 48 |
func_8018C978 (ov_SC02_005) |
— | inverted branch polarity per arm; the twin's < 0x51 order gave near 7, the target wants >= 0x51 |
func_80180258 (ov_SC04_002) |
~0.9 | polarity inverted wholesale (> 0x1000 early-return-then-body → >= 0x2401 slti+beqz over the body) |
func_801A43EC (md_SC07_004) |
≥0.9 | semantic inversion — twin destroys (call then zero the global), target lazy-creates (zero-check, call, store the return). Only the delay-slot/register trace catches it |
func_80184578 (ov_SC06_000) |
0.94 | different dataflow into the same calls — twin feeds &table to its first callee, target computes the address solely for a sw and passes the untouched $a0 |
func_8018810C (ov_SC02_005) |
0.94 | the gate result is DISCARDED — twin branches on its first call, target sequences it and passes two args |
func_80183C74 (ov_SC04_011) |
— | reversed argument order (func_8012B6D4(a0+4, &D_80126B5C), not the twin's order) |
THE LAW. Trust the twin for STRUCTURE and for DECLARATION SPELLINGS. Re-derive every literal,
every branch polarity, every argument, and the instruction COUNT from your own .s — and diff your
.s's instruction count against the twin's body before trusting a 1:1 transcription. This is law 2
stated with teeth; six of the seven rows above are law-2 violations that cost a compile.
4. THE MISLEADING-LOW SCORES ARE JUST AS COMMON, AND THE FIX IS ALWAYS §194-E. Across this
harvest, cards whose advertised twin scored 0.55–0.72 and turned out to be a false relative
(func_801A3A6C 0.55, func_80182AA4 0.57, func_801877FC 0.57, func_80184A68 0.56,
func_80185A5C 0.60, func_8018251C 0.67, func_80183968 0.67, func_80184524 0.67,
func_80184C20 0.68, func_80184840 0.69, func_80188034 0.72) all resolved through a
same-TU neighbour instead. §194-E is not a fallback; on a sub-0.75 card it is the primary source.
§215 — PIN ECONOMY: the twin's pins are NOT part of the shape, and §17's "pin every call-crossing value" is over-broad (P31 S58)
THE DEFAULT IS PLAIN LOCALS. Run that A/B first. Five cards in this harvest copied a twin's
register T v __asm__("$16"/"$17"/"$18") triple and were made worse by it:
| card | binary | with the twin's pins | with plain locals |
|---|---|---|---|
func_80182A30 |
ov_SC06_000 | every pin permutation left a 4-insn SCHEDULE-REORDER in bb0; statement order controlled which pairs permuted (i=0 last → s2,s1,s0; i=0 mid → s0,s2,s1) but never reached the target's s2,s0,s1 |
s32 i; s32 *ptr; s32 a0; → the exact target weave, first try; statement order became irrelevant |
func_8018B684 |
ov_SC03_006 | the ≥0.9 twin's __asm__ barrier gave gcc nothing to hoist; a fresh local + barrier gave 24 ins / wrong fills |
duplicate the call per arm with literal constants → cross_jump merges the tail, MATCH 25 |
func_80184840 |
ov_SC04_011 | the twin matched without any pin, so the twin was actively misleading on the regalloc axis | this card needed register s32 p __asm__("$4") — the opposite conclusion from the same family |
func_801AD5B4 |
md_SC07_004 | the twin's pins were entirely unnecessary for this relative | plain locals sufficed once statement order was right |
func_8017E424 |
ov_SC07_007 | the TU-neighbours' house hints (register pins, casted calls) were counterproductive for this body | typing the parameter plain s32 won — one pseudo, no pointer/int allocno split |
AND §17's ABSOLUTE IS WRONG. func_8018130C (ov_SC03_124, wave af): §17 prescribes pinning
EVERY call-crossing value. Pinning arg0 to $17 alone → near 21. Pinning only $s0 to $16,
leaving s2/s3 unpinned so gcc keeps its own density ranking → near 4. Pinning s2/s3 as well
→ near 47 LENGTH-DRIFT (the pins on the mult-chain temps broke gcc's negu/mflo scheduling).
Only the values that genuinely cross a jal boundary need pins.
WHEN THE PIN IS THE ANSWER — four shapes, all "pin exactly one thing":
- A caller-saved colour rotation where the target register IS the incoming-arg register.
func_801AE908(md_SC07_004): target colours p→$a0/ t→$v0/ test→$v1against first-fit's$v0/$v1/$a0. Tried and inert:s32-vs-u16on the temp; §13259/§15519's copy-preference respellings (bothvoid*ands32parameter — the$4preference did not survive); field-based spelling with no temp.register s32 p __asm__("$4")→ MATCH 34. This is the explicit complement of §2425's "noregister __asm__pins needed". - A lone
lwin the wrong GPR whose only use is a branch.func_801A2364(md_SC07_004): a plains32 tempput the0xD4field load in$v1;register s32 flag __asm__("$4")forcedlw $a0,0xD4($s0)+beqz $a0. MATCH 39. - Pin the CONSTANT interloper, not the pointer.
func_8018CAF0(ov_SC04_011): unpinned, the0x80000000constant landed in$a1and the pointer/value in$v0/$v1; target wants constant=$v1.register s32 mask __asm__("$3"), with the mask statement placed before the pointer load, closed all 5. (§176-B2's exemplar contested a load against a constant; this is the same fix on a different contested pair — a bareluiconstant feeding an OR mask.) - A zero-byte pin to raise a use count, placed BEFORE the branch.
func_801852C8(ov_SC03_006, 15 ins): ONE__asm__ __volatile__("" :: "r"(hw))immediately after the halfword's birth raised its use count enough for allocno priority to hand it$v1while the long-lived pointer kept$a1. Placement is load-bearing: inside thethen-block it did nothing.
AND THE DIRECTION IS OFTEN INVERTED FROM INTUITION. func_80188104 (ov_SC02_017): the residual
was two early loads reading the restored $a0 where the target reads $s0.
(a) Pinning the SAVED value to $16 fails — the allocator gives the plain parameter its own
callee-saved home ($s1), adds an sw/lw pair (+3 ins), and still serves the early loads off
$a0. (b) Pinning the TAIL web to $4 snaps it: since that value occupies $4 across the
call, the parameter must survive the call elsewhere → it stays in $s0, the early loads read $s0,
and the copy materialises as the target's addu $a0,$s0,$zero in the beqz slot. (c) Ordering
trap: writing the copy FIRST lets dbr thread it into the delay slot before the two loads, so cse
serves them from a fresh $a0 again — the copy must stay after the store pairs.
Read (a)/(b) as the rule: pin the register you want OCCUPIED, not the value you want MOVED.
PROVENANCE. Ten cards, six overlays, waves ab–ag. Also confirms §176-C practically: the $4 pin
scheduled fine around the call on func_80188104 despite the sched.c:1704 hard-reg anti-dep bug
(that bug only adds false deps, and first-fit had no better choice than $4 anyway).
§216 — DISTINCT ADJACENT SCALARS vs ONE ARRAY: one lui per access is the tell, and the array decl is UNUSABLE (P31 S58)
THE TELL. Adjacent byte symbols, each accessed through its own %hi materialisation:
lui $v0, %hi(D_801EFEA0) ; lbu $v0, %lo(D_801EFEA0)($v0)
lui $v1, %hi(D_801EFEA1) ; lbu $v1, %lo(D_801EFEA1)($v1)
...
lui $at, %hi(D_801EFEA0) ; sb $v0, %lo(D_801EFEA0)($at)
lui $at, %hi(D_801EFEA1) ; sb $v1, %lo(D_801EFEA1)($at)
(✔ byte-checked: asm/ov_SC04_011/.../func_8018BB44.s — six independent luis in the first block,
plus addiu $a0, %lo(D_801EFEA3) at function entry as a real base pointer.)
THE LAW. An array declaration forces gcc to materialise ONE base register and address every
member off it. When the target shows an independent lui+%lo per byte, each byte must be a
DISTINCT SCALAR OBJECT in the C — a block-scoped extern u8 per address, with an explicit __asm__
name where the symbol is not spellable.
func_8018BB44 (ov_SC04_011, 49 ins) is the measured case: the card's authoritative
extern u8 D_801EFEA0[] was unusable — accessing through the array, or &arr[3], or arr[i], all
collapsed to one base pointer and 43 ins against the target's 49. Four separate scalar decls,
plus a separate u8 *flag = &scalar3; to force the $a0 = &D_801EFEA3 materialisation at entry,
matched. func_8018BA9C (ov_SC04_011, 10 ins) is the minimal version: three sbs to D_801EFEA0,
D_801EFEA1, D_801EFEA2, each with its own lui $at — folding them into D_801EFEA0[1]/[2]
would drop two lui+relocation pairs. It also reuses the TU's existing extern u8 D_801EFEA0[]
verbatim and indexes [0], which is codegen-identical to a scalar.
BOUNDARY — and it is a real one. The opposite direction is §183's array spelling, and it is just
as often correct: func_80186C70 (ov_SC02_005) needs extern T *D[] because the target
materialises the table base once via lui/addiu with no intervening load, and func_8017F3B0
(ov_SC02_017) needs D_8018E19A as an array so that the <<2 index rides its own single lui $at. Count the luis: N accesses / N luis ⇒ N scalars; N accesses / 1 base ⇒ one array.
AND THE THIRD CASE — the TU already spells it as a scalar. Several ov_SC04_011 cards
(func_80186908, func_80184FC4, func_80181868, func_80185C04) hit D_801EFC4C declared
file-scope as a scalar extern s32 while the target indexes it. The house escape is
((s32 *)&D_801EFC4C)[i] (the TU's own banked func_80185FAC uses exactly that), or the
__asm__-label alias per §200 — never retype the file-scope declaration.
§217 — DECODING A CALL'S STACK ARGUMENT SLOTS: sw at 0x10/0x14/0x18 are params 5/6/7 (single observation — not yet cross-confirmed) (P31 S58)
func_8017C624 (ov_SC06_000, wave ad, 16/16) near-missed not on scheduling but on a mis-decoded
argument map. The pre-call block reads like value shuffling:
addu $v0, $a0, ... (the prologue copy chain)
addu $a1, $v0, ...
addu $a2, $zero, $zero
sw ... 0x10($sp) <- param 5
sw ... 0x14($sp) <- param 6
jal func_80146A6C
sw ... 0x18($sp) <- param 7, in the delay slot
THE READING RULE. In a call with more than four arguments, sw to 0x10/0x14/0x18($sp) are
outgoing parameter slots 5, 6 and 7 — not spills, not locals. Read them as arguments before
reading the register moves as dataflow: $a2/$zero are frequently just still-live in their ABI
slots while the sw/addu pair interleaves around them. On this card the extra 16th instruction
addu $a2,$zero,$zero is param3's zero being materialised into its register, which only makes sense
once you have read param3=0 and param6=$a2. The first draft put the entry $a2 into param3 and
drifted.
Sibling evidence that settles the family: banked ov_SC01_084:func_8017BEBC is
func_80146A6C(0x19, a0, 0, 0, 0, 0, 1); same-TU func_8017FE8C is identical.
§218 — A NARROW TYPE AT THE ABI BOUNDARY COSTS AN IN-PLACE sll/sra PAIR — on the RETURN as well as on the PARAMETER (P31 S58)
Extends §43, whose tell is stated in the negative ("absence of in-place sll/sra proves 32-bit
params"). This is the cost direction, and it applies to the return type too, which §43 does not
cover.
1. A NARROW RETURN TYPE IN THE PROTOTYPE COSTS A SIGN-EXTENSION AT EVERY CALL SITE.
func_80183258 (ov_SC04_011, 23 ins): the TU's authoritative prototype is
extern s16 func_80174774(void); (atlas tu=('s16',())). Calling it directly forces an
sll v0,v0,0x10 / sra pair the target does not have — even though the result feeds only a
zero-test. The atlas row is byte-true for the SYMBOL but must not be allowed to reach the call site
uncast. Fix, which is the TU's own house style: a cast-at-call through an s32-returning function
pointer, ((s32 (*)(void))func_80174774)().
2. A NARROW ARG CAST EMITS THE PAIR IN PLACE ON THE ARGUMENT REGISTER — AND PERTURBS HEAD
ALLOCATION. func_8017EAC0 (ov_SC02_011, 16 ins) is the clean A/B: without the (s16)a1
narrowing of arg4 the head is the naive move $a3,$a1 shape, 15 ins; with it, the sll/sra
pair displaces the allocator into routing $a0 through $v0, producing the target's odd
addu $v0,$a0 / addu $a1,$v0 head — 16 ins, MATCH. func_8017C624 shows the same 2-insn
pre-call form (§18484). So a strange-looking head copy chain is a tell for a narrowed argument,
not a scheduling artefact.
3. WHEN THE TU'S DECLARATION IS THE NARROW ONE AND THE TARGET IS 32-BIT, USE §202's DEF-SIDE
ALIAS. func_80185C6C (ov_SC06_029, 7 ins): the TU carries
extern void *func_80185C6C(s16, s32); at four sites, but the target has no sll/sra on $a0
at all — both args are 32-bit. Adopting s16 costs a sign-extension from instruction #1, and a
K&R (int,int)-promoted definition conflicts with the s16 prototype. The definition was banked as
aF80185C6C __asm__("func_80185C6C") with the TU's declarations untouched and callers unaffected.
§219 — COMPOUND +=, FULL ASSIGNMENT, AND AN EXPLICIT TEMP ARE THREE DIFFERENT SCHEDULES OF ONE READ-MODIFY-WRITE (P31 S58)
THE LAW. When a read-modify-write's schedule is one slot off — or when the value lands in the
wrong register — respell the OPERATOR before touching statement order. x += d, x = x + d and
t = x + d; x = t; are semantically identical and produce three distinct RTL shapes.
| card | binary | what changed | why |
|---|---|---|---|
func_8017FC64 |
ov_SC02_005 | an explicit sum = hw[0x20E] + var temp promoted the sum into $v0 and mis-scheduled; the compound += kept it in $v1 (the load's own register) and emitted addu $v1,$v1,$v0 before sh 0x210 |
the RMW stays glued to its load |
func_801804C0 |
ov_SC03_124 | the plain temp form still picked $a2; expressing the store as compound *ptr += v0 flipped the rematerialised base to $a1 |
the += form changes gcc's operand-order/register preference for a post-call rematerialisation |
func_80188BEC |
ov_SC03_028 | the two adjacent RMWs need DIFFERENT spellings — the first written as a full assignment x = (x + d) & 0xFFF, the second as +=; uniform spelling drifts length by 1 |
cse folds the first reload into the delay-slot store chain differently |
func_80184738 |
ov_SC02_011 | doing the store through a u8 * produced the target's double lhu 0($v1) around the RMW for free; writing the OR through the same u16 lvalue gcc already chose would merge it away |
char-aliasing forces the reload without a register-variable hack |
AND THE INCREMENT OPERATOR IS ITS OWN THIRD FORM (already §165-06 / §164-XX; two more instances
here): func_80186B14 (ov_SC04_002) — the counter MUST be (*p)++ or ++(*p), never += 1 or
*p = *p + 1; the one-insn expand difference shifts sched1's dependence ranking, and on this card it
also decides whether the last copy's sh lands in the jr delay slot.
A FOURTH SPELLING, for a modular decrement. func_8018009C (ov_SC03_011): the halfword op is
addiu -0x71 / andi 0xFFF — a modular 12-bit decrement (x - 0x71) & 0xFFF, not the twin's
additive (x & m) | k bitmask-or. A bitwise form cannot wrap, so the subtraction is forced. Read
the addiu sign before assuming the twin's operator.
BOUNDARY. All four rows are single-function A/Bs against match_one. What generalises is the
procedure (respell the operator, it is free) — not a claim that += is always the winner. On
func_80188BEC the two spellings won on adjacent statements of the same function.
§220 — THE PARAMETER ITSELF IS A REGALLOC DIAL: use it directly, prefer s32 to void*, and place the save-copy AFTER the first call (P31 S58)
Four cards, four overlays, one direction.
1. DO NOT COPY THE PARAMETER INTO A LOCAL. func_801877FC (ov_SC02_005, 49 ins): naming the
parameter s0 and using it directly matched; s32 s0 = arg0; made gcc double-buffer through
$s1 and add a spill/restore pair. Same on func_8018251C (ov_SC02_000): drafting the twin's
u8 *s0 = a0 prologue forced an early move+sw and drifted 28 → 31 ins.
2. PREFER PLAIN s32 TO void* WHEN THE ALLOCATOR SPLITS ON POINTER-vs-INT. func_8017E424
(ov_SC07_007): the last diff was sh $v0,8($a0) vs 8($s0). A register __asm__("$16") pin lost
the delay-slot fill; a void* spelling made the same $a0 choice; typing the parameter plain
s32 won — one pseudo, no pointer/int allocno split, and regalloc keeps $a0 live through the
tail store. func_801878A8 (ov_SC03_028) reports the identical requirement independently.
3. THE s0 = a0 COPY'S POSITION RELATIVE TO THE FIRST CALL DECIDES THE WHOLE PROLOGUE.
func_80189E1C (ov_SC03_006, 27 ins), the cleanest measurement in this harvest:
| copy written | result |
|---|---|
before the first call (the naive s32 s0 = a0; first) |
gcc computes the copy eagerly → move s0,a0 before the jal, then burns a SECOND callee-saved reg ($s1) for the body copy — 29 ins, frame 0x20, 25 mismatches |
| after the call statement (the twin's order: call, then copy) | the copy's live range starts post-call, local-alloc keeps it solely in $s0, and sched2 sinks addu $s0,$a0,$zero into the jal's delay slot — MATCH, frame 0x18, saves s0+ra only |
One-line reorder was the entire residual, and the twin's statement order encoded it without saying
so. Compare func_80183968 (ov_SC03_029), where the target births s0←a0 at idx 2, before
sw ra, and plain C hoisted the copy too late — an unpinned inline-asm launder at the top fixed
the prologue order and freed $a0 so the first pointer load landed there like the target. The two
cards are the two ends of the same dial.
4. AND THE COPY CAN BE THE PROBLEM. func_80184C20 (ov_SC02_005): a naive
cbptr = *(s32*)(s0+0xCC) let local-alloc's optimize_reg_copy_1 forward-substitute $a0 for $s0
and defer move s0,a0 into the beqz delay slot. Fix per §11402: make the use insn also SET the
source (a0 = *(s32 *)(s0 + 0xCC);) so reg_set_p aborts the substitution scan.
Addendum (P31 S64 t5j-t5m, func_8018AAE4): the RESIDUAL SIGNATURE for this addendum's pin case, and the closeness trap that hides it. func_8018AAE4 (75 ins) runs the whole ladder in three match_one calls: register s32 s0 __asm__("$16"); s0 = a0; → near, closeness 1, one masked residual at idx 65, mine 00000000 nop vs target 02002021 addu $a0,$s0,$zero, klass DELAY-SLOT / profile schedule / sig DELAY-SLOT/1; the plain s32 s0 = a0; neighbour-house-style copy → closeness 64, nins 78 vs 75, 02008821 move $s1,$s0 at idx 8 plus the extra sw $s1 (§161b's +3, to the instruction); deleting the alias entirely and referencing a0 at every site → MATCH 75/75, closeness 0.
Two things to carry. (1) The pin's failure is a bare nop where the target has a call-site re-materialize addu $aM,$sN,$zero. The pinned local coalesces onto the same physical register but leaves the incoming-parameter pseudo simultaneously live with an identical value, and in this function's else-arm copy-propagation substitutes the parameter pseudo for the pinned one and deletes the copy — §220 card 4's optimize_reg_copy_1 forward-substitution, one block later than card 4 measures it.
(2) That is the exact MIRROR of §263, and the polarity is the discriminator. §263 reads mine-has-move $aN,$sM / target-has-nop as a callee-arity error; mine-has-nop / target-has-the-copy is an aliasing defect and the cure is this addendum's (delete the alias), not §263's (flip the arity). Check which side the copy is on before flipping any callee's arity. Same caution as the §195 DELAY-SLOT/1 misdiagnosis note: the schedule profile is wrong here too.
(3) Do not let closeness rank the candidate shapes. The pin reads 1 and the plain copy reads 64, yet the MATCH is neither of them — a closeness-1 pinned draft on a parameter that must survive calls is a signal to delete the alias, not to grind the last instruction.
§221 — A CONSTANT SHARED BY TWO STORES DIES AT THE CALL WHOSE DELAY SLOT REFILLS ITS REGISTER (single observation — not yet cross-confirmed) (P31 S58)
func_80183344 (ov_SC02_027, 22 ins). The constant 1 feeds BOTH sh 0x100($s0) and
sh 0xFE($s0), and it lives in $a2 — which is exactly the register the following
jal func_8012C658 refills with arg3 in its own delay slot.
where the +0xFE store is written |
result |
|---|---|
| before the call | the pseudo is call-free, stays in $a2, no $s0/$s1 spill pair, no second li — MATCH 22 |
| after the call (the natural source-order guess) | +2 ins ($s1 save/restore + a re-materialised li) and sched sank the store below the calls |
THE LAW. When one constant serves two stores and the call between them will refill that
constant's register with an argument, both stores must be written above the call. §193-B covers a
narrow-cast placement across a jal; nothing covered a shared immediate whose register is stolen by
the call's own argument copy.
§222 — SWITCH vs IF-CHAIN, PART 3: source arm order IS emission order, a leading EMPTY case buys the median split, and a 2-way dispatch with a shared post-block is a switch (P31 S58)
Composes §206.2 (an empty case owns a table slot), §164-54 and §193-G (the dispatch-topology oracle). Three new decisions, three cards.
1. ARM ORDER IN THE SOURCE IS ARM ORDER IN THE ASM — including a non-ascending case sequence.
func_80183A1C (ov_SC06_000, wave af, 65/65): the residual after the twin was case ORDER —
case 1: must precede case 0: because case 1's body sits at the first beq's target. And the
empty case 2:/case 3: arms are required to synthesise the slti/beqz/bnez range-check
prologue. §164-55/§165-12 price arm order for if/return shapes; the dense-switch-with-a-leading-
non-zero-case ordering law was unstated.
2. AN EMPTY LEADING case 0: IS WHAT PUTS THE MEDIAN SPLIT IN. func_8017F3B0 (ov_SC02_017,
49 ins): the target has a lone slti $v0,$v1,2 between two beqs — §164-54's dispatch oracle reads
that as a switch with a median-split tree. But with cases {1,2} + default, a bare
if(v==1)…else if(v==2)… emits bne/li/bne and no slti at all. Writing an empty
case 0: makes case 1 the tree's low leaf so emit_case_nodes puts case 1 left of the split and
case 2 right — reproducing the target. Two earlier drafts using if/else-if chains gave 45 and 47
ins against 49.
3. A TWO-WAY u16 DISPATCH WHOSE ARMS SHARE A COMMON POST-BLOCK COMPILES AS A switch.
func_80181968 (ov_SC04_004, 46/46): the lhu/beqz+beq pair on *(u16*)(a0+0x34) is not
if/else-if — gcc emitted a shared tail (addu a0,s0 / two fall-in labels) that only a switch
reproduces. The if/else-if chain hoisted the addu into delay slots and dropped the
j (−2 ins LENGTH-DRIFT). This sits below §193-G's three-node threshold, so it is a distinct tell:
the discriminator is the SHARED POST-BLOCK, not the arm count. (single observation at 2 arms —
not yet cross-confirmed)
4. WHEN THE GAPS ARE INTERIOR, DO NOT WRITE THE EMPTY CASES. The counter-instance, and it is
§165-28 working exactly as documented: func_8017D740 (ov_SC03_007, 41/41) reconstructs
switch((func_800291B4(0xCC)&0xFF)-3) over cases 3..14 from a 12-entry table whose even slots
4/6/8/10/12 hold the epilogue address. Those are interior default-filled gaps with 7 live nodes
≥ CASE_VALUES_THRESHOLD, so no empty case labels are written. Read §206.2 and §165-28 together:
an epilogue-pointing slot is an empty case only when §165-28's two-question test says the gap is
not interior-and-dense.
5. READ THE JUMP TABLE FIRST — it settles the case structure with zero compiles.
func_80180204 (ov_SC03_031, wave af, 56 ins, MATCH first draft): with the twin at ~0.59, the whole
structure fell out of jtbl_801C4B60's entry list plus the two merge points — .L8018029C as a
store-tail and an absent default (out-of-range falls straight past the switch). The table even
carries a 6th pad word (0x01222211) that is never addressed, per §8e.
§223 — READING A jal DELAY SLOT: the value in it was produced BEFORE the call, so it is NEVER that call's return (P31 S58)
§194-M owns the direction "a store in a CONDITIONAL branch's slot ⇒ its statement dominates the
branch", and §194-M BOUND 1 explicitly excludes jal slots. §176-A/L17616 and L17636 own the
placement lever ("move the store above the call"). Nothing owned the READING rule, and four cards in
this harvest lost a draft to it.
THE RULE. A delay slot executes BEFORE its jal transfers control. Therefore whatever the slot
reads was live at the SLOT, not at the call: it is the PRECEDING call's $v0, or a constant
materialised before the call, or the completion of a preceding statement. It is never the return of
the call whose slot it occupies. Ask "which value is live at the slot", not "which call is on the
line above".
Instance 1 — the preceding call's return. (✔ byte-checked:
asm/ov_SC06_029/.../func_80186DA0.s idx 6-13.)
jal func_8012B744
addiu $a0, $a0, 0x4
...
jal func_8012B2CC
sh $v0, 0x12($v1) <- $v0 is func_8012B744's return
The naive reading ("store func_8012B2CC's return through *(state+0x20)+0x12") drifted by exactly
one instruction everywhere after the slot. The correct C nests func_8012B744 directly as the stored
value and leaves func_8012B2CC as a separate void statement.
Instance 2 — a pre-call CONSTANT. (✔ byte-checked: asm/ov_SC06_000/.../func_80184524.s
idx 3-6.)
addiu $v0, $zero, 0x4 <- the constant
sw $ra, 0x14($sp)
jal func_8012B200
sw $v0, 0x1C($s0) <- stores 4, NOT the call's return
Drafting x = func_8012B200(...) mis-schedules the whole tail. func_801871EC (ov_SC03_028) is the
same shape twice over — both sw $v0,k($s0) sit in the FOLLOWING jal's slot with $v0 pre-loaded
by li (0xF and 0x23) — and both callees' results are discarded. Tell: a li/addiu …,$zero,K
above the jal and a sw $v0 in its slot ⇒ a constant store, written before the call.
Instance 3 — the completion of a preceding statement. func_80188C18 (ov_SC03_006): target line
28 sh $v0, 0x1A($sp) sits in the jal func_80133784 slot and is the scheduler-hoisted completion
of out[1] += 8, not a store of the call result. (✔ byte-checked above in §210 — lhu 0x1A(sp) /
addiu $v0,8 / jal / sh $v0,0x1A(sp).)
THE COROLLARY THAT SAVES A DRAFT — an EMPTY slot with an untouched $a0 means the incoming
argument passes straight through. func_80184524 again: lw $a0,0xD0($s0) / beqz $a0 /
jal func_80184AD4 with a nop slot ⇒ the callee's argument is the loaded pointer, which
survived the branch, not the entity. func_801887CC (ov_SC02_005) reports the same thing as a
missed call: a jal was invisible on first read because its slot held an sh and there was no
arg-setup insn at all — $a0 still carried the incoming argument. func_8018BF88 (ov_SC06_029),
func_80184B18 (ov_SC03_029) and func_801884C8 (ov_SC02_005) are three more of the same family;
on the last one, the absent a1/a2 setup means the call passes garbage in those registers,
which is expressible only through a casted function pointer.
BOUNDARY. This is a reading rule and it is one-way. It tells you what the source must have
said; it does not promise that writing the statement there will refill the slot — that is
fill_eager_delay_slots' problem and §199-F BOUND 2 / §194-M BOUND 3 own its failure modes.
§224 — CROSS-JUMP: WRITE THE DUPLICATE, AND READ A SHARED DELAY SLOT AS THE MERGE SIGNATURE (P31 S58)
Bounded by §193-C (cross_jump merges the scheduled common SUFFIX only — there is no head merge) and extends §1893's "exploit cross-jumping" with the recognition tell and two new bounds.
THE RECOGNITION TELL (new). An argument setup sitting in a branch's delay slot and executed on
BOTH paths is the signature of two cross-jumped call sites — not a scheduling pin. func_8018C3C0
(ov_SC02_011, 25 ins): the residual after the twin-shaped single-call drafts was
ADDRESSING/lui!=addu, and the target's addu $a0,$s0,$zero in the beqz delay slot was the
clue. The twin's __asm__ barrier idiom was the wrong tool here (it forced the move early, idx
7); splitting into if/else with two literal calls (0x40000/0xC4000 vs 0x9000/0x24000)
reproduced the merged layout naturally.
THE PRESCRIPTION, three confirmations.
| card | binary | what factoring cost | what duplicating bought |
|---|---|---|---|
func_80185CA4 |
ov_SC02_017 | every draft that factored the |= 0x20 RMW out (single temp, early return, sequential ifs) lost 3 instructions |
plain nested if/else{if/else} with one RMW statement per leaf; the two arms tail-merge onto one shared sw via j — MATCH 27 |
func_8018B684 |
ov_SC03_006 | the ≥0.9 twin's barrier gave 24 ins / wrong fills | duplicate the whole call per arm with literal constants; cross_jump merges only the common jal + ori $a2 tail, landing move a0,s0 in the beqz slot |
func_801866C8 / func_801832B4 |
ov_SC02_017 / ov_SC03_007 | — | write the jal func_8012A828 literally in both arms; cc1 merges them into one site reached by j-from-if + fall-through-from-else |
THE NEW BOUND (worth adding to §1893's bullet). An instruction occupying a MERGED jal's delay
slot pins a pre-call statement in that arm. func_801832B4: the else-arm's sh $zero,0x34($s0)
sits in the shared jal's slot, so it must be emitted before the call in the else arm's source.
The first draft copied the neighbour's visual order (call first, store after) and got near 6 — gcc
scheduled the store late and shifted the whole tail by 4 slots. The banked twin
ov_SC02_017:func_80187044 had it right.
AND THE FOURTH FACE — a shared tail can be ONE boolean expression, not two ifs.
func_801883FC (ov_SC06_029, 29 ins): the layout looks like an if/else-if chain but is a single
short-circuit expression whose arms share one tail-merged call body. Two tells: (a) the st==7
arm falls THROUGH into the st==0xA || st==0x42 tests (no jump skips them), and (b) every arm's
delay slot reloads li v0,0xA — gcc's bool-to-int lowering recomputing the accumulator in each slot.
Drafting it as two separate ifs keeps base live across the call (gcc allocates $s1, extra
save/restore, 36 ins vs 29).
§225 — THREE CONTROL-FLOW SHAPES NO STRUCTURED SPELLING REACHES (P31 S58)
1. A SHARED CALL THAT BOTH FAILURE EDGES JUMP INTO NEEDS EXPLICIT gotos LANDING MID-FLOW.
func_80180EF4 (ov_SC02_017, 38/38, 7 oracle calls): gcc emitted ONE shared func_8012CAE4 call
that both early-failure edges branch into and the success path falls into, then a forward j
over a flag-set block to the epilogue. Structured nestings gave 37 or 39 ins with either a
duplicated CAE4 or a stray nop; only goto cae4 / goto set landing mid-flow reproduced it.
The shape to recognise: the "common" label sits BETWEEN two calls rather than at the end.
2. EARLY-RETURN vs if-BLOCK IS A PROLOGUE/EPILOGUE DIAL, NOT A STYLE CHOICE.
func_80184D50 (ov_SC02_011, 44/44): an early-return countdown guard makes gcc save s0/ra at
frame top with sp-0x28 and emit a mid-function epilogue; the if(t==0){…} block form
reproduces the target's prologue order (sw s0 then sw ra, sp-0x20), the bnez delay-slot store
and the single fall-through epilogue carrying the timer reload. §5583's wait/countdown row shows
the decrement idiom but not this control-flow constraint. The complementary read is func_80186C98
(ov_SC03_028): a j in lieu of a branch at a block end is the fingerprint of an early RETURN, not
an else.
3. AN EARLY-RETURN LADDER FOLDS v0 = 0 INTO THE FAILING COMPARES' DELAY SLOTS; THE && CHAIN
DOES NOT. func_80184FC4 (ov_SC04_011, 23/23): the winning shape is
if (A || B) return 0; return C == D; — both failed compares fold their v0=0 into bne delay
slots converging on one epilogue, while the third emits xor+sltiu. The semantically identical
&& chain allocates a boolean accumulator to $a2 with a final move; a goto-ladder and three
separate returns also miss. Delay-slot zero-folding is what distinguishes early-return spelling
from value-chain spelling, even when both are "pure short-circuit branching" (cf. §164-56's
BRANCH_COST note).
4. AND THE SAME AXIS FOR A GUARDED FLAG RETURN. func_801802B8 (ov_SC03_028, 15 ins): the naive
ret = 0; if (…) { call(); ret = 1; } return ret; does NOT match — gcc homes ret in a call-saved
register ($a0) across the jal and emits move v0,a0 at the end (7-ins diff). Only the
conditional-expression form cond ? (call(), 1) : 0 makes gcc duplicate the constant
materialisation per path — li v0,4 before the compare, addu v0,zero,zero in the bne slot,
addiu v0,zero,1 after the call. It also hoists the global load above the sw $ra prologue for
free, which the if/ret form did not. func_801AA4EC (md_SC07_004) is the mirror: gcc only
produces li $v0,1 in the bnez slot when the guard is written inverted (== 0 → do the rest →
return 1), i.e. as a short-circuit ||.
5. AND THE NEGATION LEVER FOR A CHAINED RANGE TEST (single observation). func_80181A44
(ov_SC03_007, 48/48): the final beqz-vs-bnez at idx 18 would not flip with plain if/else-if
nesting — gcc canonicalises the second test back to slti+bnez. Writing the middle arm's condition
explicitly negated in the source (else if (!(v1 < 0x61))) keeps gcc's branch on the NOT-taken
edge while preserving identical semantics.
§226 — FRAME PADS: FOUR WAYS §162i1/§2429's DEAD-LOCAL LEVER MISFIRES (P31 S58)
The pad lever ("declare a dead s32 pad[N≥2] to reserve var_size") fired correctly on five cards
this harvest (func_8018ED84 Δ0x10, func_8017D118 Δ8, func_801871EC Δ8, func_8018BA38 Δ0x10,
func_801815A8 Δ8). These are the four cards where reaching for it was wrong:
1. IT OVERSHOOTS WHEN REAL NARROW LOCALS ALREADY EXIST. func_8018B830 (ov_SC04_011, 36 ins):
the 0x8 phantom frame (addiu sp,-8, no saves, no spills) is not pad-induced — an
address-taken pad gave 0x10, and combined with the s16 locals gcc rounded to 0x10. The halfword
locals s16 a2; u16 temp alone reserve exactly 8 bytes of var_size. Count the real locals'
contribution to get_frame_size() before adding anything.
2. A DEAD ARRAY ELEMENT SURVIVES WHERE A SEPARATE PAD ARRAY IS TRIMMED. func_801870B8
(ov_SC03_029, 24 ins): frame 0x28 with ra at 0x20 needs a 16-byte locals block for 3 live words.
A plain s32 pad[N] would have been trimmed; growing the real array to a dead 4th element did
the job. Prefer widening an existing aggregate over adding a new one.
3. THE DEAD LOCAL IS SOMETIMES LOAD-BEARING, NOT PADDING. func_801AA210 (md_SC07_004, 58/58):
frame 0x38 requires a dead third local at sp+0x20 whose address is cached in $s0 across the
three RotTransSV calls — declare it even though nothing reads it. That is not a size lever; it is
a register lever that happens to change the size.
4. THE PAD CAN BE A DECOY FOR A HOISTING RESIDUAL. func_8017E424 (ov_SC07_007): frame 0x28 vs
0x20 was "fixed" by pad[2] — and that was wrong. The real cause was hoisting a narrow read into a
local; running §167-10 BACKWARDS (un-hoisting the else-arm re-read *(s16*)(s0+8) - 1) regenerated
both the addu $v1,$v0,$zero copy AND the orphan 8-byte slot, and deleting the pad was then
required, not optional. §167-10 documents only the cure direction ("hoist to delete") and its gate
table's "short, read hoisted into int n ⇒ no copy" row reads as if hoisting were always the fix.
THE INVERSE LAW, worth stating: a target showing copy + frame + no pad is EVIDENCE OF THE
UN-HOISTED SOURCE FORM.
5. AND NEVER COPY THE TWIN'S PAD. func_8017D684 (ov_SC07_007, 48 ins, sim 0.85): the twin's
dead-local pad existed to push ITS buffer to sp+0x18; this target's buffer is the FIRST local at
sp+0x10, so copying the pad would have shifted it and broken all four swl/swr offsets.
6. ONE MORE FRAME CAUSE THAT IS NOT A PAD AT ALL. func_80183C7C (ov_SC02_005): LENGTH-DRIFT −4
because gcc keeps $s0 live across the early-return path — s0 = &D_80126614 is set before the
guard, reused as the call's third argument and as the receiver of the return value — which forces
the full 4-slot frame even though the fast path returns immediately. Return-value reuse of a
pre-call address register is a frame-size cause; it looks like a pad problem and is not.
§227 — TYPE THE SOURCE BY THE LOAD WIDTH, NOT BY THE STORE WIDTH (P31 S58)
The access-width-by-store heuristic (§183 law 2, "type by access width") picks the wrong answer whenever a narrowing conversion sits between a wide load and a narrow store. Four cards.
| card | binary | the target | the trap |
|---|---|---|---|
func_80184CA4 |
ov_SC02_005 | lw from a table at +0x20, then sh into 0x6/0xA/0xE |
s16 = s32 narrowing emits lw + sh; s16 = u16 emits lhu + sh. Writing the source as an s16 lvalue read gave lhu and failed |
func_8018A0E4 |
ov_SC02_005 | three loads at +0x48/+0x4C/+0x50 |
must be s32 lw even though only 16 bits are stored — a u16 source type emits lhu |
func_8018558C |
ov_SC03_028 | lhu of D_80078E92 |
the atlas fleet row says s16, which would emit lh + sra and break the match. The TU had no declaration, so the access width wins — extern u16 |
func_80180908 |
ov_SC02_027 | D_800AE620 struct-copied into D_801DA730 as 8 words |
the fleet s16 hint for a struct-copy DESTINATION must be reconciled against the .data dlabel size (32 B ⇒ a 32-byte record), not taken literally |
THE LAW. Read the LOAD's opcode to type the source object and the STORE's opcode to type the
destination lvalue; they are independent, and a card that gives you one type for a symbol is telling
you about only one end. Mixed signedness within one statement is normal and must be spelled
per-instruction — func_8018CCF8 (ov_SC02_005) has an lh/slti 0x380 guard on a field whose
increment is lhu-load/s16-store; func_80180F8C (ov_SC02_017) has a counter that is lhu/sh
while the guard test on it is lh; func_80188818 (ov_SC04_011) uses s16 guard compares and u16
store-backs on the same fields per §15764.
§228 — READING THE DIVIDE, PART N: the off-by-one compare is % K == 1, and three more discriminators (P31 S58)
Extends §167-39 (the reconstruction-compare rule), §167-25 (the unsigned form) and §194-I (the divisor formula).
1. AN addiu −1 ON THE DIVIDEND BETWEEN THE MAGIC MULTIPLY AND THE mfhi MEANS x % K == 1,
NOT "divide then decrement". func_80187C14 (ov_SC02_005, 34/34): the bne compares the
reconstructed product against $a0 after an addiu $a0,$a0,-1. gcc folds the ==1 into the
reconstruction side — (x-1) == (x/3)*3 ⟺ x%3==1 under C truncating division — computing q = x/3
from the ORIGINAL x while comparing against x−1. §167-39's literal prescription
(write x == x/K*K, never x%K==0) drifted −1 instruction here; temp % 3 == 1 landed byte-exact
including the whole sra/mfhi/subu/sll/addu chain.
2. %30 vs %15 IS READ OFF THE POST-mfhi SHIFT, NOT OFF THE MAGIC CONSTANT. func_80181C58
(ov_SC02_017, 38 ins): use §194-I's formula (2^(32+4)/0x88888889 = 30) rather than magic-table
recognition — %15 compiles to the /15 sra 3 form, %30 to the target's sra 4 plus a
q*30 reconstruction (sll 4; subu; sll 1). The discriminator is the sra amount plus the
presence of the final sll 1.
3. A break 7 / break 6 GUARD PAIR MEANS A SIGNED DIVIDE BY A VARIABLE. func_80180F18
(ov_SC03_011, 30 ins): the divisor is an lh-loaded s16, so --expand-div emits the two break
traps. Also on that card: the source increments-and-stores FIRST and takes the remainder separately
(x += 1; if (x % m == c)), not (x = (x+1)%m) — the tell is the target storing $v1 (the raw
incremented counter) after the div/mfhi pair while the bne consumes $a0 (the mfhi).
4. A % WITH THE SIGN-CORRECTION RESIDUE DANCE MUST STAY A TRUE %. func_80185A5C
(ov_SC06_000, 42/42): signed % 8 is kept as a real % because the target carries the sra/subu
correction; §176-E1's & (K-1) shortcut applies only when that dance is absent.
5. AND >>2 IS NOT /4. func_8017F470 (ov_SC04_002, 31 ins): a bare sra ,2 requires >>2;
/4 expands to bgez+addiu 3 fixup chains (+7 ins).
6. THE UNSIGNED FORM, CONFIRMED. func_80187C28 (ov_SC03_028, 51/51): lui/ori 0x55555556 ; mult ; mfhi with no trailing sra/subu is the unsigned /3; write it literally per §19322's
"never respell" rule. func_80188EA0 (ov_SC03_006) reads 0x78787879 as 2^35/0x78787879 = 17.0000000049 ⇒ /17
with the trailing sll4/addu/subu reconstructing q*17.
§229 — THE ADDRESS IS A VALUE: NAMING IT MOVES THE lui/addiu PAIR — AND §L14410 SAYS THE OPPOSITE FOR A REASON (P31 S58)
Extends §164-35 (whose existence lever is stated only for loop preheaders / move_movables) and
must be read against §L14410, which prescribes the exact opposite (write the symbol INLINE at
every call site so cse's first pseudo becomes canonical).
THE LAW. Assigning a symbol's address to a named C local BIRTHS the pseudo at the initializer —
an earlier birth, a lower regno and a LONGER live range. That is what you want when the address must
(a) dominate a branch, (b) survive a jal in a callee-saved register, or (c) be materialised once
instead of %lo-folded per use. It is what you do NOT want when you are fighting for a low hard
register — which is exactly §L14410's case. Five instances of the naming direction:
| card | binary | inline spelling | named-local spelling |
|---|---|---|---|
func_80181494 |
ov_SC07_007 | the lui/addiu %hi/%lo(D_80126B58) pair emitted inside the branch-taken arm |
base = (s32)&D_80126B58; forced the pseudo and gcc sank the materialisation into the pre-branch position (target idx 5-6). This is §164-35's lever working in STRAIGHT-LINE code with no register-pressure change at all |
func_8017E934 |
ov_SC03_124 | initialising after the call: near 3, ADDRESSING/move!=lui |
s32 *p = &D_8018F1B4; as the FIRST statement ⇒ kept in callee-saved $s0 across jal func_80029504, lui/addiu scheduled before move v1,v0 |
func_8018DFDC |
ov_SC04_011 | deref inline in each arm ⇒ gcc sinks it past the join, +1 ins | ptr = &D_801F12C8; above the branch pins the %hi/%lo pair before the beqz |
func_80182EA8 |
ov_SC03_006 | D_80078E78[0x37] folds %lo per use and drops the lui/addiu pair |
u8 *p = D_80078E78; survives in callee-saved $s0 across the jal |
func_80187FCC |
ov_SC02_005 | guards written directly on the extern ⇒ lui/lw twice, frame 0x18 |
naming the address as a local s32* keeps it in call-saved $s1 (frame 0x20, sw s0/s1/ra) and gives the post-call reload for free |
AND WHEN GCC REMATERIALISES THE ADDRESS AT EVERY USE, BREAK THE SINGLE-SET GATE. func_80184A68
(ov_SC06_000, 16 ins, leaf): naive drafts let update_equiv_regs rematerialise the global address
(lui/$at) at each neighbour use — 20 ins vs 16. The fix pins the base to $a0 and breaks
the remat gate with an opaque zero-byte self-copy, __asm__("" : "=r"(a0) : "0"(a0)), so the pointer
stays multi-set/opaque and every access emits off $a0. The "=r"/"0" self-copy is documented
elsewhere as an anti-CSE device for a loaded value; this is its use against a symbolic
(lui/addiu) address. Compare func_80186F44 (ov_SC04_011), where forcing an $a0 reload before
member stores needed the same trick with an early-clobber output — a plain "=r" let gcc reuse
$4 as the input operand, giving addu a0,a0,s0; "=&r" with a literal $0 third operand was what
worked. Plain-C levers that FAILED there: storing through s0; an explicit temp (gcc propagated it
to the parameter pseudo and burned $s1 + a 24-byte frame); a bare
asm volatile("addu $a0,$s0,$zero") (gcc ignored the clobber and kept addressing $s0).
THE DECLARATION COROLLARY — a fleet row can name the VALUE type while the use site takes the
ADDRESS. func_80185B7C (ov_SC02_000): the fleet said ('s32','') but the asm takes the address
(addiu %lo into $a1), so it must be declared as an ARRAY (u8 D_8018F288[]) or gcc emits lw
instead of addiu — the first compile diffed exactly there. The tell is whether the symbol feeds
an addiu/lui pair or a load. The same discrimination settles func_8018A314 (ov_SC06_029),
where the compare is an ADDRESS compare — the asm materialises lui/addiu of the symbol and
bnes it against a loaded word, so the C is == (s32)&D_801D4D0C, not == D_801D4D0C.
func_80183E18 (ov_SC03_007) is the counter-instance and shows the tell working in reverse: the
atlas's "FUNCTION POINTER" warning on D_8018C510 did not apply because the target lws it as a
plain scalar word.
AND ONE ORDERING RULE FOR TWO COMPETING SYMBOL BASES (single observation). func_80187348
(ov_SC02_005, 45/45): when two symbol-addressed pointers compete, the first-declared wins the
lui+addiu register pair and the other goes $at-relative. Declaring tab/base before the
lhu was required to keep the lhu in $at addressing.
§230 — THE ANCHOR PROBE: with %hi/%lo masked, the surviving addiu deltas tell you which assignment was written first (single observation — not yet cross-confirmed) (P31 S58)
func_80188904 (ov_SC06_029, 23 ins). match_one masks HI16/LO16, so the first draft's diff showed
only the addiu offsets: -8 / -0x28 / -0x20 against the target's +8 / -0x20 / -0x18.
THE READING. Those deltas are an anchor probe. When one address feeds several ±offset stores,
gcc anchors the base register at the offset of the first-emitted assignment; the draft had
written the +8 assignment first, so $v0 was anchored at base+8 and every other displacement
was rebased against it. Moving the plain-base assignment (D_801DFFE0) to statement 1 flipped
all three offsets and matched.
THE LAW. When a relocation-masked diff shows a consistent constant shift across several addiu
displacements off one base, read it as an anchor probe and reorder the assignments — do not reach for
struct-layout, /s or pin levers.
§231 — TRANSCRIPTION AND SEMANTIC-READ HYGIENE: six ways the listing misleads (P31 S58)
1. REBUILD SPLIT CONSTANTS FROM THE MASKS, NEVER FROM VISUAL ADJACENCY. func_80181960
(ov_SC07_007): splat renders lui/ori as masked halves ((K>>16)) / (K&0xFFFF). The draft
reassembled 0x000F0140 by concatenating rendered fragments of two different constants — the
lui 0xF0 belonged to 0x00F00140. Always rebuild K = (hi<<16) | lo from the two halves of the
SAME pair.
2. READ THE lui/ori PAIR; DO NOT ASSUME A REPEATING BYTE PATTERN. func_80185520
(ov_SC06_000, 67/67): the tail constant is 0x202020 (lui 0x20 / ori 0x2020), not
0x20202020.
3. THE LISTING'S TEXT CAN DISAGREE WITH ITS OWN HEX COLUMN. func_801AA4EC (md_SC07_004): a
line rendered addiu $a2,$a1,0x10 for hex 0x24A60010 (correct), but a neighbouring line's text
disagreed with 0x02280021, which decodes as addu $a1,$s0,$zero. When an argument wiring reads
implausibly, decode the /* */ hex column directly — it settled that both calls take
(param_1, param_2, …) and the second is not chained off param_2+8.
4. USE THE NUMERIC EXIT-LABEL OFFSET TO DISAMBIGUATE break FROM continue. func_80184CCC
(ov_SC04_011, 59/59): two per-entry guards look like loop exits; the exit-label offset (+0x4C,
landing on the lh/0x8000 check) proved they are independent jumps to loop bottom, not
breaks. Writing them as one && gave the shared-target branch shape instead. (This is the
masked_diff blind spot §195-D names, applied as a positive drafting tool.)
5. A "CLEAN REGISTER SWAP" RESIDUAL CAN BE A SEMANTIC ERROR — RE-READ THE addu SOURCES.
func_801A9810 (md_SC07_004, 47/47): the atlas's REGALLOC-PERM label was a misdiagnosis trap. The
4-instruction residual looked like an $s0/$s1 swap but was a wrong argument: the target does
addu $a0,$s0,$zero where $s0 is the OBJECT, so the tail call is func_8012AD44(object, 2), not
(s1, 2). Passing the wrong pointer forced gcc to keep it live across three calls and cascaded the
allocation; fixing the argument alone flipped everything. §176-B ("REGALLOC-PERM 1-4 instructions
off ⇒ usually NOT register allocation") generalises: re-read the target's own addu SOURCES before
believing a swap signature. func_80186248 (ov_SC02_027) is the same class from the other side —
a read-modify-write whose value feeds only a store leaves that value out of the argument set
entirely, and the diff's missing $a0 move was the tell.
6. sra N IS NOT ALWAYS A DIVIDE. func_80180EC8 (ov_SC07_007, 28/28): sll 16 ; sra 14 is
sext(u16) << 2 — a folded sign-extend-plus-scale, sra amount = 16 − log2(scale). §167-03's
HImode-division reading of sra 14 is a false friend; the discriminator is that the value feeds an
lw ADDRESS, not an addu/subu sum. Write the index unscaled ((v<<16)>>16) and let gcc fold
the scale and the extension into the one sra; a literal *4 gave an sra 12 mismatch.
§232 — WHEN THE jr DELAY SLOT'S STORE STORES THE RETURN VALUE, TIE THEM WITH ONE PSEUDO (single observation — not yet cross-confirmed) (P31 S58)
func_801A7CDC (md_SC07_004, 15/15). The target tail is:
addiu $v0, $v0, 0x5
jr $ra
sh $v0, 0x2($a0) <- the final store consumes the RETURN VALUE
THE TRAP. store; return 5; — and the store-equals-return and pinned-temp respellings, all
three byte-identical to each other — make gcc materialise the constant TWICE: once for the store,
once for the return copy. The dead second li v0,5 then steals the jr delay slot, giving
sh ; jr ; li(dead) — near 3, LENGTH-DRIFT.
THE FIX. A §30 #3 anonymous-asm re-tie forces ONE real pseudo set shared by the store and the
return:
__asm__("" : "=r"(r) : "0"(5));
The return copy becomes an identity, the jr slot stays empty through reorg, and dbr fills it with
the sh.
REFUTED ON THE SAME CARD. register s32 r __asm__("$2") — pinning r to $2 removes it from
the scratch pool and perturbs allocation for the whole body (near 14, WIDTH cascade). Do not
reach for the hard pin here.
ADDENDA TO EXISTING SECTIONS (P31 S58 wave ab–ag harvest)
Appended rather than inserted, per the file's append-only rule. Each entry belongs to the section named in its heading.
§30 addendum (P31 S58) — the /s grant closes a SCHEDULING residual and a REGISTER residual with one edit
§30 establishes that a bare/zero-offset deref never gets MEM_IN_STRUCT_P while an anonymous-struct
member ref always does. func_801824D4 (ov_SC06_000, 35/35, 9 oracle calls) adds the composite:
granting /s fixed both halves of a 4-insn tail knot at once. Plain source orders all failed —
a bare += last gave the lhu BELOW the aliasing store with v0=load / v1=const (wrong
registers); a temp-load-first gave the right position but v1=load / v0=const (REGALLOC-PERM).
Writing the increment as an anonymous struct member ref,
((struct { u8 pad[2]; u16 f; } *)a0)->f += 1;, granted /s: the lhu hoisted above the
fixed-symbol store and the pseudo ordering flipped so the constant 1 landed in $v1 and the
load in $v0. The anonymous struct is required — dedup_propagate rejects inline named structs.
(Also on that card: declaring D_801AEAFC as u16 truncates a 0x1310000 constant to the sh-only
path; it must be s32/u32.)
func_801831E4 (ov_SC06_000, 30 ins) is the same law read forwards, plus a composite §30 does not
mention: the reload idiom composes with direct-param-register walking, and the local-copy variant
silently changes the loop counter's register even when the reload itself is correct
(param_1 = (s32*)((s32)param_1 + 0xCC) then param_1++ keeps the giv chain on $a0 so the counter
lands in $a1; copying into a fresh local pushed it to $a2, near 11). And on the same card, named
locals vs anonymous nested derefs flip the $v0/$v1 colouring in a double-indirect RMW.
§194-B addendum (P31 S58) — BOUND 2 is byte-wrong; see §209 Direction A
§194-B's bound "fewer than two surviving consumers ⇒ no copy" would have steered func_801A8738
away from its winning spelling. One sb store plus a compare is enough to keep the
addu $rA,$rB,$zero. Full evidence, the ablation list and three corroborating cards are in §209.
§176-B addendum (P31 S58) — the misdiagnosis direction
§176-B says a 1-4 instruction REGALLOC-PERM residual is usually not register allocation.
func_801A9810 shows the strongest form of that: it was a wrong call argument. Re-read the target's
own addu SOURCES before believing a swap signature. See §231.5.
§165-40 addendum (P31 S58) — the barrier goes at the COPY SITE, not inside the region it protects
§165-40 states the placement of a bare __asm__ __volatile__("") but not this scope constraint.
func_801836B0 (ov_SC02_027, 27/27): gcc floated an addu $a0,$s0,$zero argument copy into a
beqz delay slot where the target keeps a nop. The barrier had to sit immediately before the
defining statement (the call) — i.e. at the copy's own site — to deny the upward hoist. Placing it
inside the if-body, i.e. inside the guarded region whose branch it was meant to protect, did
nothing.
§164-63 addendum (P31 S58) — the interposed asm works when the SECOND value is an ordinary assignment
§164-63's evidence table shows the zero-byte non-volatile input asm placed between two pin sets.
func_80186F9C (ov_SC03_029, 45 ins) shows it works equally when the second set is a plain
assignment after the declaration block — no register-pinned declaration is required for the second
value. The residual was SCHEDULE-REORDER/4 in the prologue pair only (sw $s1/move $s1,a0 had to
precede sw $s0/move $s0,a1); declaration order alone did nothing (A/B'd), and
__asm__("" :: "r"(s1)); before s0 = a1; is what flipped the pair.
§193-A / §194-E addendum (P31 S58) — where the twin's body actually lives
See §214: seven cards in this harvest lost time because the twin's C is a DEFINE_func_* macro
in src/shared/engine_core.h rather than a body in its overlay .c, and one because the twin's .s
had been pruned from nonmatchings/ on banking. Add both greps to the twin-reading step.
§233 — THE WAVE aa–bg HARVEST (P31 S58b): 1,101 byte-banked notes → 24 new laws, 21 addenda, ~700 already-covered
Every note behind §233–§257 came from a function that byte-matched and passed the byte gate; the
function is proven, the drafter's explanation was not. Claims below were re-read against asm/ where
they would change how a future function is written (four spot-checks are called out inline). Entries
marked (single observation) have exactly one card behind them.
Shape of the corpus. Roughly two thirds of the 1,101 notes self-report "nothing the cookbook did
not already cover" — the flywheel is working. Of the remainder, the single largest cluster is not
codegen at all: ~90 cards banked a body whose instructions were already correct and whose only
defect lived in the declaration layer (§236). The second largest is symbol/target identity (§238).
The third is the li/addiu constant law (§234), which cost a one-instruction residual on twelve
separate cards before anyone named it.
Index of what is new here.
- §234 constant materialisation: the STORE LVALUE's signedness picks
addiuvsli/ori - §235 the phantom symbol, and
%hi(SYM + K)relocation text - §236 the declaration layer: nine ways a byte-perfect body fails the whole-binary gate
- §237 the cast-at-call-site decision table (what §17a-1 cannot fix, and the four escapes)
- §238 same name, different function — the overlay-homonym trap
- §239 two-statement integer-space address materialisation; plus-tree operand order
- §240
A + K + B: write the constant BETWEEN the runtime terms - §241 the folded sign-extend-and-scale shift pair
- §242
*kvs<<n,/2^nvs>>n: spelling owns load width and the rounding chain - §243 the reassigned pointer as a deliberate CSE barrier
- §244
volatileas counting instrument, store-order pin, and asymmetric lever - §245 the call's argument list is a scheduling slot
- §246 three-live-value scan loops want address-from-index; two symbols can share one giv
- §247 two branches to ONE label means the source condition is negated
- §248 split the load from the arithmetic
- §249 three ways to make a deleted instruction real
- §250
%hi/%lovslw: the extern's array-vs-scalar shape - §251 immediate-spelling triggers (
+= 0xFF,~Kfull width, two-OR splits) - §252 the guarded pre-decrement
(x != 0) && (--x == 0) - §253 postfix
++vs+= 1picks a different scratch register (single observation) - §254 the dead parameter as a register-placement tool (single observation)
- §255 the empty case, part 2: four tree shapes it buys
- §256 gotos in the target's block order reproduce switch placement without switch's side effects
- §257 the dead-end ledger: eleven levers that measured NULL or backfired
- §258 ADDENDA to §30, §194-B, §202, §205, §208, §210, §211, §213, §214, §215, §217, §220, §222, §223, §224, §225, §226, §229, §230, §231, §232 (append-only, so they live in one block)
- §259 the DISCARD LEDGER — the eighteen big clusters that were mined and rejected, and two open problems left deliberately unwritten
§234 — CONSTANT MATERIALISATION: THE STORE LVALUE'S SIGNEDNESS PICKS addiu vs li/ori (P31 S58b)
The law. For a halfword (or byte) store of a compile-time constant, gcc-2.7.2 chooses the materialising instruction from the declared signedness of the LVALUE being stored through, not from how the literal is written in C:
| you write | lvalue | emitted |
|---|---|---|
*(s16*)p = -K; |
signed | addiu $rt,$zero,-K (sign-extended) |
*(u16*)p = -K; or = 0xFFFF-K+1; |
unsigned | li/ori $rt,$zero,0xFF.. (zero-extended) |
*(u16*)p = K; with bit 15 set (0x8000, 0xAA10, 0xC006, 0xC010) |
unsigned | ori $rt,$zero,K |
*(s16*)p = K; with bit 15 set |
signed | li/addiu of the sign-extended pattern — wrong |
K with bit 15 clear (0x7FFF, 0x3000, 0x40) |
either | addiu — the dial is inert |
BYTE-VERIFIED. asm/ov_SC06_029/.../func_80187CE4.s reads lhu $v0,0xA($a0) … addiu $v0,$zero,-0x82 … sh $v0,0xA($a0): the same field is read unsigned and written signed, so the
read spelling and the store spelling are independent (this is §227's law seen from the constant side).
asm/ov_SC03_091/.../func_80187EF8.s carries ori $v0,$zero,0x8000 three lines from addiu $v0,$zero,0x7FFF — the two halves of the table above, in one basic block.
THE DIAL EXTENDS TO A LOCAL ARRAY'S ELEMENT TYPE. func_80187F18 (ov_SC02_017): the stack
message buffer had to be s16[], not u16[], purely so the -2 element emitted addiu $v0,$zero,-0x2 instead of li $v0,0xfffe; the callee still takes u16* (cast at the call site).
func_80183750 (ov_SC04_004) is the same card in the same direction. func_8018BE74/func_80183AF4
show the opposite: keep the array u16[] to get ori.
AND TO A GLOBAL'S EXTERN. func_80186044 (ov_SC06_029): the atlas says D_80126B62 is u16, but
u16 emitted ori $v0,$zero,0xff00/0xfa80 for the clamps while the target has addiu $v0,$zero,-0x100/-0x580. The fix is a block-scoped extern s16 D_80126B62; shadowing the file-scope
u16 — legal C89, decl-only, zero bytes, and it does not disturb any other function in the TU.
Cards. func_80180CB0 (ov_SC07_001), func_8017FA94 (ov_SC07_002), func_80181000 (ov_SC01_001),
func_80183418/func_8017F780 (ov_SC02_027), func_80181810 (ov_SC04_002), func_8018A0E4,
func_8018876C, func_8017F88C (ov_SC05_001), func_801863E0/func_8017D9CC, func_80180424
(ov_SC06_022), func_80182810 (ov_SC02_005, unsigned suffix 0x38000u for lui+ori),
func_80187FD8 (ov_SC03_090, |= 0xD8000000 not |= -0x80000000).
BOUNDARY. Word stores are immune (lui/ori is chosen by the constant's low half alone), and a
constant with bit 15 clear is immune in both directions. Do not reach for this when the residual is a
whole extra instruction — that is §251 or §239, not this.
§235 — THE PHANTOM SYMBOL: A MASKED MATCH CAN CARRY A RELOCATION THAT DOES NOT EXIST (P31 S58b)
match_one masks HI16/LO16/jal identity (Law 1c). Therefore a draft can score a clean MATCH
while naming a symbol that is not in the binary at all, and the failure surfaces only at link time
in the whole-binary gate, with no per-instruction diff to steer you.
THE CARD. func_801800D0 (ov_SC03_029, banked twice, waves ar/bc). The Ghidra seed invented
DAT_801d9752. No such label exists: asm/ov_SC03_029/data/tail18.data.s defines only
D_801D9750/54/58/5C. The target reaches the middle halfword through the REAL label —
BYTE-VERIFIED in func_801800D0.s:
lui $at, %hi(D_801D9750 + 0x2)
sh $v0, %lo(D_801D9750 + 0x2)($at)
THE C SPELLING THAT FOLDS A CONSTANT INDEX INTO symbol + offset INSIDE %hi/%lo IS ARRAY
INDEXING ON AN INCOMPLETE-ARRAY EXTERN: extern u16 D_801D9750[]; … D_801D9750[1] = v;
A scalar extern u16 D_801D9752 emits bare %hi(D_801D9752) — right address, wrong relocation
TEXT, and an undefined symbol at link.
GENERALISATION. Whenever the target's own relocation lines show %hi(SYM + K), the source used one
object and indexed into it. Whenever they show two distinct %hi(SYM_A) / %hi(SYM_B) pairs for
adjacent addresses, the source used two distinct objects (§216, and see func_8018BB44 in §250).
Read the relocation TEXT, not the effective address.
BOUNDARY. This is not the extern-type conflict class (§12/§14c) — the symbol simply does not exist. Symptom is a link/gate failure with a green oracle and a clean type audit.
§236 — THE DECLARATION LAYER IS THE DOMINANT BANK-BLOCKER: NINE WAYS A BYTE-PERFECT BODY FAILS THE GATE (P31 S58b)
Roughly ninety cards in this harvest report the same story: the instruction stream was already
correct; the whole-binary gate rejected it for a C-level reason match_one structurally cannot see
(it compiles the snippet standalone, sees no other TU, and masks relocations). §12/§14b/§14c/§1027
name the class; this is the field taxonomy, ordered by frequency. Audit all nine before re-deriving
a single instruction.
1. YOUR EXTERN'S TYPE CONFLICTS WITH A DECL ELSEWHERE IN THE SAME TU. The commonest. Both
directions occur: extern s32 D_80192D4C vs the TU's extern u8 D_80192D4C[] (func_80183C84,
ov_SC05_010); extern u8 D_801857BC vs extern s32 (func_80180840); extern void func_8012BEE8(u8*) vs s32(s32) (func_80180A20); void vs s32 return (func_801805F8,
ov_SC06_030); (void*,s32,s32) vs (s16*,s32,s32) (func_8017FCF4). Copy the in-TU spelling
verbatim and cast at the use site. In-TU beats fleet consensus, always (§196's fleet vote loses
to one local declaration).
- Corollary — the self-inflicted rival.
func_8017E900(ov_SC07_010): the atlas showedfleet=('s16','[]') n=1455vsrivals=('s16','') x1— and that lone rival was this TU's own scalar decl. A rivals entry pointing back at your destination TU is a conflict flag, not an alternative spelling.
2. TWO BLOCK-SCOPE EXTERNS OF ONE SYMBOL, IN DIFFERENT FUNCTIONS OF ONE TU, WITH DIFFERENT TYPES.
func_8017F0B0 (ov_SC03_031): my block-scope extern void (*D_801860AC[])(void) vs a sibling
function's block-scope extern u8 D_801860AC[] at line 3499 — gcc rejects the whole TU. Block scope
does not insulate you from another function's block-scope decl of the same name.
3. A SAME-TU CALLEE DEFINED BELOW YOUR SPLICE POINT. Calling it undeclared gets an implicit
int f(), which then conflicts with the later void definition. Whole TU dies; zero instruction
diff. func_8017DD08 (ov_SC03_030), func_80181D68 (ov_SC02_003), func_8017DED8 (ov_SC06_025),
func_8017E250 (ov_SC03_013), func_80184814 (ov_SC03_091), func_8018C154. Fix: a forward extern
above your body, with an unspecified () parameter list — C89-composite with any later prototype.
4. A DUPLICATE typedef AT FILE SCOPE. The shared headers already own Blk8, SVECTOR,
MATRIX, Blk20, Local_8017EE34, Blk8_80126940_8017D6D0_8017F6D4. Re-declaring is a C89
redefinition error. func_80185910 (ov_SC02_000), func_8017E7C0, func_8017F780 (ov_SC02_027),
func_801802BC (ov_SC03_030), func_80181EC0, func_80185FB4. Fixes, in order of preference:
(a) drop it and consume the header's; (b) rename it address-derived (Blk8L, S8_8017DA98);
(c) shadow it at BLOCK scope — legal C89, byte-neutral, satisfies both the standalone oracle and
the real TU (func_8017DCD8, func_8017D684); (d) use an anonymous struct of identical layout
(func_80183D38).
5. A HOSTILE FILE-SCOPE PROTOTYPE ABOVE YOUR SLOT. Your call needs a different arity, but a
same-scope duplicate ?-list decl compile-fails ("too few arguments"). Fix: block-scope the
() decl inside your function so it shadows the prototype — this TU's own banked precedent usually
exists. func_8018A498 (ov_SC06_018, the TU documents the pattern at its line 3693), func_801E53A8
(md_SC02_009), func_80180480 (ov_SC07_010). See §237 for when a cast is enough and when it is not.
6. A NARROW CALLEE PROTOTYPE CAUSES A DEF-SITE EAGER MASK. func_8017ECA8 (ov_SC03_030): the TU
declares extern u8 func_8014BF6C(void);. Standalone, the unprototyped call got an implicit int
return and lazy andi masks at the use sites — target shape. In-TU, gcc truncates eagerly at the
def site (andi $s0,$v0,0xff) and deletes both use-site masks: a 3-instruction miss invisible to
the oracle. Fix per §20/§17a-1: leave the narrow prototype alone, cast at the CALL site
((s32(*)(void))func_8014BF6C)(). A sub-word callee prototype should be a first-check residual
whenever a shape-MATCHed body fails the gate.
7. A STRUCT TAG THAT EXISTS NOWHERE IN THE TU. func_801819D0 (ov_SC03_031): a prototype whose
first parameter is struct S80131E00 * only warns standalone ("declared inside parameter list"),
but in a TU where the tag appears nowhere gcc invents an incomplete file-scope tag mid-decl-layer —
fatal under this project's warning-intolerant gate. Fix: forward-declare the tag at file scope
before the prototype.
8. A STALE DECL OF THE FUNCTION YOU ARE DEFINING. extern void func_801A1E94(void); vs an
(s32) definition; extern void func_801A5C44(void); vs a one-pointer definition; extern void func_80180CCC(void); feeding a wrapper. This is a TU EDIT, not a body fix: reconcile the stale
line to the unprototyped extern void f(); form (default-promotion-safe, byte-neutral at the argless
call site) or to the real signature. Cards: func_801A1E94, func_801A5C44, func_80180CCC,
func_8017E008. See §237 escape (3) when you may not edit the TU.
9. A FILE-SCOPE memcpy DECLARATION DISABLES THE BUILTIN TU-WIDE. Any TU that declares extern void *memcpy(...) lowers an 8-byte copy to a real jal memcpy instead of inline lwl/lwr +
swl/swr. Use the §48-C2/§160a align-1 struct-assign form instead — it routes through
emit_block_move and references no memcpy symbol. func_8017DA98 (×2), func_8018723C,
func_8017F748, func_801856E4, func_80187664, func_8018A314.
THE PROCEDURE. Before re-deriving anything: grep the destination TU for every identifier you
declare (with the hit cap raised above file length — func_80183C84's first grep silently truncated
at line 3706 and hid the conflict at 4173), check whether each callee is defined later in the file,
diff your typedef names against the include chain, and read the TU around the splice point rather
than only the .s.
§237 — THE CAST-AT-CALL-SITE DECISION TABLE: WHAT §17a-1 FIXES, WHAT IT CANNOT, AND THE FOUR ESCAPES (P31 S58b)
§17a-1's ((s32 (*)(s32))func_X)(a) is the workhorse of this project — it appears in over a hundred
banked cards. This harvest bounds it.
IT FIXES (byte-neutral, decl untouched):
- a
void-declared callee whose$v0you must consume (else "void value not ignored"); - a canonical 0-arg prototype where the target sets up
$a0in the delay slot — the cast restores the arg copy (func_80182428,func_80184F68,func_80178D18across a dozen TUs); - an
s16-returning prototype whose sign-extension pair the target does not have (func_80183258); - an extra argument past a
(void)prototype (func_801855CC— and there the cast beat an asm-label alias, which compile-failed on conflicting types and changed nothing anyway).
IT DOES NOT FIX — the boundary, byte-measured. func_80180480 (ov_SC07_010): a cast-call does
not bypass a conflicting file-scope prototype; gcc-2.7.2 still errors "too many arguments". The
working idiom there is a block-scope extern s32 func_8013D13C(s32 a0); shadowing the TU's
authoritative (void)-arity decl. Cast fixes types at the call; only a shadowing declaration fixes
arity against a hostile prototype in scope.
THE INVERSE ESCAPE — SUPPRESS AN ARGUMENT. func_80187008 (ov_SC04_011): the target's jal func_801805F8 has a bare nop delay slot even though $s0 is live, but the TU's only visible
prototype is void func_801805F8(s32), so a plain call emits addu $a0,$s0,$zero into the slot. Fix:
a block-scope UNPROTOTYPED extern void func_801805F8();, composite-compatible with the
prototyped definition under C89, called with no arguments. Same lever on func_80182EB0's
func_80185764 and func_8017F470's func_8017F534 (whose fleet row claims three params but whose
own .s sets only $a0; a prototyped decl cost +2 instructions of $a1/$a2 setup).
ESCAPE 2 — THE DEF-SIDE ALIAS (§202), NOW ALSO FOR A RETURN-TYPE CLASH. func_8017EBB8
(ov_SC07_010): the TU declares extern void func_8017EBB8(void*, void*) above the splice point while
the body must be s32 f(s32,s32) — a conflict on both axes. Define under an alias identifier with
__asm__("func_8017EBB8"); the C identifiers never collide and the emitted symbol is unchanged. Also
func_80185C6C (s16-vs-s32 params), func_80184358 (address-taken as a callback under a (void)
decl).
ESCAPE 3 — THE IMPLICIT-INT DEFINITION WITH NO return. func_801830F4 (ov_SC02_005): the TU
declares extern s32 func_801830F4(s32,s32); at three call sites, but the target never writes $v0
before jr $ra. A void definition is a conflict; s32 … { …; return 0; } emits move $v0,$zero
and mismatches. The form that satisfies both is an implicit-int definition with no return
statement — C89-compatible with the s32 externs, and gcc-2.7.2 emits no epilogue $v0 write.
ESCAPE 4 — (void)-PARAMETERISED DEFINITION + register __asm__("$4") CAPTURE. Where the TU
declares extern void f(void) at live call sites you may not touch, define f(void) and recover the
incoming argument with register void *a0r __asm__("$4");. This escape is two-sided: it was
byte-identical on func_80182EB0 (ov_SC02_005) and func_80181FA8, but on func_801A1E94
(md_SC07_004) it coalesced the body to 36 instructions — with no declared parameter gcc treated
$a0 as scratch, re-homed $s0, and lost both addu $s0,$a0,$zero copies. Probe it; do not
assume it. When it fails, the answer is the §236-8 TU edit.
AND THE ARITY EVIDENCE ITSELF. The atlas def row can contradict an observable $v0 consumer
(func_8012CBCC prints void(s32) fleet-wide, n≈1653, yet dozens of targets branch on its result;
func_8012D624's canonical ('void',('s32',)) drops two argument setups the target has). The asm
tell — delay-slot argument setup, a pre-branch load feeding $a2, a bnez on $v0 — outranks the
fleet vote at a given call site. The canonical void spelling is a fleet-wide convention, not
evidence the engine function returns nothing.
§238 — SAME NAME, DIFFERENT FUNCTION: THE OVERLAY-HOMONYM TRAP (P31 S58b)
Thirty-plus cards in this harvest burned turns — several burned entire sessions — on a body that belonged to a different binary's function with the same symbol name and often the same VMA. §214's similarity warnings cover twins; this is the harder failure, because the artefact is not a twin at all.
THE FOUR SOURCES OF THE WRONG BODY, in descending cost:
- A replayed/carried "previous attempt".
func_80182CA0(matched ov_SC06_022's 26-ins namesake instead of ov_SC05_010's 118-ins target — and the discarded Ghidra leftover had been ~90% right for the real function).func_80181224,func_8017FA94,func_80180B1C,func_80186A00,func_80182F4C,func_801849E4,func_80180030,func_80182674,func_8017F15C. - The card's own "banked twin" pointer.
func_801830EC,func_80184358,func_801887D0,func_801835B0,func_8018694C,func_80186BAC,func_80180A3C. - A grep by symbol name across
src/.func_8017EF0C,func_801807F8,func_80184814,func_8017DC20,func_8018301C,func_800CBCBC. - A stale
.runshard body. Roughly forty cards; usually detectable in seconds (see below).
THE FOUR CHEAP TELLS.
- The card header's instruction count.
func_80180030: "36 instructions" vs the 90-ins namesake.func_8017F15C: theglabelsize header reads0xFCvs0x264. This is the single fastest check and it is free. - The symbol sets do not intersect.
func_80182EB0(ov_SC02_005 vs ov_SC01_001): zero overlap. Diff the shard's callee set against the target's own relocation lines before refining anything. - The file PATH on the card header. Several cards resolved the moment someone re-opened the exact path printed on the card instead of a name-based grep hit.
- A "phantom target" that matches no
.syou have read means you are reading the wrong.s, not that the harness drifted (func_801819D0, ov_SC01_084).
AND THE POSITIVE USE. func_80187050 (ov_SC03_090) found five byte-identical unbanked twins
across binaries by grepping asm/ for the literal encodings 7F010224|FF7F0224 — a search that
src-side grep for = 0x7FFF; could never find, because every sibling was still an INCLUDE_ASM stub.
func_800CBCBC and func_80183A70 did the same with a distinctive addiu immediate.
Grep asm/ by encoding, not src/ by name.
BOUNDARY. Address+name collision across binaries is not a twin signal. Never mine a same-named
function from another overlay directory for shape, literals, register roles, or return type — see
func_801887D0, where the same-VMA sibling's s32 return was evidence against (this target writes
no $v0, and an s32 definition length-drifted by hoisting jr $ra above the tail sh).
§239 — TWO-STATEMENT INTEGER-SPACE MATERIALISATION REORDERS la vs sll; AND THE PLUS-TREE OPERAND ORDER (P31 S58b)
§164-01/§164-02 explain why parenthesisation and operand order are inert for &sym[i]. They are
not inert for the two constructs below, and both were found the hard way.
1. TO PUT THE BASE lui/addiu BEFORE THE INDEX sll, LEAVE POINTER SPACE. func_80186304
(ov_SC02_005, 72/72): every natural &D_xxx[i] / pointer-index spelling emits index-sll first,
then lui/addiu (cse qty_const swap, §164-02). The target emits base first. The fix is a
two-statement integer-space materialisation:
base = (s32)&sym; /* lui/addiu here */
p = (void **)(base + i * 8); /* sll, then addu */
That gets the expansion order right, but first-fit then assigned base→$a0/index→$v0; a
short-lived register s32 base __asm__("$2") (never live across a call, so §42e-safe) forced
base→$v0, index→$v1, and the card snapped to MATCH. This is the cheap hard-reg fix when only a
REGALLOC-PERM remains after the order is right.
2. FOR A REGISTER-HELD BASE, THE PLUS-TREE OPERAND ORDER IS THE DIAL. func_801A5C44
(md_SC07_003, 41/41): the target's addu $v1,$v1,$s1 ; lhu $v1,0x2($v1) appears only when the C
spells the shifted index term FIRST: ((idx << 2) + base + 2). Writing base + (idx << 2) emitted
addu $v1,$s1,$v1 on one arm and, worse, folded the +2 into the address so gcc reused $s1 itself
(addu $s1,$s1,$v1 ; lhu 2($s1)), clobbering a callee-saved register. §195-K's array-of-struct-vs-
address dichotomy covers global symbol bases; for a pointer local the dial is operand order.
3. AND FOR AN OT/TABLE POINTER, INTEGER FORM BEATS POINTER ARITHMETIC. func_80185910
(ov_SC02_000/ov_SC02_003, 140/140): (otz << 2) + (s32)tbl gives lw $v1,sp / sll $v1,2 / addu $v1,$v1,$s1; the tbl + otz pointer form misallocates. Same finding on func_80183478
(ov_SC06_029): folding the scale into a single initializer (idx = load * 8) makes cc1 expand the
index pseudo first, pinning it to $a0 and leaving $a2 for the counter.
BOUNDARY. None of this applies while the base is a plain global symbol reached by &sym[i] —
there §164-01/§164-02 still hold and the spelling is inert.
§240 — A + K + B: WRITE THE CONSTANT BETWEEN THE TWO RUNTIME TERMS (P31 S58b)
§164-16 says parentheses and operand order are inert for +/-. That is true of where the
constant ends up; it is NOT true of which runtime term the addiu attaches to.
func_8018364C (ov_SC03_105, 17 oracle calls): the target emits addiu $a1,$a1,0x3000 before
addu $a1,$a0,$a1. Every left-to-right spelling of i*0x200 + r%1024 + 0x3000 emitted addu then
addiu. Measured:
| source | emission |
|---|---|
A + B + K |
addu first, then addiu — wrong |
A + K + B |
addiu on B, then addu with A — target |
| explicit temp statement | correct order, but register-allocation drift ($a0/$a1 swap) |
| inline call in the final expression | extra move |
split_tree's rebuild attaches the constant to the term it was written next to.
THE COMPOUND-ASSIGNMENT COROLLARY. func_801824FC (ov_SC01_080, 14 oracle calls): in
mem = mem - 0x20 + D*40 + (rand()&0x3F), every explicit-statement spelling — parenthesised, term-
reordered, u32-barriered, temp-hoisted — canonicalised to mem + (D40 + rnd − 32). Only the
compound mem -= 0x20; mem += … form put -0x20 on the memory operand as lw $a0 ; addiu $a0,-0x20 ahead of the D-term chain. (Cf. §219: += is not merely a style of RMW, it is an
association dial.)
RELATED: ASSOCIATION CONTROL BY STATEMENT. func_80185EAC (ov_SC06_032): w = r + 0x200; as its
own statement makes gcc reuse $s0 incrementally (addiu $s0,$s0,0x200 riding a jal rand delay
slot); written inline it reassociated into an extra addiu $v0 and length-drifted.
§241 — THE FOLDED SIGN-EXTEND-AND-SCALE: sll 16 ; sra (16 − log2 scale) (P31 S58b)
A sll $x,16 followed by sra $x,N with N < 16 is not a division and not a plain
extension: it is a sign-extension of a halfword fused with a left shift of 16 − N.
| pair | meaning | scale |
|---|---|---|
sll 16 ; sra 16 |
plain (s16)x |
×1 |
sll 16 ; sra 14 |
((s16)x) * 4 |
×4 — indexes a 4-byte table |
sll 17 ; sra 14 |
(s16)(x*2) * 4 |
doubled index, 8-byte elements |
sll 16 ; sra 17 |
((s16)x) >> 1 |
signed halve (N > 16) |
BYTE-VERIFIED. asm/ov_SC07_007/.../func_80180EC8.s lines 11–16: sll $v0,$v0,16 ; sra $v0,$v0,14 feeding an lw off a %lo base — i.e. a 4-byte-element table indexed by a
sign-extended halfword. The first draft used explicit *4 scaling and produced sra 12.
THE C SPELLINGS.
- ×4 fold: write the index unscaled and sign-extended —
((v << 16) >> 16)— and let gcc fold the scale into thesraamount (func_80180EC8). - doubled index into 8-byte rows:
(s16)(m * 2)used as the subscript givessll 17 ; sra 14exactly, with the second element asbase+4off the sharedlui/addiu(func_801AE734, md_SC07_004). - signed halve:
((s16)u16_global) >> 1giveslhu ; sll 16 ; sra 17(func_8017F30C, ov_SC06_022;func_801800D0, ov_SC03_029;func_801AC3C8's accumulator, where the temp must bes16nots32or you get a baresra 17with no leadingsll).
THE DISCRIMINATOR vs §167-03's HImode-division reading: ask what consumes the result. If it feeds
an lw/lhu address, it is a scaled index (this section). If it feeds an addu/subu sum or a
compare, it is a divide (§167-03/§167-25).
§242 — *k vs <<n, AND /2^n vs >>n: EXPRESSION SPELLING OWNS THE LOAD WIDTH AND THE ROUNDING CHAIN (P31 S58b)
Two semantically identical spellings, two different instruction streams. Both cost a card apiece.
1. * 4 KEEPS A DEAD SIGN-EXTENSION ALIVE THAT << 2 LETS GCC DELETE. func_8017F300
(ov_SC06_029, 12/12): with the natural << 2 spelling gcc emitted lhu 0x100($a0) where the target
has lh — the sign extension is semantically dead under the later sh, so extend-propagation
weakened the load. Spelling the scale as a multiply (* 4) keeps SIGN_EXTEND alive through expand
— gcc-2.7.2 does not run the dead-high-bits analysis through MULT — and the load emits as lh.
Nearest prior art (§164-46/§164-47, §162k1) covers dead compares and declared-local width, not
expression-spelling-driven lh/lhu selection.
2. /512 KEEPS THE ROUNDING CHAIN THAT >>9 FOLDS AWAY. func_8017FCBC (ov_SC03_030, 230/230):
*0x50 >> 9 folded to srl 5; the target wants expand_divmod's bgez / addiu 0x1FF / sra 9
round-toward-zero chain, which only survives when written as * 0x50 / 512. Same for /256.
Complementary card func_8017F470 (ov_SC04_002) runs the dial the other way: the target has a bare
sra ,2, so the source is >> 2; writing /4 expands to bgez+addiu -3 fixup chains (+7 ins).
THE RULE. Read the fixup chain first. Bias addiu 2^k−1 present ⇒ the source divided
(/2^k). Bias absent ⇒ the source shifted (>>k). Scale on the left of a narrow load ⇒ prefer *k
if the target shows lh; prefer <<n if it shows lhu.
RELATED SPELLING TRIGGERS FROM THE SAME HARVEST. A folded multiplier survives where its
decomposition does not: *341/*11 written as single constants emit the target's shift-add chains,
while hand-written sll/addu/sll/subu or *5*68 decompositions get re-folded to li 736
(func_80185520, ov_SC06_000). x*2/3 emits sll v0,1 before the mult; plain /3 costs a slot
(func_80182CA0).
§243 — SPELL THE CSE BARRIER AS A REASSIGNED POINTER; AND THE nop-AFTER-EVERY-lh SIGNATURE (P31 S58b)
THE SIGNATURE. When a target reloads the same narrow global before every store, with an
unfilled load-delay nop after each load, the source did not read the symbol directly and did
not mark it volatile. It read through a pointer variable that is reassigned before each use —
the address pseudo is redefined between loads, so cse cannot common them, and no opcode changes.
func_80189B74 (ov_SC06_029, 16/16). Measured alternatives on the same card:
| spelling | result |
|---|---|
*(s16*)&D_x direct cast |
cse folds four reads to two loads + hoisted stores — 10 ins vs 16 |
volatile on the extern |
24 ins, lhu+sll/sra — wrong width, wrong count |
| reassigned block-scope pointer | MATCH 16/16 |
§176-B1 documents the narrow-symbol alias problem for schedule permutations; this is its inverse — a deliberate CSE barrier that costs zero bytes.
THE COMPLEMENTARY LEVER (same mechanism, opposite direction). When the target reloads a
pointer-typed field after an intervening store, the alias is already conservative and plain
sequential C reproduces it for free — do not hoist (func_8017EC00, ov_SC07_000, which also
explains why: gcc cannot prove *(arg0+0x20)+0x2C does not alias arg0+0x20). And when the target
reloads a global's address three times, the fix is the opposite again: take the address once into
a local (func_801A7B08, md_SC07_004; func_8017DB44, ov_SC01_084 — where sw $s0/lw $s0 with no
other $s0 use is the demand for a pointer value live across a call).
READING RULE. Count the reloads in the target and compare with the number of textual reads your draft has. Equal ⇒ leave it alone. Target has more ⇒ you need a barrier (reassigned pointer, or §244). Target has fewer ⇒ you need a name (§208/§253-adjacent).
§244 — volatile IS A COUNTING INSTRUMENT, A STORE-ORDER PIN, AND MUST SOMETIMES BE ASYMMETRIC (P31 S58b)
Four distinct uses, three of them not previously named.
1. THE RELOAD COUNT IS THE DECOMPILER'S OCCURRENCE COUNT. func_801814AC (ov_SC05_010, 113 ins):
the target reloads lui/lw of D_801C7E30 before all six accesses; a non-volatile extern CSE'd
them away (108 ins). The Ghidra seed's six DAT_801c7e30 occurrences were the reload count, not a
cached local. A pointer global read once into a local but reloaded per-access implies volatile,
not a struct-pointer parameter.
2. ASYMMETRIC volatile — ON ONE OF TWO STORES TO THE SAME ADDRESS. func_801815A0
(ov_SC03_001): the target has a dead sh $zero,0x2C followed by sh $v1(0xC006),0x2C. volatile on
both or on neither collapses the pair (the scheduler keeps only the last store); volatile on
the zero store only reproduces both. Same shape on func_801816A8 (ov_SC05_005) and
func_80185B78's self-assign (§249).
3. volatile ON A POINTER AS A STORE-ORDER PIN. func_801ADD98 (md_SC07_004, 33/33): declaring
both byte pointers volatile forces the literal sb→lbu read-back of each just-stored byte
instead of forwarding from a cached register; with only one volatile gcc hoisted the other's stores
(+2 ins). volatile-on-pointer is a store-order pin for interleaved write/read-back loops, not only a
reload lever.
4. volatile AS A DECLARATION-ONLY FRAME PAD. func_801828E0 (ov_SC05_008): volatile s32 pad;
reserves the stack slot without emitting code at -O2, where a plain unused local is DCE'd. Two of them
give the 8-byte vars area (func_801814AC). See §258/§226 addendum for the full pad catalogue.
BOUNDARY — WHEN volatile IS THE WRONG TOOL. It punches load widths: on func_801AC9CC a
volatile-punned local turned lh into lhu and broke the other half of a deliberate lh/lhu pair
(the fix there was a zero-byte __asm__ __volatile__("" ::: "memory") barrier between the two reads —
the cookbook documents that barrier as a cross-jump bailer and scheduling anchor, not as a cse
load-merge blocker, which is what it is here). On func_8018F230 the same barrier broke a
value-CSE between two reads of one global where volatile produced an "ugly lui/addiu
address-object form". Reach for the barrier before the qualifier when the goal is duplicate loads of
one width.
§245 — THE CALL'S ARGUMENT LIST IS A SCHEDULING SLOT (P31 S58b)
Five constructs that move an instruction no statement reorder, pin, or barrier will move. All are "put the work inside the call expression".
1. FOLD THE STORE INTO THE ARGUMENT LIST TO CHOOSE WHICH INSN FILLS WHICH SLOT. func_80185298
(ov_SC03_090, 47/47): the target has addu $a0,$s0,$zero in the bnez delay slot and sw $s0,0x20($s1) filling the jal slot; every statement order gave the reverse. The unlock:
func_8001CA1C(*(s32*)(a0 + 0x20) = s0, (s32)D_801A3060);
This sequences the store's RTL after the argument copy at expansion time without changing
statement order, so dbr takes the addu for the branch slot and the sw lands in the jal slot.
Rejected on the same card: a $4 asm pin (store still scheduled first), a plain temp copy (CSE'd
away, +1 nop), early-return inversion (wrong polarity). Generalises to: "a store consumed by the very
next call can be folded into that call's argument list."
2. NEST THE CALL IN THE ARGUMENT LIST. RotMatrixY(func_8012B6D4(...), &mat) reproduces the exact
addiu a0,s1,4 / addiu a1,a2,4 / jal / move a0,v0 / addiu s0,sp,0x20 / jal chain with zero pins
(func_80187240, ov_SC03_090; func_80181EC0). Conversely func_8017DBB0 (ov_SC06_008): hoisting two
sin/cos lookups into temps let gcc reuse $v0 across the second call and killed the target's
addu $a0,$s0,$zero arg reload inside the first jal's delay slot — nested-call-in-initialiser is
itself the delay-slot-filler control.
3. LOAD THE GUARD VALUE STRAIGHT INTO THE ARGUMENT. func_8017F2A0 (ov_SC06_006): the guard value
is the call's first argument, so the beqz delay slot stays nop ($a0 is live). Passing it
through a temp gave lw v0 / move a0 / nop. Same law on func_8017F790 (ov_SC01_005/006, 154/154 ×2):
u8 c = D_801BBA7F[idx]; … if (func_8017D9F4(c) != 0) makes the lbu double as the argument setup,
pinning it to $a0 — every spelling that treated the byte as compare-only failed.
4. THE POST-INCREMENT AS AN ARGUMENT. func_8017E520 (ov_SC07_010): f(a0, (*(s16*)(a0+0xFC))++)
expands as lhu + sll/sra into $a1 (the sign-extended OLD value) with the sh in the jal
delay slot. No statement ordering of load/call/store reproduces it, because gcc never sinks a store
across an opaque call into a delay slot — the store must come from the same expression tree as the
argument. It also defeats forwarding of the guard's lh into round 1 with no barrier; every
explicit wedge tried (empty asm, memory clobber, volatile reads/stores) failed or overshot.
5. THE NARROWING CAST AT THE ARGUMENT. (s16)f(...) as a call argument emits sll $v0,16 / sra $a0,16 on the argument register; routing the same value through a named local emits the pair on the
wrong register or drops it (func_8018741C, func_80188FB0, func_801A6610, func_8017C624). And
(u16)x at the argument emits the andi $a1,$a1,0xFFFF mask that the prototype alone will not
produce (func_80181ED8, ov_SC07_002; func_80186C0C, where the mask is literally the 53rd
instruction and vanished when the extern block was dropped).
BOUNDARY. Do not reach for these while a whole instruction is missing or extra — they are placement levers, not count levers.
§246 — THREE-LIVE-VALUE SCAN LOOPS WANT ADDRESS-FROM-INDEX; AND TWO SYMBOLS CAN SHARE ONE giv (P31 S58b)
1. base + i*stride INSIDE THE LOAD EXPRESSION. func_800CAED8 (md_MAIN_025): when the target's
loop keeps three live values ($a0 unit counter, $v1 stepped pointer, $a1 accumulator),
neither of the two obvious spellings works —
p += 0x34(pointer form) is fused down to two values bycombine_givs;base[i].field(array-index form) strength-reduces to a scaled-offset giv anchored on%hi(sym)+offvia$at.
The winning spelling is the address computed from the index inside the load expression:
*(u16 *)((s32)&sym + i * 0x68) == K
This keeps i alive for the exit test while letting induction-variable reduction materialise the
FULL base into the stepped giv (lui/addiu, then addiu step 0x68); the scaled-index giv never
appears. Three-register scan loops want address-from-index — not pointer arithmetic and not array
syntax.
2. TWO DISTINCT GLOBALS AS PARALLEL ARRAYS OF ONE STRIDE-TYPE SHARE A SINGLE ADDRESS giv.
func_8017E95C (ov_SC06_025, 23/23): the target walks D_801B12F0 and D_801B12F8 with one
byte-offset IV ($v1 += 0x104) feeding both through lui-$at/addu/sh %lo(). Declaring both
externs as [4] arrays of a common 0x104-byte record type makes gcc's giv-combine fuse the two
address computations onto one induction variable. §2397's IV-COMBINE is stated for fields off ONE base
pointer; here the single-IV result is obtained across two different symbols by giving them
identical element strides.
3. AND THE OPPOSITE CASE. func_8017DC50 (ov_SC05_008): a loop-carried p += 0x10C compiled to a
second address register; q = &base[i*stride] recomputed per iteration reused one register and
matched. func_80187754 (ov_SC06_000): two walkers of different scale off one base must both be
spelled as indexed expressions (((s16*)a0)[i+2], a0[i+5]) so IV elimination creates the two givs in
the target's order. func_8017F13C (ov_SC03_001): switching a pointer-IV to an index-biv form
changed the movable ranking so exactly ONE loop-invariant constant hoists — §193-F's "target hoists one
constant, leaves its twin inline" tell, reachable by redraft rather than by pinning.
UNSOLVED, RECORDED. func_801AB5D4 (md_SC07_004, 13 oracle calls across two sessions): no spelling
suppresses combine_givs' second address-giv for a constant +2 offset off an unconditional walked
base when the target keeps it as a displacement. §162n's cant_derive cure needs a conditional
alias; there is no if-arm to hide the assignment in. Nine drafts, all near-40. If you meet this
shape, go straight to the permuter.
§247 — TWO BRANCHES TO ONE LABEL MEANS THE SOURCE CONDITION IS NEGATED (P31 S58b)
func_80184A98 (ov_SC03_091, 19/19). The natural reading —
if (type == 0x178 || type == 0x1E8) f(); else g(); — produces beq and bne to different
labels. The target has two beqs to the same label .L80184AC8. That is the fingerprint of gcc
negating the condition and laying the else body out as the straight-line then-block:
if (type != 0x178 && type != 0x1E8) { g(); } else { f(); }
which reproduces both beq polarities and gcc's block ordering (then-block inline, else-block after
the second beq's delay slot).
GENERALISE THE READ, not the fix: count how many branches converge on each label before choosing a source polarity. Two same-sense branches to one target ⇒ inverted source condition. One branch each to two targets ⇒ the naive reading. This is the branch-count analogue of §225's layout laws and it is free to check.
RELATED POLARITY TELLS FROM THE SAME HARVEST (each cost 1–3 compiles):
slti ; xori 1 ; bnez= a>=computed as a VALUE then branched on —flag = f() >= K; if (flag) goto skip;. The negated-ifform (if (!(x < K))) does not work: gcc foldsTRUTH_NOTback into an inverted branch (func_8017F224, ov_SC04_019; extends §194-L's table with a worked instance).xori $v0,$v0,1 ; sltu $v0,$zero,$v0=x != 1, notx == 1(func_801805CC, ov_SC05_017 — §60b holds both halves but framesxorias the wrong form, which is why a 0.89-similar twin won over the target bytes for one compile).- A
bnez-forward with the teardown on the fall-through is a guard clause (if (c == 0) { teardown; return; }), not anif/else(func_80184A78, ov_SC03_024 — draft 1 was otherwise perfect and still 15 instructions off). slti C, x ; bnezwith the constant on the left isx > C;slt v1,const,retisret < const+1— write the strictly-less form with the incremented literal (func_80180880).slti $v0,$v0,-0x1FF ; bnezskipping the store is clamp UP to a floor (m1 > -0x200), not clamp down (func_80180DAC, ov_SC07_002 — the seed's< -0x1FFgivesbeqzand can never match even at equal length).
§248 — SPLIT THE LOAD FROM THE ARITHMETIC: A FUSED g + K DENIES THE CALLEE-SAVED REGISTER ITS DIRECT HOME (P31 S58b)
func_80182F20 (ov_SC06_025, 30/30). Written fused —
x = D_801B1748 + 4;
— combine folds the HImode load and the add into one pseudo whose home is chosen by reference
count; the value lands in $v0 and needs a move $s0,$v0 copy (+2 ins, wrong prologue order).
Written split —
x = D_801B1748;
x = x + 4;
— the lh is the single-def root of the addiu, so local-alloc homes it directly in $s0 and the
addiu re-sets it in place: lh $s0,%lo(...) ; addiu $s0,$s0,4. No address pseudo, no copy.
x must stay s32; an s16 local emits lhu plus an sll/sra extension pair (+3).
THE READING RULE. A callee-saved register that is both the load destination and the addiu
destination (lh $sN then addiu $sN,$sN,K) is evidence for the split form. A load into $v0
followed by move $sN,$v0 is evidence for the fused form. Nearest prior art is §193-K
(address-vs-reference spelling) and the §12282 guard re-read; neither covers a fused load+add denying
a callee-saved register its direct home.
§249 — THE SELF-ASSIGN, THE DEAD RE-ASSIGN, AND THE + zr COPY: THREE WAYS TO MAKE A DELETED INSTRUCTION REAL (P31 S58b)
gcc-2.7.2 deletes copies, self-assignments and redundant loads aggressively. Three byte-proven ways to force one back into existence, in increasing invasiveness.
1. THE SELF-ASSIGN SURVIVES AS AN lw+sw PAIR. func_80185B78 (ov_SC01_084, 37/37): the target
holds lw 0x10 ; lw 0x18 ; sw 0x10 ; lw 0x14 ; ori ; sw 0x18 ; addu ; sw 0x14 — the source is
f10 = f10; f18 = f18; f14 += 0xC000; with no CSE across the pairs. One volatile is needed, on
the f10 self-assign only, to kill last_mem_set deletion; f18's pair survives unaided because its
load feeds a real use. §244-2's asymmetry law again.
2. RE-ASSIGN A DEAD LOCAL TWICE PURELY TO PIN ONE HARD REGISTER. func_8018AE6C (ov_SC06_032,
8 ins): two independent pointer chases; two nested expressions gave a fresh pseudo each (lw $v0,
lw $v0) and a REGALLOC-PERM. A single named local assigned twice lengthens the live range so
reload coalesces both loads into one hard register — the target's shape. §124's anti-dependence fence
covers re-setting a variable that was read; this is its write-only sibling: nothing anywhere
reads the local.
3. THE + zr OPAQUE COPY (§2494 / RC-12), AND ITS COMPOSITION. register s32 zr __asm__("$0"); a1 = (s32)arg0 + zr; emits a byte-identical addu as RTL that make_regs_eqv/cprop never link
(func_80181638, ov_SC05_003 — a plain s32 a1 = (s32)arg0; is reversed by combine per §2494).
func_8018188C uses it to keep two live registers per clamp where plain copies were deleted (61→36).
func_801AB54C composes it with pins (v=$2, c=$3) to reproduce a lh→copy→slti-on-load→
addiu-on-copy schedule. New this harvest: func_800CFB3C (md_MAIN_003) needed the
__asm__("" : "=r"(s1) : "0"(arg0)) launder plus a second __asm__ volatile("" :: "r"(s1))
use-barrier — either alone fails: the matching-constraint form alone still lets the later body
re-propagate, and the barrier alone leaves lhu 0($a0).
AND THE ANTI-REMAT VARIANT. func_80184A68 (ov_SC06_000, 16 ins): to stop update_equiv_regs
rematerialising a symbolic (lui/addiu) address at each neighbour use, pin the base to $a0 AND
break the single-set remat gate with __asm__("" : "=r"(a0) : "0"(a0)). §30's re-tie is documented for
load-CSE'd values; this is the address-constant case. The full-strength version is §2 of
func_8018270C (ov_SC06_000, banked twice): asm-INITIALISATION with the symbol in the template —
s32 *p; __asm__("la %0, SYM" : "=r"(p));
one pseudo, single SET whose source is ASM_OPERANDS (hence never CONSTANT_P, so no REG_EQUIV and
the absolute fold stays blocked), no input operand for reload to merge, zero bytes emitted, and the
%hi/%lo relocations carry the right symbol because it is spelled literally. It is the missing
third option between §167-45 LAW 1 (plain p = &SYM, which folds) and the §30 re-tie (which blocks the
fold but reschedules or gets elided — a documented catch-22 in straight-line functions).
§250 — %hi/%lo vs lw: THE EXTERN'S ARRAY-vs-SCALAR SHAPE DECIDES ADDRESS MATERIALISATION (P31 S58b)
The single most common type-level residual in this harvest, worth one instruction each time.
| target shows | the extern must be | why |
|---|---|---|
lui/addiu pair, no load |
an array (extern T D_x[];) or &scalar |
array decay is a link-time constant gcc folds straight into the pair |
lui/lw pair |
a scalar of pointer/word type | the address is materialised, then dereferenced |
lui $at + addu $at,$at,idx + %lo(...) load |
an array indexed in C | keeps the index in the addressing mode |
separate lui per adjacent byte/half |
distinct scalar objects (§216) | one array collapses them to one base |
Cards. func_8017DE00 (ov_SC07_000): extern s32 D_801D0C78[] — as a plain s32 it emitted lw $a0,0($a0) where the target wants lui/addiu. func_801826CC (ov_SC03_002): declaring a
function-pointer table as a scalar void (*D)(void) forced a load path; extern void (*D_80189CFC[])(void) folds the address directly, including under | 0x40000000. func_80185B7C:
the fleet row prints a value type while the use site takes the address — the tell is whether the
symbol feeds an addiu/lui pair or a load. func_8017FCBC, func_8017D68C, func_80186C70,
func_8018372C, func_80183C24 all repeat it.
AND THE &-vs-DECAY DISTINCTION IS REAL. func_8018269C (ov_SC05_018): (u32)&D_8018AFC0 | 0x40000000 emits an extra move (the la is forced through a pseudo); the array-decay form
(u32)D_8018AFC0 | 0x40000000 gives the target's exact lui/addiu/or chain. Nearest prior art is
§162c's constant-OR rules, which do not mention the la-vs-decay distinction.
THE INVERSE — WHEN THE ARRAY DECL IS UNUSABLE (§216 confirmed). func_8018BB44 (ov_SC04_011,
49/49): the card's authoritative D_801EFEA0 as u8[] makes gcc materialise ONE base and address all
four bytes off it — 43 ins vs the target's 49. The target does an independent lui/lbu/sb
triple per byte, which only falls out when each byte is a distinct scalar object (block-scoped
extern u8 with an explicit __asm__ name), plus a separate u8 *flag = &scalar3; to force the
entry-time $a0 materialisation. func_801AA2F8 (md_SC07_004) is the same finding: the TU's
Blk4_801A7358 D_801F88B8 decl cannot be used for the three per-byte stores without collapsing the
luis.
BOUNDARY. When a symbol's address is only ever formed (never loaded, never indexed), the object type is codegen-neutral — pick the fleet/TU spelling and move on. The dial only bites when a load, an index, or an adjacent sibling symbol is involved.
§251 — IMMEDIATE-SPELLING TRIGGERS: += 0xFF, FULL-WIDTH ~K, AND THE TWO-OR SPLIT (P31 S58b)
Three cases where the arithmetically-equal spelling emits a different immediate.
1. += 0xFF IS THE BYTE DECREMENT. func_801814B4 (ov_SC02_005) and func_8017EBBC
(ov_SC04_002, where the lever is recorded in the TU's own neighbour comment): cnt - 1 on a byte field
encodes as addiu … 0xFFFF; the target wants addiu … 0x00FF. Both are mod-256 identical through the
sb; only cnt + 0xff produces the target's immediate.
2. MASK WITH THE FULL-WIDTH COMPLEMENT, NOT THE TRUNCATED LITERAL. func_801811A0 (ov_SC07_007):
~0x1000 gives the target's addiu $v0,$zero,-0x1001 (= 0xFFFFEFFF); the natural-looking
& 0xEFFF emits a different and. Same on func_800CD1F4 (~0x1000, not the twin's ~2) and
func_801845F4 (~0x80 ⇒ addiu $v0,$zero,-0x81, not ~0x81).
3. DO NOT HAND-FOLD A TWO-CONSTANT OR (§162c, confirmed four more times). 0x40000000 | 0x20000000 written as 0x60000000 loses an instruction; | 0x40000000 | 0x10000000 is required to
get the target's lui 0x4000 ; or ; lui 0x1000 ; or (func_8017FD00, ov_SC03_031 — the same card
twice, waves av/ar). func_801804A8, func_8018269C, func_80186E60, func_80180878 repeat it.
Conversely a constant whose low half is zero (0x50000000, 0xFFD80000, 0x00A00000) is a
bare lui with no companion ori — §199-A's plain-literal split, and no two-step spelling is
needed (func_8018A878, func_8018A5D8).
§252 — THE GUARDED PRE-DECREMENT: (x != 0) && (--x == 0) (P31 S58b)
func_801831E8 (ov_SC01_080, 28/28). The naive --x == 0 length-drifts −1 because gcc emits the
!= 0 guard test before the decrement — beqz with addiu $v0,$v0,-1 in its delay slot — so
the source is the two-condition form, not a plain pre-decrement compare.
THE READING RULE. beqz-in-front with the decrement in the delay slot ⇒ the source guarded first.
The decrement store landing in the branch's own delay slot instead ⇒ the decrement is unconditional
and the test follows (func_8018223C, func_801855AC, func_80182F4C, func_800CD7D8 —
"the sw in the bne delay slot means the store executes on both paths, so write it before the
if, not inside the taken arm").
RELATED DECREMENT SPELLINGS FROM THIS HARVEST.
- In-place
--*(s16*)(a0+0x2C)compared to-1beats load/addiu/store-then-test, which emittedli v1,-1after the$s0save and forceda0throughs0(+3,func_80182F04). *(u16*)p -= 1emits anandi 0xffffzero-extension; the separate-s16-temp form (t = load-1; store; if (t==0)) yields thesll 16idiom (func_8017EB2C, ov_SC03_023 — and a mid-stream "-=cleanup" broke a working draft, so this is a regression to guard against).--a0[0xA] == -1on ans32*parameter reproduces load/addiu -1/bne/sw-in-delay-slot with no temp at all (func_8017CEA0).
§253 — POSTFIX ++ vs += 1 PICKS A DIFFERENT SCRATCH REGISTER (single observation — not yet cross-confirmed) (P31 S58b)
func_8017C104 (ov_SC06_000, 10 ins): two independent u16 halfword adds on a0. gcc-2.7.2 assigns
the FIRST-emitted load to $v0 and the second to $v1. Writing the +0x2 update as += 1
allocated it to $v0 (wrong); writing it as a postfix ++ while the +0xE update stays += 0x10
produced the target exactly:
lhu v0,0xE ; lhu v1,2 ; addiu v0,0x10 ; addiu v1,1 ; sh v0,0xE ; sh v1,2
§165-06 records that increment-operator vs compound-assignment differ by one expand instruction (and
func_80186B84/func_80186B14 confirm (*p)++ is mandatory there); this card says the axis also
reaches the allocator's vreg choice for otherwise identical expressions. One card only — probe it,
do not assume it.
§254 — THE DEAD PARAMETER IS A REGISTER-PLACEMENT TOOL (single observation — not yet cross-confirmed) (P31 S58b)
func_8017D890 (ov_SC06_000, 28/28): declaring an unused second parameter makes gcc keep the
object pointer in $a1 and lets the counter live directly in $v0/$v1; without it gcc emits an
extra addu move (27 ins, wrong shape). The TU's siblings func_8017D678/func_8017D75C show the
same pattern, which is why it is believable, but only this card A/B'd it.
Two adjacent, better-attested facts: an unused parameter that the target genuinely passes must stay
in the signature even though nothing touches $a0 (func_80183AFC, func_801A1924); and
func_8018944C (ov_SC06_018) is 3-arg with a1/a2 unused at no frame cost, required because the
TU's forward decl and live caller pass three — do not "fix" an arity that the caller side pins.
§255 — THE EMPTY CASE, PART 2: FOUR TREE SHAPES IT BUYS (P31 S58b)
§222 records that "a leading EMPTY case buys the median split" and §193-G gives the ≥3-node floor.
Four more shapes, each byte-proven, each reachable only by adding a case K: break; that emits nothing:
1. EMPTY case 2: TO ROOT THE TREE ON ==1. With cases {0,1} only, gcc-2.7.2 roots the decision
tree on ==0 and lays case 0's body first. Adding case 2: break; gives three case values, so gcc
roots on ==1 and folds v>=2 || v==0 into two default edges — beqz on the slti result to the
tail, bnez $v1 to the tail. func_80180B90 (ov_SC07_000; the banked twin func_801AE220 carries the
same empty case, which was the tell). func_801824E4 (ov_SC02_031) is the same lever stated from the
balance_case_nodes side (stmt.c:5361's i > 2 gate).
2. EMPTY case 0: SO CASE 1 BECOMES THE TREE'S LOW LEAF. With {1,2} + default, a bare
if(v==1)…else if(v==2)… emits bne/li/bne and no slti; the empty case 0: puts case 1
left of the slti split (func_8017F3B0, ov_SC02_017; func_80181AB8, ov_SC03_107 — where §199-G
predicted the residual signature exactly).
3. BOTH case 0: break; AND default: break; FOR A {1,2} FALL-THROUGH TARGET. func_80180D3C
(ov_SC03_029): default: break; alone changed nothing (near-45); adding the empty case 0: as well
made three case nodes and emit_case_nodes produced the median split byte-for-byte.
4. AN EMPTY MEDIAN CASE AS THE TREE ROOT, EMITTING AN EPILOGUE-TARGETING beq. func_80180A44
(ov_SC06_006): the first beq (v1 == 0x200) targets the epilogue — case 0x200 is empty, and
0x200 is the median of {0x000…0x400}, so gcc's comparison tree roots there and emits the slti
guard in that beq's delay slot. Writing the cases in ascending order with an explicit empty
0x200 reproduces the tree exactly.
AND CASE-BODY PLACEMENT. gcc-2.7.2 emits balanced-tree case bodies in DFS order (root body
first), so the physical order of arms in the .s does not name their case values — map them from
the beq/slti chain, not from emission order (func_801855CC, ov_SC03_001; func_8017E614,
ov_SC05_017, where the middle case is tested first and its body sits last-but-one; func_80186238,
ov_SC06_032, where a 0.86-similar twin mislabelled which arm owned which label). Where cases are dense
and the source arm order matters, §222's "source arm order IS emission order" still holds
(func_80183A1C: case 1 must precede case 0).
§256 — GOTOS IN THE TARGET'S BLOCK ORDER REPRODUCE SWITCH PLACEMENT WITHOUT SWITCH'S SIDE EFFECTS (P31 S58b)
Three cards found the same escape from opposite directions.
THE PROBLEM. For a small dispatch, switch and if/else-if are not the only two spellings,
and both can perturb things outside the dispatch. func_80181F78 (ov_SC06_000, 244 ins): switch
nested the bodies inside the compare region and rotated the registers of an unrelated upstream copy
loop; if/else-if emitted a bne staircase. The actual source shape is a flat two-target
dispatch written as explicit gotos in the target's own block order:
if (t == 0) goto body0;
if (t == 1) goto body1;
goto end;
body0: …; goto end;
body1: …;
end: ;
which emits beqz v1,.Lbody0 / beq v1,v0,.Lbody1 / j .Lend with both bodies after the tests and an
unconditional j over body0 — the target layout — while leaving the loop's allocation untouched.
THE DISCRIMINATOR vs a switch's inline median tree (func_8017FB98, ov_SC01_009): both carry
slti. What separates them is whether the arm bodies sit inline after their tests (switch's
inline-tree emission) or after the whole dispatch, reached by forward jumps with delay-slot-filled
compare constants (goto-dispatch). Inline-tree emission never produces the delay-slot-filled
compares.
AND THE THIRD CARD. func_801F23D0 (md_SC03_076): a goto-ladder with one shared join is required
for the tail shape — an if/else-if ladder with per-arm returns duplicated the lw/store in every
arm. Zero-byte __asm__("") barriers between the chain tests were what kept gcc from threading the
goto joins back into an else-if ladder; without them every variant collapsed to the 32-ins
duplicated-tail form regardless of goto-vs-return spelling. The t=0/t=1 flag then materialised
through the branch slots on its own.
BOUNDARY. Reach for gotos only when the dispatch has already been proven not to be a switch (no
jtbl, no median tree, or a switch measurably perturbs code outside it). §225's warning stands: gotos
landing mid-flow are for shapes no structured spelling reaches, not a first resort.
§257 — THE DEAD-END LEDGER (P31 S58b): ELEVEN LEVERS THAT MEASURED NULL OR BACKFIRED
Recording these is worth as much as recording the fixes. Each was tried on a card that eventually banked by other means.
- §30's re-tie is a catch-22 in a straight-line function.
__asm__("" : "=r"(p) : "0"(p))afterp = &SYMdoes blockupdate_equiv_regs' absolute fold (reg_n_sets == 2) — but it inserts ala→asm→use dependence chain whose priority hoists thelato the top of the function; moving the re-tie after the last use gets the asm elided as dead and the fold returns. Use §249's asm-initialisation form instead (func_8018270C, banked twice). registerpins on call-clobbered registers are silently ignored when the function has ≥1 call.register s32 *p __asm__("$2")produced output identical to unpinned (func_8018270C);func_8017F790observed the same for a$2pin on a byte local.__asm__register pins are syntactically invalid on PARAMETERS in this cc1 (func_8017F188).- Pins must use NUMERIC register names.
$a1fails with "invalid register name";$5works. The cookbook's examples are all numeric but never say why (func_801AB78C). - Scope-bracing a pin is a total no-op — pins are resolved at
local_allocregardless of C block nesting (func_801AB5D4). - Pinning MORE than the target's callee-saved set is itself the blocker for call-slot filling.
With
$v0/$v1/$s1all pinned, every value becomes hard-reg-dependent on call-clobbered registers and sched2 can never fill ajaldelay slot. Pin only what allocation would choose anyway (func_8017F10C, ov_SC07_010, two independent notes). - A transplanted twin pin can MANUFACTURE the residual.
func_8017F648(ov_SC05_018): the twin's$17pin let cse'srecord_jump_equivsubstitute the known-zero pinned register for a literal0argument (move $a1,$s1instead ofaddiu $a1,$zero,0). §165-03's tie-to-self barrier did not fix it (+1 ins — the pinned register conflicts with the asm operand); dropping the pin did. - §16x's interposed-asm lever DIES under
__volatile__. Every__volatile__spelling acts as a hard scheduling fence and keeps the prologue save pair swapped; only the non-volatile__asm__("" :: "r"(arg))form flips it. The cookbook's own barrier sections (§17, §135-13) all spell it volatile (func_80186C0C, banked twice). - Hoisting a temp to "pin" a register can INVERT the delay-slot choice.
func_8017E270(ov_SC03_107): register-explicit drafts forcedli v0,16into the secondbneslot; the plain seed statement order let gcc fill it correctly. Try the naive body first once the shape is confirmed. - §194-B bound 4's "wrong place" is TARGET-RELATIVE.
func_80182224(ov_SC06_016): the two-arm select spelling puts the copy above theslti, which §194-B calls wrong for its own exemplar and which is exactly right here — and it needs no pin, contradicting §136-3 rule 3's pin requirement for the straight-linev1=f(); rnd=v1;shape. - The pad lever overshoots whenever real narrow locals already exist (§226-1, re-confirmed on
func_8018B830,func_801A876C,func_801AC940,func_80185920) — and onfunc_8017E424the pad was an outright decoy for a §167-10 hoisting residual, where deleting the pad was required, not optional.
§258 — ADDENDA TO EXISTING SECTIONS (P31 S58b)
The cookbook is append-only, so these live here rather than inside the sections they extend. Each block is a clearly-marked addendum; read it whenever you read its parent section.
§30 addendum (P31 S58b) — THE ANONYMOUS STRUCT MEMBER REF GRANTS /s, AND THAT IS A TWO-FOR-ONE
§30's law: a zero-offset/bare-deref load never gets MEM_IN_STRUCT_P (/s), so it carries a hard
dependence on an aliasing fixed-symbol store and sticks below it; an offset/member load gets /s and
hoists freely. Two cards show that granting /s also flips the pseudo colouring, closing two
residuals with one edit.
func_801824D4 (ov_SC06_000, 35/35): plain source orders gave either the lhu below the D_801AEC60
store with v0=load/v1=const (wrong regs) or the right position with the registers swapped. Writing
the increment as an anonymous struct member ref
((struct { u8 pad[2]; u16 f; } *)a0)->f += 1;
granted /s: the lhu hoisted above the aliasing store and the constant 1 landed in $v1
with the load in $v0. func_801E5894 (md_SC02_009, 11/11) is the same lever with the same two-for-one
effect. The struct must be ANONYMOUS — dedup_propagate rejects inline named structs (§30's own
note), and a named typedef risks the §236-4 redefinition wall.
Also note on func_801824D4: declaring the paired global u16 truncated a 0x1310000 constant to the
sh-only path — it must be s32/u32.
§194-B / §209 addendum (P31 S58b) — TWO MORE INSTANCES, AND THE BOUND IS NOW REFUTED FOUR WAYS
§209 already records that §194-B's "needs ≥2 sh stores" bound is byte-wrong. Two further cards:
func_801A8738(md_SC07_004, 13 ins, ~10 drafts): onesbstore, the second consumer being the compare itself. Declaring the accumulators16makes the HImode "assigned then tested" pattern emit a deferred-truncation copy (addu dst,src,$zero) before the compare and forces the pseudo to$a1via copy-preference off the incoming argument. Everys32spelling gave 11 ins with the load landing directly in$v1and no copy. §194-B's bound #2 ("fewer than two surviving consumers ⇒ no copy") would have steered away from the winning spelling.func_8018B830(ov_SC04_011, 36/36): theaddu $a2,$v0,$zeroafterlh 0x18($a1)is a truncation-copy, not a CSE duplicate-read (duplicating the read got folded).s16 a2produces it;s32loses it.- And the mechanism, named by
func_801AC940(md_SC07_004):LOAD_EXTEND_OPsplits the compare-pseudo from the arithmetic-pseudo when the cached field is ans16local. That single declaration fixed a branch-target offset and anaddiu-delay-slot shape simultaneously. Rule 10's own example shows the copy feeding laterlhloads; here it feeds onlyslti+addiu+sh, so the tell generalises to any second use, not just reload pairs. - Counter-instance for the other direction:
func_8018516C(ov_SC06_000) — no value local at all, and two textual reads of the field per RMW, is what produces theadducopy there; everyshort varspelling gave a second load instead. §209's table rows are all local-based; this is the no-local row.
§202 addendum (P31 S58b) — THE DEF-SIDE ALIAS ALSO CLEARS A RETURN+PARAM DOUBLE CONFLICT
func_8017EBB8 (ov_SC07_010, 45/45): the TU declares extern void func_8017EBB8(void*, void*) ABOVE
the splice point while the body must be s32 f(s32,s32) — a conflict on both axes at once.
aF8017EBB8 __asm__("func_8017EBB8") clears both; the jal sites elsewhere in the TU are unaffected.
Also func_80185C6C (s16 vs 32-bit params — adopting s16 costs a sign-extension from instruction
#1, and a K&R (int,int)-promoted definition conflicts with the s16 prototype) and func_80184358
(address-taken as a callback under a (void) decl). Boundary: on func_801855CC the alias
compile-failed on conflicting types and changed nothing anyway — there the cast-at-call-site won.
Try the cast first; the alias is for conflicts the cast cannot reach (§237).
§205 addendum (P31 S58b) — CHAINED ASSIGNMENT: N≥3 IS INNERMOST-FIRST, AND THE TEXT MIRRORS EMISSION
§205's evidence case is a two-store pair. func_8017FC14 (ov_SC03_095): for N ≥ 3 the same
inner-first rule applies and the textual order is the mirror of the emission order —
*(u16*)(p+0xFC) = *(u16*)(p+0xFE) = *(u16*)(p+0x100) = v; /* emits sh 0x100, sh 0xFE, sh 0xFC */
Writing the chain in visual store order emits the reverse and mis-schedules. func_801810B0
(ov_SC04_000) states it from the other end: write 0x18 = … = 0x1A = … = 0x1C = 1 so gcc emits the
lowest offset last, with that store riding the jal delay slot.
Two more chained-assignment facts: narrow = wide = v pairs (func_801A3A6C, func_801A39D0) need
the wide store as the INNER assignment so its lw is issued before the component lh and the
loaded halfword stays live in $v0 for the outer sh — narrow-first produced lhu reloads (+4 ins);
and a triple byte store whose RHS is (byte) - 2 must be one chained expression, because three
separate statements reload the byte for the second store (func_80182F04).
§208 addendum (P31 S58b) — IT SCALES TO SIX SITES, AND IT HAS AN EXACT INVERSE
Scale. §208's examples split two reload sites. func_801865A4 (ov_SC02_028, 49/49) needed six
locals for six reload runs of *(a0+0x20): one reassigned pointer let local-alloc coalesce all six
onto one long-lived pseudo coloured $a1, while the target refetches per store-separated run into
short temps that recycle $v1 and leave $a0 free for the 0x7FFFFFFF mask. func_80185028
(ov_SC03_028) is the two-site version stated as a positive recipe. func_801818F8 (ov_SC02_016,
124/124) generalises the mechanism: splitting temps per live value fixes a REGALLOC-PERM without any
pin — reusing one temp across two independent $v0-chains is what flips the colouring.
The inverse — naming can cost you. Four cards where the fewer-names direction was correct:
func_801804F8 (naming ONE local for the first deref makes cse turn the second reload into a bare
register copy, which both deletes a redundant load and leaves the two nop load-delay slots unfilled
exactly as the target has them); func_801801AC (ov_SC03_029 — writing the pointer expression
inline three times makes gcc cse it into $a0 and conservatively RELOAD into $v1 after the
store; a named local put it in $v1 with the copy in $a0); func_80187644 (hoisting a reused
pointer local perturbs regalloc even though the asm reloads it every time); func_80180AEC
(ov_SC05_017 — removing the joined result pseudo, i.e. three separate returns, is what unlocks
direct-to-$v0 address materialisation). func_80182EA8 states the same for returns: three separate
returns beat one shared ret local, which gets allocated $a0 with a trailing move v0,a0.
Companion cards. func_80180D60 (ov_SC03_013): when a tail's register pair is inverted, try
temp-on-array-load before temp-on-field-load — the latter cost +1 ins. func_80183B14
(ov_SC03_006): per-arm block-scoped distinct locals mint independent allocnos where arm-shared
pseudos coalesce to the wrong colour (and note C89 forbids mid-block declarations under this cc1 —
keep else-arm decls at block top). func_8017EDA0 (ov_SC06_016): the single-local-for-both-callee-
returns trap — one ret local for two different calls looks like plain CSE and flips the IV's
register class.
§210 addendum (P31 S58b) — THREE CONFIRMED SPELLINGS OF THE BOOLEAN TAIL
§210's dial is confirmed by two independent cards that arrived at it from opposite spellings, so all three forms are now byte-attested:
| spelling | emits |
|---|---|
return (x & K) != 0; / !!(x & K) / (x&K) ? 1 : 0 / (u32) cast |
srl n ; andi 1 (bit-extract) |
r = x & K; return r != 0; (named-local split) |
andi K ; sltu $zero,r |
if (x & K) return 1; return 0; (control-flow form) |
andi K ; sltu $zero,v |
func_8018723C (ov_SC03_029) found the named-local split; func_80188C18 (ov_SC03_006) found the
control-flow form. The mechanism is mask visibility to fold_truthop/the extract-bitop expander,
controlled by statement boundaries even with no control flow present — i.e. §195-E's value-vs-no-value
axis has this third dial. Neither ternary nor a u32 cast reaches it.
§211 addendum (P31 S58b) — INIT PLACEMENT: FIVE MORE DIALS BEYOND THE GUARD HOIST
§211 hoists the loop init above a dominating guard. The same lever has four other faces and one inverse:
- Comma-init in the
forheader vs hoisted statements decides the$v0/$v1pair.func_8018AA88(ov_SC02_005):for (i = 11, ptr = &D_x; …)gives counter=$v0/pointer=$v1(REVERSED vs target); hoisting both inits into preceding statements (or ado{}while(--i>=0)) yields the target's counter=$v1/pointer=$v0. Local declaration order between the two is irrelevant — tested both ways. The determinant is init-statement placement. - But comma-init is sometimes required.
func_801871E4(ov_SC02_005):for (v = -1, i = 0; …)landsaddiu $a0,$zero,-1andaddu $v1,$zero,$zeroadjacently; a separatei = 0;statement got duplicated by the scheduler into both branch delay slots.func_801826C8(ov_SC05_001) needs the binding in the for-init comma list plus a$18pin; §2985's LUID-ordering law supplied the missing half.func_80180518(ov_SC03_013) needsfor (i = 0, p = …)to emitaddu $s0,zero,zerofirst. i = 0;BEFORE the address materialisation is a §47 live-length slider.func_8017FE60(ov_SC03_029): hoistingi = 0;above thelui/addiulengthensi's live range so its allocno density drops below the three pointers — flipping the whole$spermutation to the target's[tbl→s0, tbl2→s1, a0→s2, i→s3]in one edit (near-25 → near-2).func_8017F2A0andfunc_80189EB0are the same lever;func_801824E4is it applied to the counter (splittingi = 0;out of the for-init so gcc allocates the counter first,$s1, leaving$s0for the cursor).- Declaration order of two independent inits IS the scheduler's emission order.
func_801AAA6C(md_SC07_004): the target initialisesjbeforeyOff; loop-header initialisers emitted them backwards, textual declaration in that order at block top reproduced the schedule. - The inverse: a loop-invariant constant floats ABOVE an address materialisation unless you give it
a dependence.
func_801ACD4C(md_SC07_004):i = 0had to be ordered behind a load from that address (first = base[0];placed beforei = 0;);__asm__ volatile("")over-pinned and draggedsw $radown; an initialiser-listi=0did nothing.
And the loop-form/increment dials. The counter type is observable: u32 gives sltiu, s32 gives
slti (func_80181F78); an s16 counter sign-extends (sll/sra) where the target counts in a full
register (func_801847B0). i != -1 is not interchangeable with i >= 0: only the equality form
produces the target's register-held comparator addiu $a3,$zero,-1 + bne $v1,$a3 — --i >= 0 and
i >= 0 give bgez and no $a3 at all (func_800D2AD8, md_MAIN_003). The increment's clause
matters: i++, p += stride in the for-increment flips the addiu order vs putting p += stride in
the body (func_80184738, func_801847B0) — and func_8017FB78 needs the opposite (increment in the
body, or the addiu $s0 reorders past the branch delay slot), so A/B it. An increment written as
the last statement of the body puts its addiu in the bnez delay slot (func_80183478); a
counter write-back placed after the body's stores emits last (func_801F2868); and a second loop needs
a distinct counter variable or cse hoists the bound test out (func_8017DC50, func_8017E3AC,
func_8017F308).
§213 addendum (P31 S58b) — THREE MORE PERMUTATION LAWS FOR INDEPENDENT SAME-BASE STORES
§213 establishes that emission order is a permutation of source order and not always the identity. The permutations now have names:
- THE RIGHT-ROTATE-BY-ONE.
func_80182FAC(ov_SC02_005, 24 ins): with source order[0xE, 6, 0xA]gcc emitted[0xA, 0xE, 6]; shifting the source one slot left to[6, 0xA, 0xE]rotated emission to the target's[0xE, 6, 0xA]. For independent same-base stores, emission order is source order right-rotated by one. (single observation of the rotation as a rule — but a very cheap probe: if your three stores are a cyclic rotation of the target's, rotate the source.) - GCC COMMUTES ADJACENT SAME-SHAPE STORES, SO SOURCE MUST BE THE OPPOSITE OF ASM.
func_8017F614(ov_SC04_011, 31 ins): the target emits+0x10, +0x18, +0x14; writing that order makes gcc commute statements 2 and 3 back to0x10, 0x14, 0x18(4 offset mismatches). The matching source order is plain ascending0x10, 0x14, 0x18, which gcc then permutes into the target's non-monotone emission. The cookbook documents "asm order == source order"; this is its inverse case. - EXPLICIT-INITIALISER STORES FOLLOW DECLARATION ORDER ACROSS TWO ADJACENT ARRAYS.
func_8017FDDC(ov_SC06_030, 48/48): four lonesh $zeroat0x10/0x12/0x14/0x1Aare not four scalar locals — they are the interleaved initialisers of two adjacents16[3]arrays (pos@0x10,vel@0x18), and gcc emits them in declaration order across both (pos[0], vel[1], pos[1], pos[2]), reproducing the odd0x10→0x1A→0x12→0x14order. Six separates16scalars emit in source order and cannot.
And the read that saves the probe: a non-monotonic offset sequence in the target is
order-faithful — gcc preserved source order, so copy the odd order verbatim rather than "cleaning up"
to ascending (func_80181AD4 0xC,0x14,0x20,0x22,0x30,0x24,0x2E,0x32,0x9C,0xA0; func_801853D4
0x14,0x18,0x10,0x44,0x48,0x4C; func_800CBAAC 0x10,0x18,0x14; func_80187050
0x1A,0x20,0x1E,0x1C; func_80180030 0x27,0x1A,0x18,0x26,0x25,0x24). Descending runs are equally
real (func_8017F53C: far offset +8 before near +6; func_801826B4: 0xC, 0x8, 0x4 to steer which
store falls into the first jal delay slot; func_80183FA8: 0x14, 0x12, 0x10).
§214 addendum (P31 S58b) — FOUR MORE WAYS A HIGH-SIMILARITY TWIN LIES
§214 lists six. Add:
- THE PARAMETER SLOT SHIFTS BETWEEN FAMILY MEMBERS.
func_80181140(ov_SC07_007): the twin reads its struct pointer from$a0; this target reads it from$a1(addu $s1,$a1,$zero), i.e. the SC07 variant takes two parameters and ignores the first. Only the.sdisambiguates.func_8017E278(ov_SC07_001) is the same trap found via a REGALLOC-PERM/$a0>$a1diff. - REGISTER ROLES ARE NOT PORTABLE EVEN AT HIGH SIMILARITY.
func_8017DF50(ov_SC06_025): the twin holdsarg0in$16and the sub-pointer in$17; this target has them swapped, so the declaration order of the two pins matters.func_80183B14(ov_SC03_006) is the sharper case: a similarity-1.0 twin's register assignment is actively misleading, because one extra tail store lengthens the if-arm live ranges enough to flip gcc's priority race. Copy the twin's SHAPE and expect to re-derive its COLOURS.func_801F14A8gives the heuristic for which variable to pin: pin the one whose register the TAIL instruction reuses, not the one the twin pinned. - ±ONE CALLEE, ±ONE STATEMENT, AT SIMILARITY 1.0.
func_8017D9E0(twin has an extrafunc_800D0C48(1)between two calls — dropping it closed 31→29);func_8017F66C(the twin lacks a lone global zero-store in the preamble, worth 2 instructions);func_8018810C(the twin branches on a gate result this target discards, at similarity 0.94);func_801A43EC(a ≥0.9 twin that is the semantic inverse — destroy-vs-create — caught only by the delay-slot/register trace). Diff the instruction COUNT against the twin's body before transcribing. - TWIN LITERALS ARE NEVER PORTABLE — and that includes encodings.
func_80180864(ov_SC06_010): the only residual across 74 instructions was-1where the twin had-0x1001.func_8018855C:0xFFF20000vs the twin's0xFFF00000. When similarity is 1.0 and the gate still rejects, diff the twin's asm ENCODINGS, not its C text.
And the retrieval note, repeated by ~25 cards: a "banked twin" frequently resolves to a
DEFINE_<sym>() macro in src/shared/engine_core.h rather than to C in the twin's own TU — sometimes
via a two-hop chain (twin's TU holds only a stub; the macro holds the body; the family member you
actually need is a different DEFINE_ in the same header, as on func_80181384). Grep
src/shared/engine_core.h for DEFINE_<name>() before concluding a twin is shape-only. A twin whose
.s was deleted at bank time leaves its C as the only artefact (func_801880A8), and
func_80182A08 shows the useful converse: feeding a banked twin's C through match_one with
relocations masked measures its construct's true frame size, cleanly separating "inherited
skeleton" from "this function's own +8".
§215 addendum (P31 S58b) — PIN ECONOMY, PART 2: NINE REFINEMENTS
- PIN ONLY ONE OF TWO SWAPPED REGISTERS.
func_80181200(ov_SC02_027): pinning both fixed the names but distorted sched1 (alhuhoisted above a flag store). Pinning one (t8C→$3) let the other first-fit$4naturally and kept both allocation and schedule.func_8017ED9C(ov_SC03_013) is the same finding on a prologue-save-order residual: pin only the value the target puts in$s1and leave thea0-copy unpinned. - PIN THE TAIL WEB, NOT THE SAVED VALUE — the direction is inverted from intuition.
func_80188104(ov_SC02_017): pinning the saved value to$16gaveparam_1its own callee-saved home (+3 ins); pinning the tail web to$4forcedparam_1to survive the call elsewhere (it stays in$s0, so the contested early loads read$s0) and materialised the target's delay-slotaddu $a0,$s0,$zero. - PIN THE CONSTANT INTERLOPER (§176-B2), including a bare
luimask.func_8018CAF0(ov_SC04_011):register s32 mask __asm__("$3")on the0x80000000OR-constant, statement placed before the pointer load.func_80183A70pins the0x7FFFFFFFconstant to$4(it dies at theand, before bothjals, so no marshalling collision) — and the byte evidence for the right colour came from greppingasm/for the literal in a matched relative, not from any section. - THE PIN MUST BE ON A DECLARATION, NOT AN INIT-EXPRESSION LOCAL.
func_80185B78(ov_SC01_084):register s8 *s0 __asm__("$16")as a pinned declaration changes the allocator's pseudo order; the same value produced by an init expression did not. - TWO PINS OF THE SAME HARD REGISTER COEXIST IF THEIR LIVE RANGES ARE DISJOINT BLOCKS.
func_80181894(ov_SC01_005, 155/155): two block-scopedregister s32 x __asm__("$2")declarations, one per consuming block. A function-scope$2pin starves the tail temp into$4and produces an unfixable mirror.func_8017F33C(ov_SC02_016) uses the same multi-pseudo trick to steer which pseudo gets$2vs$v1. - "PIN vs PLAIN" IS ITSELF A LEVER — A/B IT BEFORE FIGHTING WEAVE ORDER INSIDE EITHER.
func_80182A30(ov_SC06_000): under pins, statement order controlled which pairs permuted but never reached the target; under plain locals the same statement order became correct on the first try, because RA grants$s0/$s1/$s2naturally when the loop body has no arg conflict.func_8018C88C(ov_SC06_018) is the strongest form: dropping a$16pin is what forces the two fresh$a0materialisations the target has — an UNpinned base local reproduces per-call$a0copies, the opposite of the usual advice. - A PINNED VALUE THAT IS LIVE ACROSS ITS OWN CALL FORCES THE ARG SETUP TO BE RECOMPUTED.
func_801AB5D4(md_SC07_004): the duplicateandi $a0,$v0,0xFFabove thejaldisappeared once the post-call consumer store was hoisted above the call, ending the pinned pseudo's live range at the call so local-alloc/sched put the singleandiin the delay slot. None of §162d1/§165-19/§167-08 cover this "pin dead at call" inversion. - PINNING A GLOBAL-LOAD VALUE (not a parameter) FORCES PER-CALL ARG COPIES.
func_80186710(ov_SC04_011): §163d copy-elision deleted the second argument copy until a$16pin restored the value's$s0residency — and with it thesw/lw $s0frame.func_801AA2F8pins a base to$2because the target materialises it into$v0and then copies it to a saved register. - §176-D's PIN REGISTER MUST BE RE-DERIVED PER FUNCTION.
func_801880E8(ov_SC03_091): the template's"$6"was the entire head-crack — squatting on$a2forced a pointer load into$a1, killed arg1 at entry, dragged a copy into the prologue and left abeqzslot empty. Moving the pin to"$5"(the target's actual copy base) fixed prologue order, delay-slot fill and frame layout in one edit.
And §17's "pin every call-crossing value" is over-broad in one more direction: func_8018130C
(ov_SC03_124) pinned the two values that genuinely cross jal boundaries and left the mult-chain temps
free — pinning those too was actively harmful (near-47 LENGTH-DRIFT, broken negu/mflo
scheduling).
§217 CROSS-CONFIRMED (P31 S58b) — AND THE INCOMING HOME SLOT IS THE MIRROR
§217 was banked as a single observation. It is now confirmed by four independent cards:
func_8017C624 (ov_SC06_000 — sw at 0x10/0x14/0x18 are outgoing params 5/6/7 of a 7-argument
call; the addu $a2,$zero,$zero that looks like value shuffling is param 3's zero materialised into
its ABI register), func_8017DBD8 (ov_SC02_031 — four sw at 0x10–0x1C are params 5–8, matching a
fleet-wide banked call idiom), func_8017FF68 (ov_SC07_010 — sw $v0,0x10($sp) in the jal delay
slot is outgoing param 5), and func_800D2454 (md_MAIN_003 — the frame holds no locals; sw $zero,0x10($sp) is a 5th stack argument, per §204-B's trigger shape).
THE MIRROR, new: an incoming stack argument is read from the caller's home slot before the
sw $ra prologue. func_8017E230 (ov_SC05_008): lw $t0, 0x68($sp) issued above sw $ra, i.e. above
the 0x58 frame, kept live in $t0 across the whole body and dereferenced late — that is a fifth
pointer parameter, and missing it skewed every downstream regalloc choice (one root cause, seven
diff lines). func_8017FA94 (ov_SC07_010) found the same fourth parameter by noticing an lw reading
raw $a3.
§220 addendum (P31 S58b) — THE PARAMETER, NOT A COPY (SEVEN CARDS)
§220 says the parameter is itself a regalloc dial. The strongest form of the law: a redundant pointer local costs you a second callee-saved register under -O2.
func_80180508(ov_SC06_030):int param_1+ an explicits0copy allocated two callee-saved regs (param→$s2, copy→$s0) and drifted to 63 ins. Declaring the parametervoid *a0and using it directly with casts — this TU's house style — closed it.func_8017F188(ov_SC05_018): a separates32 s0 = a0;copy forces$s1plus a save/restore pair; gcc assigns the long-lived parameter pseudo directly to$s0when the parameter itself is used everywhere. (Andregister __asm__pins are syntactically invalid on parameters here — §257-3.)func_801877FC,func_80180360(ov_SC06_011 — avoid *s0 = a0local cost an extra callee-saved reg and a 24-byte frame),func_801811F4(ov_SC02_028 — a named local spilled an extra s-register, 31 vs 28),func_8018CEB0(ov_SC02_011 — passing the raw incoming$4rather than a copied local is what yields "no arg materialisation" in a delay slot),func_801831E4(walk the raw parameter register:param_1 = (s32*)((s32)param_1 + 0xCC)thenparam_1++keeps the whole giv chain on$a0so the counter lands in$a1; a fresh local pushed the counter to$a2).
AND THE INVERSE, equally attested: where the target does keep a callee-saved copy, an explicit
named local is the reliable way to pin it (func_8017F390, ov_SC04_005 — s0 = (s32)arg0 once, then
plain arithmetic off s0, emits addu $s0,$a0,$zero in the jal delay slot; func_80182408,
func_80184C20, func_801818E4). And func_801801C0 (ov_SC05_003) shows the third face: reading a
field off the raw a0 rather than the s0 copy defeats gcc's CSE of two identical loads and
keeps the load below the call where the target has it.
§222 addendum (P31 S58b) — IF-CHAIN vs SWITCH: THREE MORE DISCRIMINATORS
- A TWO-WAY
u16DISPATCH WITH A SHARED POST-BLOCK COMPILES ASswitch. Confirmed four more times:func_80181968(ov_SC04_004 — if/else-if hoisted theadduinto delay slots and dropped thej, −2 ins);func_8017EAE8(ov_SC03_002 — switch givesbeqz ; li $v0,1 ; beq, the if-chain givesbnez-first, 17 ins apart);func_8017E684(ov_SC05_003 — a two-arm if/else-if on a singleintlowers as a switch-shaped head:beqz→arm0 ; li $v0,1 ; beq→arm1 ; j end);func_80180F90(ov_SC03_011 — switch gives the balanced treebeq ==1 → slti <2 → beq ==0 → beq ==2, if/else tests==0first and inlines case 1's body at the head, 55 ins). - AND SWITCH HAS SIDE EFFECTS OUTSIDE THE DISPATCH.
func_80181F78andfunc_80182720: a switch can rotate the registers of an upstream loop or change a value's lifetime across a call. When it does, §256's goto-in-block-order form is the escape. - CASE BODIES ARE EMITTED IN DFS ORDER; PHYSICAL ARM ORDER DOES NOT NAME CASE VALUES. See §255.
Also: a dense switch may need the shared increment duplicated inline into each case rather than
hoisted to one tail —
func_80180DB4(ov_SC07_000) andfunc_8017EEE0both lost ±1–3 ins (and ~25 near-diff instructions on the seed) to a single-tail form; the duplicatedlhu-reload-per-case is the gcc signature distinguishing the two sibling shapes.
§223 addendum (P31 S58b) — FIVE MORE CONFIRMATIONS, AND THE CONSTANT-IN-$v0 CASE
§223's rule (a value in a jal delay slot was produced BEFORE the call, so it is never that call's
return) is now the most-confirmed law in this harvest — roughly thirty cards. Two sharpenings:
A. THE VALUE MAY BE A CONSTANT LOADED FOR THE STORE, NOT A RESULT AT ALL. func_80184524
(ov_SC06_000): sw $v0,0x1C($s0) in the FIRST jal's delay slot stores the pre-call constant 4;
drafting x = func_8012B200(...) mis-schedules. func_801803F0 (ov_SC07_000): sw $v0,0x34($s0) in
rand's own delay slot stores the 0x4000 that the beqz delay slot had just placed. func_8018205C
(ov_SC03_094) needed register s32 v0 __asm__("$2") + a cast-call to keep the constant 0x18 live
across the jal so the store could sink into the slot — plain C cannot express it.
func_801E5748, func_80184034, func_80188C18, func_8017FD44, func_80186DA0, func_8018323C
all repeat the shape. func_80186DA0's version is the sharpest read: the $v0 in the slot is the
return of the PRECEDING call.
B. A STORE IN A DELAY SLOT IS EVIDENCE FOR SOURCE ORDER, because gcc never sinks a store past a
call. So "store visible after the jal in the target" ⇒ "store written BEFORE the call in the
source" (func_80186838 — and there gcc-2.7.2's reorg redundant_insn copies the insn immediately
preceding the jal into the slot; func_8018B900, func_8017FB80, func_80182B4C, func_80186E60,
func_801877FC, func_80187B0C, func_801AC47C, func_80180C34, func_8018876C, func_80189C90).
The corollary saves a compile: a value consumed only after a call but STORED in that call's delay
slot requires moving the store before the call, so the value dies in a call-clobbered register rather
than being promoted to a callee-saved one.
§224 addendum (P31 S58b) — CROSS-JUMP MERGES CALLS, AND THE DELAY SLOT IS THE DISCRIMINATOR
- CROSS-JUMP MERGES DUPLICATED CALLS, NOT ONLY TAILS — so "one
jalsite in the asm" cannot distinguish a ternary-selected argument from an if/else with two duplicate calls.func_8017EDAC(ov_SC03_023):(rand()&1) ? &BFC : &C24producedbeqz-with-nopand drifted −2; two explicit call arms letjump2cross-jump into the exact target shape. The discriminator is the delay-slot occupancy of the branch feeding the merge point (func_8018C3C0: a sharedaddu $a0,$s0,$zeroin thebeqzdelay slot, executed on both paths, is the signature of cross-jumped duplicate calls, not a scheduling pin). Alsofunc_801832B4,func_801866C8,func_8018B684,func_80185CA4,func_80180B4C(where only the goto/label spelling keeps two separate arg setups, each sinking into its own branch's delay slot — nested if/else merges the identical arms). - §164-69's DOSE-2 NEEDS EACH COPY TO TERMINATE ITS OWN PATH WITH AN EXPLICIT
return.func_80181D14(ov_SC02_027): §164-69's exemplar merges three arms through aj-to-jjoin; here the second predecessor falls through to the mergedjal(§162h minimum=1 path), and the goto-shared-label form never reproduces the argument hoist. Six earlier attempts stayed near-2. - AN INSTRUCTION IN A MERGED-
jalDELAY SLOT PINS A PRE-CALL STATEMENT IN THAT ARM.func_801832B4(ov_SC03_007): the else-arm'ssh $zero,0x34($s0)sits in the SHARED jal's delay slot, so it must be emitted before the call in that arm's source order — copying a neighbour's visual order (call first) cost near-6. - THE GOTO TARGET MUST BE THE INNERMOST ELSE-ARM LABEL, NOT A POST-
ifJOIN.func_801853F0(ov_SC06_000): placing the shared call site after the if/else chain re-lengthened the epilogue by onejand broke theaddiu $s1/sltiordering. Same law from the other side onfunc_80189238: thefail:label must live inside the==0arm, not be a shared out-of-line label. - STORE DUPLICATION CAN FORCE THE TAIL MERGE.
func_8018406C(ov_SC03_089): duplicating the*(s16*)(arg0+2)store inside both arms makes gcc tail-merge the commonshpast the branch, producingj-over-else withli v0,1in the delay slot; hoisting the store or threading one variable givesli a0arms and no jump (−2).func_801AA91C(§162h) andfunc_8017EE6Care the same lever;func_801A13A0is its regalloc face (per-arm literal stores free the next statement's arg-setup slot).
§225 addendum (P31 S58b) — THE GUARD-CLAUSE FINGERPRINT, AND THREE MORE SHAPES
- A
bnez-FORWARD WITH THE TEARDOWN ON THE FALL-THROUGH IS A GUARD CLAUSE.func_80184A78(ov_SC03_024, 34 ins): draft 1 was structurally perfect asif/elseon the decremented counter and still 15 instructions off.if (*counter == 0) { teardown; return; }reproduces the exact polarity and layout.func_8017ECA8states the general triage: when the diff shows BRANCH-POLARITY / DELAY-SLOT classes on an if/else skeleton, try inverting to guard-clause / early-return form before touching pins. - A
jIN LIEU OF A BRANCH AT A BLOCK END IS THE FINGERPRINT OF AN EARLY RETURN, NOT ANelse.func_80186C98(ov_SC03_028) — already cited in §225-2, now the primary read for a whole family (func_80181824ov_SC03_030: inverting toif (p == 0) { return 0; }took near-120 → near-10 and fixed the epilogue, branch polarity and scheduler context at once). - NESTED IFs vs A FLAT
||DECIDE WHETHERaddu $v0,$zero,$zeroIS MATERIALISED PER EDGE.func_8017D68C(ov_SC05_007): gcc-2.7.2 materialises the zero once per guard edge only in the nested form; the flat||gives a single shared exit block and the wrong length. The complementary card isfunc_80184FC4(already §225-3): the early-return ladder foldsv0 = 0into the failing compares' delay slots where the&&chain allocates a boolean accumulator. - A CONSTANT DUPLICATED IN A DELAY SLOT AND AT THE HEAD OF THE FALL-THROUGH BLOCK IS AN
UNCONDITIONAL STORE, NOT A TERNARY.
func_801894B0(ov_SC03_006, 11/11):addiu v0,-0x30twice, byte-identical, is dbr sinking a copy of the store value into the slot while keeping the original. §164-57 documents that shape as a ternary collapse; here there is no ternary — the correct source has no cross-branch variable at all (*(s16*)(a0+0x52) = -0x30;after theif).func_80185338(ov_SC02_017) is the same finding stated as jump.c's single-set-of-one-pseudo inversion, andfunc_8017E2C4is its inverse-polarity twin (there hoisting the single assignment wins and duplicating loses the delay-slot placement). Check whether the two constants are byte-identical before assuming a per-arm value.
§226 addendum (P31 S58b) — THE FRAME CATALOGUE: SEVEN MORE LEVERS, AND SLOT ORDER IS DECLARATION ORDER
SLOT ORDER. Declaration order is slot order, lowest address first: declaring u32 arr[6]; then
u8 buf[32]; puts arr at sp+0x30 and buf at sp+0x48 (func_8017FA94, ov_SC07_010 — whose own
note calls this "reverse declaration order", which is the same fact read from the top of the frame);
s32 pad[4] declared first forces mat to 0x20 and grows the frame by exactly 0x10
(func_80187240, func_80181EC0); three locals land at 0x10/0x14/0x18 in decl order i/ptr/a0
(func_80182A30); declaration order of three blocks fixes which sp slot each gets, and the third
call may use the middle-declared buffer (func_8017F53C).
SEVEN PAD FORMS, all byte-proven, in increasing invasiveness.
s32 pad[N≥2](§162i1 floor) — the default.s32 frame_pad[1]; (void)&frame_pad;— an address-taken single-element pad reserves 8 bytes where §162i1's floor says it should be trimmed (func_801EFA60md_SC03_076,func_801EDF80md_SC05_026,func_801AB54Cmd_SC07_004 — where the seed's only fatal omission was the missing(void)&frame_pad;). This bounds §162i1's ≥2-element rule: address-taking substitutes for size.volatile s32 hole[4]— reserves a region the target leaves empty without emitting code (func_801880E8);s32 frame_pad[8] + (void)&frame_padfor 0x20 (func_8017FABC).- Declaration-only
volatile s32 pad;— where a plain unused local is DCE'd at -O2 (func_801828E0); two of them for an 8-byte area (func_801814AC). - A DEAD ARRAY ELEMENT where a separate pad array would be trimmed (§226-2, re-confirmed) — and
its cousin, an oversize address-taken array:
s16 sp10[8]with only[0..2]written (func_801AC9CC),s16 buf[12]with only[0..5]written (func_80182EB0ov_SC01_001, where 6 elements give-0x40and 16 give-0x50; only 12 reproduce-0x48). - A PADDED TAIL STRUCT MEMBER — grow the real aggregate rather than adding one (
func_8017E744ov_SC03_010: two never-touchedu32fields;func_8018562C: packing matrix+temps into ONE struct so gcc lays them back-to-back; andfunc_8018562C's corollary that struct-tail padding absorbs scalar stores and turnsshintosw). - AN UNREFERENCED
f64WHOSE ADDRESS IS CONSUMED BY AN EMPTYasm volatile"m" OPERAND — forces one 8-byte slot before the callee-save spill area, which no pad array reaches (func_8018A688, ov_SC06_018, found by bisecting a LENGTH-DRIFT; banked twice).
AND FOUR MORE NON-PAD FRAME CAUSES.
- Frame position, not size:
u8 dead[0x20]declared beforechar buf[0x20]movesbuftosp+0x38with no other instruction change (func_800CD508, md_MAIN_044). §279 covers uniform offset shifts, not placement. - The frame is dictated by the SAVE-slot layout, so count backwards from the saves rather than
forwards from the live locals (
func_80182EB0,func_801A1924— where the twin'su16 sp38[8]would have been copied and cost +2). - Real narrow locals already reserve
var_size— two halfword locals alone giveaddiu sp,-8(§226-1, andfunc_801AC940, where the §193-I pad trick pushedrato0x20and was wrong). - Tail-only padding works like head padding (
func_8017FE9C: no head pad, 8 bytes after the last local); and an address-taken dead third local cached in$s0across three calls is a register lever that happens to change the size (§226-3, re-confirmed onfunc_801AA210).
§229 addendum (P31 S58b) — NAME IT INSIDE THE ARM
§229's five exemplars all name the address in a top-of-function initialiser. func_8017EB08
(ov_SC03_105, 40/40): the named local must be born inside the if-body; declaring it at function
top hoists the lui/addiu above the branch and mismatches (the target's pair sits after the bne,
before the lhu). func_801EDC70 (md_SC05_026) is the strongest form: a post-call cached-pointer
increment must be a fresh block-scoped pointer declared after the call, or &D_x is live across the
jal and gcc adds an $s0 save/restore even though the C semantics are identical.
func_8017F0B0 (ov_SC03_031) states the same as "declare the pointer after the first use site" —
declaring it before an intervening store sank the lui/addiu below that store.
§230 CROSS-CONFIRMED (P31 S58b)
§230 was banked as a single observation. func_80188904 (ov_SC06_029, 23 ins) confirms it
independently: with HI16/LO16 masked, the surviving addiu offsets (-8/-0x28/-0x20 vs the
target's +8/-0x20/-0x18) decoded to which assignment gcc anchored on — it had anchored at base+8
because the +8 assignment was written first. Moving the plain-base assignment to statement 1 flipped
all three offsets at once. Read the masked-out addiu deltas as an anchor probe rather than reaching
for struct/layout levers.
§231 addendum (P31 S58b) — FOUR MORE WAYS THE LISTING MISLEADS
- REBUILD
K = (hi << 16) | loFROM THE MASKS; NEVER TRUST THE VISUAL FRAGMENT ORDER.func_80181960(ov_SC07_007): reassembling0x000F0140by concatenating rendered fragments of two different constants (thelui 0xF0belonged to0x00F00140). Same class:func_801816A8(0x0C000040renders 7-digit as(0xC000040>>16)and reads like0xC0000040);func_80185520(0x202020, not0x20202020— read thelui/oripair, do not assume a 4-byte pattern);func_80180E3C(-786432vs-49152, a dropped hex digit). - THE
(K >> 16)RENDERING IS THE GROUND-TRUTH VALUE OF THE HIGH HALF.func_80180A50(ov_SC06_000): the target stores0x00A00000, and the raw wordA000033Cinvites0xA0000000. Sign-extension intuition from the raw hex lies here.func_80185FB4decodes3C020020/3C020040as0x200000/0x400000(bits 21/22) — not0x20000000/0x40000000; a first draft with the bit-31 guesses mismatched exactly those twoluis. - WHEN THE TEXT AND THE HEX COLUMNS DISAGREE, DECODE THE HEX.
func_801AA4EC(md_SC07_004): the renderedaddu $a1,$s0,$zerodecodes from0x02280021correctly, but a neighbouring line's text was wrong for its own word — and the correct decode changed the argument mapping of both calls. - BASE-RELATIVE OFFSETS: A HOISTED
p + KRE-BASES EVERY OFFSET IN THE LOOP BODY.func_80184738(ov_SC02_011): gcc hoistsv1 = p + 0x70, sosh …,0x8C($v1)is really*(s16*)(p+0xFC). A first draft wrotep+0x8Cand diffed on exactly that instruction. Two adjacent reading traps: a store's base register may belong to a pointer loaded from a field rather than to the argument (func_80181770: the0x20belongs to*(a0+0x20), not toa0), andlw $a1,0xCC($s0)passes the pointer stored at 0xCC, not&entity+0xCC(func_801861A4). - SCAN BACKWARDS THROUGH EARLIER BRANCH-SHADOW SLOTS BEFORE MODELLING A "BARE" CALL.
func_80180360(ov_SC06_011):jal func_80143970has no adjacent$a0setup — its delay slot holds an unrelated store — because gcc hoisted theaddu $a0,$s0,$zerointo the FIRSTbeqz's delay slot, five instructions earlier. Two drafts built a §161c loose-prototype call instead (−1). - THE COMPARE CONSTANT ALWAYS LANDS IN THE BRANCH'S DELAY SLOT, NEVER THE CALL'S.
func_8017FCA4(ov_SC05_005): when both ajal's and abnez's delay slots carry constants, the one in the branch slot is the compare operand — which settles which arm of a guarded head-block is the "then".
§232 CROSS-CONFIRMED (P31 S58b)
§232 was banked as a single observation. func_801A7CDC (md_SC07_004, 15/15) confirms it: the target's
tail is addiu v0,5 ; jr ; sh v0,2(a0), and every naive spelling (store; return 5,
store-equals-return, pinned temp — all byte-identical to each other) materialises the constant twice,
with the dead second li v0,5 stealing the jr delay slot. The §30#3 anonymous re-tie
__asm__("" : "=r"(r) : "0"(5)) forces ONE real pseudo set shared by the store and the return; the
return copy becomes an identity and dbr fills the slot with the sh. Refuted on the same card:
pinning r to $2 with register __asm__ removes $2 from the scratch pool and perturbs allocation
for the whole body (near-14 WIDTH cascade).
§259 — THE DISCARD LEDGER FOR THE aa–bg HARVEST (P31 S58b): WHAT WAS MINED AND REJECTED, AND WHY
Roughly 1,040 of the 1,101 candidates did not become sections. Recording the big rejected clusters so nobody re-mines them. If a future harvest surfaces one of these again, it is confirmation, not news — add a card reference to the cited section rather than a new one.
| # | cluster | ≈cards | why not a section |
|---|---|---|---|
| 1 | "Nothing the cookbook did not already cover" — self-reported no-gap | ~700 | The flywheel working as designed. Almost all resolve to §193-A (twin-first), §194-E (neighbour-in-own-TU), §195-A (signature rows), §48-C2/§160a (block copies), §167-25/§167-39 (divide reads), §88b (literal position), §3-T4 (branch polarity). |
| 2 | "The banked twin gave shape, all literals came from my own .s" |
~120 | This is law 2, already stated. It recurs on every seeded-crack card and needs no new entry. The exceptions (parameter-slot shifts, register roles, ±one callee) are folded into the §214 addendum. |
| 3 | Per-card symbol audits ("all N symbols verified against the target's own relocation lines") | ~400 | Law 1c compliance reports, not idioms. Valuable as evidence that the discipline holds; zero transferable content. |
| 4 | Post-hoc "if the gate still rejects, the fault is outside this body" | ~60 | Honest, but a null result about the harness, not a codegen law. The actionable half — the nine decl-layer classes — is §236. |
| 5 | "The Ghidra seed / leftover shard was a different function; discarded wholesale" | ~55 | Now §238; individual instances add nothing. |
| 6 | Access-width typing (lh⇒s16, lhu⇒u16, sb⇒u8, lw+sh⇒s32 source) |
~90 | §227 and §183 law 2 already own this, including the mixed-signedness-per-use-site case. The genuinely new half was the constant side, which is §234. |
| 7 | "Declared the fresh symbol by access width because no tu row existed" |
~70 | Standard practice per §183/§195-A. The one real law hiding in here — array-vs-scalar decides lui/addiu vs lw — is §250. |
| 8 | House-style adoption ("copied the extern block verbatim from TU line N") | ~80 | §194-E and law 2. The failure modes are §236. |
| 9 | Statement-order observations with no stated mechanism ("moving X before Y fixed it") | ~70 | Kept only where the mechanism generalises (§213 addendum, §223 addendum, §245). Bare "reordering fixed it" notes are unindexable — a future drafter cannot look them up. |
| 10 | Branch-polarity flips reported as one-line fixes | ~65 | §3-T4/§16201/§136d-2 already cover the read. Only the non-obvious polarities were kept (§247). |
| 11 | "match_one compiles standalone, so externs/typedefs must ride along in the snippet" |
~40 | A harness fact, already in §2363/§12. Recorded once in §236-4 for the typedef half. |
| 12 | Frame-size fixes by pad, reported without arithmetic | ~35 | §162i1/§2429/§226 own the lever; only the seven distinct pad forms and the slot-order law were new (§226 addendum). |
| 13 | Delay-slot store readings ("the sh in the jal slot means store-before-call") |
~30 | §223's own law, now heavily cross-confirmed — folded into the §223 addendum rather than re-stated. |
| 14 | GTE / RotTransSV / matrix-macro cards |
~25 | Almost all resolve to §179-D/§195-J plus the TU's own macro text; the one transferable finding (a flat s32 l[16] scratch buffer beats a named struct, 195→107 ins on func_801853D0) is a §176-D2 companion and is cited there in passing, not promoted. |
| 15 | "Similarity 0.5–0.7 twin contributed nothing" | ~50 | Expected behaviour of the metric; §214 already says so. |
| 16 | Single-function offset/literal corrections ("the store is +0x12 not +0x102") | ~45 | Pure per-card reading errors with no rule behind them. |
| 17 | Cast-at-call-site applications | ~55 | §17a-1. Only the boundary (what it cannot fix) and the four escapes were new — §237. |
| 18 | "register __asm__ pin fixed a REGALLOC-PERM" with no stated selection rule |
~40 | §137/§17/§215 own pins. Only the selection refinements are new (§215 addendum), plus the dead ends in §257. |
TWO CLUSTERS DELIBERATELY LEFT AS OPEN PROBLEMS RATHER THAN LAWS (each has ≥2 cards, no working lever, and would be a false law if written up):
- Forcing
own_thread_pon a conditional branch whose successor is a call-bearing join when the argmoveis the only candidate insn.func_80182C78(ov_SC02_027, 20 + 8 oracle calls across two sessions, ~10 distinct spellings). §165-24's ≥2-copy law targets unconditionaljs and does not transfer to abeqz. Untried lever recorded on the card: make the join unreachable-by-fallthrough with an explicit label+goto from the outerif. - Suppressing a second address-giv for a constant
+2offset off an unconditional walked base (§246,func_801AB5D4). Nine drafts, all near-40.
Both are permuter jobs, not source-lever jobs. Do not spend a wave's budget re-deriving them.
§260 — THE §154-A LEADING-ISLAND SPLIT: ONE CONFIG LINE, AND THE ISLAND PEELS FROM THE END (P31 S59, byte-proven)
An md_* module binds .rodata at offset 0 to the same subseg as its code, so the object's rodata
order is the C file's include chain: the INCLUDE_RODATA blobs, then every INCLUDE_ASM'd
function's still-migrated jump table, in source order. That island reproduces byte-exactly while
the functions are stubs, and the moment one is matched its table leaves the chain and cc1 re-emits
it at the end of the object's .rodata — the +8-and-everything-shifts failure P30 S48 measured.
THE WHOLE FIX IS ONE INSERTED CONFIG LINE PLUS AN ISOLATION. On md_SC03_076 /
func_801F218C:
- [0x0, .rodata, md_SC03_076] # leave the island piece ALONE
- [0x268, .rodata, md_SC03_076_jr_801F218C] # the split: the function's own table
- [0x27c, c, md_SC03_076]
- [0x2d24, c, md_SC03_076_jr_801F218C] # jr_isolate_all.py --only
Do not create a _pre piece (a dotted .rodata with no sibling .c points splat's ld at a
never-built implied C file and unbinds the island from its module). Do not touch
ld_interleave — the generated script is already rodata-first in yaml order. Use
jr_isolate_all.py --only, not jr_isolate.py (its backend sys.exits on a top-level extern
block).
THE CONTROL THAT SEPARATES "IT WORKED" FROM "IT DID NOTHING." A green SHA proves nothing on its own here, because a split that silently failed to happen is also green. Pair it with the object-level size:
| whole-binary sha1 | md_SC03_076.o .rodata |
md_SC03_076_jr_801F218C.o .rodata |
|
|---|---|---|---|
| before | 9a165e36… |
0x27c (the whole island) | — |
| after | 9a165e36… |
0x268 | 0x14 (the 5-entry table) |
THE ISLAND IS A STACK. Census of md_SC03_076 (file offsets): D_801EF468 0x000, D_801EF540
0x0D8, then func_801EFBB4 0x144 · func_801F0210 0x1B4 · func_801F0734 0x1EC ·
func_801F0A9C 0x214 · func_801F0F28 0x23C · func_801F218C 0x268 → 0x27C, the island end. Only
the end-adjacent table carves cheaply; each isolation makes the next one end-adjacent, so a
module peels from the end, one function at a time. Picking a middle table first is what makes the
job look like cascading isolation.
TWO TOOL BLINDNESSES THIS EXPOSED, both md_*-only and both now fixed. (1)
jtbl_carve.parse_config took the FIRST data/.rodata piece in the file rather than the trailing
run after the last c — identical on 171 configs, and on the 42 md_* it pointed at the island, so
apply()'s splice DELETED the c line and wrote the yaml to disk before erroring for unrelated
reasons. (2) jr_isolate_all.jr_inventory asserts every .rodata piece resolves to exactly one
banked owner (R32) — true where jtbl_carve created every piece, false for the island, which has no
owner; it aborted with UNOWNED 0x801ef468 and md_* could not be isolated at all. The structural
discriminator, verified over all 213 configs: a .rodata piece at offset 0 whose subseg is the
binary's own alias exists in exactly the 42 md_* and in none of the others.
§260-A — STAGE 2 IS PROVEN, AND THE WHOLE jtbl PIPELINE IS AUTOMATED AT THE GATE (P31 S59, same day)
Three byte-proofs, one per structural class, each carve→draft→bank through
tools/jtbl_lane.py → gate_stage.run_gate → harvest_verify._jtbl_prep_one with the live
campaign running (per-binary flock + exclusive shared lock + the campaign's draw lock held across
gate+commit — gates never overlap):
- tail (ov_SC03_014/func_8017DCC0, 45 ins, commit
commit:2661): the §8b same-subseg ADJACENT MERGE + §8e single-table-predecessor pad recovery, untouched machinery — jr_8017AE2C.o .rodata 0x14→0x28 TIGHT,JTBL_PADS := 0,0 tables=+0x0,+0x14self-derived, shad84b01a2…green. - covered (md_SC03_076/func_801F218C, 83 ins, commit
commit:2663): §260 STAGE 2 — the matched C emits the island table itself; sha9a165e36…IDENTICAL to stage 1, jr .rodata 0x14 now COMPILER-EMITTED, md .rodata 0x268. The first md_ jr function ever banked.*jtbl_carvenow reports this state as a no-op success instead of the historical refusal. - island-end (md_SC03_135/func_801E5358, 83 ins, commit
commit:2664): the FULL split done BY THE GATE on a virgin module — isolate → insert- [0x268, .rodata, <ov>_jr_<ADDR>]→ re-extract → covered no-op → bank; shab901fda5…, md.o 0x27c→0x268 + jr.o 0x14. ~1.1 s wall (an md module is 34 KB; R40-checked against artifact mtimes before believing it).
The joins that made it automatic (each one line to find): jtbl_carve.island_probe — the
READ-ONLY structural classifier (tail / covered / island-end / island-blocked / island-pads /
main-manual) that build_wave_atlas (--levers jtbl-carve only) and jtbl_lane --census route
on; jtbl_carve --island-split — the one-line §260 insert, refusing non-end-adjacent tables;
harvest_verify._ISLAND_WALLS — the prep branch that runs isolate (body spliced, §61b) →
split → extract → re-carve on the §154-A refusal. Census over all 245 jtbl members (R32):
tail 173 / island-end 7 / covered 1 = 181 members, 26,445 ins reachable unattended;
main-manual 47 (parked), island-blocked 10 (stack-ordered, convert as peels land), island-pads 6
(needs §8e pads wired for config/modules.mk — jtbl_carve's mk writers are overlays.mk-only),
no-jtbl 1 (atlas mislabel). ORDER LAW for the island branch: isolate FIRST, insert SECOND —
jr_inventory's 1:1 owner assertion runs inside the isolation, and the fresh line has no owner
until the body is real C. Drafter nuance that cost the only iteration: a callee reached via an
eagerly-filled branch delay slot (a0=s0 serving the ELSE path) reads like an argument —
func_801EF6E4 takes NONE (extern void f(void)), and the one-mismatch signature is a redundant
move a0,s0 in the jal's own slot. Full design + failure semantics:
docs/tool-designs/jtbl-automation-s59.md.
§261 — THE -O0 ORACLE: DERIVE THE OPT LEVEL FROM THE TARGET, AND $fp IS NOT THE TELL (P31 S59)
match_one --o0 existed for a year and nothing ever passed it. api_draft.match_one() — the
oracle every wave agent iterates against — builds a fixed argv without it, so an agent handed an
-O0 target was shown an -O2 compile of its own C and a mismatch on every instruction: feedback
that cannot converge, for a reason that never appears in the diff. It hit even the functions already
sitting in _o0 objects, where a match was otherwise bankable today.
DERIVE IT (R33), FROM TWO ORACLES (R34), BECAUSE NEITHER ALONE COVERS THE TREE:
- the target's own prologue —
sw $fp, N($sp)+addu $fp, $sp, $zero(21F0A003) inside the function's first instructions. gcc-2.7.2 keeps a frame pointer at -O0 and omits it at -O2. - the subseg's build flags —
bootand every*_o0*object are compiled -O0 by the Makefile.boot/start.sis -O0 with no ordinary prologue; only this oracle sees it.
$fp MENTIONS ARE NOT THE POPULATION. $fp is $s8, an ordinary allocatable callee-saved
register at -O2. Over 14,400 .s files: 311 mention $fp, 167 carry the -O0 prologue. Sizing a
lane off the 311 over-counts by 1.9×. Anchor the prologue scan at glabel, not the top of the file
— two md_MAIN_011 targets open with a migrated jump table / .asciz blob, and a naive "first 8
encoded lines" reads table words as the prologue and calls an -O0 function -O2.
A MATCH IS NOT A BANK FOR THIS CLASS. An -O0 function in an -O2 subseg cannot bank however
perfect the body: the object's CC1FLAGS decide, and the Makefile's -O0 globs cover boot,
ov_SC01_077_o0, src/ov_*/ov_*_o0.c and src/ov_*/ov_*_o0?.c — nothing matches src/md_*/.
Of the 167, 51 are inside an -O0 object and 116 (14,148 ins) are stranded in -O2 subsegs, so
match_one now says so on sight rather than letting an agent chase a body that can never land.
§261a — THE -O0 FRAME-RELOAD GRAMMAR: reload COUNT disambiguates the C spelling (P31 S59, byte-proven)
func_80184058 (131 ins, ov_SC03_014_o0c) banked on the FIRST compile — MATCH(131) then
whole-binary BYTE-IDENTICAL — because at -O0 the number of frame reloads of a pointer is a
fingerprint of the exact C spelling. Read it off the target before writing anything:
p->f += k(value unused) → 2 reloads ofp(STORE base first, read base second), no copy.--p->f/++p->f→ 3 reloads (the FIRST is dead), anaddu rd,rs,$zerocopy after the add, and — only when the value is used —sll 16; sra 16on the STORED register (no re-load). Soif (--p->f == 0)vsp->f -= 1; if (p->f == 0)(re-loads the field) vsp->f -= 1;(2 reloads, no copy) are all distinguishable from the asm alone.- s16 fields:
lhat a plain read, butlhuinside a read-modify-write. - Overlapping s32/s16 (a 16.16 accumulator at 0xE0 whose high half is read as the s16 at 0xE2):
spell the wide access through a SECOND struct view and cast the pointer —
((Ent_fx *)p)->unkE0 += 0x5CCCC;— the cast is free at -O0; no union needed. - §127a compounded: the decisive spellings (the call-arg marshaling,
++p->unk02) were lifted VERBATIM from banked siblings in the same_o0*TU. Read the TU's banked bodies first, always.
With the §261 auto-oracle + this grammar, an -O0 function is STRICTLY easier than -O2 of the same size: no scheduling, no regalloc steering, no permuter — every miss is a spelling miss, and the spelling is enumerable from the reload count.
§262 — A LANE'S YIELD IS ONLY A LANE FACT IF IT IS SIZE-MATCHED (P31 S59)
The tells lane was removed from the drafting rotation on four waves that gated 3–6 of ~56 drafts.
Two confounds, both measurable from artefacts the campaign already writes:
- The band. All four cited waves ran at 120-2000. Pooled by band: tells @120-2000 = 228 drafts → 18 banked (7.9%); tells @ full band = 655 → 161 (24.6%); default @ full band = 2,996 → 1,335 (44.6%).
- The size mix. Join each wave's cards to the functions its own commit banked (the removed
INCLUDE_ASMlines are the ground truth — no roster, no ledger):
| nins | default | tells |
|---|---|---|
| 0–50 | 303/528 57% | 27/67 40% |
| 50–80 | 43/145 30% | 20/73 27% |
| 80–120 | 9/41 22% | 10/100 10% |
| 120–200 | 1/30 3% | 1/68 1% |
| 200+ | 2/35 6% | 0/30 0% |
At equal size the lanes are close below 80 instructions and both collapse above it. What separated them was the pool: default's cards are median 37–39 ins, the tells pool median 89–95 — 2.4× larger. "The lane is broken" was a statement about the population.
THE CHEAP CHECK BEFORE YOU RETIRE A LANE: compare bank rate inside a size bucket, and read the
recorded pre-gate verdicts first (R38). Here the recorded reloc_identity rows also refuted the
standing hypothesis (§235, the phantom symbol): among MISMATCH? rows the fraction whose
instruction SHAPE already matched — the symbol-only class §235 describes — is 25/87, 16/79, 17/66,
13/57 on default waves but 6/66, 4/81, 5/47 on tells. Tells drafts fail because the BODY is wrong,
which is what a 2.4× larger median predicts, not because of symbols.
§263 — A STOLEN DELAY SLOT WHOSE INSTRUCTION IS AN ARG-REGISTER COPY IS AN ARITY ERROR, NOT A SCHEDULE (P31 S59, byte-proven)
Symptom. Equal instruction count, exactly ONE mismatch, and it reads like a scheduling loss:
idx | MINE | TARGET
49 | 02002021 move a0,s0 | 00000000 nop
residual_class calls it DELAY-SLOT [permuter] profile=schedule, and it is right about the shape
and wrong about the cause.
The mechanism. An argument-register copy is only available to the delay-slot filler if your C
says the call needs it. Declare a no-argument callee as taking a parameter and pass it, and the
copy must dominate that call — so the filler puts it in the call's delay slot, where the original
has a nop. The same instruction still appears in the target, one block later, in the branch
delay slot, because there it serves only the successor that really does take the argument.
The tell that beats §195-A. §195-A says the asm carries no tell for callee arity. This is the exception: if a callee took the argument, its setup would have to dominate the call on every path that reaches it. A setup sitting in a branch delay slot, needed by only one successor, is positive evidence that the call does not use that register.
Test first, grind never. Flip the suspect callee's arity and recompile — one A/B, ~20 seconds:
extern void func_801EF6E4(void *); func_801EF6E4(a0); /* DIFF 83 vs 83, 1 mismatched */
extern void func_801EF6E4(void); func_801EF6E4(); /* MATCH (83 ins) */
Evidence and the cost of not doing that. func_801F218C @ md_SC03_076. Before the A/B: the
§5a zero-byte fence in both placements, if/else inversion, an early-break restructuring, and a
1,200 s decomp-permuter run at -j12 — all failed, because the permuter cannot change call
arity. Any residual in this class is guaranteed permuter waste. Roughly forty minutes of hand
levers and twenty CPU-minutes went to a defect one deleted argument closed.
Where the wrong arity comes from. A drafter types the argument because every other call in the
function takes the struct pointer, or because a seed's sibling had it. decl_prior (the fleet
declaration consensus on the card) answers this when the symbol is known fleet-wide — read it before
inventing a signature.
§264 — FOUR TELLS-LANE C RECIPES, EACH DRIVEN TO MATCH (P31 S59)
The extend-tell / swaprepeat-tell / s16-div-tell population banks well below the default lane,
and the measured cause is not the models: the card names a lever that appears nowhere in this
cookbook (see the LANE ALIASES block at §172b) and the drafts stack 3-5 idioms. These four were
proven this session by taking real failed wave drafts to MATCH; full derivations, transcripts and
probe files in docs/tool-designs/tells-lane-s59.md and .run/s59_tells/.
1. The (s16) promotion belongs INLINE in the call argument (func_8017D8E8, 33/33). Spelled
inline, its sra lands in the jal delay slot; hoisting it into a temp kills that fill. Three
companions in the same function: hold the table base in a source pointer local so it owns its
$sN across calls (gcc-2.7.2 has no cross-bb CSE, L2425); index with p[i] so (field<<3)+base
stays ONE association unit (writing base + f8 + i2 lets fold re-associate the constant out); read
an s16 field directly in the expression for a plain lh. The failing draft had pinned EIGHT
registers to transcribe the asm 1:1 and plateaued at near-2 over 23 oracle calls — pins are the
last resort (§17), not the opening move; they block the parm-copy and arg-copy shapes.
2. NEW LAW — a save-order-only residual is a bb0 anti-dependence (func_801808AC, 67/67).
Symptom: your prologue emits sw $s1 before sw $ra where the target has $ra first, and nothing
else differs. Saves are emitted descending-regno and woven at sched2, so an anti-dependence from a
basic-block-zero write of that $sN promotes its store. Cure: move the first write of that
variable out of bb0 and into a branch arm — i = 0; if (…) goto found; i = 1; becomes
if (…) i = 0; else if (…) i = 1; … else return;. reorg then re-creates the target's
addu $s1,$zero in the beq delay slot by thread fill.
3. NEW LAW — an opaque bound must live in a LOCAL, assigned LATE (func_80180E58, 34/34).
if (v < 0x10000) and if (v <= 0xFFFF) both canonicalize to li 0xFFFF; slt(0xFFFF,v); bnez.
The target's lui 0x10000; slt(v,K); beqz — plus the arm's store REUSING that register — is only
reachable with the bound in a local, and assigned late, so its live range starts after the block
temps die (assigned at the top it takes $a1, not $v1). Two companions: use the PARAMETER as the
multi-use variable (its expand-time parm copy survives exactly when $aN is re-used; a
self = arg0 local emits double moves), and name the inner term of a nested a - (b - K) as a temp
or fold distributes it away.
4. §172b-1 made concrete — a naked sll/sra pair on a REGISTER is a branch-merged multi-def
variable (func_8018A6A0). Not a cast of a fresh load: mirror the memory slot with a variable
written on BOTH paths (vec0[1] = w = *s1; at entry, w = vec0[1] + …; vec0[1] = w; in the loop)
and compare (s16)w after the join. (s16)vec0[1] instead emits a fresh lh — store-forwarding
cannot cross a two-predecessor label. This flipped the residual from structural to a 3-instruction
commutative register tie, i.e. from an idiom problem to permuter fuel.
Two honest walls recorded so nobody re-spends them: func_801AE3A4's missing same-value copy is
reachable by an "0"(b) re-tie but sits behind §172's +8-frame canonicalization wall; and the
swaprepeat siblings func_80187E08/func_80182FD0 already carry the right doubled-compare
structure — their 2-10-instruction scratch permutation is the S11 knife-edge, permuter fuel, and one
fix banks both by remap.
Addendum (P31 S63 t5e-t5i, func_8017EDD4): §264-2's save-order-only residual has a second, cheaper cure that needs no control-flow surgery: inside bb0, move the def statement to the far side of the adjacent BLKmode aggregate-copy statement. func_8017EDD4 (ov_SC03_030, 67 ins) already carries the §30#3 boost-kill re-tie __asm__ volatile("" : "=r"(p)) at end-of-function — bought for the §201-C addiu-sinks-past-jal shape — and still emits the inverted pair: idx3/4 sw $s1,28($sp) / sw $ra,32($sp) against the target's sw $ra,0x20($sp) / sw $s1,0x1C($sp), SCHEDULE-REORDER/2, span [3,4], every other word already exact. Writing the copy first — buf = D_801E0764; (the §48-C2 lwl/lwr+swl/swr align-2 struct assign) before p = (u8 *)&buf; instead of after, re-tie unchanged and nothing else touched — is the whole delta from closeness 2 to MATCH 67/67 (.run/t5i/stage_union/ov_SC03_030/func_8017EDD4.c:27-28,54). TRIAGE — do not read this residual as the re-tie failing. With the re-tie present, a clean 2-instruction prologue swap is no longer the call/branch delay-slot contention the re-tie was spent on, and re-spending §30#3 / §257-1 placement levers on it costs a wave: the same re-tie moved into the THEN arm regressed to closeness 33 / LENGTH-DRIFT, and replacing it with a plain second p = (u8 *)&buf; also gave 33 (both re-confirming that only the volatile re-tie survives cse). Move the def, not the re-tie. Honest scope: n=1, one single-axis A/B pair; the agent's sw $sN→def anti-dependency account agrees with §264-2's mechanism but no RTL dump was taken here — the position-sensitivity is byte-proven, the attribution is not.
§265 — THE VERBATIM-ASM BANK LANE: A FUNCTION NO -O2 C CAN EVER MATCH BANKS AS A RAW __asm__ BODY (P31 S59b; two banked cards, two in-tree precedents)
The symptom you see in a diff. Every draft plateaus at SIZE-MISMATCH or an unclosable structural
residual, and the target has one of the un-compilable tells: the -O0 prologue (addu $fp,$sp,$zero /
21F0A003, nop in every delay slot, per-statement li) while the object's Makefile globs compile it
-O2 (§261: 116 functions / 14,148 ins are stranded this way — nothing matches src/md_*/); or the
splat header says /* Handwritten function */; or the shape is one gcc-2.7.2 never emits (a bgez
whose OR-path falls through into a j chain, hand-placed GTE hazard nops). No C source can score
MATCH against these under the object's real CC1FLAGS. Stop drafting C.
The mechanism. A raw __asm__ body is opt-level-independent: cc1 passes the string through
untouched, so it emits the identical instructions whether the TU compiles -O0 or -O2, and the ordinary
whole-binary gate accepts it the moment the INCLUDE_ASM stub line is replaced. Two byte-banked forms:
- File-scope block (for a stranded ordinary function): transcribe the whole
.s—__asm__(".text\n.align 2\n.globl <fn>\n.ent\t<fn>\n" "<fn>:\n.frame $sp,40,$31\n.mask 0xC0010000,-16\n.fmask 0,0\n" ".set\tnoreorder\n" …every instruction… ".set\treorder\n.end\t<fn>\n");Banked:func_800D0440(93/93, md_MAIN_003 — a gcc -O0 body inside an -O2 module, atsrc/md_MAIN_003/md_MAIN_003.c:329). Precedent:func_80185810(src/ov_SC03_105/ov_SC03_105_jr_8017C8D0.c:5493). - C-definition wrapper (for a
/* Handwritten function */):void <fn>() { __asm__ __volatile__(".set\tnoreorder\n" … ); }Banked:func_800CBA44(109/109, md_MAIN_027, handwritten GTE add-halfwords loop, atsrc/md_MAIN_027/md_MAIN_027.c:225). Precedent:src/800b2.c.
The transcription rules (each one cost a compile or a session somewhere):
- MASPSX REJECTS HEX IMMEDIATES inside
__asm__strings — the error isinvalid literal for int() with base 10: '0x20'. Every immediate must be decimal (65535,-40,24).%hi(SYM)/%lo(SYM)and barejal <sym>work and relocate correctly. - Transcribe verbatim, model nothing. The -O0
$fpprologue, every delay-slotnop, and fake frames all stay:func_800CBA44carries a mid-bodyaddiu $sp,-8+lw 0x18/0x1C($sp)whose only purpose is reloading stack args 5/6 and hostingcfc2flag spills — transcribe it, do not turn it into C locals. - Ship NO C externs with the file-scope form. The asm resolves symbols at link time; a guessed type
for an otherwise-undeclared global (
D_800EC894) is pure §236-1 conflict risk with zero benefit. - Leave the recovered C semantics in a comment beside the body so the eventual real decomp (post §260/§261 lane work) starts warm.
What was tried and failed (the reason this lane exists): three sessions of near-79/83
SIZE-MISMATCH C drafts on func_800D0440 against a hardcoded -O2 oracle — a feedback loop that cannot
converge (§261); and a 7-oracle-call full lever sweep (polarity flips, goto tails, ternaries, pins,
hoists, rtps nop orders) on the /* Handwritten function */ homonym of func_8017FB04
(ov_SC03_030, 83 ins, rtps/ctc2) whose bgez-with-OR-fallthrough tail no compiler output ever
contains — see Refuted #3 below. Check the handwritten tag and the §261 prologue oracle FIRST;
grep -l 'Handwritten' asm/<ov>/nonmatchings/*/<fn>.s is free.
ACCOUNTING (added at review, P31 S59b). A verbatim-asm body is not an INCLUDE_ASM stub, so
corpus.stubs() — and therefore every progress number derived from it — counts it as MATCHED. That
is a true number about something narrower than the reader assumes (R32/R41): the bytes are proven,
the function is not decompiled. Two exist in the tree today (func_800D0440 @ md_MAIN_003,
func_80185810 @ ov_SC03_105) plus the src/800b2.c wrapper form, so the distortion is currently
negligible — but the §261-stranded population is 116 functions, and banking them this way would move
the headline metric without decompiling a line. Use it for hand-written asm, where no C is the
right answer; for an -O0 C function, the right answer is the -O0 object (§261/§261a) and this lane is
a temporary hold at best. If a future session banks more than a handful this way, split the metric
before the number is quoted anywhere.
Boundary. This banks bytes, not understanding — it is the escape hatch for the §261-stranded
population and hand-written asm only. An ordinary -O2 function that merely resists you is not in
scope; neither is an -O0 function already inside an _o0* object (those bank as real C per §261a and
are STRICTLY easier). Grep bait: verbatim asm, asm body, handwritten, maspsx hex immediate,
decimal immediates, -O0 stranded, .set noreorder body.
§266 — THE INERT-RIDER LAW: A LEVER IS ONLY CITABLE WHEN ITS SOLO REMOVAL BREAKS THE MATCH (P31 S59b; measured 4-of-8 on this batch)
The symptom. A victory note — or your own session memory — credits a MATCH to one named lever
("the volatile is what makes the ordering stick", "fell to a single $2 pin", "only the named local
splits the pseudos", "separate statements block the fold"). The lever was added in the same edit as
other changes, and the note's confidence is absolute.
The mechanism. Near-miss iteration stacks edits. When the miss closes, the last-added lever
gets the credit — and byte-inert riders survive into the banked body precisely because they are
inert: a register __asm__("$2") pin in a function with a call is silently dropped (§257-2), a
volatile on an RMW the scheduler already orders is a no-op, a named constant local coalesces to the
same RTL. The banked artifact then contains the rider, which reads as proof.
The instrument. match_one the banked body with the lever removed — one edit, ~20 s, private
scratch. This distillation ran it on all 8 solo-attributable lever claims in the batch. Four were
inert (the banked body matches with the lever stripped):
| card | credited lever | solo A/B |
|---|---|---|
func_801816C8 (ov_SC03_093) |
register s32 q __asm__("$2") on the modulo result |
pin / no-pin both MATCH 21/21 — §257-2's silent drop, confirmed live |
func_80188770 (ov_SC05_010) |
volatile on the post-call counter RMW |
volatile / plain both MATCH 54/54 |
func_80183994 (ov_SC02_011) |
t = 0; … return t; naming to split return pseudos |
named / literal return 0 both MATCH 103/103 |
func_8018046C (ov_SC03_024) |
separate load/dec/store statements to block a (x−1)==−1 fold |
separated, fused-temp, and compound-in-condition all MATCH 69/69 |
and four were load-bearing (removal breaks it): func_8017EF64 (+2 LENGTH-DRIFT), func_801E28D0
(−4), func_801822D8 (25 mismatches), func_801AEB94 (SCHEDULE-REORDER/4) — plus func_801E2BE0
(1 mismatch) and func_8018117C (−2) from the same campaign. A/B files: .run/s59_distill/.
The law. Before a lever enters a note, a card comment, or this cookbook: strip it from the
otherwise-final body and recompile. If the match holds, the lever is a rider — record the actual
spelling that survives (here: the named quotient local, the statement shapes, the plain RMW), not the
rider. This is R40 (exonerate the instrument) applied to the flywheel itself, and it is why three of
this batch's five "law-grade" candidate claims died in review. Harvest-note convention: write
"banked WITH X; X not solo-A/B'd" unless you ran the A/B. Grep bait: inert rider, lever attribution, solo A/B, pin did nothing, volatile did nothing.
§267 — ADDENDA HARVESTED FROM WAVES at/bh/bk/bl (P31 S59b)
Eleven amendments to existing sections, distilled from 165 byte-gate-banked harvest notes (145 of
which were already covered — the harvest is mostly re-derivation, which is itself the finding). Each
block names the section it amends, so a grep for that § finds the amendment too. Six of the eleven
carry a fresh match_one A/B; the rest are artifact-verified. Sources and per-candidate dispositions:
docs/distill/atbhbkbl.md.
ADD-1 → §231 addendum (also cross-ref from §195-D) — THE MASKED-jal "MISSING CALL" ILLUSION
A relocation-masked jal can render in match_one's diff pane as if the call were DELETED — the
aligner shows the slot's instruction where you expect the jal, and a drafter reads "my call didn't
emit" (several near-15/17 verdicts on func_80188770 were exactly this misdiagnosis, in both its
wave notes). The call was always there; §195-D's mask_for returns 0 for every j/jal word.
The 20-second probe: compile a one-line body f(p){g(p);} and diff — if the jal appears there, it
was never missing in your draft either. Read the masked columns as alignment, not absence.
ADD-2 → §42a addendum — A SHARED CONSTANT NAMED IN A LOCAL ACROSS A jal IS AN ISO→TU DRIFT HAZARD; WRITE BARE LITERALS
§42a lever 4 documents const-load sinking past a call, but not this: a s32 k = 0x40; reused on both
sides of a jal keeps a pseudo live across the call, giving global-alloc the choice between a
callee-saved home and rematerialising li — a choice that is stable in isolation and can flip under
the real TU's ambient pressure (the §42a iso-MATCH→real-TU class). Bare literals at each use delete
the freedom. Cards: func_80180820 (ov_SC03_094 — the true twin func_80181644's banked C uses bare
literals; the named-local draft iso-MATCHed and failed the gate) and func_800CB2CC (md_MAIN_027 —
"passing literal 1 to both calls lets gcc schedule li s0,1 after the address calc; a named const
got scheduled too early"). Honesty bound: iso-side both spellings can MATCH, so this is a bank-side
rule; it cannot be A/B'd by match_one alone.
ADD-3 → §236, item 10 — THE UN-DELETED INCLUDE_ASM STUB IS A DUPLICATE DEFINITION
The tenth way a byte-perfect body fails the gate, and the only one that is pure splice mechanics: the
C definition lands but the function's own INCLUDE_ASM(...) line is not removed — two definitions of
one symbol, whole binary red, zero instruction diff. func_80180800 (ov_SC03_030) burned a full
session on body levers before the stub was found; the shard-replay framing ("wrong in ONE place")
points at the body when the wrong place is the line above it. Add to §236's PROCEDURE: grep the TU
for INCLUDE_ASM.*<fn> after every splice (a lingering .s file in nonmatchings/ is normal and
harmless — §238; the stub line is the killer).
ADD-4 → §225-3/-4 addendum — THE MIRROR ROW: VALUE-RETURN IN THE TAKEN ARM + TRAILING BARE return 0
§225-3's exemplar puts the constant on the early-return edges and the computed value on the
fall-through. The mirror shape exists and has its own spelling: when the target's guard beqz jumps
to the epilogue with the zero written in its own delay slot — counter-intuitively clobbering the
just-tested register (andi $v0,0xFF ; beqz $v0 ; addu $v0,$zero,$zero) — the source is
if (A != 0) { return <computed>; } return 0;: guard-positive, computed predicate in the taken arm,
zero on the fall-through. A/B-proven on func_8017EF64 (ov_SC01_009, 24/24): rewriting it as the
early-return ladder if (A == 0) return 0; return B == 0; drifts +2 — the beqz slot empties to
nop and a trailing move v0,zero block appears. The wave note also byte-refuted (a) A && B as a
value (boolean homed in $s0, +sw pair), (c) the ternary cond ? B : 0 (per §195-F it re-canonicalises),
and (d) an accumulator local (homed callee-saved). Reading tell worth its own grep string: a
delay-slot write that clobbers the branch's own tested register is the other path's return constant.
ADD-5 → §1/I1 addendum — THE INVERTED RANGE TEST: (u32)(x-lo) >= N WITH THE ZERO-ARM AS THE TRAILING else
I1 shows only the positive layout. When the target emits sltiu $v0,(v-lo),N ; bnez → <zero-store>,
the source is the NEGATED range test with the in-range body as the trailing else:
if ((u32)(v - 5) >= 0x4B5) { …out-of-range arms… } else { D = 0; }. A/B-proven on
func_801E28D0 (md_SC03_135, 35/35): the natural if (in-range) D = 0; else … polarity drifts −4
with 22 mismatches (beqz layout, different join). This is §247's branch-count read applied to I1 —
cite both.
ADD-6 → §172b-1 / §264 addendum — SHIFT-AS-TEST: (x << 16) != 0 TESTS THE LOW HALF WITHOUT TRUNCATING THE PSEUDO
A lone sll $v0,$sN,16 feeding only a beqz/bnez, while $sN itself stays RAW for a later sh,
is not a cast and not an extend-pair: the source tests the low halfword as an expression, leaving
the variable's mode alone: s32 v; … if ((v << 16) != 0) { …; store(v); }. Three-way A/B on
func_801E2BE0 (md_SC03_135, 56/56): the (x<<16)!=0 spelling MATCHes; (u16)v != 0 emits
andi v0,s0,0xffff (1 mismatch, OPCODE-MIXED at the tail test); declaring the local u16 also emits
andi. File this beside the extend-tell LANE ALIASES block at §172b so the lane label greps to it.
ADD-7 → §256 addendum — THE SINGLE-GUARD GOTO: THEN-BLOCK OUT OF LINE AT THE TAIL
§256 covers goto-dispatch. The degenerate single-guard case exists and no structured spelling reaches
it: the target lays the guard's then-body after the function's return flow, entered by one
forward beq, with the inline else-chain ending in j .Lepilogue. The source is an explicit goto:
if (v0 == -1) goto L1; …else-work…; return; L1: …then-work…;. A/B-proven on func_801822D8
(ov_SC01_009, 36/36, banked with the goto): the structured if/else-if inlines the then-body at the
head — 25 mismatches, −1. (Its decrement is also a §252-reading instance: v0 = load − 1; store; if (v0 == -1) — but per §266, that statement split is byte-inert here; the goto is the load-bearing
half.) The §162 "backward j into a sibling arm" law reads the same fact from the cross-jump side;
this is the forward/out-of-line face.
ADD-8 → §245 addendum — THE RE-TIE BARRIER PINS CALL-ARG SETUP TO SOURCE ORDER (construct 6)
When call N+1's two argument addius must issue at exact early slots while cheaper independent inits
(q = …; i = 0;) sink below them, no statement permutation works — sched1 re-ranks them freely. Birth
each argument as a local immediately after call N and nail it with the zero-byte re-tie:
a0v = (s32)loc; __asm__ __volatile__("" : "=r"(a0v) : "0"(a0v)); (one per argument), then pass
f(a0v, a1v). The __volatile__ asm is scheduler-immovable (§257-8's fence effect, used
constructively), so the two addius hold source position while the untied inits sink. A/B-proven on
func_801AEB94 (md_SC07_004, 41/41, banked with the barriers): stripping the two re-ties gives
SCHEDULE-REORDER/4 — the arg addius sink to slots 12/18, q/i rise to 10/11. Boundary: this is a
placement lever (§245's rule); it moves nothing across a call and adds zero bytes.
ADD-9 → §213-3 / §217 addendum — ADJACENT PRE-CALL FRAME STORES ARE ONE AGGREGATE; THE UNESCAPED SCALAR NEIGHBOR IS DEAD-STORED
§213-3 reads interleaved sh $zero runs as adjacent-array initialisers. The escape-analysis corollary
bites on call setup: sh $zero,0x18($sp) ; sh $v0,0x1A($sp) ahead of a call taking $sp+0x18 is a
two-element s16 buf[2] whose whole address escapes. Spelled as two scalars with &sp18 passed,
gcc dead-stores the neighbor whose address never escapes. A/B-proven on func_8018117C
(ov_SC03_116, 34/34): the s16 buf[2] form MATCHes; s16 sp18, sp1A; … &sp18 drifts −2 — the
sp1A = 0x20 store (and its li) vanish. Reading rule: adjacent stack stores feeding one escaping
address = one aggregate, never sibling scalars.
ADD-10 → §237 addendum (arity-evidence paragraph) — AN la PAIR ABOVE THE PROLOGUE sw $ra IS AN OUTGOING-ARGUMENT MATERIALISATION
Add to the asm arity tells: a lui/addiu %hi/%lo(SYM) → $aN pair issued above sw $ra — while
the incoming $a0 is never touched — declares SYM the callee's argument N+1 and the raw parameter
argument 1, i.e. a two-arg call even when every visible decl says void f(). func_8017EB38
(ov_SC02_005, 21 ins): lui/addiu $a1,%hi/%lo(D_801E4C50) sits two instructions above sw $ra; the
banked C is func_8017EDF8(a0, p) with the atlas row a bare ?. §217's mirror covers incoming
stack args read above the save; this is the outgoing face.
ADD-11 → Cross-confirmation card block (per §259's standing instruction: confirmation, not news)
Append these card references to the named sections — each was independently re-derived and byte-banked in waves bh/bk/bl:
- §209-addendum no-local row + §226-4 INVERSE ←
func_80180EFC(ov_SC04_005, bl) — un-hoisted double textual read regenerated theadducopy AND the 0x20 frame in one edit; both entries were single-card, now cross-confirmed. Its sharpening stands: a guarded value stored once should not be given a name even when guard and use are textually adjacent. - §223-A/-B ←
func_800CB61C(bl; the slot$v0is the PRECEDING call's return),func_800CB234,func_80180F68(bk; pre-call store fills the slot),func_801875A4(bk; every slot store is a source store-before-call — see Refuted #1). - §234 ←
func_8017EDC4(u16→ori 0xff00/ s16→addiu -0x100),func_801803F0(s16 record element →addiu -2),func_8017FF6C(0xC800 u16→ori),func_80181DC0(negative chain constants spelled-0xB2foraddiu) — all bh. - §236-2 ←
func_801819A0(bh) — tu-typedextern s32MUST be block-scoped because sibling functions carry block-scopes16decls of the same symbol; store through*(s16*)&. - §236-1/-2 (fn-ptr tables) ←
func_8017E448(bk) — the conflict class on a function-pointer TABLE extern; fleet spellsvoid (*D[])(void), dispatch passes nothing. - §238 ← ~14 fresh cards across bh/bk/bl (
func_801808F0,func_80181090,func_80184500,func_801816D8,func_8017F4DC,func_801857F4,func_801802CC,func_8017EE80,func_8017F7F0,func_8017E940, …) — including the sharpened tells: re-confirm the mounted oracle target'sglabel- ins count after any session resume; the task-header asm path outranks every replayed artifact.
- §246-3 ←
func_8017F724(bh) — pointer-chasep += 6IV split killed by the index-loop form; §1412's for-vs-do-while note supplies the loop shape. - §255 ←
func_8018F194(bh) — emptycase 1: break;between {0,2} roots the tree on ==1. - §225-3 ←
func_8017E7C4(bh) — the SAME mixed construct at the opposite constant polarity:if (c1||c2||c3) return 1; return c4;, all threeli v0,1folds landing in the failing branches' delay slots (22/22). - §214-addendum-10 ←
func_801830B4(bk) — a MATCHED twin's C comment carried*24while its own bytes compute*48; diff the twin's encodings, never its comments. - §252-related / §172b ←
func_801914A0(bh) — signed-short temp flips the post-decrement test fromandi 0xfffftosll 16. - §165-17-correction (L19102) ←
func_80181D04(bh) — a pure bb0 swap around a masked shift fell to retyping the temps32→u8; statement order was inert. - §20 pointer-var bullet / §243 ←
func_8018208C,func_80180108,func_80181230(bh) — the held-pointer lever on an indexed global, a global RMW, and an RMW-across-store respectively; on the last, the memory-clobber barrier andvolatile(both directions) were measured FAILURES — the naming is the lever. - §257-2 ←
func_801816C8(bh) — now A/B-proven live: the$2pin in a rand()-calling function is byte-inert (see §266). - §226-addendum data point ←
func_8017D710(bk) — an address-takens32 frame_pad[2]measured 0x10, not 8; the address-taken pad-form table's size column isCEIL(size,8)+ rounding, worth a row note. - §220-addendum × loop-form interaction ←
func_8017DF88(bl) — only the combination "plaina0param +forloop" reproduces the delay-slot refill; "nameds1local +for" still misses by spill. The two dials compose; A/B them jointly.
§268 — A register __asm__ PIN ON A CALL-CLOBBERED REGISTER IS HONORED EXACTLY WHEN THE PINNED RANGE CROSSES NO CALL (P31 S59c; three A/B'd cards, unifying §257-2's two)
The symptom you see in a diff — two opposite faces. Face A (§257-2's): you add a $2/$4 pin
and the output does not move an instruction — the pin "is silently ignored." Face B (this batch's):
a REGALLOC-LOCAL / REGALLOC-PERM residual where values rotate through $v0/$v1/$a0 and no
naming, ordering, or width spelling sticks — and a pin FIXES it, in a function that has calls, which
§257-2 says should not work.
The mechanism (behavioral; both directions byte-proven). The pin binds the variable to the hard
register only where the value's live range avoids calls. A range that crosses a jal cannot live in
a call-clobbered register, and gcc-2.7.2 neither errors nor saves/restores — it silently falls back
to ordinary allocation (that is §257-2's observed identical-to-unpinned output). A range that sits
between calls is honored fully, and inside that window the pin also steers first-fit for every
neighboring value. §257-2 is therefore not "pins on call-clobbered regs do nothing"; it is "pins on
call-CROSSING values do nothing." Check which one you have before writing the pin off — or in.
The C spelling. Pin only values born after one call and dead before the next:
register s32 x __asm__("$4");
register s32 r __asm__("$2");
...
r = rand() & 0xF; /* born from $v0, used immediately */
x = *(u16 *)(s0 + 6) - 8;
*(u16 *)(s0 + 6) = x + (*(u16 *)(param_1 + 0x104) + r); /* both dead before next jal */
The evidence.
func_8017F964(ov_SC05_001, 45/45, banked, THREE calls): both pins' ranges are call-free windows. Solo strip → 14 mismatched, REGALLOC-LOCAL ($a0/$v0/$v1rotate). Honored, load-bearing. A/B:.run/s59_distill2/func_8017F964_{base,nopins}.c.func_8018BAC4(ov_SC04_011, 32/32, banked, LEAF):$2/$3/$4pins,$3shared by two disjoint ranges (r/x). A leaf has no calls anywhere, so every pin is honored: strip → +3 LENGTH-DRIFT / 34 mismatched. A/B:func_8018BAC4_{base,nopins,bare}.c.func_80182480/func_8017FE78: pins on CALLEE-SAVED$16— outside this law's register class (callee-saved pins are always honored) but confirming the strip-test discipline; see ADD-6.- §257-2's
func_8018270C/func_8017F790:$2pins on values living ACROSS calls → output identical to unpinned. Ignored. (Unchanged; now explained.)
What was tried and failed. The func_8017F964 drafter burned its session on free-local
spellings and parenthesisations ("no free-local spelling reaches that assignment") — correct, and
now explained: inside a call-free window the assignment is local-alloc first-fit, which C spelling
barely reaches; the pin is the intended tool there. Conversely REF-5 of the previous batch
(func_801816C8) and this §'s Face A are the SAME test read in the other direction.
Boundary and composition. §257-6 still stands (pinning MORE than the target's callee-saved set
blocks jal-slot filling — pin only what allocation would choose anyway); §257-3 (pins on
parameters are a syntax error); §162p/§37 (a pin forfeits ×134 family propagation — try the unpinned
spelling first, §257-9). And compose with §266: before CREDITING a pin, strip it; before DISMISSING
one as a rider, check whether its range crosses a call. Grep bait: pin ignored, pin did nothing,
call-clobbered pin, $2 pin rand, pin between calls, REGALLOC-LOCAL pins.
§269 — ADDENDA HARVESTED FROM WAVES ax/bm (P31 S59c)
Ten amendments to existing sections, distilled from 82 byte-gate-banked harvest notes — 67 of which
(81.7%) were already covered, the same ~85% re-derivation rate the previous batch measured. Each
block names the section it amends, so a grep for that § finds the amendment too. Sources, the
per-candidate ledger and the re-runnable A/B files: docs/distill/axbm.md, .run/s59_distill2/.
THE INERT-RIDER RATE IS NOW A MEASUREMENT, NOT AN ANECDOTE (§266). This batch ran 19 solo-lever
strip-tests across 13 banked bodies (34 match_one runs): 6 of 19 credited levers were byte-inert
— 32%, against the previous batch's 4-of-8. One in three "this is what made it match" claims credits
something that changes nothing, and the banked artifact contains the rider precisely because it is
inert. One whole proposed section dissolved under its own strip test (func_8017F0A8: a named
shifted temp, a *0x10000 spelling and an init-then-override select were ALL riders; the only
load-bearing part was one (s16) cast's sign semantics). Run the strip before you write the law.
ADD-1 → §257-8 addendum — THE INTERPOSED ASM'S __volatile__ IS A PER-SHAPE DIAL, AND THE "NO-OUTPUT ASM IS IMPLICITLY VOLATILE" LORE IS BYTE-FALSE IN gcc-2.7.2
§257-8 records that §16x's interposed-asm prologue lever DIES under __volatile__ and only the
non-volatile __asm__("" :: "r"(arg)) flips the save pair (func_80186C0C, banked twice). The
opposite polarity is now equally byte-proven: func_800CEED0 (md_MAIN_003, 13/13, banked) needs the
prologue order sw $ra FIRST and gets it only with __asm__ __volatile__("" : : "r"(idx)) on
the $2-pinned incoming index; the non-volatile spelling swaps sw $ra below the sll (5
mismatched, OPCODE-MIXED — A/B .run/s59_distill2/func_800CEED0_{base,novol}.c). Two corollaries:
(1) the volatile-ness of an interposed asm is a per-shape ~20-second A/B, not a rule — do not carry
either polarity between functions; (2) the folk rule "an asm with no outputs is implicitly volatile"
does NOT hold behaviorally in this cc1 — the explicit keyword changes scheduling. Grep bait:
interposed asm volatile, prologue save order asm, sw $ra first.
ADD-2 → §236 addendum (item 1 corollary) — THE SILENT-SPLICE DECLARATION LADDER: WHERE THE TU HAS NO SPELLING, UNPROTOTYPED () BEATS THE FLEET VOTE
§236-1 says in-TU beats fleet consensus, always. The vacuum case needed stating: at a splice point
where the TU carries NO declaration for a callee (defined below, never declared above), the robust
default is the unprototyped () extern for every callee — C89-composite with any later
prototype or definition, immune to §236's classes 1/3/5/8. Copying the atlas/fleet def-signature
instead is what failed the gate once on func_801824E4 (ov_SC02_031, 63 ins — atlas
('void',('void*',)) lifted, gate red; () banked). func_8017FCBC (ov_SC03_030, 230 ins)
corroborates: canon votes for its callee come from OTHER binaries' same-address functions (§150-B),
so a prototyped canon lift would be actively wrong; () vs prototyped verified byte-neutral. The
ladder: in-TU spelling > unprototyped () > fleet vote — the fleet is LAST even in a vacuum
(§196's loss extended). Grep bait: no in-TU spelling, undefined splice point, unprototyped default, fleet vote vacuum.
ADD-3 → §238 addendum — TWO BINARIES CAN EACH DEFINE THE SAME func_ NAME WITH DIFFERENT BYTES, AND NO GENERATED REPORT SHOWS IT
func_800CF370 is defined in src/md_MAIN_003/md_MAIN_003.c:141 (13 ins, this card's bytes) AND in
src/resident/resident.c:281 with contradicting content — both correct for their own binaries
(separate links; resident's own comment admits its disassembly matches neither image).
duplicates.cross.md inventories only byte-IDENTICAL groups, so a byte-DIFFERENT same-name pair is
invisible to every generated report, and a "reconcile" toward either side silently breaks the other
binary. This is §150-B's "same address + same name ≠ same body" restated at the DEFINITION level:
before trusting an in-TU splice for a func_800xxxxx/shared-address symbol, grep the
sibling-binary TUs (src/resident/, other src/md_MAIN_*/) for a second definition, and never
conform one binary's def to another's. Grep bait: cross-binary duplicate, same name different bytes, resident twin, reconcile trap.
ADD-4 → §167-37 addendum — THE FIFTH PRECONDITION, NOW MEASURED: DOWNSTREAM FIELD RE-READS ⇒ NAME IT AND STORE INSIDE THE ARM
§167-37's boundary note ("do not widen past its four preconditions") now has its byte-proven
counter-case. func_80181164 (ov_SC05_005, 48/48, banked): same three-line shape (call result,
stored to +0x20, null-guarded, handed to the next call) plus downstream uses through five
field re-reads — and the target's store sits in the jal's slot inside the else arm (addu $a0,$v0 in the bnez slot, sw $a0,0x20($s0) in the call's slot), not in the guard's slot. Both
prescriptions §167-37 would suggest measure 2-mismatch by solo A/B: the no-local store-and-re-read
form AND the named-local store-before-guard form each swap sw↔move at idx 6/14
(.run/s59_distill2/func_80181164_{preguard,noname}.c). The winning spelling names the result and
places the store between the guard and the first consuming call, inside the arm. Read the store's
HOME first: store in the GUARD's slot ⇒ unconditional, §167-37/§252-reading (write it before the
if); store in the ARM's call slot ⇒ conditional, this row (write it inside the arm). Mechanism per
§48-A4: the named local dies at the second call's argument setup, so its range crosses zero calls
and takes the arg-register copy preference; dbr then fills both slots. Grep bait: store inside the arm, sw in jal slot inside arm, name nothing failed, 167-37 counter.
ADD-5 → §267-ADD-6 / §172b-1 addendum — THE SIGN-TEST FACE: (s16)v < 0 IS sll 16 + bgez ON THE SHIFTED COPY, RAW STAYS LIVE — AND THE HALFWORD-ABS SHAPE NEEDS NO RITUAL
ADD-6 (§267) gave the ZERO-test face ((x<<16) != 0). The SIGN face: testing (s16)v < 0 —
equivalently (v << 16) < 0 or v * 0x10000 < 0 — emits a lone sll $t,$v,16 feeding
bgez/bltz on the SHIFTED copy with no sra (only the sign is needed), while raw v stays
live for both arms. The halfword-abs composition seen on func_8017F0A8 (ov_SC03_092, 91/91,
banked) — subu; sll 16; bgez (slot: addu $a0,raw); negu $a0,raw — is reached by ANY select
spelling over that test: named-shifted-temp, inline expression, (s16) cast, and plain
if/else all compile byte-identical (4-way A/B, .run/s59_distill2/func_8017F0A8_*.c). The ONLY
load-bearing choice is the semantics: the raw s32 test v < 0 drops the sll and drifts −1. The
drafter's "two-liveness" construction (t = v1 * 0x10000; a0 = v1; if (t < 0) a0 = -v1;) is three
inert riders around one s16 cast — do not transcribe the ritual into future drafts. Reading rule: a
bgez on a freshly-sll'd copy whose ARMS use the unshifted register = abs/negate-select of the
low halfword; write if ((s16)x < 0) and any select shape. Grep bait: sll 16 bgez, abs idiom,
negu delay slot abs, sign test halfword, two liveness.
ADD-6 → §220-addendum — THE FOURTH FACE: WHEN THE NAMED COPY ITSELF BUYS THE EXTRA CALLEE-SAVED, PIN THE COPY TO $16
§220-addendum's three faces end at "an explicit named local is the reliable way to pin a
callee-saved copy." The fourth face: sometimes the plain named copy is what OVERSHOOTS — parameter
pseudo and copy each take a callee-saved register. func_80182480 (ov_SC03_098, 22/22): plain
s32 s0 = arg0; → +2 LENGTH-DRIFT, frame 0x20 vs 0x18, extra $s1 save/restore;
register s32 s0 __asm__("$16"); s0 = arg0; banks — the pin unifies copy and home so the parameter
pseudo dies at the copy. func_8017FE78 (ov_SC02_017, 52/52) is the same face at two registers:
stripping both pins costs +3 with the whole s-register file shifted ($s0→$s1, constant→$s2).
Both solo-A/B'd (.run/s59_distill2/func_80182480_nopin.c, func_8017FE78_nopins.c). Distinct
from §136d-1 (pin-as-CSE-defeat): this pin SHRINKS a live range. Compose with §266 — FE78's second
($17) pin was a rider (strip-tested MATCH); pin the copy, not the constants. And the standing
tax: a pin forfeits ×134 propagation (§162p) — try §220's plain-parameter form first; reach for the
pin only when the plain copy demonstrably buys the extra register. Grep bait: pin the copy,
extra callee-saved from local, s16 pin live range, frame 0x20 vs 0x18.
ADD-7 → §229 addendum — THE VALUE FACE: A LOOP-STORE CONSTANT SPELLED AS A LITERAL HAS NO SOURCE POSITION; NAME IT TO PIN THE PROLOGUE INIT ORDER
§229's law is stated for ADDRESSES. The same birth-order mechanism reaches a plain constant:
func_80182AC4 (ov_SC01_080, 27/27, banked) needs the prologue order la $a1(D_801C7560),
li $a2,1, li $v1,3, la $v0(D_801C755C). Writing the loop store as literal *p = 1 leaves the
constant with no statement of its own — gcc materializes it at its own rank and the surrounding
address setups re-order (4 mismatched, ADDRESSING/lui!=addiu, profile=cse — A/B
.run/s59_distill2/func_80182AC4_lit.c). A single-set named local (val = 1; before the loop,
*p = val; inside) gives the constant a birth position, and §3-T2's source-order law then governs
all four inits. Boundary note for §3-T2: a literal OPERAND is not a statement — source order cannot
place what has no statement. Grep bait: literal loop constant, prologue init order, li reordered against la, name the constant.
ADD-8 → §30a addendum — THE THIRD COLUMN: *p++ IS ALSO SERIALIZED BUT BURNS addiu; ONLY CAST-PLUS GIVES SERIALIZATION AND DISPLACEMENT FOLDING
§30a#1 documents that *(T*)((s32)p + k) denies /s (NOP_EXPR over the PLUS) while bare q[k]
grants it. Complete the matrix with the third spelling and the codegen-shape consequences, from
func_8017D164 (ov_SC01_000, 20/20, banked — six lockstep lhu/lui/sh pairs):
| spelling | /s |
consequence |
|---|---|---|
bare p[k] |
granted | loads hoist above the aliasing stores into a 3-load cluster — 16 mismatched, WIDTH/lhu!=lui (A/B .run/s59_distill2/func_8017D164_index.c) |
*p++ |
denied | serialized, but real addiu pointer steps materialize — LENGTH-DRIFT (+2 per drafter's bracket) |
*(u16*)((s32)p + k) |
denied | serialized AND combine folds k into the load displacement — zero extra instructions; the only column that matches a lockstep copy chain |
Grep bait: p++ addiu burn, cast plus displacement, serialized field copy, three spellings aliasing.
ADD-9 → §255 "AND CASE-BODY PLACEMENT" bound / §222-addendum-3 — ON A LARGE SPARSE TREE, BODIES FOLLOW SOURCE ORDER (measured by a one-word probe)
§255 states case bodies emit in DFS order (root body first) and §222 holds source order for dense
switches. Measured on the 29-case sparse balanced tree func_8017F328 (ov_SC01_001, 307/307,
banked): swapping the SOURCE positions of the case 2:/case 3: blocks (identical instruction
shapes, only masked symbols differ) moves exactly ONE unmasked word — the dispatch beq's target
(1 mismatched, IMM-OFFSET/−8; A/B .run/s59_distill2/func_8017F328_swap23.c). Bodies are laid in
source order here, and the drafter's recovery rule holds: the tree's beq targets, read in
ascending physical order, name the source case sequence. Both regimes now have measured
exemplars — when reading a switch, recover case VALUES from the beq/slti chain (per §255), and
recover BODY order from the physical layout only on the source-order regime; when transplanting a
big switch, preserve the source case order exactly. Grep bait: case body order, switch source order probe, beq targets name cases.
ADD-10 → Cross-confirmation card block (per §259's standing instruction: confirmation, not news)
- §210 ←
func_801856F0(bm, 24/24) — second live gate for name-the-mask, at the== 0polarity (sltiu $v0,$v0,0x1); both direct spellings ((x&M)==0,!(x&M)) measure +1 with thesrl 15/xori/andiextract. The section's two fixes stand exactly. - §162d1 ←
func_8017D8D0(bm, 28/28) — the missing independent A/B its own text asks for: two-statement split = −1 (first mask folds into jal-2's slot, cross-calladdu $s0,$v0copy deleted); the one-expression sum is load-bearing. - §237-escape-2 ←
func_801810CC(bm) — strongest return-axis-only card yet: stalevoidself-decls left by BANKED CALLERS (:4650/:4700), cured in-body by the def-side__asm__("func_801810CC")alias; the §85 widen verified as the alternative. Also a process fact worth §236-8's margin: fixes prescribed in NOTES never reach the gate — the cure must ride in the submitted code. - §257-6 ←
func_8017F10C(ax; its own third note) andfunc_801A6A38(bm; "first thing to try on a redraft-lever card: the unpinned direct spelling" — §257-9 restated). - §236-2 ←
func_80182CF4(ax) — the DATA-symbol face: two incompatible block-scope decls ofD_801888B0in sibling functions + a new file-scope extern = whole-TU cc1 death with a green oracle; moving the three data externs to block scope cured it. - §150-B ←
func_80180CE0(ax; a non-authoritative fleet row('s32',...)for a callee whose own.sprovesvoid— ground-truth unbanked callees against their.s) andfunc_8017FCBC(ax; address-taken array beats the scalar plurality). - §253 / §165-06 ←
func_80181D08(bm) — second card for single-observation §253: the postfix-vs-compound axis also decides STORE PLACEMENT (var = (*p)++;keeps the store after themfhi/bnezpair in its own slot; bare++/pre-increment sink it before the div). - §235 / §195-D ←
func_801EFC30(bm) — a transposed digit in ajalsymbol (func_80116714forfunc_80016714) scored MATCH at 10 ins; the post-MATCH relocation walk is not optional at any size. Plus the §235 boundary caution fromfunc_801811D0(ax): a deliberate probe constant in a SUBMISSION weaponizes the same blind spot — probes belong in scratch A/Bs; the whole-binary gate is unmasked (the banked tree carries the true0x7FFFFFFF). - §229 ←
func_801A6A38(bm; negative face: reordering plain stores does NOT drag the hoistedladown — address birth position is independent of its consumers' statement order). - §164-59 ←
func_80183BA0(ax). §251-2 ←func_801A0DA4(bm;addiu -0x5reads&= ~0x4, never~0x5) +func_801820B0(ax;&= ~0x10→addiu -0x11). §243 ←func_801862C8(bm; an interveningshblocks CSE ⇒ the doublelw 0x20is inline re-derivation, not a local). §164-73/-74 ←func_801835F4(bm). §214-addendum ←func_80180150(bm; stub twin ⇒ grepDEFINE_in engine_core.h by tail sequence). §254 ←func_8018944C(ax; already cited there). §224-addendum-4 ←func_801853F0(ax; already cited there).
1a. The §266 sweep — every solo-lever A/B run for this batch
All files in .run/s59_distill2/; every baseline re-verified MATCH before the strip.
| fn (banked ins) | lever stripped/varied | solo A/B result | verdict |
|---|---|---|---|
| func_8017F0A8 (91) | named shifted temp t = v1 * 0x10000 → inline expr |
MATCH | INERT RIDER |
| func_8017F0A8 | * 0x10000 spelling → plain (s16)v1 < 0 |
MATCH | INERT RIDER |
| func_8017F0A8 | init-then-override → plain if/else select |
MATCH | INERT RIDER |
| func_8017F0A8 | s16 sign test → raw s32 v1 < 0 |
DIFF −1, 61 mism. | load-bearing (semantic) |
| func_8018BAC4 (32) | volatile on the three u8 loads |
MATCH | INERT RIDER |
| func_8018BAC4 | the $2/$3/$4 pins |
DIFF +3, 34 mism. | load-bearing |
| func_800CEED0 (13) | __volatile__ on the interposed asm |
DIFF 5 mism. (sw $ra↔sll swap) | load-bearing → ADD-1 |
| func_80181164 (48) | store moved above the guard | DIFF 2 mism. | load-bearing → ADD-4 |
| func_80181164 | named local removed (§167-37 no-name form) | DIFF 2 mism. | load-bearing → ADD-4 |
| func_8017FE78 (52) | both $16/$17 pins |
DIFF +3, 46 mism. | load-bearing (as a set) |
| func_8017FE78 | $17 pin alone |
MATCH | INERT RIDER |
| func_801856F0 (24) | named mask temp → return (s0&0x8000)==0 (and ! form) |
DIFF +1, 9 mism. (srl/xori) |
load-bearing → §210 ⊕ |
| func_80182AC4 (27) | named val = 1 → literal store |
DIFF 4 mism. (cse profile) | load-bearing → ADD-7 |
| func_8017D8D0 (28) | one-expression rand-sum → two statements | DIFF −1, 22 mism. | load-bearing → §162d1 ⊕ |
| func_8017F964 (45) | $4/$2 pins |
DIFF 14 mism. REGALLOC-LOCAL | load-bearing → §268 |
| func_80182480 (22) | $16 pin → plain local |
DIFF +2, 21 mism. (frame 0x20≠0x18) | load-bearing → ADD-6 |
| func_801F0D88 (33) | byte-offset IV → indexed D[i][0], i++ |
MATCH | INERT RIDER |
| func_8017D164 (20) | cast-PLUS → bare p[k] index |
DIFF 16 mism. WIDTH/lhu!=lui | load-bearing → ADD-8 |
| func_8017F328 (307) | case 2/3 SOURCE blocks swapped | DIFF 1 mism. IMM-OFFSET/−8 | load-bearing → ADD-9 |
6 of 19 credited levers are riders (32%) — the same one-in-three rate §266 measured on the previous batch (4 of 8). The harvest-note convention ("banked WITH X; X not solo-A/B'd") is not yet being followed by drafters; every rider above arrived stated as a certainty.
§270 — The A-prop 0-bank anatomy: a byte-correct body still needs FOUR layers to agree (P31 S59)
The free A-prop lane staged 117 mechanically-remapped drafts a pass and banked ZERO, three passes running. The autopsy decomposed into four INDEPENDENT defects — none of them the body:
82/117 shape-DIFF staged anyway (the filter read reloc status==AGREE and ignored shape)
35/82 ONE ordinal-pairing defect (§271) — the value/offset swap
16/117 match_one MATCH, TU rejected (§272 — the (void)-decl + empty-call wall)
19/117 standalone COMPILE-FAIL used as a TU verdict (§273 — wrong oracle)
Law: a mechanical remap is judged four times — instructions (match_one), symbols
(reloc_identity), TU coexistence (rtu_match / the gate ladder), whole-binary bytes (the gate) —
and a lane that stages on fewer than all four burns a build per missing layer, per pass, forever.
The four verdicts disagree BY DESIGN (each sees what the others mask); the lane must consume all
of them. .run/maintenance.sh now stages on status∈{AGREE,UNRESOLVED} ∧ shape==MATCH, keyed
(binary, fn) — overlays share function NAMES (three binaries each carry a func_8013BCDC), so
an fn-keyed set/rowmap silently acts on the wrong binary's row (bit reloc_identity --fix too).
§271 — Ordinal IMM pairing: text order is NOT emission order; emit CANDIDATES, let the oracle pick (P31 S59)
35 staged drafts shared one residual, byte-for-byte:
draft: li v0,2 ; sh v0,3(s0) target: li v0,3 ; sh v0,2(s0)
i.e. *(p + 3) = 2 drafted for a target of *(p + 2) = 3. Two stacked causes in the §T87
ordinal resolver (family_remap._ordinal_edits):
- It scanned the literal's SPELLINGS one form at a time (
0x2before2) and paired against the first form with any occurrences — a seed spelling the offset0x2and the value2exposed only the offset span to the pairing. - Even unioned, an in-order zip is wrong for
*(p + OFF) = VAL: the C names OFF first, gcc emits VAL'slifirst.
Law: no static pairing of asm imm-positions onto C literal occurrences is right for every
shape. Enumerate the order-preserving assignments (union of spellings, capped) and adjudicate
with match_one at DRAFT time — seconds against the gate's minutes. Implemented as
family_remap._ordinal_candidates + imm_map_tier1(want_alts=True) + the match_one loop in
aprop_autodraft; the fix also un-skipped the ~50 members the single guess had refused as
asm-ambiguous. Spot-proof ov_SC04_018/func_8018067C: *(short *)((char *)arg0 + 0x2) = 3;.
§272 — The (void)-decl + empty-call wall: K&R the DEFINITION, not just the decls (P31 S59)
A family member's TU (and engine_core.h, inside DEFINE_ macros the TU instantiates — 42 of them
for func_80162CCC, at the infamous engine_core.h:57282) declares the member
extern void f(void); because every banked caller calls it K&R-style with no args. The draft's
ANSI definition void f(u8 *a0) then fails TWICE, in sequence:
conflicting types for 'f'against the(void)decls — the gate's arity pre-pass (fix_arity_callers --any-proto) fixes exactly this, and DID fire in every 0-bank pass;- …after which the TU's own EMPTY CALL SITES
f()die against the ANSI definition's prototype:too few arguments to function 'f'. This is the layer nothing addressed.
Law: when the destination TU calls the member with fewer args than its true definition takes,
the definition must be K&R (void f(a0) u8 *a0; {…} — §99's zero-blast-radius lever): a K&R
definition establishes no prototype, so the sloppy calls stay legal. Promotion-safe params only
(pointers/int-width; a K&R narrow param compiles differently, §43/§102). Byte-proof:
func_80162CCC rtu-FAILED as ANSI at both layers, rtu-MATCHed (23 ins) as K&R + pre-pass, then
banked at the whole-binary gate. aprop_autodraft.kr_definition now emits every eligible draft
K&R.
§273 — A standalone compile is the WRONG oracle for a TU-destined draft (P31 S59)
The lane's pre-filter compiled each draft standalone (match_one inside reloc_identity) and
dropped every COMPILE-FAIL before the gate: 43/182 on the measured pass, unjudged. But the draft
is written to land in a TU that provides what the standalone compile lacks — typedefs
(parse error before 'Blk8'), struct defs (dereferencing pointer to incomplete type),
canonical decls. Re-judged against the REAL TU (tools/rtu_second_chance.py, rtu_match per draft
— no build tree, no locks, thread-parallel): 7 of 27 were TU-byte-MATCHes and staged straight
to the gate. Law (R33 shape): judge a draft with the oracle that sees its destination context;
a standalone verdict may only REFUSE a standalone claim, never a TU one. Corollary from the
same session, the other direction: the destination TU's decl spelling may only be adopted into a
draft when the seed BODY still compiles against it — a void-return spelling for a callee whose
value the body reads, or a prototype stricter than the body's K&R-sloppy empty calls, breaks the
draft in both worlds (decl_for's guards; 42 and 15 drafts measured respectively).
§274 — ADDENDA HARVESTED FROM 18 WAVES (P31 S60): 315 candidates, 255 already covered, 21 sharpenings, 3 new laws
Provenance. Waves be bj bo bq br bs bt bu bv bw bx by bz ca cb cc cd ce — the ore that piled up while a stale READY marker blocked the distill lane for 10.5 h. Three reviewers (Sonnet, disjoint wave groups) read all 315 harvested notes against this corpus. 255 were already covered — expected and healthy: §193–§273 are themselves earlier rounds of this same harvest cycle, and a note restating a law we own is the flywheel working, not waste. 26 were rejected (one-offs, restatements of the card, claims that did not survive checking). What follows is the 21 that sharpen an existing section; the three genuinely new laws are §275–§277.
Verification status (R14/G3). Every function named below is byte-gate BANKED — that part is not in question. The reviewer checked each claim against the target .s and the committed C before writing it up. At merge I independently re-verified the four load-bearing ones: the leftover-register read (§275), the CSE-defeat spelling (§276), the compound-rounding pair (live in ov_SC03_095), and func_801846F0's delay-slot addu $v0,$zero,$zero (§277). One reviewer claim did NOT survive merge and is recorded as a correction inside §275.
ADDENDUM to §179-C — the .type NAME, @function requirement
Symptom. A file-scope __asm__ transcription (§179-C's own lever) that assembles and links fine
is reported by the checker as "my object has no function <NAME>" — which reads exactly like "the
oracle rejects file-scope asm at all," and can burn sessions chasing the wrong cause.
Mechanism. §179-C's three sub-rules (.ent/.end as literal text, un-doubled %hi/%lo, and a
second .ent/.end pair for a shared-tail successor) are necessary but not sufficient. Without an
explicit .type NAME, @function directive, the assembled object carries no STT_FUNC symbol type for
NAME, and the harness's function-discovery pass (which the checker's boundary walk relies on, same
mechanism as §179-B rule 5) finds nothing there — a pure ELF-symbol-table gap, indistinguishable from
"the transcription didn't work" until someone reads readelf -s instead of the diff.
Fix, byte-verified (src/md_MAIN_003/md_MAIN_003.c:919-927,1157-1160, banked, func_800D3234,
208/208):
__asm__(
".set\tnoreorder\n"
".set\tnoat\n"
".section\t.text\n"
".align\t2\n"
".globl\tfunc_800D3234\n"
".type\tfunc_800D3234, @function\n"
".ent\tfunc_800D3234\n"
"func_800D3234:\n"
/* ... body ... */
".set\treorder\n"
".set\tat\n"
".end\tfunc_800D3234\n"
".size\tfunc_800D3234, .-func_800D3234\n"
);
Two more directive facts confirmed at the same site: (1) .set noreorder must be paired with
.set noat for the block's %hi/%lo forms to assemble at all (lui $t0,%hi(D) / addiu $t0,$t0,%lo(D)
needs $at free of the reorder-mode implicit-nop insertion, or as reports a bad-expression class of
error); (2) both directives (.set reorder / .set at) must be restored before .end, matching the
target's own object boundary.
Why file-scope asm can be the ONLY correct spelling, not merely a workaround. The banked tail is
jr $ra / addi $v0,$zero,1 — a trapping addi (opcode 0x20), not addiu (0x24). gcc-2.7.2 never
emits a trapping addi for an integer literal from C source (li/addiu only); this exact byte pattern
is unreachable from any C spelling, and file-scope __asm__ is the only route to it.
ADDENDUM to §134 — a typedef defined BELOW the splice point is stripped anyway
Symptom. A draft that compiles clean standalone (match_one MATCH) fails the whole-binary gate with the struct/typedef name undefined at the insertion point, even though that same name IS defined, verbatim, later in the same TU.
Mechanism. §134 documents family_remap's preamble-scanner bugs (D1–D5) around wrapped/multi-line
typedefs; this is a related but distinct failure in the ordinary single-function harvest path.
harvest_verify strips a draft's local copy of any typedef the destination TU "provides"
(cdecl.strip_provided_typedefs, cited at cookbook L8748) on the assumption that the TU's own copy is
already visible. That assumption does not check position: if the TU's only definition of the type
sits textually AFTER the function being spliced, C89 has not seen it yet at that point, but the
stripper removes the draft's copy regardless — the splice compiles with the name undefined.
Fix. Verified in-tree (src/ov_SC02_005/ov_SC02_005_jr_80181D30.c): func_8018692C (defined at
line 5539, block-scope extern s32 D_801E43CC[];) sits ABOVE the TU's own F5C_S typedef and its
pointer-typed extern F5C_S *D_801E43CC[]; redeclarations (lines 5712, 5730, 5750, 5760, each inside a
different function). The banked fix does not fight the stripper: it declares the symbol at the
scalar type (s32[]) rather than the TU-eventual struct-pointer type, matching cookbook §14c/§183's
general "block-scope externs dodge TU type rivalry" lever — but the causal trigger here (why the
scalar spelling was forced rather than optional) is this visibility gap, not an ordinary type-rivalry
choice. Rule for future drafts: when a "TU-provided" typedef your callee/data symbol needs is only
defined LOWER in the same TU than your splice point, do not rely on the stripper to supply it — either
declare the symbol at a type that does not need the struct (as here), or confirm the typedef's
definition line number is above your insertion point before trusting strip_provided_typedefs.
ADDENDUM to §179-D (GTE macro reference family) — gte_SetRotMatrix/gte_SetTransMatrix bodies
Mechanism. Same class as the already-documented gte_stflg/gte_stszotz house macros (§179-D):
the PsyQ SDK's SetRotMatrix/SetTransMatrix were never linked into this binary as callable functions
in the relevant TUs — their effect is inlined GTE control-register loads. Reconstructed and byte-verified
(src/ov_SC06_029/ov_SC06_029_jr_8017C954.c:6177-6196, part of the 210/210 MATCH for func_801852E4):
#define gte_SetRotMatrix(r0) __asm__ volatile ( \
"lw $12, 0( %0 );" "lw $13, 4( %0 );" "ctc2 $12, $0;" "ctc2 $13, $1;" \
"lw $12, 8( %0 );" "lw $13, 12( %0 );" "lw $14, 16( %0 );" \
"ctc2 $12, $2;" "ctc2 $13, $3;" "ctc2 $14, $4" \
: : "r"(r0) : "$12", "$13", "$14" )
#define gte_SetTransMatrix(r0) __asm__ volatile ( \
"lw $12, 20( %0 );" "lw $13, 24( %0 );" "ctc2 $12, $5;" \
"lw $14, 28( %0 );" "ctc2 $13, $6;" "ctc2 $14, $7" \
: : "r"(r0) : "$12", "$13", "$14" )
Tell to look for: a target with lw/ctc2 pairs writing GTE control registers 0–7 in this exact
sequence, adjacent to (or instead of) a jal to a rotation/translation-matrix setter, is this macro
pair inlined — reach for these bodies rather than re-deriving the lw/ctc2 stream by hand.
ADDENDUM to §82 — struct copies must stay MEMBER-WISE, not block-moved, to match a spill-slot save
Symptom. The target saves N words from an object into what are clearly its OWN stack spill slots
(interleaved lw/nop/sw, no register caching), and every "obvious" C spelling of the save either
turns it into N separate scalar locals (wrong: promotes to callee-saved registers) or a single
struct/array block copy (wrong: emit_block_move collapses the interleave into a lwl/lwr+swl/swr
pair or a paired-load form).
Mechanism, byte-verified (src/ov_SC04_002/ov_SC04_002_jr_8017BEBC.c:7138-7152, func_80182618,
92/92 MATCH): gcc-2.7.2 never promotes a struct member access into a pseudo register the way it
does a plain scalar local — each tmp.f0/tmp.f1/tmp.f2 reference stays an independent
memory-to-memory word move with its own load-hazard nop, byte-identical to the target's interleave:
typedef struct { s32 f0, f1, f2; } S_80182618;
s32 func_80182618(s32 a0, s32 *a1) {
S_80182618 tmp;
tmp.f0 = *(s32 *)(a0 + 0x10);
tmp.f1 = *(s32 *)(a0 + 0x14);
tmp.f2 = *(s32 *)(a0 + 0x18);
/* ...calls that clobber a0's fields... */
*(s32 *)(a0 + 0x10) = tmp.f0;
*(s32 *)(a0 + 0x14) = tmp.f1;
*(s32 *)(a0 + 0x18) = tmp.f2;
/* ... */
}
Whole-struct assignment (tmp = *(S*)(a0+0x10); / *(S*)(a0+0x10) = tmp;) collapses to a 12-byte
block-move (emit_block_move, paired lw/lwl-class loads) — wrong shape. A volatile scalar,
array, or struct adds a load-barrier nop each access — length-drift +3, also wrong. This refines
§82's "scalar vs. aggregate decides WHEN the slot allocates" finding with a distinct, complementary
fact: for a struct that must stay a memory-resident spill save (not promoted, not block-moved), member-
wise access is the only spelling that reproduces both the memory residency AND the per-word interleave
— block-move and per-scalar-local are the two failure directions on either side of it.
ADDENDUM to §136d-1 (RC-12, the $0-add / opaque-copy family) — two symptoms beyond "compare reads the wrong register"
§136d-1 documents RC-12 for exactly one symptom: a copy-pair whose beqz/bnez reads the SOURCE
register while plain C always makes the compare read the COPY. Two byte-verified banks extend the same
underlying mechanism (an opaque copy cse/cse2 cannot fold through) to different symptoms:
(a) An ARGUMENT COPY stolen into a jal's own delay slot. func_80180724 (ov_SC06_024, 92/92
MATCH, verified src/ov_SC06_024/ov_SC06_024_jr_8017BEBC.c:4082):
register s32 zr __asm__("$0");
v0 = func_8012BE54(s1 + zr);
Here the missing instruction was not a comparison but the call's own argument-copy (addu $a0,$s1,$zero)
that the target places in the jal's delay slot; every plain func_8012BE54(s1) spelling left that
slot a bare nop because gcc passed $s1 straight through without a copy to place. The +zr opaque
add forces a real copy instruction to exist, which dbr then sinks into the call's own slot — same
mechanism as RC-12, applied to arity setup rather than a compare.
(b) A two-arm CONSTANT SELECT feeding a call-crossing loop induction variable, using the alternate
"":"=r"(x):"0"(x) spelling rather than the $0-add form. func_801864E4 (ov_SC05_010, verified
src/ov_SC05_010/ov_SC05_010_jr_80181CDC.c:4219-4220):
sel = (rand() & 1) ? -0x255555 : -0x1D5555;
__asm__("" : "=r"(sel) : "0"(sel));
start = sel;
Every plain if/else, ternary, or split-variable spelling of the two-constant select folds BOTH arms
into the loop's start-value register directly (gcc's if(c) t=A; else t=B; → t=B; if(c) t=A;
canonicalization, itself §136d item 2) — losing the target's separate scratch-register selection step
before the value is copied into the callee-saved loop-bound register. The no-op "=r"(x):"0"(x) asm
constraint is a second working spelling of RC-12's "opaque copy," useful when a bare $0-add would
also perturb the surrounding arithmetic (here, the select's own constant materialisation). The same
spelling also closed func_80185A24 (ov_SC03_007, raw = call(); flags = raw; +
__asm__ __volatile__("":"=r"(flags):"0"(flags));), confirming it generalizes beyond one function.
ADDENDUM to §20 — a global declared as T * may itself BE the array base, not a pointer to dereference
Symptom. Every draft compiles clean and shape-matches except for one or two extra lws the target
does not have, immediately around a global whose canonical type is a pointer (s32 *D_x / T *D_x).
Mechanism, byte-verified (src/ov_SC06_029/ov_SC06_029_jr_8017C954.c:8054-8059, func_80188568,
22/22 MATCH):
extern s32 *D_80126B78;
...
r = func_8012BB3C((s32)&D_80126B5C, *(s32 *)(arg0 + 0xD4) + 4,
((s16 *)D_80126B78)[9], 8);
((s16 *)D_80126B78)[9] += r;
return (s16)r == 0;
D_80126B78 is declared s32 *, and the natural reading is "dereference it, then index" — but the
target's own lw %lo(D_80126B78) loads the address of the storage itself (the linker-provided
.data label), and everything downstream indexes off that value directly: ((s16*)D_80126B78)[9], not
((s16*)*D_80126B78)[9]. Writing the extra dereference level compiles to a real, semantically-different
extra lw that survives to the diff. This is distinct from §20's existing "hold a pointer-var to a
global and index off it" lever (which is about a T *p = &D_x; local you introduce yourself): here
the pointer-typed GLOBAL SYMBOL is the base, by construction of its declared type, and the fix is to
respect that type rather than "helpfully" dereferencing it. Tell: two redundant lws where the target
has none, on a pointer-typed global, is the fingerprint of an invented indirection level.
ADDENDUM to §1-I5
Do not misread a hand-written u32 round-to-nearest bias (q += q>>31; q>>=1;) as the compiler's
own signed-/2^k bias. Byte-evidence: func_80180474 (ov_SC03_096, 0x108 bytes / 66 ins, MATCH,
wave bs). The target's srl $v0,$v1,31 / addu $v1,$v1,$v0 / srl $v1,$v1,1 triplet (appears twice in
the function) superficially resembles §1-I5's bgez+addiu(2^k−1)+sra k round-toward-zero
correction for a compiler-generated x / 2^k, but it has no branch at all, and its final shift is
srl (logical), not sra (arithmetic) — the discriminator. §1-I5 only documents the branchy bgez
form and only for a following sra; it does not cover a branchless, srl-ending triplet, so an agent
pattern-matching on "shift-by-31-then-a-shift" risks writing a real /2 and reaching for the wrong
(branchy, sign-extending) shape.
The target is not a division at all: it is the SOURCE's own explicit round-to-nearest formula written
on an unsigned temp: q = expr * 3; q += q >> 31; q >>= 1;. Two things are load-bearing (both
byte-checked against the section's neighbours):
- The accumulator must be declared
u32. Ans32(or any(s32)cast anywhere in the chain) flips the final shift tosra, byte-diverging immediately. - It must be spelled as the two COMPOUND assignments (
+=,>>=) — this is §219's read-modify-write scheduling law applied here: the fully-expanded single-statement formq = (q + (q >> 31)) >> 1;routes the sum through a fresh pseudo and loses the accumulate-in-placeaddu $v1,$v1,$v0(REGALLOC-PERM).
TELL. srl K,31 immediately followed by addu back into the SAME register, then a SECOND srl
(never sra) of the same register, with no branch anywhere in the block ⇒ a hand-written unsigned
bias-and-halve on a u32 local, not a signed division. Apply §219's compound-assignment form, not
§1-I5's branchy bgez recipe.
THE SAME LAW ARRIVED TWICE, ON THREE OVERLAYS. Waves bt/bv reached it as an addendum to §219 (a chained rounding computation needs the compound form at EACH step, not one composite expression) from func_801808F4 and func_8018232C; the byte-check at merge found the identical m += m >> 31; m >>= 1; pair live in ov_SC03_095. Treat §1-I5's discriminator (branchless, srl-ending) and §219's per-step compound form as one rule with two symptoms.
§219 establishes that x += d, x = x + d, and t = x+d; x=t; are three distinct RTL shapes for a
single read-modify-write. This is the same law applied to a CHAIN of two compound operations, and it
is confirmed independently on two different functions in two different overlays:
/* func_801808F4, ov_SC03_095 */
m = func_8004787C(...) * 3;
m += m >> 31;
m >>= 1;
/* func_8018232C, ov_SC03_093 — independently, different overlay */
t = r * 3;
t += t >> 31;
t >>= 1;
(✔ byte-checked on BOTH: asm/ov_SC03_095/.../func_801808F4.s and
asm/ov_SC03_093/.../func_8018232C.s each show srl $v0,$v1,31 / addu $v1,$v1,$v0 / srl $v1,$v1,1
— the addu's destination equals its FIRST operand's own register, an in-place tie.) Writing the
same arithmetic as one composite expression, (t + ((u32)t >> 31)) >> 1, gives the sum its own FRESH
register instead (addu $v0,$v1,$v0; srl $v1,$v0,1 — measured on this family elsewhere in the same
harvest), breaking the tie.
The law: for a rounding/bias chain of the shape t = X; t += t >> 31; t >>= 1; (unsigned
accumulator, logical shifts throughout), BOTH steps must be written as their own compound-assignment
statement — not folded into one expression — for the addu's destination to stay tied to the first
operand's register. This generalises §219's single-RMW law to a two-step chain and is now confirmed
on two independent functions.
ADDENDUM to §164-51
The copy+frame pseudo pair (§164-51) fires BARE — with no andi 0xFFFF re-widen — when an SImode
temp feeds an slt directly instead of a narrower comparison. Byte-evidence: func_801840D0
(ov_SC02_031, wave ca, MATCH 47/47). §164-51 documents the copy+phantom-frame pair only alongside a
re-widening andi; here the pair appears with NO andi anywhere near it, because the HImode clamp
local (s16 r) feeds an slt on its SImode-promoted value directly rather than being re-masked back
to a narrower width for a later use. The diagnostic tell in §164-51 ("count andi sites preceded by a
copy") therefore undercounts this class by design — a copy+frame instance with zero adjacent
andi instructions is not evidence the pair is absent; declare the clamp value as an HImode (s16)
local and check for the copy+frame signature independently of any andi.
ADDENDUM to §176-F5
Goto-loop invisibility to loop.c also blocks combine_givs (induction-variable MERGING), not just
invariant-hoisting; a hand-written label loop can therefore PRESERVE two separate loop-carried values
that a structured for/while/do would coalesce into one. (P31 S60; byte-proven func_8017FC94,
ov_SC06_014, wave ca, MATCH 56/56.)
§176-F5 states the invisibility rule only for hoisting a loop-invariant into the preheader. The same
precondition — loop.c operates only inside NOTE_INSN_LOOP_BEG/END brackets that a recognised
for/while/do construct emits — also gates combine_givs (the induction-variable-merging pass
already cited at §164-81/§135-17). When a loop's body walks with TWO independently-bumped values (here:
a counter and a derived offset pointer), every STRUCTURED spelling let combine_givs fold the offset
onto the counter's biv, landing on one register and 52 instructions against the target's 56. Rewriting
the loop as an explicit goto-based label loop (two guard tests spelled if (...) goto next;, the
update as an early goto) removed the loop from loop.c's view entirely, so nothing merged the two
values and both survived as separate registers, matching the target 56/56 — a register-pin-free,
statement-order-driven fix once the shape was right.
TELL. A target keeps TWO loop-carried values alive across a whole loop (a counter plus a second,
arithmetically-derived pointer/offset) where your structured-loop draft naturally combines them into
one biv+giv pair — matching instruction count but wrong register roles, or a handful of instructions
short. Before reaching for pins on the merged pair, try rewriting the loop as a goto-based label loop:
this removes it from loop.c's optimisation scope altogether, a stronger and cheaper lever than
fighting combine_givs's merge decision from inside a real loop construct.
ADDENDUM to §225
SIXTH SHAPE: a gate built from two overlapping sign/range compares needs a FULLY FLATTENED
explicit-label form, not nested if/goto, to get BOTH arms' branch polarity right at once.
(P31 S60; byte-proven func_8017EC64, ov_SC04_006, wave bw, MATCH 55/55.)
The target's z-gate tests z < 0, then (on each side of that sign split) |z| < 0xA1, and calls only
when both fail to disqualify. Every NESTED spelling — if (z<0) { if(-z>=0xA1) return; } else { if (z>=0xA1) return; } call();, and equivalent goto-based nestings that still route through one shared
join — forced gcc-2.7.2 to INVERT the positive-z arm's polarity (beqz→skip-call instead of the
target's bnez→take-call), a residual that reads exactly like an unmovable scheduler tie-break (three
distinct nested spellings converged on the identical wrong byte stream). The fix was to abandon nesting
altogether: write each of the four tests as its own top-level if (...) goto label;, landing on four
flat labels with no block nesting at all:
x = out[0];
if (x >= 0) { if (x < 0xB5) goto zcheck; } else { if (-x < 0xB5) goto zcheck; }
return;
zcheck:
z = out[1];
if (z < 0) goto negarm;
if (z < 0xA1) goto docall;
goto end;
negarm:
if (-z >= 0xA1) goto end;
docall:
func_8002D4C8(0x98A, 0);
end: ;
This reproduces the target's exact per-arm polarity because each compare now feeds exactly one branch with no shared convergence point for gcc to canonicalise against.
TELL. A residual that looks like a pure branch-polarity flip or a small delay-slot drift on a gate
built from a DUPLICATED compare across a sign split (x</>=0 combined with |x| vs K) — where every
nested if/else or nested-goto spelling reproduces the SAME wrong polarity — means the arms still
share a compiler-visible join point. Flatten every test in the gate to its own if (...) goto label;
with no nesting before concluding the residual is scheduler noise. §172b-2's swapped-arm tell covers
the analogous case for a ?: SELECT; this is its branch-statement counterpart.
ADDENDUM to §224 — CROSS-JUMP: THE DUPLICATE CAN BE A PLAIN STORE, NOT ONLY A CALL
func_801E7FD8 (md_SC04_027, MATCH 75/75). §224's recognition tell and prescription are stated for
merged call sites (jal). This function shows the identical mechanism firing on a merged plain
store, with no call anywhere near the join:
if (D_801F2188 == 0) {
D_8011512C = 0;
D_801EB9B0 = (void *)D_801EB918; /* if-arm's OWN store */
} else {
D_8011512C = 9;
D_801EB9B0 = (void *)0x1A; /* else-arm's OWN store, different value */
}
(✔ byte-checked: asm/md_SC04_027/nonmatchings/md_SC04_027/func_801E7FD8.s.) The target's else-arm
test (bnez $v0,.L801E8058) carries addiu $v0,$zero,0x9 in its OWN delay slot — the constant that
arm is about to store to D_8011512C, clobbering the just-tested $v0 (§267 ADD-4's clobbering-slot
tell). Both arms then reach .L801E8064: lui $at,%hi(D_801EB9B0) / sw $v0,%lo(...) — one arm via
fall-through, the other via its own j — a single shared sw fed by two different producers ($v0
holds either the loaded D_801EB918 value or the literal 0x1A depending on path). Factoring this
into one shared local (v = ...; ...; D_801EB9B0 = v; written once after the if/else) collapses the
two arms' producers into one pseudo and drops the merged-tail shape entirely — the same failure §224
describes for a factored call, just for a store.
The law, generalised: cross_jump's tail-merge doesn't care whether the duplicated instruction is
a jal or a plain store — write the duplicate in BOTH arms whenever the target shows two branches
converging on one instruction, whether or not a call is involved.
ADDENDUM to §164-64 — AN EMPTY CLOBBER ON AN ARGUMENT REGISTER CAN BE THE DELIBERATE FIX, NOT JUST THE ACCIDENTAL BUG
func_80186FA4 (ov_SC03_028, MATCH 67/67). §164-64 documents the mechanism — a local-alloc'd
anonymous temp can claim $a0 and evict parameter 1 from its incoming register — and prescribes
avoiding the anonymous temp as the cure. This function shows the OPPOSITE-polarity use of the same
mechanism: the incoming parameter riding untouched in $a0 all the way to a tail call is itself the
problem (it lets gcc reuse $a0 for free at the call, deleting the target's explicit
addu $a0,$s0,$zero setup and starving local-alloc's first-fit of $a0 for a different variable,
shifting a whole downstream register class). The fix is to deliberately manufacture an occupant:
register s32 s0 __asm__("$16");
s0 = (s32)a0;
__asm__("" ::: "$4"); /* breaks the free ride, zero bytes */
...
((s32 (*)(s32))func_8012CBCC)(s0); /* target: addu $a0,$s0,$zero in the jal delay slot */
(✔ byte-checked: asm/ov_SC03_028/nonmatchings/ov_SC03_028_jr_8017DF98/func_80186FA4.s idx
addu $a0,$s0,$zero immediately precedes jal func_8012CBCC.)
The law: when the target's parameter is NOT riding through to a later call for free, and your
draft's plain C spelling gives it that free ride, an empty __asm__("" ::: "$N") clobber on the
argument register right after the parameter's canonical copy is the zero-byte way to force the setup
instruction back and free $N for a different variable's allocation. This is §164-64's eviction
mechanism used as a tool, not only diagnosed as a defect.
ADDENDUM to §244 — THE COMPILER BARRIER, NOT volatile, BLOCKS A STORE→LOAD HOIST THROUGH A POINTER-OFFSET FIELD
func_8018A404 (ov_SC03_028, MATCH 25/25). §244 documents volatile's several roles and one case
where a barrier is needed instead of volatile (func_801AC9CC, a lh/lhu width-pair case). This
function shows a second, distinct case needing the barrier over volatile: a load through a
pointer-relative offset (not a fixed global symbol) that the compiler otherwise hoists across an
intervening store.
*(u16 *)((s32)s0 + 0x5C) = 0x8000;
__asm__("" ::: "memory"); /* forces the lw below the sh */
t = *(s32 *)((s32)s0 + 0xC4);
*(s32 *)((s32)s0 + 0x58) = (s32)&D_801907C4;
*(s32 *)((s32)s0 + 0xC4) = t | 0x2;
(✔ byte-checked: src/ov_SC03_028/ov_SC03_028_jr_8017DF98.c:6489-6493; the barrier sits between the
0x5C store and the 0xC4 RMW exactly as claimed.) Without it, gcc hoists lw $v0,0xC4($s0) to the
top of the block (10 mismatched near the top of the else-arm). volatile on the field does not
express this — the field isn't a stable lvalue you're reading repeatedly, it's a plain load that
needs to stay ORDERED after an unrelated store to a DIFFERENT offset off the same base register.
The law: when the target keeps a $sN-offset load in place after a store to a different offset
of the same base, and a plain statement reorder or volatile on the field doesn't hold it there, use
__asm__("" ::: "memory") between the two statements — this is the general store→load ordering
barrier, not limited to fixed-symbol globals (§244's documented case).
ADDENDUM to §172b-1 — TWO PLACEMENT DIALS THE PROMOTION LAW DOESN'T NAME: NARROW THE COUNTER'S DECLARED TYPE, AND MOVE THE CAST INTO THE LOOP BODY
Two byte-confirmed refinements of §172b-1's "a naked sll/sra pair = a register-held promotion; a
plain lh where you emit the pair = drop the temp" law — neither says WHERE in the loop to put the
promotion, or that the FIX for an unwanted pair can be the variable's declared TYPE rather than
removing a temp.
1. Narrow the counter's type (func_80183C54, ov_SC04_002, MATCH). The target's loop has NO
sll/sra pair anywhere and a single shared addiu $v0,$s2,0x200 feeding both a delay slot and a
fall-through tail. Declaring the loop counter s32 — even with (s16) casts sprinkled at each use —
emits a spurious lui 0x200/addiu plus a real sll/sra before the compare. Declaring it s16
directly keeps gcc in HImode for the whole loop and removes both:
s16 i; /* not s32 i, even with (s16) casts */
(✔ byte-checked against asm/ov_SC04_002/.../func_80183C54.s — no sll/sra pair exists in the
target's loop.)
2. Move the cast into the loop body (func_801801E8, ov_SC03_030, MATCH 38/38). The target
computes (s16)arg1 as a loop INVARIANT, hoisted ONCE before the loop, sll $v0,$a1,16 immediately
followed by sra $s2,$v0,16 — landing the result in $s2, a DIFFERENT register than the intermediate
$v0. This two-register shape only appears when the cast is written INSIDE the loop body:
while (*(s16 *)((u8 *)p + 6) == 0) {
lim = (s16)arg1; /* written INSIDE the loop — loop.c hoists it as an invariant */
...
}
(✔ byte-checked: src/ov_SC03_030/ov_SC03_030_jr_8017AE2C.c:5014 places the cast inside the
while; asm/ov_SC03_030/.../func_801801E8.s idx 4-5 shows sll $v0,$a1,16 / sra $s2,$v0,16 sitting
between the guard branch and the loop label — i.e. loop.c's invariant-hoist, through a fresh
intermediate pseudo, not the entry-block copy §172b-1's existing examples show.)
The combined law: when the target shows NO extend pair where your draft shows one, first try narrowing the variable's declared TYPE (not just dropping a temp) before reaching for anything else; and when the target's promotion sits as a loop-hoisted invariant through an intermediate register that differs from its final home, write the cast as a statement INSIDE the loop body, not at function entry — loop.c's invariant motion is what produces the two-register shape.
ADDENDUM to §229 — A POINTER'S NUMBER OF USES DECIDES WHETHER ITS ADDRESS FOLDS OR SURVIVES A CALL
Two byte-confirmed refinements of §229's naming lever, both turning on how many times the pointer is used AFTER its first assignment.
1. Single-use folds, multi-use hoists-and-survives (func_80189F28, ov_SC02_027, MATCH 44/44).
The target materialises D_801D2B1C's address into $a1 via a hoisted lui/addiu, and that SAME
register survives UNRELOADED into the delay slot of a later jal func_80189FD8. This only happens
because the pointer has a second use after the store — it is handed on to the callee:
p = &D_801D2B1C;
*p ... = ...; /* first use: store through p */
func_80189FD8(a0, p); /* second use: p passed on, not re-taken as &D_801D2B1C */
A single-use pointer (address taken once, used once) instead folds to the compact $at-relative
macro form. (✔ byte-checked against asm/ov_SC02_027/.../func_80189F28.s, per the candidate's own
relocation walk — the second use is what keeps the register alive.)
2. One address, one store AND one call argument (func_80189CFC, ov_SC03_105, MATCH 39/39). The
target forms D_801B68F8's address exactly ONCE and reuses the SAME register both as a store base
and as the second argument to func_80189DAC:
u16 *ptr = &D_801B68F8;
ptr[0] = ...; /* store through ptr */
func_80189DAC((s32)a0, ptr); /* same ptr, passed on */
(✔ byte-checked: src/ov_SC03_105/ov_SC03_105_jr_80189798.c:2991-2995; asm/ov_SC03_105/.../func_80189CFC.s
shows exactly one lui $a1,%hi(D_801B68F8) (idx 14) and no second materialisation before the
jal func_80189DAC at idx 36 — $a1 survives across an intervening rand() call and two other
global stores.)
The law: §229 already says naming an address births an earlier, longer-lived pseudo. The
refinement: whether that pseudo SURVIVES into a later call (hoisted la-form, held in a register
across intervening code) or FOLDS to the compact $at form is decided by whether the pointer has a
SECOND use after its first — most commonly, being passed on as a call argument. A single-use pointer
local is not enough; the target's shape tells you how many uses to give it.
ADDENDUM to §172b-4 — THE PLAIN CAST-DIVISION ALREADY PRODUCES THE PATTERN; DON'T HAND-ROLL THE BIAS, AND KEEP THE OPERAND WIDE
func_800CBEC0 (md_MAIN_019, MATCH 40/40). §172b-4 documents that s16 / K via magic-multiply can
read a PRE-truncation <<16 intermediate for its sign correction, and says "keep the promotion as an
expression… so the intermediate exists to reuse" — worded as if the round-toward-zero idiom must be
hand-spelled. This function shows the plain, un-hand-rolled cast-division already producing exactly
that pattern:
s32 w; /* NOT u16/s16 — SImode, not HImode */
...
r = (s16)w / 2; /* plain cast-division, no "+ ((u32)(s16)w >> 31)" anywhere */
(✔ byte-checked: src/md_MAIN_019/md_MAIN_019.c:253,265; asm/md_MAIN_019/.../func_800CBEC0.s idx
31-35 shows sra $v1,$v0,16 — the pre-truncation intermediate — then srl $v0,$v0,31 /
addu $v1,$v1,$v0 / srl $v0,$v1,1: gcc-2.7.2's OWN signed-divide-by-2 expansion, not a hand-rolled
bias.) Declaring w narrow (u16/s16) instead forces a bad HImode allocation ($a0 plus a
spurious andi 0xFFFF truncation before the join stores, one instruction too many).
The law: a sra-then-srl/addu/srl "round toward zero" pattern on (s16)x / 2 is very
often the compiler's OWN power-of-2 signed-division expansion, not a hand-written bias correction —
try the plain (s16)x / 2 first, with the dividend declared s32 (SImode), before reaching for an
explicit + (x >> 31) term. Reserve the hand-rolled bias for the (rarer) case where the compiler's
own expansion measurably fails to appear.
ADDENDUM to §8 — WHEN INCLUDE_RODATA NEEDS A STANDALONE .s YOU CAN'T CREATE, CARRY THE FRAGMENT AS A FILE-SCOPE __asm__ BLOB
func_801A40CC (md_SC07_004, MATCH 44/44, independently landed in both wave bz and wave by). §8
documents two build-system-level mechanisms for compiler rodata: "migrate" (a single-ref jtbl/const
co-locates automatically in its function's own .s, flowing into the object for free via the
INCLUDE_ASM stub) and INCLUDE_RODATA(...) (splat-generated, for multi-ref rodata, requiring a
standalone .s). Neither survives an agent BANKING the function by hand: migrated rodata rides in via
the INCLUDE_ASM stub, which disappears the moment the function's C replaces it — shifting every
later rodata symbol — and INCLUDE_RODATA needs splat to have already extracted a standalone .s
that may not exist and that a drafting agent has no tool to create.
The fix used here, confirmed in the committed source:
extern s32 D_801A01B4[];
__asm__(
".section .rodata\n"
"dlabel D_801A01B4\n"
".word 0x00000000\n"
".word 0x00000000\n"
"enddlabel D_801A01B4\n"
".section .text\n"
);
void func_801A40CC(s32 a0) { ... }
(✔ byte-checked: src/md_SC07_004/md_SC07_004.c:892-909, positioned at the exact source line the old
INCLUDE_ASM stub occupied, immediately before the function definition — preserving the rodata
island's ordered position among the TU's other INCLUDE_RODATA/migrated fragments, per §8's own
ordering law.)
The law: a file-scope __asm__ block using the project's own dlabel/enddlabel assembler
macros, placed at the exact source position the INCLUDE_ASM stub occupied, is a legitimate
drafter-level replacement for a migrated single-ref rodata fragment when INCLUDE_RODATA's
standalone-.s prerequisite isn't available — add it to §8's two documented mechanisms as a third,
C-source-level one.
ADDENDUM to §226 — A DEAD LOCAL SCOPED TO A NESTED BLOCK BUYS A MID-FUNCTION addiu sp PAIR, NOT A FRAME-SIZE CHANGE
func_8017D474 (ov_SC03_107, MATCH, independently landed in both wave cd and wave cb). §226
documents the pad[N≥2] dead-local lever entirely in terms of the FUNCTION's overall vars/frame
size (prologue/epilogue). This target instead shows a bare MID-FUNCTION addiu $sp,$sp,-0x8 /
addiu $sp,$sp,0x8 pair, with no register saves and no loads/stores touching that 8 bytes, sitting
between ordinary body instructions:
void func_8017D474(s32 a0, s32 a1, s32 a2) {
*(u16 *)(a0 + 0xA0) = *(u16 *)(a2 + 8);
*(u16 *)(a0 + 0xA2) = *(u16 *)(a2 + 0xA);
*(s32 *)(a0 + 0x14) = *(s32 *)(a2 + 0xC);
{
s32 y = *(s16 *)(a2 + 2);
s32 x = *(s16 *)(a2 + 0);
s32 frame_pad[1];
(void)&frame_pad;
*(s16 *)(a1 + 0) = x + (y - x) / 2;
}
}
(✔ byte-checked: src/ov_SC03_107/ov_SC03_107_jr_801789AC.c:5943-5954; asm/ov_SC03_107/.../func_8017D474.s
shows addiu $sp,$sp,-0x8 at idx 10 — AFTER the first three stores, right before the y/x load-and-compute
sequence — and addiu $sp,$sp,0x8 at idx 18, right before the final sh/jr. No instruction reads or
writes the reserved 8 bytes.) The dead pad array is scoped to a C block { } NESTED inside the
function body (not declared at the function's top level), which is why gcc emits the sub-allocation
localized to that block's extent rather than folding it into the whole-function prologue/epilogue.
The law: a bare mid-function addiu sp,-N / addiu sp,+N pair, with no visible save or spill,
is the frame-pad lever (§226) applied to a NESTED block rather than the function's top-level locals —
scope the dead pad[N] array to the same { } block the target's stack adjustment brackets, not to
the function body, and it will not touch the function's overall frame size.
ADDENDUM to §172a — RE-READING MEMORY (NOT NAMING A TEMP) IS WHAT KEEPS AN INCREMENT'S DELAY-SLOT FILL ALIVE
func_800CB11C (md_MAIN_036, MATCH). §172a's lhu/lh tell already distinguishes a plain copy from
a promoted use of the same load. This is the companion case for a compare-then-increment on ONE
memory cell: writing the increment as a fresh RE-READ of the field on its right-hand side (not a named
temp) is what makes CSE merge the compare's load and the increment's load into ONE lh, and that
single load's register copy is what fills a downstream delay slot.
if (*(s16 *)(arg0 + 0x62) == 0) {
*(s16 *)(arg0 + 0x62) = *(s16 *)(arg0 + 0x62) + 1; /* re-read, not a named temp */
func_80147324(0x8EC);
}
(✔ byte-checked: src/md_MAIN_036/md_MAIN_036.c:114-117; asm/md_MAIN_036/.../func_800CB11C.s shows
exactly ONE lh $v0,0x62($s0), then bnez $v0,.L800CB188 with addu $v1,$v0,$zero in the branch's
OWN delay slot — a register COPY of the just-loaded value — then addiu $v0,$v1,0x1 / sh $v0,0x62.)
A named-temp spelling (t = *(s16*)(...); if (t==0) { ...; t = t+1; *(s16*)(...) = t; }) is
semantically identical but, per this same family's other reports, lets the increment fold to a bare
immediate and loses the delay-slot copy.
The law: for a if (mem == 0) { …; mem = mem + 1; } compare-then-increment shape, spell the
increment's right-hand side as a FRESH re-read of the memory location — not a named temp reused from
the compare — when the target shows a register-to-register COPY (not a fresh load) feeding the
increment; this is the CSE-merge lever that keeps the copy (and the delay slot it fills) alive.
ADDENDUM to §225 / §256 — A GOTO TO A SHARED SET-POINT PREVENTS IF-CONVERSION FROM COLLAPSING A LATER BRANCH TEST
func_801F098C (md_SC05_026, MATCH 34/34). §225 and §256 both document control-flow shapes that need
explicit gotos to reach a target layout; this is a shape where the goto's job is specifically to
STOP gcc from if-converting a later comparison into a bare sltiu (an scc-style boolean materialise)
instead of a real conditional branch.
r = func_80029504();
if ((u32)(r - 0xC8) < 0x64) {
f = 0;
} else if (r >= 0x12C) {
if ((func_80029178(0xFA) & 0xFF) != 0) {
goto isCAC; /* forward, past "f = 1;", to a shared set-point */
}
f = 1;
} else {
f = 1;
}
goto join;
isCAC:
f = 0;
join:
if (f != 0) { ... } else { ... }
(✔ byte-checked: src/md_SC05_026/md_SC05_026_jr_801F0748.c:201-229;
asm/md_SC05_026/.../func_801F098C.s keeps the func_80029178 test as a REAL branch —
jal func_80029178 / andi $v0,$v0,0xFF / bnez $v0,.L801F09D4 with addu $v0,$zero,$zero (f=0) in the
delay slot, and addiu $v0,$zero,0x1 (f=1) on fall-through — never collapsed to a bare sltiu.)
The law: when a mid-chain test's C condition would otherwise reduce to a single boolean value
consumed identically by every path (the shape gcc-2.7.2's if-conversion likes to collapse into
sltiu), write the TRUE arm as an explicit goto landing at a shared set-point AFTER the test, rather
than as a structured if/else or ternary — this is what keeps the test in real branch form with its
delay slots filled by the set-point constants, matching a target that never shows the collapsed
sltiu-only shape.
ADDENDUM to §211 — AN IN-LOOP ACCUMULATOR WANTS A CLOSED-FORM EXPRESSION WHEN THE TARGET REMATERIALISES ITS CONSTANT AFTER EVERY CALL
func_801ADA9C (md_SC07_004, MATCH). §211 covers loop-INIT placement relative to a dominating guard;
this is a distinct placement dial for an in-loop ACCUMULATOR's constant. The target's loops 2 and 3
show a FRESH lui $v0,(0x2000000>>16) immediately after EVERY jal, each followed by an addu
accumulate into the same register — not a lui hoisted once to a preheader with a plain += inside
the loop. The C that reproduces this is a CLOSED-FORM expression, not an accumulator variable:
for (i = 0; i < 8; i++) {
func_801ADBB0(0x200, (0x1000000 + i * 0x2000000) >> 16, (j << 16) >> 16);
j++;
}
(✔ byte-checked: src/md_SC07_004/md_SC07_004.c:6706-6709;
asm/md_SC07_004/.../func_801ADA9C.s idx 0x801ADB40-0x801ADB4C shows jal func_801ADBB0 /
lui $v0,(0x2000000>>16) / addu $s2,$s2,$v0 repeating each iteration, with only ONE preheader
lui $s2,(0x1000000>>16) before the loop — the closed form's i * 0x2000000 re-derives the same
in-loop rematerialisation instead of carrying an accumulator across the whole loop with a single
preheader constant.) A running k += 0x2000000; accumulator instead hoists the constant into a
callee-saved preheader load and never re-materialises it per iteration (an extra instruction,
measured elsewhere in this family as a permanent near-miss).
The law: when the target shows the SAME constant re-materialised (fresh lui) after EVERY call
inside a loop rather than hoisted once to a preheader, write the accumulated value as a CLOSED-FORM
expression of the loop index (base + i * step) rather than as a running += accumulator — the
closed form is what reproduces the per-iteration rematerialisation; the accumulator form hoists the
constant instead. (The companion observation on this same card — a caller-saved pointer pin whose
live range never crosses the function's one call is legal even under a blanket "no pins first" rule —
is already §268's exact law.)
§275 — THE LEFTOVER-REGISTER READ
A function whose entry block consumes $v0/$v1 before ANY EARLIER INSTRUCTION IN THAT FUNCTION
writes it is reading the CALLER's last return value, not a missing rand()/helper call. (Later
jals are irrelevant and usually present — func_80181F54 has five. What matters is that no write to
the register precedes the read.) (P31 S60;
byte-proven func_80181F54, ov_SC03_104, wave bw, MATCH 57/57.)
MECHANISM. PSX/MIPS o32 does not clear $v0/$v1 across a jal-less function entry, and
gcc-2.7.2 gives every function a fresh allocation for its own values — it never treats an incoming
$v0 as meaningful unless the C body says so. When the ORIGINAL source read that stale value on
purpose — almost certainly a copy-paste from a sibling that genuinely called something with an s32
return, with the call itself dropped in the shipped game code — the only way to reproduce the bytes is
to declare a register-pinned local with no assignment and consume it immediately:
register s32 vin __asm__("$2"); /* never assigned — reads whatever is in $v0 at entry */
...
(s16)(((s16)vin) % 128)
THE TELL. The function's disassembly opens with an operation on $v0/$v1 (sll $v0,$v0,16,
an andi, a comparison, etc.) before any jal, lw, or argument-register use establishes a value for
it — the read has no producer anywhere in the function. Do NOT spend turns hunting for a dropped call
to explain it. Check a structurally-similar sibling first: if it genuinely has a jal at the analogous
slot (its own .s shows the call), that sibling's C cannot be copied verbatim for this target — only
its skeleton transfers, and the target's own .s (no jal anywhere, an entry-block $v0/$v1 read)
is the ground truth that this is the leftover-register class, not a missing-call defect. Cast the value
at each use exactly as the register width implies ((s16) for a halfword-looking use) — no promotion
instructions exist in the target because none were emitted for a value that was never loaded.
BOUNDARY. Distinct from §223's delay-slot corollary (which reads a value the SAME function's
caller-of-a-callee definitely set on purpose, one instruction earlier in the same function) and from
§176-A (a store's C statement dominating an upcoming call). This law applies only at a function's true
entry block, before any control transfer of its own, and only when no assignment to the consumed
register exists anywhere in the target .s.
§276 — MIXED ADDRESS-EXPRESSION SPELLING FOR ADJACENT RELOCATABLE SYMBOLS IS A CSE-UNIFICATION DIAL, NOT JUST A BYTE-ENCODING CHOICE (P31 S60; func_80180FE8, ov_SC06_006, byte-proven)
The mechanism. gcc-2.7.2's cse pass unifies repeated uses of the identical SYMBOL_REF RTL into
one shared address pseudo — held resident across the whole function once it is CSE-canonical — which
can evict other live values from the register pool. Two syntactically different C expressions that
happen to evaluate to the SAME relocatable address (e.g. (&SYM)[1] and the adjacent symbol's own
name, when the two symbols are consecutive words in memory) build DIFFERENT RTL (PLUS(SYMBOL_REF(SYM), 2)
vs SYMBOL_REF(SYM+2)) even though both encode to byte-identical lui/addiu/lh sequences. cse's
unification is syntactic (RTL-equal), not semantic (address-equal) — so mixing the two spellings for
what is really the SAME address defeats the unification and forces a FRESH, independent materialisation
at every use, while using the SAME spelling for every use lets cse merge them into one shared,
register-resident pseudo.
The byte evidence. func_80180FE8 computes six table entries from consecutive halfwords of
D_801F8A10[]. The banked C mixes forms for the SAME two symbols across two different table entries:
extern s32 D_801F89E0;
extern s16 D_801F8A10;
extern s16 D_801F8A12;
extern s16 D_801F8A14;
...
register s16 *pa __asm__("$6");
pa = &D_801F8A10;
__asm__ __volatile__("" : "=r"(pa) : "0"(pa)); /* anti-fold fence, §229's technique */
...
*(s32 *)(self + 0x3C) = (*(s16 *)(p + 0) * D_801F89E0 >> 12) + pa[0];
*(s32 *)(self + 0x40) = (*(s16 *)(p + 2) * D_801F89E0 >> 12) + (&D_801F8A10)[1]; /* == D_801F8A12 */
*(s32 *)(self + 0x44) = (*(s16 *)(p + 4) * D_801F89E0 >> 12) + (&D_801F8A10)[2]; /* == D_801F8A14 */
*(s32 *)(self + 0x48) = (*(s16 *)(p + 6) * D_801F89E0 >> 12) + pa[0];
*(s32 *)(self + 0x4C) = (*(s16 *)(p + 8) * D_801F89E0 >> 12) + D_801F8A12; /* SAME address, plain name */
*(s32 *)(self + 0x50) = (*(s16 *)(p + 0xA) * D_801F89E0 >> 12) + D_801F8A14; /* SAME address, plain name */
(✔ byte-checked: src/ov_SC06_006/ov_SC06_006_jr_8017BEBC.c:5490-5519;
asm/ov_SC06_006/.../func_80180FE8.s.) D_801F8A12 is referenced via (&D_801F8A10)[1] (self+0x40)
AND via its own plain name D_801F8A12 (self+0x4C) — and the target shows TWO INDEPENDENT
lui/lh materialisations of it (idx 0x8018105C and again at 0x801810C4), NOT one shared load —
proving cse never unified them, exactly because the two uses are spelled differently. The same holds
for D_801F8A14 (idx 0x80181080 and 0x801810E8). By contrast, D_801F8A10 itself is read via the
SAME spelling (pa[0]) at both its uses (self+0x3C and self+0x48) — and per the note's own account,
an EARLIER draft that spelled every symbol consistently let cse unify all of them into one
register-resident pseudo, evicting other live values and demoting several mflo destinations to a
different register (a net −4 instruction miss relative to target).
The law. When the target materialises the SAME relocatable address independently at every use
(no CSE-shared register), and two or more of those uses are for symbols that are numerically adjacent
in memory, spell them with DIFFERENT address-expression forms — offset-indexing off one symbol's &
for some uses, the adjacent symbol's own name for others — even where both forms are byte-identical.
Using the SAME spelling throughout lets cse unify what should stay independent; mixing forms is the
lever that defeats it. This composes with, but is distinct from, §165-10's cross-jump-ARM identity
dial (which governs whether two ARMS' tails merge) and §229's naming lever (which governs whether an
address survives as a hoisted, call-crossing pseudo at all) — this is cse's straight-line,
single-block unification of repeated SYMBOL_REFs.
SECOND, INDEPENDENT ARRIVAL (wave bs, a different reviewer, same function). The lever was reached again from the other direction, and that pass recorded the NEGATIVE CONTROLS: volatile, an __asm__ barrier and a register pin were each tried first and each failed — they change what the value IS or where it lives, while the thing that has to change is whether cse SEES two occurrences as one expression. Its write-up, kept for the extra spellings:
(P31 S60; byte-proven func_80180FE8, ov_SC06_006, wave bs, MATCH 78/78, reproduced twice including a
full HI16/LO16 relocation-resolve audit against the target .o.)
THE SURPRISE. gcc-2.7.2's cse.c recognises two textually-identical accesses to the SAME extern
symbol within reach of one basic block as the same value and caches its %hi/%lo address in one
register — even across intervening unrelated stores — so a naive extern s16 D_X; ... D_X ... D_X ...
written twice compiles to ONE lui/lh pair reused for both reads. The target func_80180FE8 does
the opposite for two of its symbols: it reads D_801F8A12 twice and D_801F8A14 twice, and
MATERIALISES lui/lh FRESH, independently, at each site — no caching at all. §176-B1 documents the
adjacent problem (declaring aliasing halfwords of one wide word as separate extern u16s loses a
memory DEPENDENCY cse needs); this is the caching side of the same family — cse is TOO willing to
share a repeated symbol read here, and the fix is to make the two C expressions structurally unequal
RTL even though they resolve to the identical relocation.
THE LEVER. D_801F8A10, D_801F8A12, D_801F8A14 are three consecutive s16 halfwords (one
struct's worth of small fields). Access SOME occurrences through offset arithmetic on the FIRST
symbol's address — (&D_801F8A10)[1] is RTL MEM(PLUS(SYMBOL_REF(D_801F8A10), 2)) — and the
REMAINING occurrences through the aliasing symbol's own bare name — D_801F8A12 is RTL
MEM(SYMBOL_REF(D_801F8A12)). Both forms encode to the identical %hi/%lo(D_801F8A12) relocation
pair (arithmetically, D_801F8A10's address + 2 == D_801F8A12's address), but cse's
equivalence-class merge operates on RTL SHAPE, not on the resolved address — a PLUS-offset
expression and a bare SYMBOL_REF are never recognised as the same value, so no merge is even
attempted and both re-materialise. Applied per-site: the FIRST triplet of accesses (offsets 0/2/4 into
the record) used the offset-form; the SECOND triplet (offsets 6/8/0xA) used the plain symbol names —
exactly reproducing the target's four independent materialisations, plus the one legitimately-shared
pair (D_801F8A10 itself, read twice through a genuinely reused, register-pinned pointer via the
standard §176-D1 persistence launder: register s16 *pa __asm__("$6") = &D_801F8A10; __asm__ __volatile__("" : "=r"(pa) : "0"(pa));).
NEGATIVE CONTROLS (all measured, all failed before the mixed-form fix). volatile-qualifying the
reads kills the unwanted sharing but ADDS an unwanted lhu+sll/sra promotion pair per read (+15
ins — wrong shape, not just wrong count). A bare __asm__ __volatile__("" ::: "memory") barrier
between the repeated reads does NOT stop the sharing — cse's constant/address folding crosses a
memory-safe barrier. Pinning the shared address pseudo to a hard register gets silently dropped once
its uses fold back to nothing (§257-2's class). Only the RTL-shape mismatch (offset-form vs
bare-symbol-form) worked, and it costs zero instructions either way.
TELL. Two or more accesses to the SAME extern symbol within one function, where the target
re-materialises lui/lh/lw at EACH access instead of caching one register — and the symbol is one
of a cluster of consecutive same-typed globals (a struct's fields split across separate extern
declarations, or a table's adjacent entries). Before reaching for volatile, an asm barrier, or a pin,
check whether an ALIASING sibling symbol exists at a nearby offset; route one occurrence through
(&sibling)[k] offset arithmetic instead of the symbol's own name to force the RTL shapes apart.
Reserve the shared-register form (one bare name, reused, optionally with a §176-D1 persistence
launder if a call intervenes) only for the specific occurrences the target's own .s shows actually
sharing a register.
§277 — RETURN-TAIL C SPELLING PICKS THE DELAY-SLOT-FILL vs TRAILING-MOVE TOPOLOGY, AND A NARROWER SECOND VARIABLE KEEPS TWO PSEUDOS INSTEAD OF ONE (P31 S60; func_801846F0 ov_SC03_104, func_801A44C4 md_SC07_004, both byte-proven)
The mechanism, part 1 — return c ? A : B topology. For a tail of the shape "compute a
boolean-ish result and return it," gcc-2.7.2 gives structurally different C spellings different
delay-slot topologies. The spelling that reproduces a beqz-to-join branch with the ZERO value
HOISTED INTO THE BRANCH'S OWN DELAY SLOT and NO trailing move $v0,... after the join is:
ret = 0;
if (c) {
ret = A;
}
return ret;
— zero-init, conditional overwrite, return the variable. A ternary, a straight if(c) return A; return B;,
its if/else twin, or the inverted if(!c) return B; return A; each instead give a bnez-over-the-true-arm
layout WITH a trailing move (one extra instruction).
(✔ byte-checked, func_801846F0: src/ov_SC03_104/ov_SC03_104_jr_80182038.c:4357-4361 uses exactly
this form; asm/ov_SC03_104/.../func_801846F0.s shows beqz $v1,.L80184798 with
addu $v0,$zero,$zero in its OWN delay slot, and the epilogue begins immediately at .L80184798 with
no trailing move.)
The companion shape — v1 = func(...); ...; return v1;, assigning the call's result to a temp before
returning, rather than return func(...) directly — is what reproduces a target's
beqz/call/j/addu-$v0 layout when the return value must merge with a zero'd fallback on the
other path:
v0 = func_8012C31C();
if (v0 != 0) {
v1 = func_8012C890((s32)&sp[0], v0, a0);
} else {
v1 = 0;
}
return v1;
(✔ byte-checked, func_801A44C4: src/md_SC07_004/md_SC07_004.c:1125-1129;
asm/md_SC07_004/.../func_801A44C4.s shows beqz $v0,.L801A4558 / addu $a1,$v0,$zero (arg setup) /
jal func_8012C890 on the taken side, addu $v0,$zero,$zero (the v1=0 fallback) at the join
target — matching the temp-then-return spelling, not a direct return func(...).)
The mechanism, part 2 — a narrower second variable resists coalescing. For an abs-value block of
the shape e = d; if ((s16)d < 0) e = -d;, declaring e NARROWER than d (s16 vs d's s32)
keeps them as TWO separate pseudos, so the positive-path copy survives as a real instruction in a
branch's delay slot; declaring e the same width as d (s32) lets gcc coalesce them into one
pseudo and emit an in-place negu with no copy at all.
(✔ byte-checked: src/ov_SC03_104/ov_SC03_104_jr_80182038.c:4342-4355 declares s32 d; s16 e;;
asm/ov_SC03_104/.../func_801846F0.s idx 0x80184758-0x80184760 shows bgez $v0,.L80184764 with
addu $a1,$v1,$zero — a real register COPY — in the delay slot, and negu $a1,$v1 on the taken
branch; both write into $a1 directly, proving d($v1) and e($a1) are separate pseudos.)
The law. (1) For a return c ? A : B-shaped tail, before reaching for any pin or reorder, try
the "zero-init, conditionally overwrite, return-the-variable" spelling (ret=0; if(c) ret=A; return ret;) when the target shows a delay-slot-hoisted zero with no trailing move; try the "assign to a
temp, return the temp" spelling (rather than return func(...) directly) when a call's result must
merge with a zero fallback on the other path. (2) For a value that is copied-then-conditionally-negated
(or similarly copied-then-conditionally-modified), declare the copy's variable NARROWER than the
source when the target shows a real copy instruction surviving in a branch delay slot — same width
lets gcc coalesce the two into one in-place operation and delete the copy.
§278 — ADDENDA HARVESTED FROM WAVE cf (P31 S60): 34 candidates, 13 already covered, 8 sharpenings, 4 new laws
Provenance. Wave cf — the first wave drafted after the soft-429 fix, and the largest single-wave harvest yet reviewed. One Sonnet reviewer read all 34 notes against a corpus that had just grown by §274-§277 from the 18-wave batch earlier the same day, checking each ADDENDUM/NEW claim against the banked C in src/ and the target .s before proposing it.
Verification (R14/G3). Two claims did NOT survive byte-checking and are recorded as refuted rather than laundered in: func_800CAE74's note described a "parameter reassignment" idiom that is not in the banked C at all (the real law is §279), and func_800CBCD4's note named the wrong register ($v1 where the bytes use $v0). At merge I re-verified §279 independently: the target carries one slt and zero sltu, and the banked C drives the bound test with a plain s32 cursor beside a separate s32 * — the direction the law predicts.
Three harness suspicions, not idioms (recorded, not acted on): func_800CAFE4, func_8017F39C and func_800CBC14 each describe a byte-correct body that would not bank. All three resolve to documented mechanisms (§42b stale-object gate trap, §238 overlay-homonym trap), but func_8017F39C suspects a binary-registration mixup with a same-named function in ov_SC01_009 — worth a maintenance look at the GATE side, not the drafting side.
ADDENDUM to §74 (func_800D0E30, resident)
DELIBERATELY CO-PINNING TWO NAMED LOCALS TO ONE HARD REGISTER, NOT JUST TOLERATING INCIDENTAL SCRATCH REUSE
§74's "SCRATCH REUSE" bullet documents an incidental case: gcc uses a pinned register as a temporary
for an unrelated value before the pinned variable's own value lands there, and calls it "usually
benign." func_800D0E30 (resident, 37/37 MATCH) shows the deliberate form of the same fact, usable
as an active decoding lever rather than something to merely tolerate: when the target's disassembly
shows ONE physical register carrying two unrelated short-lived value-chains at two different points in
the function with a genuine gap between them (no instruction anywhere reads chain A's value after
chain B's first write), declare TWO separate register T x __asm__("$N") locals — same $N, different
C names — one per chain, rather than forcing a single C variable to span the whole gap (which would
wrongly extend its live range and risk cse/global-alloc treating it as one value) or leaving the
register unpinned (which gives the allocator no reason to reuse the same physical register for both
chains at all).
Byte evidence (src/resident/resident.c:1369-1398 vs asm/resident/nonmatchings/resident/func_800D0E30.s):
register u32 t3 __asm__("$3") and register u32 h __asm__("$3") are two different C locals sharing
register $3. t3 is defined and consumed only inside the first if block
(t3=(b+0x3C)&0xFF; b=t2|t3;), dead afterward; h is defined right after that block (h=t2>>8;) and
consumed in the second if's condition, entirely inside t3's dead gap; t3 is then redefined fresh
in the second if body. The two pinned locals' live ranges are provably disjoint.
Precondition: verify the gap is real (no read of the earlier chain survives past the later chain's first write) before co-pinning — this is not a general "reuse pins freely" law, it is a same-hard-reg case of §72's "preference, not reservation."
Note also: the PARAM-CARRY half of this candidate's note (assign a packed value back into the parameter variable when the target redefines an argument register in place) is already §167-21 verbatim — covered, not repeated here. Its TWO-USE SPLIT claim (hoisting a shift into its own statement to block a fold) describes an untried counterfactual spelling and was not promoted.
ADDENDUM to §172b-4 (func_80185054, ov_SC03_097)
A hand-written (x + (x>>31)) >> 1 on an explicitly-unsigned expression is sometimes REQUIRED, not just "not required" — §172b-4's own boundary, sharpened.
§172b-4's existing addendum shows a plain (s16)w / 2 (SIGNED context) auto-producing
srl 31/addu/srl 1 from gcc's own constant-division expansion — no hand-rolling needed. §1-I5
separately states an unsigned divide-by-2 emits a bare srl with no bias at all. Both are true and
do not contradict this entry: func_80185054 (ov_SC03_097, MATCH 66/66) needs the FULL 3-instruction
bias tail on a value the C code casts to u32, in a context where gcc's automatic lowering (per §1-I5)
would never emit it, because unsigned division by a power of two needs no correction.
Why. gcc cannot prove a u32 expression is always < 0x80000000, so a literally hand-written
round-toward-zero bias — ((u32)(t*3) + ((u32)(t*3) >> 31)) >> 1 — is not simplified away even though
it is a mathematical no-op for every value the expression can realistically take (t*3 is far below
2^31 here). This is dead-in-practice-but-live-to-the-compiler code, faithfully lowered
instruction-for-instruction: sll/addu (the *3), srl $r,$r,31 (extract bit 31), addu (apply
bias), srl $r,$r,1 (final shift — LOGICAL because the C type of the shifted expression is unsigned).
The discriminator vs the automatic case (§172b-4/§1-I5). If the target's bias tail sits on a value
that is genuinely SIGNED in the source's own arithmetic (no (u32) needed to explain it) — the plain
cast-division (s16)x / K already produces it; do not hand-roll. If the target's tail sits on a value
that is unsigned, OR signed-but-explicitly-cast-to-unsigned for that one sub-expression, and a bare
srl (§1-I5's automatic-unsigned form) is too short by 2 instructions — the bias must be spelled out
literally, (u32) cast included even when the underlying variable is s32/s16.
Evidence. func_80185054: block 1 (u16 dividend, w declared u32) — w*3 then
srl 31/addu/srl 1, all logical, final op srl. Block 2 (dividend is
t = (s16)func_8004787C(...), itself signed) — the identical 3-op tail is reached ONLY by writing
((u32)(t*3) + ((u32)(t*3)>>31))>>1; omitting the cast routes through gcc's own signed-division
machinery, giving a shorter sra-terminated shape that fails to match. Source:
src/ov_SC03_097/ov_SC03_097_jr_80182B9C.c:4084-4085,4093-4094. Target:
asm/ov_SC03_097/nonmatchings/ov_SC03_097_jr_80182B9C/func_80185054.s, idx 8018508C-80185098 and
801850FC-80185108.
ADDENDUM to §265 (func_8017DC80, ov_SC07_002)
A card's embedded seed transcription can itself be a stale sibling copy — re-open the real target .s and diff line-by-line before finalizing, even inside this same verbatim-asm lane.
Symptom. A §265-lane card ships a pre-transcribed __asm__ block as its seed (a prior session's
attempt, or a copy adapted from a structurally similar sibling function). The seed looks right —
same instruction count in the ballpark, same handwritten-asm tells — and it is tempting to trust it
since §265's own workflow is "transcribe verbatim, model nothing," which reads as if there is nothing
left to verify once a transcription exists. Byte-proven case: func_8017DC80 (ov_SC07_002, 346 ins,
banked at src/ov_SC07_002/ov_SC07_002_jr_8017C8D0.c:3298). Its precedent function in the same
handler family, func_80185810, is structurally close enough that a seed built by adapting one for
the other reads as plausible but is not byte-identical.
Mechanism. §265 already establishes that a raw __asm__ body must be transcribed verbatim with no
modelling — but that rule presumes the thing being transcribed is a correct read of this function's
.s. Two concrete deltas surfaced only on a fresh read of the true target, both invisible from the
seed alone: (1) one conditional arm's tail jumps to a shared label (j .L8017E0D4) instead of
duplicating the inline tail the seed had written out (+2 ins if missed); (2) one arm has no nop
between its last lbu …,0x29($v0) and the following j, while a structurally similar twin arm (which
has two more sb stores after the same lbu 0x29) does carry a nop there — the seed had copied
the twin's slot layout onto the wrong arm (+1 ins). Both are exactly the class of instruction §265
says to "leave alone, transcribe it" — but that only holds if the source of the transcription is the
real .s, not a look-alike seed.
The law. Before finalizing a §265-lane transcription, read the actual target .s fresh and diff
it against the seed's __asm__ block instruction-by-instruction — do not trust a seed just because it
already looks like a correct verbatim-asm transcription. This is the same "wrong-body" risk §238
documents for ordinary C drafts, but it survives inside the §265 lane specifically because that
lane's own instruction ("transcribe verbatim, model nothing") reads as license to stop checking once
a transcription exists.
Verification. Confirmed in the current tree: func_8017DC80 is banked verbatim-asm at the cited
line; asm/ov_SC07_002/nonmatchings/ov_SC07_002_jr_8017C8D0/func_8017DC80.s contains both the
lbu $v0, 0x29($v0) / nop pair and, later, the j .L8017E0D4 the note describes.
ADDENDUM to §17 (func_800CB794, md_MAIN_036)
THE ASYMMETRIC LOOP-BOUND LEVER, RESOLVING THE §17-NOTED "IV-FINAL-VALUE ADDRESSING" STUB
§17's own residual notes (the func_8012C2D0 passage) flagged a case where a loop's two bound tests
address logically the same value with two DIFFERENT instruction shapes — one folded (base + N
materialized as a single lui/addiu off the array symbol), one a wholly separate symbol
(lui/addiu of its OWN name) — and concluded "no clean C form (pointer-var, struct-array index)
reproduces the separate base materialization. Still a genuine residual — stub."
The resolved lever, byte-proven on func_800CB794 (md_MAIN_036, MATCH 0x120/0x120): write the two
tests as two DIFFERENT C-level bound expressions, not one reused bound:
u8 *p = D_801202A0;
if (p < D_801202A0 + 0x6480) { /* entry guard: folds to lui/addiu(D_801202A0) + addiu 0x6480 */
do {
...
p += 0x10C;
} while (p < D_80126720); /* back-edge: a SEPARATE, independently-materialized symbol */
}
The entry test's bound is spelled as <array-base> + <constant> (gcc folds this into one
lui/addiu off the base plus a single addiu for the offset). The back-edge test's bound is spelled
as a distinct extern symbol name (D_80126720) even though it is numerically the same address as
D_801202A0 + 0x6480 — this materializes its OWN independent lui %hi/addiu %lo pair rather than
reusing/folding through the base. There is no single bound variable or expression that produces both
shapes; the two tests must be written with two different spellings of "the same" address.
Evidence. asm/md_MAIN_036/nonmatchings/md_MAIN_036/func_800CB794.s: entry guard at
800CB7A4-800CB7B0 (lui $s0,%hi(D_801202A0); addiu $s0,%lo; addiu $v0,$s0,0x6480; sltu); back-edge
at 800CB884-800CB88C (lui $v0,%hi(D_80126720); addiu $v0,%lo(D_80126720); sltu) — two independent
%hi/%lo materializations for numerically-identical addresses. Source:
src/md_MAIN_036/md_MAIN_036.c:287-317.
When it applies. A loop whose entry guard and back-edge test bound the SAME logical range but
whose target asm shows one bound folded (addiu off an already-loaded base) and the other as its own
lui/addiu pair — do not chase a single C bound expression; declare a second extern for the
numerically-identical end address and use it only at the back edge.
(The candidate's induction-variable-fusion half — "write NO second pointer, let gcc's strength reduction fuse the givs" — is fully covered already, §30-2/§52-2/§135-17/§145/§36; not repeated here.)
ADDENDUM to §162p (func_8017C120, ov_MAIN_012)
RUNG 1 ALSO COVERS A POINTER-PLUS-OFFSET ADDRESS, NOT ONLY A BARE COPY OR A GLOBAL'S ADDRESS
§162p rung 1's ladder ("use inside a loop, and the loop already hoists something → write the copy IN
THE LOOP BODY... move_movables carries it to the preheader, in BODY order") was stated for a plain
register-register copy (§48-B extended to dim = flag;). func_8017C120 (ov_MAIN_012, jr_801789AC,
MATCH 34/34) shows the same lever fires for a computed address, s16 *q = (s16 *)p + 20;, when
declared as a loop-body statement inside a loop that already hoists another invariant
(D_80115130[i] array-load chain).
Placement is body-order, exactly as rung 1 predicts. Declaring q's initializer OUTSIDE the loop
(as a preheader statement written by hand) schedules it EAGERLY — before the array-load chain that also
gets hoisted — landing it in the wrong preheader slot. Declaring it as a loop-body statement lets LICM
hoist it, and its position among the OTHER hoisted movables is set by where it sits in the body
relative to them, landing it in the target's slot 47 (immediately after the array-load chain's
preheader materialization).
Precondition already covered elsewhere (§167-33), noted for completeness: the base pointer must be
routed through an actual pointer local (p = &D_80115130; *p = 0;) rather than a bare
D_80115130 = 0;, so both the zero-store and the loop's q = p + 20 derive from the SAME register —
this is §167-33's own law, not new here.
Evidence. src/ov_MAIN_012/ov_MAIN_012_jr_801789AC.c:5843-5850; target
asm/ov_MAIN_012/nonmatchings/ov_MAIN_012_jr_801789AC/func_8017C120.s.
When it applies. A loop-invariant ADDRESS (not just a value) that must land in a specific preheader slot relative to another invariant the loop already hoists — try the loop-body-initializer placement before reaching for statement reordering outside the loop or a register pin.
ADDENDUM to §20 (~L1947) (func_80186AD0, ov_SC06_032)
THE "FILL A LOAD-DELAY SLOT IN A TAIL-STORE SEQUENCE" LEVER ALSO FIRES FROM PLAIN SOURCE-ORDER PLACEMENT, NO MANUAL TEMP NEEDED, AND REACHES PAST A NON-CONSTANT WORD OP
§20's existing entry for this lever requires pulling the deferred field into an explicit named temp one
statement early (c = out.c; <constant stores>; *(short*)(p+0xe)=c;) and is worded only for a tail of
CONSTANT stores. func_80186AD0 (ov_SC06_032, 40/40 MATCH) shows a narrower-effort variant: simply
writing the halfword copy statement *(u16*)(dst+K) = *(u16*)(src+K2); in SOURCE POSITION before an
unrelated statement — here a non-constant *(s32*)(dst+4) |= 0x50000000; word RMW, not another
constant store — is enough on its own; no manual temp hoist is required. sched1 still hoists the
halfword LOAD early (to fill the load-delay slot ahead of the OR's own load) while independently
sinking the STORE to the end of the block, past the OR and three later constant stores.
Read as: the entry's law is broader than its own example — the "tail" can contain a non-constant RMW, and the C lever is plain statement placement, not necessarily a named early temp.
Byte evidence. src/ov_SC06_032/ov_SC06_032_jr_80182890.c:5347-5364 vs
asm/ov_SC06_032/nonmatchings/ov_SC06_032_jr_80182890/func_80186AD0.s:5E9C4-5E9E8
(lhu $a0,0xE($s1) at the OR's load site; sh $a0,0xC($s0) as the LAST store in the block, after
three unrelated constant stores).
(The candidate's other claim — that a register __asm__("$4") pin on the copy source would delete
the copy instruction entirely — describes a spelling the final banked code does not use (it uses a
plain local s32 a; a = s0;); this could not be checked against the banked bytes and was not
promoted. See "could not verify.")
ADDENDUM to §164-56 (func_8017F644, ov_SC04_005)
switch NEVER PRODUCES range_test'S SHAPE; A MIXED TARGET (SOME CASES FOLDED, SOME NOT) MEANS RESPELL AS SEPARATE || GROUPS BY K,K+1 ADJACENCY
Symptom. A target dispatching on a small set of case constants shows range_test's
addiu(-K)/sltiu(2) shape (§164-56) for SOME of the values but a plain andi/compare chain for the
rest — inside what a switch-shaped seed reads as one statement. switch{case 0x31,0x32,0x33,0x27B: ...} compiles via expand_end_case's dense-compare tree (stmt.c), which never calls
fold_truthop/range_test at all: no spelling of a switch can ever reproduce a mixed
range_test/plain-compare target, regardless of case values.
Mechanism, byte-confirmed on func_8017F644 (ov_SC04_005). The case set
{0x31,0x32,0x33,0x27B} splits into exactly two if-groups by §164-56's own K,K+1-consecutive rule:
{0x31,0x32} IS a K,K+1 pair and folds to addiu $v0,$v1,-0x31 ; sltiu $v0,$v0,2 ; bnez — no
andi 0xffff mask, because the operand is a fresh lhu result (already zero-extended, no prior
arithmetic to lose that guarantee). {0x33,0x27B} is NOT K,K+1-adjacent, so range_test's
consecutive-constant test never fires and it stays a plain beq/bne pair — preceded by a defensive
andi $v1,$v1,0xFFFF, because the earlier addiu/sltiu on the same register loses the compiler's
zero-extension tracking on it. Source: two SEPARATE if statements, each its own || pair —
if (x==0x31||x==0x32) return 1; if (x==0x33||x==0x27B) return 1; — banked at
src/ov_SC04_005/ov_SC04_005_jr_8017BEBC.c:5057-5075; asm at
asm/ov_SC04_005/nonmatchings/ov_SC04_005_jr_8017BEBC/func_8017F644.s.
Rule. When a target's case-dispatch mixes range_test and plain-compare shapes: (1) never write it
as a switch — go straight to if/||; (2) group the case constants into pairs strictly by K,K+1
adjacency, one if per pair/singleton, matching whichever pairs the target folded — don't chain all
cases into one ||. §164-56's own "count andi 0xffff between addiu and sltiu" tell can point
backwards here: the FOLDED pair may show no mask (raw unsigned load operand) while the UNFOLDED pair
still shows one (zero-extension tracking lost after the folded pair's arithmetic). Trust the K,K+1-
adjacency test over the mask-counting tell when the two disagree.
ADDENDUM to §237 (func_8017F7FC, ov_SC03_092)
A BLOCK-SCOPE UNPROTOTYPED EXTERN DOES NOT ALWAYS SHADOW A HOSTILE FILE-SCOPE PROTOTYPE
Symptom. §236 point 5's fix ("block-scope the () decl inside your function so it shadows the
prototype") is not universal. func_8017F7FC (ov_SC03_092): a block-scope
extern void func_80178D18(); was written to call the callee WITH an argument, but the TU's dominant
declaration for that identifier is the FULL prototype extern void func_80178D18(void); — present at
file scope (src/ov_SC03_092/ov_SC03_092_jr_8017AE2C.c:2542) and repeated at nearly every call site in
this TU, including line 5358 immediately above the target's own definition. Under C89's composite-type
rule, a declaration of an identifier with external linkage composites with every other visible
declaration of the SAME external object — the inner unprototyped () form does not erase the outer
(void) prototype, it merges with it. The call with an argument then fails "too many arguments" at
the CALL SITE, not a warning at either declaration, which reads as an unrelated breakage because
nothing points at the declaration itself.
Fix. Do not contest the arity via a competing declaration. Adopt the TU's own prototype AND its
cast-at-call-site idiom (§237 "IT FIXES" bullet 2):
extern void func_80178D18(void); ... ((void (*)(s32))func_80178D18)(arg0); — banked verbatim at
src/ov_SC03_092/ov_SC03_092_jr_8017AE2C.c:5358-5364, and the identical pattern recurs for this exact
callee at nine-plus other sites across this overlay family (lines 4872, 5107, 5301, and
ov_SC03_092_jr_80178D40.c / jr_8017A4AC.c / jr_80181694.c).
Boundary for §236-5. When a callee's dominant TU declaration is a full prototype, redeclaring it unprototyped at block scope is not a reliable escape from its arity — cast at the use site instead.
§279 — A do/while (p < end) LOOP UNDER AN ENTRY GUARD: the guard decides the compare, not the loop (P31 S60; wave cf, func_8017F2A4, ov_SC03_096, byte-proven)
§NNN — A DO-WHILE'S GUARD AND LATCH MUST REPEAT THE SAME BOUND EXPRESSION, NOT SHARE ONE LOCAL: THE REPETITION IS WHAT BUYS THE CSE'D COPY INTO A SECOND REGISTER (P31 S60; func_8017F2A4, ov_SC03_096, byte-proven 25/25)
Mechanism. For a do { ... } while (p < end) loop dominated by an entry guard
(if (p < end) { do {...} while (p < end); }), when BOTH occurrences of end are written as the
identical literal expression (base + K) rather than hoisted to a shared named local, cse commons the
two into ONE lui/addiu materialisation at the entry guard — but still emits a plain reg-reg COPY of
that value into a SECOND register for the latch test, because the register holding it at the guard
gets reused as scratch inside the loop body and is not live-range-safe to reuse directly at the
back-edge. A single shared local (s32 end = base + K; referenced at both sites) does not
reproduce this: it keeps one pseudo referenced directly at the latch with no extra copy — one
instruction shorter, and wrong.
Evidence. asm/ov_SC03_096/nonmatchings/ov_SC03_096_jr_8017BEBC/func_8017F2A4.s: entry —
addiu $v1,$a1,0x6480 ; sltu $v0,$a1,$v1 ; beqz $v0,.L8017F300; loop-prep —
addu $a2,$v1,$zero (the copy — $v1 gets reused inside the loop for lhu $v0,0($a1) /
lw $v1,0x20($a1)); latch — addiu $a1,$a1,0x10C ; sltu $v0,$a1,$a2 ; bnez $v0,.L8017F2C8. Banked
source, src/ov_SC03_096/ov_SC03_096_jr_8017BEBC.c:4560-4571, literally repeats
D_801202A0 + 0x6480 in both the if guard and the while condition — no shared local. 25/25 MATCH;
a shared-local rewrite measured one instruction short (24).
When it applies. Any do-while loop with an entry guard sharing a bound with its own back-edge
test, where the guard's bound register is reused for something else in the loop body. Tell: a
lone, otherwise-unexplained addu $rX,$rY,$zero (or OR-with-zero) between the guard's delay slot and
the loop label, copying the value the guard just tested — write the bound expression twice in source,
not once in a local.
(This candidate's harness-pitfall half — "a wrong-path first read poisons every later oracle verdict invisibly" — is COVERED by §238's existing "replayed previous attempt" / stale-shard tells; not repeated here.)
§280 — THE CURSOR'S DECLARED TYPE PICKS sltu vs slt: a T* bound test is UNSIGNED by C rule, an integer cursor is signed (P31 S60; wave cf, func_800CAE74, md_MAIN_031, byte-proven)
§NNN — A LOOP CURSOR'S C TYPE (POINTER vs PLAIN INTEGER) SELECTS sltu vs slt FOR ITS BOUND TEST, INDEPENDENT OF THE VALUES INVOLVED (P31 S60; func_800CAE74, md_MAIN_031, byte-proven; cross-confirmed same wave by func_8017F2A4, ov_SC03_096)
THE LAW. C mandates unsigned comparison semantics for T* relational operators (<, >=, …), so
a loop cursor DECLARED as a pointer (u8 *p; ... while (p < end)) always lowers to sltu, while the
identical byte-address arithmetic done through a PLAIN INTEGER cursor
(s32 p; ... while (p < base+K)) lowers to slt — even though the underlying values, the loop body,
and the generated addresses are byte-identical either way. This is a pure declared-TYPE dial with zero
effect on the address arithmetic itself (both spellings emit the same addiu/addu for the bump and
the same base computation); it only flips the one comparison opcode.
ASM EVIDENCE. func_800CAE74: cursor p/end base+0x1F declared s32, target emits
slt $v0,$v1,$a3 (signed) at func_800CAE74.s:D8, bound hoisted at addiu $a3,$a1,0x1F (line B8).
Independent same-wave confirmation, func_8017F2A4 (ov_SC03_096): the submitting agent independently
reports "sltu forces pointer-typed rec/end (s32 arithmetic emits signed slt)" for a different
record-scan loop in a different overlay — same law, opposite direction, unrelated function.
WHEN IT APPLIES. Any bottom-tested or top-tested loop whose residual is a slt/sltu MISMATCH on
the bound test with everything else (bump, base, body) already byte-identical. Check the target opcode
first: sltu/sltiu ⇒ declare the cursor and its bound as a pointer type; slt/slti ⇒ declare both
as a plain signed integer (s32) and do the pointer arithmetic in integer space (*(u8*)p/*(u8*)s
for the dereference, p += 1 for the bump). Do not reach for a cast-only fix ((s32)(p < end) does
not change the opcode — the DECLARED type of the compared operands is what the signed/unsigned dispatch
reads, not a cast at the compare site) — this needs the variable's own declaration changed, not a
use-site cast.
(The candidate's other claims — a "parameter reassignment" narrative for arg1, and a two-cursor
split — do not survive checking against the actual banked C: arg1 is untouched and lives in $s2
only because it is read across a later call, ordinary call-crossing liveness already covered by
§167-21/§76; not promoted. See the "could not verify" section.)
§281 — GROUP COPY-THEN-RMW BY OPERATION KIND, NOT FIELD BY FIELD: sched1 does the interleaving, the source must not (P31 S60; wave cf, func_80180FB4, ov_SC03_111, byte-proven)
§NNN — A DEPENDENT COPY-THEN-RMW BLOCK MUST BE SOURCE-GROUPED BY OPERATION KIND, NOT BY FIELD, AND SCHED1 DOES THE INTERLEAVING (P31 S60; func_80180FB4, ov_SC03_111, byte-proven)
The symptom. A run of N dst[k] = src[k] field copies immediately followed by N dst[k] += C
read-modify-writes on the SAME destination fields (a struct/array init-then-adjust idiom) diffs as a
scheduling residual no amount of pin/fence work closes — because the actual target instruction stream
is neither "all copies then all RMWs" nor "each copy immediately followed by its own RMW"; it is an
sched1-interleaved mixture where a LATER copy's load/store is threaded in among an EARLIER field's RMW
chain.
The law. Write the SOURCE in two flat groups — every copy statement first, in field order, then every RMW statement, in field order:
p[4] = s0[3];
p[5] = s0[5];
p[6] = s0[7];
p[4] += 8;
p[5] += 0x88;
p[6] += 8;
Do not pair each copy with its own RMW (p[4]=s0[3]; p[4]+=8; p[5]=s0[5]; p[5]+=0x88; ...) — that
spelling collapses via store-to-load forwarding and loses instructions the target keeps. sched1,
working within the basic block, reorders and threads the resulting RTL into the target's exact
interleave on its own; the grouping is the only C-level lever, and there is exactly one grouping (of
the small number of legal topological orders respecting each RMW's data dependency on its own copy)
that reaches the target.
Evidence. func_80180FB4 (ov_SC03_111, jr_8017EC58, MATCH 40/40, 0xA0 bytes). Source:
src/ov_SC03_111/ov_SC03_111_jr_8017EC58.c:3661-3676. Target:
asm/ov_SC03_111/nonmatchings/ov_SC03_111_jr_8017EC58/func_80180FB4.s. Instruction trace confirms the
interleave: lw $v1 (p) → copy1 load+store → copy2 load+store → RMW1-load → copy3-load (threaded
in) → RMW1-add/store → RMW2-load → copy3-store (threaded in) → RMW3-load → RMW2-add → RMW3-add →
RMW2-store → RMW3-store.
When it applies. Any target where several fields are first copied from a source struct and then each independently adjusted by a constant, and the naive per-field grouping (copy;RMW;copy;RMW;...) undershoots the target's instruction count.
Homonym caution. func_80180FB4 also exists (a different body) in asm/ov_SC06_022/ — confirm
the overlay before transcribing (§238).
§282 — gcc's OWN LOOP REVERSAL PUTS THE COUNTER INIT AFTER THE HOISTED MOVABLES — a position no hand-written down-count can reach (P31 S60; wave cf, func_80180DD8, ov_SC04_005, byte-proven)
§NNN — WRITE THE UP-COUNT LOOP: gcc's OWN REVERSAL PRODUCES A COUNTER-INIT INSTRUCTION THAT LANDS AFTER HOISTED MOVABLES, WHICH A HAND-WRITTEN DOWN-COUNT LOOP CANNOT REPRODUCE (P31 S60; func_80180DD8, ov_SC04_005, byte-proven)
The symptom. A target loop counts DOWN from a constant with a bgez/addiu -1 backedge, and the
counter's addiu $reg,$zero,K initializer sits AFTER several other pre-loop constant loads the loop
hoists (e.g. switch-dispatch case constants). Every hand-written down-count C loop
(for (i = K; i >= 0; i--)) emits its counter init BEFORE those hoisted constants — an unclosable
off-by-position residual, because the down-count statement's LUID precedes loop_start's insertion
point for the movables loop.c hoists, no matter how the source is reordered.
The law. Write the loop as an UP-count in the source:
for (i = 0; i < N; i++) { ...switch(*p){...}... }. gcc-2.7.2's own loop-reversal machinery (the
strength-reduction pass that turns a provably-monotonic up-counting biv into a dbra-style
down-count when doing so is cheaper — a bare decrement-and-test-sign beats an increment-and-compare)
rewrites it into the down-count-from-(N-1)/bgez form ITSELF, as a LATE transformation. Because this
reversal happens after the movable-hoisting pass has already placed the loop's hoisted invariants in
the preheader, the new reversed-counter's init instruction is inserted where loop.c puts it — AFTER
the already-hoisted movables — reproducing the target's exact instruction order. A hand-written
down-count loop supplies its own counter-init statement at ordinary source-LUID position, which
predates the movable-hoisting insertion point, and gcc has no later pass that moves a source-level
down-count init downward past movables it did not itself just create.
Evidence. func_80180DD8 (ov_SC04_005, jr_8017BEBC, MATCH, 0x70 bytes / 28 ins). Source:
src/ov_SC04_005/ov_SC04_005_jr_8017BEBC.c:6237-6255 — plain for (i = 0; i < 0x60; i++). Target:
asm/ov_SC04_005/nonmatchings/ov_SC04_005_jr_8017BEBC/func_80180DD8.s lines 4-10 — four addiu loads
of the switch constants (0x1FC,0x1DC,0x210,0x219) at 80180DE4-80180DF0, THEN
addiu $a0,$zero,0x5F at 80180DF4 (the reversed counter init), loop body,
bgez $a0,.L80180DF8 / addiu $a0,$a0,-0x1 backedge at 80180E38-80180E3C. The sibling
func_80180E48 in the same TU (already matched, house style) independently confirms the up-count
spelling for this loop family.
When it applies. Target shows a down-count bgez/decrement loop whose counter-init constant sits
AFTER one or more other hoisted pre-loop constants, and no reordering of a hand-written down-count
loop's source statements relocates the init past them. No jump table need be present (this target's
dense 4-case switch folds to compares, not a jtbl).
What I could not verify
-
func_80184458 (ov_SC04_005): could not independently re-verify the reported 41-vs-40-instruction delta between the pass-by-address and pass-by-value spellings — only the banked target is on disk, not the rejected counterfactual draft. The underlying mechanism (§229's declaration corollary) is already general and doesn't depend on this one instance to stand.
-
func_80181600 (ov_SC01_009): accepted the "missing TU decl → implicit-int → extend-tell silently drops the promotion pair" claim on the strength of §172b-1's already-proven mechanism plus the already-documented standalone-compile fact; did not independently re-derive the implicit-int interaction from gcc internals in the time available.
-
func_800CAE74 (md_MAIN_031) — reviewer claim did NOT survive checking. The original note's claim that the target's dataflow is a special "parameter reassignment" idiom (
arg1 = *(arg1+0xC)parking the incoming register for a later tail call) does not match the actual banked C, which uses an ordinary fresh local whilearg1itself stays untouched and lives in$s2purely because it is read across a laterjal— ordinary call-crossing liveness (§167-21/§76 family), not a distinct mechanism. The genuinely new content of this candidate (pointer-vs-integer cursor typing selectingsltu/slt) was separated out and promoted as its own NEW section above. -
func_800CBCD4 (md_MAIN_031) — reviewer claim did NOT survive checking. The original note's claim 5 ("the 4th else copy needs its own fresh temp so its value lands in v1 like the target's lhu v1/sh v1 pair") is factually wrong: the actual 4th else-copy in the target
.suseslhu $v0/sh $v0, not$v1. The rest of the note resolves to already-covered law (§193-H, §247), so this candidate was rejected outright rather than partially promoted. -
func_80186AD0 (ov_SC06_032): the note's claim that a
register __asm__("$4")pin on the copy source deletes the copy instruction and drops the delay-slot fill could not be checked — the final banked code uses a plain local (s32 a; a = s0;), not the pinned spelling the note describes, so there is no byte evidence for that half of the claim in the current tree. Only the statement-order half (verified) was promoted. -
func_800CB428 (md_MAIN_031): the note's claim that a literal
do-while+breakspelling would have compiled to a rotated/duplicated-header shape (vs the goto-based loop actually used) is a counterfactual about an untried spelling and cannot be checked against the banked bytes; not promoted, though it is consistent with the already-documented §17775/F5 "goto/label loop is invisible to loop.c" fact.
Harness-defect flags (not idioms, called out per the assignment's request): three candidates (func_800CAFE4, func_8017F39C, func_800CBC14) describe repeated non-banking of a body the submitting agent had already proven byte-correct multiple ways, with the residual explicitly attributed to harness/gate state rather than source — func_8017F39C in particular flags a suspected same-name binary-registration collision (a homonym in ov_SC01_009 sharing the symbol) as the likely cause if the non-bank persists. All three resolve to already-documented classes (§42b's stale-object gate trap, §238's overlay-homonym trap) rather than new gaps, but are flagged here in case the underlying gate behavior itself — not just the drafting-side diagnostic — merits a maintenance look.
§283 — ADDENDA HARVESTED FROM THE 36-WAVE BATCH (P31 S60): 1,216 candidates, 859 already covered, 46 sharpenings, 9 new laws
Provenance. Waves cg..dt — the ore that accumulated while the campaign ran uncollapsed waves. Five reviewers on disjoint wave groups (D1 dd/de/df · D2 dg–dm · D3 cg–cm · D4 cn–cw · D5 cx–dt), each checking every ADDENDUM/NEW claim against the banked C in src/ and the target .s before proposing it. 859 COVERED is the healthy result: §193–§282 are earlier rounds of this same cycle.
What verification caught, both directions. Six reviewer claims were REFUTED at merge rather than laundered in — three 'the harvester leaked an unbanked NEAR into a banked-only harvest' reports (all three functions are genuinely banked; the notes were written before their gate landed and read as harvester bugs hours later), a 'match_one resolves targets by bare symbol name' claim (it resolves an explicit path, and the drafting path always passes it), and two idiom claims whose mechanism was absent from the banked code. §284's fence was described as NON-volatile; the banked code uses __asm__ volatile and the text is corrected here, because that is a detail readers copy verbatim.
The most-rediscovered gap. Eight cards across four waves independently re-derived that the whole-object gate needs every sibling matched — implicit in the corpus, never stated as its own law. Recorded here as the strongest signal of a missing section; it wants writing up properly rather than being buried in an addendum.
A process note for whoever runs the next batch. Forked sub-reviewers exceeded their brief in three of five groups: one re-derived five waves it was not assigned and self-merged over the shared output path, one silently dropped 11 rows including a whole function, one produced nothing. Each parent caught its own fork. Tell forks explicitly not to spawn further forks — they infer it from the parent's own earlier actions in inherited context — and give each one a private output path.
ADDENDUM to §164-75 — the fold-reassociation law also fires at a variable's INITIALIZER, not only a later expression
Confirms and extends §164-75 ("FOLD RE-ASSOCIATES A CONSTANT OUT OF AN INLINED +/-; ONLY A STATEMENT BOUNDARY PINS IT"), whose own scope note flags it as "one function, one direction... measure it together with §163z's independent claim." func_801916BC (ov_SC06_033, wave dd, MATCH 24/24) supplies a second, independent instance of the same underlying fold/RTL-reassociation law, at a DIFFERENT syntactic position: a pointer's own INITIALIZER, not a mid-expression arithmetic term.
Byte evidence. src/ov_SC06_033/ov_SC06_033_jr_8018D98C.c:5087:
p = &D_801CB512;
q = &D_801CB512 - 1;
asm/ov_SC06_033/nonmatchings/ov_SC06_033_jr_8018D98C/func_801916BC.s idx 5-7:
addiu $v1, $v1, %lo(D_801CB512)
addiu $a1, $v1, -0x2
$a1's base operand is $v1 — the register that JUST received D_801CB512's own address one instruction earlier — not a fresh %hi/%lo materialization of D_801CB512 - 2. This is the same fingerprint §164-75 already names ("an addiu $rD,$rS,-K whose $rS is the WRONG-looking operand of a nearby address computation — same instruction count, one register wrong, no length drift") but observed in an INITIALIZER, where the alternative failure mode is not "the constant rides the wrong operand of a nearby addu" but "the compiler independently materializes the offset pointer from scratch instead of folding it onto the just-computed base register."
The law, extended. §164-75's fold/reassociation law is not confined to a later arithmetic expression — it also governs how gcc-2.7.2 builds a SECOND pointer's initializer immediately adjacent to a first pointer's own address materialization. Writing q = &SYM - K; directly (not through an intermediate decrement inside a loop, and not as q = p - K; off an already-named p) is what keeps the -K riding the SAME register the base symbol's address just landed in, reproducing a single addiu $rD,$baseReg,-K rather than a fresh lui/addiu pair.
TELL. A target's second pointer-typed local is initialized with a single addiu off the register that the PREVIOUS statement's symbol address just computed (not a fresh %hi/%lo pair) ⇒ write that second local's initializer as a literal &SYM ± K expression, evaluated immediately after the first pointer's own &SYM statement — do not decrement inside the loop and do not route it through the first pointer's own C variable.
ADDENDUM to §21 — the bltz+slti (or N-separate-compares) signed-range-split bullet is now CONFIRMED on three independent functions, and generalizes beyond lbu/u8
Two independent byte-proofs land on the same bullet this batch — merged into one entry.
Instance A — the exact bltz+slti mechanism, second byte-proof (wave dd, func_800CCBF0, md_MAIN_042).
The bullet at cookbook L1984 ("lbu value with SIGNED compares (bltz + slti, NOT sltu) → write each range bound as a SEPARATE if (…) goto statement, never a chained ||") is marked "⚠ CANDIDATE (unverified) — evidence func_801775E0, a NEAR-MISS (closeness 2, NOT byte-banked)." func_800CCBF0 (md_MAIN_042, wave dd, MATCH 92/92) is a second, independent, fully byte-banked instance of the identical mechanism.
Byte evidence. src/md_MAIN_042/md_MAIN_042.c:66-72:
s32 var = *(s32 *)(a0 + 0x23C);
if (var == 0) {
...
} else {
if (var >= 0) {
if (var < 7) {
...
asm/md_MAIN_042/nonmatchings/md_MAIN_042/func_800CCBF0.s idx 22-24:
bltz $v1, .L800CCC54
slti $v0, $v1, 0x7
beqz $v0, .L800CCC54
— exactly the target-side fingerprint the L1984 bullet predicts (a provably-non-negative-looking s32 field still carrying a real bltz, immediately followed by an independent slti, both as SEPARATE nested if statements rather than a chained if (var >= 0 && var < 7)). This is a different function, different overlay/binary, and a different reviewer/wave than the original func_801775E0 near-miss, closing exactly the gap the original bullet asked for ("A drafter MAY try them... but VERIFY before trusting").
The law, upgraded. Remove the "⚠ CANDIDATE (unverified)" qualifier from the L1984 bltz+slti bullet — it is now confirmed byte-banked on two independent functions (func_801775E0's original near-miss evidence, and func_800CCBF0, md_MAIN_042, MATCH 92/92). Keep the bullet's prescription unchanged: split a signed range/sign guard on a value that LOOKS unsigned (u8/small non-negative field) into separate nested if statements — never a chained &&/|| — whenever the target shows an independent bltz immediately followed by a separate slti/sltiu.
Instance B — the fold generalizes beyond lbu/u8 to a plain s32 local, and this is (at least) the THIRD confirmation overall (wave dd, func_8017FD34, ov_SC05_017).
Symptom/context. §21's bullet (originally appended from a NEAR-MISS, func_801775E0, and still marked "⚠ CANDIDATE (unverified) ... NOT confirmed by a byte-match") describes chained bound tests folding into a sltu-based unsigned range trick when the source reads a u8 global via lbu, prescribing a separate if (...) goto per bound instead of a chained ||. func_8017FD34 (ov_SC05_017, MATCH 33/33) shows the identical fold — and identical fix — firing on a PLAIN s32 local holding a function-call return value, never loaded with lbu at all.
Byte evidence. src/ov_SC05_017/ov_SC05_017_jr_8017AE2C.c:5892-5920; target asm/ov_SC05_017/nonmatchings/ov_SC05_017_jr_8017AE2C/func_8017FD34.s. The dispatch value code (an s32 local, set from two different call-result chains) is tested against 1 and 2 with THREE separate compares — beq $v1,$v0(1), slti $v0,$v1,2, bne $v1,$v0(2) — exactly the un-folded shape §21's bullet describes, reproduced by writing the checks as a ladder_done: if (code==1) goto do_call; if (code<2) goto epilogue; if (code!=2) goto epilogue; chain rather than a chained code==1||code==2 test.
The boundary, sharpened. The fold this bullet describes is a general property of gcc-2.7.2's range_test/fold_truthop machinery acting on any small-integer comparison chain against ADJACENT constants — it has nothing to do with lbu's zero-extension per se. The lbu+signed-compare framing in the existing bullet is one INSTANCE (where the un-collapsed shape happens to keep a provably-dead bltz), not the boundary of when the lever is needed. Apply the same separate-if-per-bound fix whenever a target keeps N separate compares against small adjacent constants instead of a folded sltu/addiu-range test, regardless of the tested value's declared width or load instruction.
Also worth recording. This is at least the THIRD independent byte-proof of this bullet's core mechanism (alongside func_800CCBF0/md_MAIN_042, cited at §274's ADDENDUM to §1-I5, and the original func_801775E0 context) — the "⚠ CANDIDATE (unverified)" banner immediately above the bullet (cookbook ~L1980-1983) is stale and should be removed at the next corpus edit pass.
ADDENDUM to §202 — the DEF-SIDE ALIAS also resolves a function-vs-DATA-symbol identifier clash, not only a function-vs-function prototype clash
§202 documents the definition-side __asm__ alias escape for a case where the destination TU declares the SAME function under a conflicting FUNCTION prototype (its own worked example: TU-wide void, real body s32). func_80181940 (ov_SC06_000, wave dd, MATCH 45/45) shows the identical fix required by a different TRIGGER: the TU declares the function's own symbol as a plain DATA object, used as a stored callback VALUE, not as a function prototype at all.
Byte evidence. src/ov_SC06_000/ov_SC06_000_jr_8017AE2C.c:7219-7230:
extern s32 func_80181940;
...
((s32 (*)(s32 *))func_8012A568)(&func_80181940); /* install-handler registration, elsewhere in the TU */
...
void aF80181940(void) __asm__("func_80181940");
void aF80181940(void) {
...
}
The TU-wide extern s32 func_80181940; is a data declaration (the symbol's address is registered as a callback pointer via func_8012A568); defining void func_80181940(void) { ... } directly under that identifier is a conflicting-types DEFINITION-side wall a standalone match_one compile cannot see (§25's over-prediction class), because the standalone draft never carries the TU's competing data decl.
The law, extended. §202's def-side alias escape is not limited to a function-vs-function prototype/return-type clash — it equally resolves a function-vs-DATA-symbol clash, where the destination TU has declared the INCLUDE_ASM function's own name as an extern <scalar-type> object (typically because some other function in the TU takes its address as a callback/handler value). Define the body under a private alias identifier (aF<addr>) bound with __asm__("func_<addr>"); the TU's data declaration and its callback-registration call site keep working untouched, and the linker resolves both spellings to one symbol. Symptom to index this under: "an INCLUDE_ASM function's own name already appears as extern s32/extern u8/etc. (not a function prototype) elsewhere in the destination TU."
ADDENDUM to §22 (volatile-qualified-global reload lever, cookbook ~L1922) — for a NON-constant, same-address double RMW, a plain memory clobber beats volatile, and volatile actively breaks a delay-slot fill
Symptom. §22 prescribes volatile to force a reload between two consecutive stores to the SAME global when gcc constant-folds/forwards a just-stored LITERAL. func_80182AB4 (ov_SC02_031, MATCH 44/44) shows the boundary case where volatile is the WRONG lever: two |= read-modify-writes to the SAME memory word (*(u32*)(p+4) |= 0x50000000; then *(u32*)(p+4) |= 0x80000000;), where the second RMW's value is not a compile-time-known constant merge (it depends on the first RMW's runtime result), and the target additionally needs the SECOND store to remain schedulable into a following jal's delay slot.
Byte evidence. src/ov_SC02_031/ov_SC02_031_jr_8017AE2C.c:7175-7179:
*(u32 *)(p + 4) = *(u32 *)(p + 4) | 0x50000000;
__asm__("" ::: "memory");
*(u32 *)(p + 4) = *(u32 *)(p + 4) | 0x80000000;
func_80128EA8((s32)p, t, (s32)&D_800D3888);
Measured on this function (per the drafting note, consistent with the committed source): qualifying the field volatile does defeat the unwanted store-forwarding (the second load becomes a genuine re-read instead of reusing the first RMW's known-result register) but ALSO pins the second store above the following call, leaving the jal's delay slot a bare nop (a 13-instruction residual, near-miss). The bare, non-volatile __asm__("" ::: "memory") clobber between the two RMW statements gets the same forwarding-defeat WITHOUT constraining scheduling — the second store still sinks into the jal's delay slot, matching the target exactly (MATCH 44/44).
The law, sharpened. §22's volatile lever is for the specific case of re-reading a just-stored COMPILE-TIME CONSTANT (gcc's constant-fold path, which only volatile defeats). For a same-address double RMW where the SECOND value is not a known constant (an OR/AND of a runtime-derived first result), a plain non-volatile memory-clobber barrier between the two statements already defeats the unwanted store-to-load forwarding — and, unlike volatile, it does not additionally forbid the store from being scheduled into a later instruction's delay slot. Reach for volatile only when the re-read value is provably a literal gcc can constant-fold; otherwise the plain clobber is both sufficient and strictly cheaper on scheduling freedom.
ADDENDUM to §195-E — a goto-ladder's STORES must sit AT the labels, after the gotos, not inline before them
Symptom. A flag-materialising join (§195-E's own shape: bnez/beqz branches each carrying a
constant in their own delay slot, converging on one consumer test) is attempted as a goto ladder —
already §195-E's documented technique for "naming across a join" beyond plain if/else — but the
draft still collapses to jump.c's store-flag sltiu form on one or more arms, or the target's per-arm
delay-slot constants don't materialise.
Mechanism, byte-verified (func_801E56A8, md_SC03_135, MATCH 71/71,
src/md_SC03_135/md_SC03_135_jr_801E5358.c:363-397;
asm/md_SC03_135/nonmatchings/md_SC03_135_jr_801E5358/func_801E56A8.s). The target keeps every arm's
flag store in its OWN branch's delay slot (addu $v0,$zero,$zero for the two zero arms,
addiu $v0,$zero,0x1 for the "u>=0x12C" arm, and a plain fall-through addiu $v0,$zero,0x1 for the
"one" arm), with all three true-arms landing at the same consumer beqz (.L801E570C). The banked
source writes the ladder as:
if ((u32)(u - 0xC8) >= 0x64) {
if (u >= 0x12C) {
if (func_80029178(0xFA) & 0xFF) {
goto zero;
}
} else {
goto one;
}
} else {
goto zero;
}
flag = 1;
goto eval;
zero:
flag = 0;
goto eval;
one:
flag = 1;
eval:
if (flag != 0) { ... } else { ... }
Every test reaches a bare goto with NO store attached to the test itself; the stores live entirely
at the two labels (zero:/one:), each holding exactly one assignment followed by goto eval;. An
earlier attempt on this same function that wrote store-then-goto INSIDE each arm (i.e. put the
flag = 0;/flag = 1; assignment before the goto, at the branch site rather than at the label)
emitted store-then-branch instead of branch-with-delay-slot-store, and did not reach MATCH.
The law. For a §195-E-shaped goto-ladder join, the constant assignments must sit AT the labelled
targets (after the gotos land), not inline before the gotos that jump to them. This gives reorg
multiple independent fall-through/branch candidates — one store per incoming edge — so each bnez/
beqz steals its own arm's constant into its own delay slot exactly as the target shows. Writing the
store before the goto at the branch site instead produces a store-then-unconditional-branch
sequence that reorg cannot redistribute into each test's own slot.
When it applies. Any §195-E join reached via a goto ladder (not plain if/else) where a
store-before-goto spelling leaves the target's per-arm delay-slot constants unreproduced, or where
jump.c's store-flag sltiu collapse persists on one or more arms despite the ladder shape.
ADDENDUM to §195-N — a GNU statement-expression slider must sit INSIDE the conditional arm's value position, not as a post-hoc barrier, to block the store-flag transform on a ternary chain
Symptom. A chained-ternary flag computation (flag = A ? 0 : (B ? 1 : (C ? 0 : <expr>));) that
should reach one arm via a genuinely-computed value collapses one leaf to jump.c's sltiu
store-flag transform, and a zero-byte __asm__ fence placed AFTER the assignment (§195-N's own
"outer if + __asm__ slider blocks the store-flag transform" aside, stated there only for a
call-bearing if (f()) return 1; chain) is inert against it.
Mechanism, byte-verified (func_801E559C, md_SC03_135, MATCH,
src/md_SC03_135/md_SC03_135_jr_801E5358.c:226-241;
asm/md_SC03_135/nonmatchings/md_SC03_135_jr_801E5358/func_801E559C.s). The banked source is:
flag = ((u32)(t - 200) < 100u) ? 0
: ((t < 300) ? 1
: (((func_80029178(250) & 0xFF) ? 0 : ({ __asm__(""); 1; }))));
if (flag) {
*(s32 *)(*(s32 *)&D_801E256C + 4) = (s32)D_801E68BC;
} else {
*(s32 *)(*(s32 *)&D_801E256C + 4) = (s32)D_801E68F0;
}
The GNU statement-expression ({ __asm__(""); 1; }) sits as the innermost ternary's ELSE-arm VALUE
itself — not between the ternary and the if, and not between an assignment and its later use. An
earlier draft (per the note, "draft #22") placed an equivalent slider AFTER the whole assignment,
between it and the consuming if; that placement is inert because jump.c's fold-based store-flag
pattern match on x=0; if(c)x=1;-shaped trees runs at TREE-EXPANSION time on the ternary's own
sub-expression, before a post-hoc statement barrier downstream of the completed assignment can have
any effect. Placing the barrier inside the arm's own value expression prevents the constant-folding
tree match from recognising that one leaf as a bare 0/1 in the first place, which is what lets
reorg later distribute each leaf's constant into its own branch's delay slot instead of jump.c
collapsing the tail to sltiu. The note also records that the if/else arms must each hold their
OWN store (not a shared post-join store through a selected pointer) to prevent jump-threading/arm
duplication — consistent with, and reusing, the tail-shape lever already sourced from the banked twin
func_8017F9D8 (ov_SC03_124).
The law. A zero-byte __asm__ "slider" that is meant to block jump.c's store-flag/constant-fold
transform must sit INSIDE the value position of the specific conditional arm whose 0/1 leaf must
survive as a real branch target — as a GNU statement-expression (({ __asm__(""); 1; })) supplying
that arm's value — not as a bare statement-level fence placed between the completed assignment and
its later use. §195-N's own aside documents the slider for a different C shape (a call-bearing
if-chain closed by return 0;); this extends the same "asm barrier defeats jump.c's constant-fold
recognition" mechanism to a chained-ternary flag computation, and pins down WHERE the barrier must be
written for it to work.
When it applies. A chained ternary (or similar nested conditional-value expression) where one leaf
is a compile-time constant and the target's branch to that leaf carries the constant in its own delay
slot (i.e. §195-E's "named across a join" shape), but a sltiu/store-flag collapse persists on that
leaf despite an attempted asm-fence placed after the assignment.
ADDENDUM to §176-B2 — in a micro-function with no long/short lifetime asymmetry, BOTH contending pseudos need their own hard-register pin
Symptom. §176-B2 prescribes pinning the short-lived INTERLOPER (not the long-lived contested value) when local-alloc's first-fit hands one hard register to two competing pseudos. In a very short leaf function, pinning only one of the two contenders does not close a REGALLOC-PERM residual, and the section's own precondition (a long-lived value vs. a short-lived single-use constant) does not obviously identify which pseudo is "the interloper."
Mechanism, byte-verified (func_80181EF0, ov_SC02_031, MATCH 12/12,
src/ov_SC02_031/ov_SC02_031_jr_8017AE2C.c:6852-6861;
asm/ov_SC02_031/nonmatchings/ov_SC02_031_jr_8017AE2C/func_80181EF0.s). The function is 12
instructions total — every pseudo in it is short-lived by construction, so there is no long-lived
value for a single interloper pin to protect. The banked source pins BOTH:
register s32 m __asm__("$4");
register s32 c __asm__("$3");
m = D_80078EBA;
c = 4;
if (m == c) {
return (u32)(D_80078EB1 - 7) < 5;
}
return 0;
(✔ byte-checked: the target's lbu $a0,%lo(D_80078EBA)($a0) and addiu $v1,$zero,0x4 land in $a0
($4) and $v1 ($3) exactly as declared.) An unpinned draft left both values on a REGALLOC-PERM
$v0>$v1>$a0 residual that no source-level reordering (named local, operand swap, ternary, zero-init
accumulator) moved — consistent with §137's "source-level levers are a dead end" for this class.
Pinning only m to $4 closed part of the gap (4→2); the residual only fully closed once c was
ALSO pinned to $3.
The law. §176-B2's "pin the interloper, not the contested value" presumes a long/short lifetime
asymmetry between the two competing pseudos, which lets you identify one of them as the thing to move
out of the way. A micro-function (single basic block, every value short-lived) has no such asymmetry —
first-fit can hand the wrong register to either pseudo, or both. When the target's own registers for
BOTH contenders are known (read them off the .s), pin BOTH pseudos directly to their target hard
registers rather than searching for a single interloper to displace.
When it applies. A REGALLOC-PERM residual of 1-4 instructions in a short, single-block leaf function where §176-B2's single-interloper-pin approach does not close the gap and no clear long-lived value exists to leave unpinned.
ADDENDUM to §199-G — A default: LABEL GROUPED ONTO THE LAST CASE REMOVES THE j default TAIL, EVEN THOUGH THE 2-NODE HEADER STAYS ALL-POSITIVE
Symptom. A 2-case-node switch whose target .s shows the expected all-positive equality-test header (§199-G's own tell), but with no trailing j <default> at all — the second test's fall-through lands directly inside a CASE body, not a separate default arm.
Mechanism, byte-verified (func_8017DFE4, ov_SC03_011, 42/42 MATCH). Source
(src/ov_SC03_011/ov_SC03_011_jr_8017C730.c:3547-3567):
void func_8017DFE4(s32 param_1)
{
s32 state;
state = func_801399F0(*(s32 *)(param_1 + 0x198));
if (state != 0) {
func_80139914(*(s32 *)(param_1 + 0x198));
*(s32 *)(param_1 + 0x198) = 0;
switch (state) {
default:
case 1:
*(s32 *)(param_1 + 0x198) = func_8013767C((s32)&D_80185F80);
func_80171990((u8 *)param_1);
break;
case 2:
*(s32 *)(param_1 + 0x198) = func_8013767C((s32)&D_80185FA8);
*(u8 *)(param_1 + 0x216) = 5;
break;
}
}
}
Target (asm/ov_SC03_011/nonmatchings/ov_SC03_011_jr_8017C730/func_8017DFE4.s):
beq $s1, 1, .L8017E034
sw $zero, 0x198($s0) ; delay slot
addiu $v0, $zero, 2
beq $s1, 2, .L8017E058
nop
.L8017E034: ; case 1 AND default fall straight in here
lui $a0, %hi(D_80185F80)
...
Both equality tests remain positive (beq→.L8017E034 for state==1, beq→.L8017E058 for
state==2), matching §199-G's header shape exactly — but there is no third branch and no j default: when neither test fires, control simply falls out of the header into .L8017E034,
which is also case 1's own body. This is only possible because default: and case 1: share
one label in the source — emit_case_nodes (stmt.c) has no separate default arm to jump to, so
expand_end_case's emit_jump_if_reachable(default_label) (stmt.c:4909, the very call that
produces the j default §199-G's rule 5 says is unconditional) has nothing to jump to and emits
nothing.
The law. §199-G rule 5 states that an explicit default: still emits the all-positive header
and a j default. That holds only when default: has its own body/label. When default:
is grouped onto (shares a label with) one of the case arms, the trailing j default disappears —
the header still reads all-positive per node, but the LAST node's fall-through is itself the
shared arm, not a jump. Tell: a 2-node all-positive header with NO trailing j at all, where
the fall-through address is a real case body rather than a distinct join/epilogue block ⇒ write
default: stacked directly above that case's label in the switch, not as a separate arm.
Boundary. Distinct from §199-G's own "empty arm" bound (rule 2, case 0: break; folding onto
the join and inverting the SECOND test) — here neither arm is empty; the grouped label is what
removes the jump, not label-collapse from a lack of body.
ADDENDUM to §220 — REFERENCING THE RAW PARAMETER (NO NAMED COPY, NOT EVEN A PIN) LETS THE CALLEE-SAVED SPILL LAND IN THE FIRST CALL'S OWN DELAY SLOT
Symptom. A function's prologue shows sw $sN (the callee-saved slot) BEFORE an unrelated
lui/addiu address materialization, with the actual addu $sN,$aM,$zero copy embedded inside a
following jal's OWN delay slot — not as a separate early move. Every spelling that introduces an
explicit copy variable for the parameter (plain named local, register __asm__ pin, or a
non-volatile fence around the copy) reorders this: the sw $sN slides to AFTER the lui/addiu
pair, or an extra move appears.
Mechanism, byte-verified (func_8017E8B0, ov_SC03_002, 79/79 MATCH,
src/ov_SC03_002/ov_SC03_002_jr_8017D604.c:3280-3299):
void func_8017E8B0(void *a0) {
s32 v1;
if (func_8012C354((s32)a0, (s32)&D_801892C8) == 0) {
return;
}
...
No s0 = a0; (or pinned/copy variant) appears anywhere — a0 is referenced directly at every
site through the rest of the function. Target
(asm/ov_SC03_002/nonmatchings/ov_SC03_002_jr_8017D604/func_8017E8B0.s):
addiu $sp, $sp, -0x18
sw $s0, 0x10($sp) ; save slot FIRST
lui $a1, %hi(D_801892C8) ; unrelated address materialization SECOND
addiu $a1, $a1, %lo(D_801892C8)
sw $ra, 0x14($sp)
jal func_8012C354
addu $s0, $a0, $zero ; the copy itself, in the CALL's own delay slot
$a0 is live only because it is used again after this first call (inside the later if-guarded
block); gcc has no reason to establish $s0 until the point where $a0 must actually survive
across a call — which is exactly the first jal. Since a delay slot needs filling there anyway,
the compiler's own callee-saved spill fills it for free, landing the register-save sw at
function entry (ordinary prologue placement, independent of when the value is copied) but the
VALUE copy itself only at its first genuine cross-call use.
The law. When the target embeds a parameter's callee-saved copy (addu $sN,$aM,$zero) inside
a call's own delay slot rather than as a standalone early instruction, and every explicit-copy
spelling (plain named local, register pin, or fence around the copy) instead produces either a
standalone move or shifts the sw $sN save to a different position — do not name a copy variable
at all. Reference the incoming parameter directly everywhere in the body; let the compiler's own
save-placement machinery decide where the spill lands, which will be at the first point the
parameter's live range must actually cross a call. This sharpens §220's "explicit named local is
the reliable way to pin a callee-saved copy" general rule with its own boundary case: a named copy
(pinned or not) is the WRONG tool specifically when the target's spill sits inside a call's delay
slot rather than at a fixed early position — that shape wants the raw, un-copied parameter instead.
ADDENDUM to §252 — a >=0/<0 split on an unconditionally-decremented value needs the POSTFIX operator INSIDE the branch condition, not a prior statement
Symptom. The target shows a decrement's STORE landing in the branch's OWN delay slot (§252's second reading rule: "the decrement is unconditional and the test follows") for a sign-test split (bgez/< 0 or the reverse) — but the branch's own compare must read the pre-decrement value while the delay-slot store writes the post-decrement value. §252's existing prescription for this reading ("write it before the if, not inside the taken arm") is ambiguous between two spellings that are NOT equivalent here: a prior statement (x--; if (x < 0) {...}) tests the value after the decrement, while the target needs the test on the value before it.
Mechanism, byte-verified (func_80186140, ov_SC03_006, MATCH 38/38): the only C spelling that reproduces both facts at once is the postfix decrement used directly as the controlling expression, relying on C's postfix semantics (yield the old value to the comparison, store the new value as a side effect):
if ((*(s16 *)(a0 + 0x2C))-- < 0) {
func_801292C8((u8 *)a0);
} else {
...
}
(✔ byte-checked: src/ov_SC03_006/ov_SC03_006_jr_8017AE2C.c:9938; asm/ov_SC03_006/nonmatchings/ov_SC03_006_jr_8017AE2C/func_80186140.s idx 10-13 —
lhu $v0,0x2C($s0) / addiu $v1,$v0,-0x1 / sll $v0,$v0,16 / bgez $v0,.L80186190 with sh $v1,0x2C($s0) in the delay slot: the sign test (sll+bgez) operates on the freshly-loaded, PRE-decrement $v0, while the delay slot unconditionally stores the already-computed POST-decrement $v1 — exactly the postfix-operator split.)
Splitting into two statements (s16 old = *(s16*)(a0+0x2C); *(s16*)(a0+0x2C) = old - 1; if (old < 0)) or writing the decrement after the store both fail to reproduce this: either the load-delay-slot content changes or the branch ends up testing the wrong value.
The law. When a target's guarded decrement shows the store in the branch's own delay slot (§252's "unconditional decrement" reading) AND the branch's own compare must read the value from BEFORE that decrement (a >=0/<0, or similarly value-dependent, split) — write the decrement as a bare postfix operator (x-- </>= K) directly inside the if's controlling expression. This is a narrower, single spelling within §252's "write it before the if" prescription, not a free choice among equivalent-looking alternatives: any two-statement decompose of "decrement, then test" tests the post-decrement value instead.
TELL. A sll(sign-extend)/bgez-or-bltz pair operating on the SAME register a following delay-slot sh/sw stores a -1'd copy of, with no intervening reload — the sign test's operand and the stored operand are two different pseudos derived from ONE load, exactly what a postfix --/++ used as a condition produces and a decompose-then-test spelling does not.
ADDENDUM to §215 — FIFTH SHAPE: reused mask constants across two call-free merge sites each get their own whole-function hard-register pin, and a shared sub-expression at the second site must be its own statement
Merge-time reclassification. The wave-df submitter proposed this as a bare NEW section; the wave-dd submitter independently reviewed the same function and called it COVERED by §215's existing pin-economy shapes 1-4 plus the L1816 non-volatile-fence law. On inspection, §215's shape 3 ("pin the CONSTANT interloper, not the pointer") is the closest existing match but only covers ONE contested constant at ONE site — it does not state the two-mask/two-site/whole-function-residency condition, or the shared-sub-expression-as-its-own-statement technique, that this function actually needs. Filing this as §215's fifth shape rather than a free-standing section, since it is a direct extension of an established list, not an unrelated mechanism. The wave-dd submitter's "fence must not be volatile" sub-claim IS already covered verbatim by the L1816 bullet (cited correctly by that note) and is not repeated here.
FIFTH SHAPE — REUSED MASKS ACROSS TWO SITES, PLUS THE SPLIT-EXPRESSION TECHNIQUE (P31, func_80187980, ov_SC06_032, byte-proven 55/55; independently re-derived in both wave dd and wave df)
The symptom. A function builds one 32-bit word from two 24/8-bit masked halves twice — once merging
a freshly-loaded word w with a passed-in value t ((w & mB) | (t & mA)), and a second time merging
that same passed-in slot's value again with the OBJECT'S OWN ADDRESS ((r2 & mB) | ((u32)p & mA)) — and
the target keeps mA/mB resident in the SAME two hard registers across both sites (no call
intervenes) while the two or-destinations land in two DIFFERENT registers ($a1 at the first site,
$v1 at the second). A plain-local draft lets cse/regalloc pick whichever registers it likes for the
masks and whichever destination coalescing prefers, and both diverge from the target.
Byte evidence. src/ov_SC06_032/ov_SC06_032_jr_80182890.c:5934-5960; target
asm/ov_SC06_032/nonmatchings/ov_SC06_032_jr_80182890/func_80187980.s.
register u32 w __asm__("$5");
register u32 mA __asm__("$4");
register u32 mB __asm__("$6");
register u32 r2 __asm__("$3");
u8 *p;
u32 t, ps;
p = (u8 *)func_80010A08(0x10);
p[3] = 3;
p[7] = 0x42;
w = *(u32 *)p;
__asm__ ("" : : "r" (w));
p[4] = a2[0];
p[5] = a2[1];
p[6] = a2[2];
*(u16 *)(p + 8) = a0[0];
*(u16 *)(p + 0xA) = a0[1];
*(u16 *)(p + 0xC) = a1[0];
*(u16 *)(p + 0xE) = a1[1];
mA = 0xFFFFFF;
mB = 0xFF000000;
t = *a3;
*(u32 *)p = (w & mB) | (t & mA);
r2 = *a3;
ps = (u32)p & mA; /* <-- the shared sub-expression, pulled into its own statement */
r2 = (r2 & mB) | ps;
*a3 = r2;
Target asm confirms every register: lui $a0,%hi(0xFFFFFF) / ori $a0,... materialises mA into $a0
(=$4); lui $a2,%hi(0xFF000000) materialises mB into $a2(? — note: mA/mB's C-level register
names are $4/$6; the compiled instructions use gcc's own $a0/$a2 mnemonics for those same
physical registers, i.e. $4=$a0, $6=$a2 under the o32 convention). First merge:
and $a1,$a1,$a2 (w&mB) / and $v1,$v1,$a0 (t&mA) / or $a1,$a1,$v1 / sw $a1,0x0($v0). Second
merge: and $v0,$v0,$a0 — ps = (u32)p & mA lands in $v0, p's OWN register, reused because p's
last read is this exact instruction; and $v1,$v1,$a2 (r2&mB, $v1=$3); or $v1,$v1,$v0
($v1=$3, matching the register u32 r2 __asm__("$3") pin exactly); sw $v1,0x0($s1).
The law. When a target reuses the SAME two mask constants across two independent masked-merge
sites with no intervening call, pin each mask to the hard register the target holds it in for the
WHOLE function (register u32 mA __asm__("$4"), etc.) rather than using a plain local — a plain local
gives cse/regalloc no reason to keep both masks resident in fixed registers across the gap between
sites. When the SECOND site's shared operand is a freshly-computed value (here, the malloc'd pointer's
own address masked) rather than a parameter already in a register, write it as its OWN preceding
statement (ps = (u32)p & mA;) instead of inlining it into the | expression — this is what lets the
about-to-die variable's register (p's $v0) be reused for the intermediate rather than forcing a
fresh allocation, and lets the merge's destination land in the mask-adjacent pinned register ($3) the
plain-inlined form does not reach.
Refuted sub-claim, worth recording explicitly (per the narrowest-form rule). The drafting note (both
the wave-df and wave-dd cards for this same function) additionally claims the FIRST site's load,
w = *(u32 *)p;, must be written textually AFTER p[4] = a2[0]; in source order for the target's
lw $a1,0x0($v0) to land in the first byte-load's delay slot, "without the fence cc1 sinks the load
next to its lui/ori." This does not hold against the committed source: line 5941 (w = *(u32*)p;)
sits BEFORE line 5946 (p[4] = a2[0];), the opposite of the claimed order, and the function still bytes
MATCH (55/55). The target's lw $a1,0x0($v0) (idx 17 of the asm) does land in the delay slot of the
FIRST byte-load (lbu $v1,0x0($s0) at idx 16, loading a2[0]) — but that is sched1 reordering the
compiled instruction stream, independent of which of the two adjacent statements was written first in
C; the non-volatile use-fence (__asm__("" : : "r"(w))) immediately after w's assignment is the part
that is load-bearing (it is present in the banked code and keeps w from being folded/sunk into its
own lui/ori constant-build window), not the claimed statement ordering. Do not carry the
"AFTER p[4]" ordering claim forward; only the fence + the mask/split/pin levers above are byte-verified.
What I could not verify
From part1:
- func_80181E64 (ov_SC07_006): the note's comparison between a
volatile/__asm__fence spelling (claimed to "materialize the copy but block a laterlihoist via the #APP/#NO_APP barrier") and the$0-add RC-12 spelling could not be checked — the banked code (src/ov_SC07_006/ov_SC07_006_jr_8017BEBC.c:4636-4641) uses the plainregister s32 zr __asm__("$0"); t = v + zr;form only; no fence spelling exists in the tree to compare against. Only the RC-12 half (already §136d-1) is verified. - func_800CB9F8 (md_MAIN_019): could not independently verify the specific claim that "a plain copy or a §194-A zero-byte fence did NOT pin" the 5th stack-borne argument's load before call #1 — only the winning
volatilespelling is observable in the committed tree; the rejected variants are not preserved. - func_800CB2E0 (md_MAIN_015): the note's central "switch head duplication" mechanism describes hoisting
func_80163408(...)'s result into a named localcmdas the fix — checked againstsrc/md_MAIN_015/md_MAIN_015.c:150-166and the banked code callsfunc_80163408(...)as a barevoid-context statement, never assigning it tocmd. The described causal mechanism does not match the banked bytes; only the pin-colour/frame-decode portions of this note were verifiable. - func_800CB1A0 (md_MAIN_019): the harvested note describes an unresolved "near 3... levers exhausted" state with no winning C shape recorded, yet the function is present as real C in the current tree (
src/md_MAIN_019/md_MAIN_019.c:129). Whatever ultimately closed the residual is not captured in this note — nothing to verify or promote from it. - func_8017DFE4 (ov_SC03_011): could not independently confirm from the note alone whether the mid-session "oracle target silently switched to ov_SC03_124's namesake" was a one-time card-metadata glitch or a reproducible harness condition; only that resubmitting the identical C against the (presumably) restored correct target re-confirmed the MATCH.
From part2:
func_800CB1A0(md_MAIN_019): the note's claim that the wrong-bank shard specifically belonged to "md_MAIN_028's function at the same VA" could not be independently re-checked — only the current, correct banked body is on disk, not the discarded wrong-bank draft. The underlying caution (validate a replayed shard's instruction count / binary against the target before trusting it) is consistent with the already-documented §238 stale/wrong-body class.func_8018271C(ov_SC03_104): the note's "tail-web direction A/B" inversion (pinning$2to the ADDRESS rather than the value) could not be checked as a generalizable lever — the note itself says this was a per-function judgement call, not a restatable rule, and no counterfactual draft is on disk to compare against.func_80182664(ov_SC06_020): the claimed causal mechanism ("pure pseudo-creation ORDER feeding allocation, not scheduling") is plausible but was not independently traced against gcc-2.7.2 internals in the time available; treated as a restatement of the already-pervasive statement-order/register-class family rather than promoted as new.
From part3:
- func_800CB3AC (md_MAIN_028): the claim that a
(void)-prototyped callee decl lets gcc sink a plain store BELOW a later call (vs. an unprototyped K&R decl "forbidding crossing the opaque call") could not be independently confirmed as a distinct mechanism within this review's time budget — a function call is ordinarily treated as clobbering memory regardless of its prototype's arg-count declaration in this compiler, so the causal claim as stated is not obviously sound. The outer if/else layout half of the same candidate IS confirmed as an ordinary case of the already-documented guard-clause triage (§225 addenda 7/8), which only prescribes TRYING early-return, not that it always wins. - func_8017FCFC (ov_SC04_020): the note describes a "near-4" plateau, not a MATCH, and lists four levers as new. Since this candidate set is defined as byte-gate-BANKED functions, this note is very likely attached to a losing/superseded attempt rather than the actual winning submission for this function — none of its four claimed levers could be checked against a final banked body from this note alone. (Its RC-12 $0-add item would be COVERED by §136d-1/§274 regardless, if it were independently confirmed.)
- func_80181E64 (ov_SC07_006, de, 9 calls): same class as above — ends at "near 11/103", no MATCH declared; could not verify against banked bytes.
- func_80181B04 (ov_SC01_009, 1st entry, 1 call): ends unresolved; superseded within this same batch by a second entry for the identical function (4 calls) that does reach MATCH 46/46 with a different, resolved lever. The unresolved first entry's specific claims were not independently checked since a resolved account for the same function exists.
From part4:
- func_801EE838 (md_SC05_025): the note's specific claim that the WAR order (store-before-load on the dead-parameter register) "falls out of the statement order for free" once the register is pinned was not independently re-derived from RTL/compiler internals — accepted on the strength of the already-general §215 pin-economy family and the note's own oracle-verified MATCH, not independently re-probed with a counterfactual.
- func_8017D9A4 (ov_SC06_018): five itemised sub-claims (missing 2nd call argument, dying-temp
naming, store-before-compare reload via cse-fold,
sra-positioning, register-coloring via a freshiVar5) were judged collectively as within already-covered families from the note's own prose; none was individually re-derived against the.sbyte-by-byte within this review's time budget. - func_80185ABC (ov_SC03_102): did not independently re-run a counterfactual (
u8 *pdeclared befores32 i) against the actual banked bytes to confirm the mirrored-allocation claim; accepted on the strength of §31's already-established, closely analogous RC-1/RC-2/RC-3 law.
From part5:
- func_800CB0D8 (md_MAIN_028): the note's headline claim — that DECLARATOR-INITIALIZER pin declaration order (as opposed to statement-level pin assignment) controls prologue expansion order for two hard-reg-pinned values — was not independently re-derived from RTL and directly conflicts with the existing, rigorously A/B'd §164-63 (which found the
sw/movepair order for two hard-reg-pinned ENTRY values invariant to declaration order, including a reversed-declaration-order control, and moved only by an interposed zero-byteasm). The candidate's scenario differs in one respect §164-63 did not test — one of the two pins is initialized from a GLOBAL ADDRESS (la-class materialization) rather than a second incoming argument register — so the mechanisms are not provably identical, but the claim as stated ("declaration order controls expansion order") is not safe to promote without a reversed-declaration-order control of this exact case, which was not run here. - func_801916BC (df) / func_8017F024 (df): both are exact duplicates (same function, same claim) of instances already reviewed and verdicted within this same 271-candidate batch from wave dd — flagged here rather than independently re-litigated, since a second write-up would just restate the first reviewer's finding.
From part6:
- func_80182F5C (ov_SC04_000): the note's specific claim about why an earlier draft misread which
addiufed theshvs thebne(a forensic .s-reading error, not a C-spelling defect) could not be independently reconstructed from the rejected draft (only the final banked text is on disk); the final banked constants (6/6 stored, mode==2 tested) are confirmed correct againstsrc/ov_SC04_000/*.cand the target.s, but the counterfactual "wrong" reading is not checkable. - func_8017D12C (ov_SC06_032): the note's claim that two decl-text spellings (A:
extern void func_8002931C(s32 a0);plain call; B:extern void func_8002931C(void);cast call) compile to byte-identical standalone asm, and that ONLY decl-text choice (not some other unlisted difference between the sessions) was the actual whole-binary-gate blocker, could not be independently confirmed — only the final, third, banked form is on disk. See also the harness-defect flag below. - func_80184CB0 (ov_SC03_097): did not independently re-derive gcc-2.7.2's chained-assignment
(
sp30[0] = sp30[1] = 0;) innermost-RHS-first store order from source; accepted on the strength of the note's own §263 citation plus the visible 2-instruction store-order match in the final banked text.
Harness-defect flags
Beyond the per-slice flags below, the single biggest harness-defect this batch surfaced was procedural, not a candidate-level note: agents given a narrow slice of this same task inherited enough top-level context to attempt re-launching their own parallel sub-agents, which is unsupported for a forked worker and fails loudly ("Fork is not available inside a forked worker") — but not before enough side-effect work happened that three of the six slices (df 1-39, de 1-53, de 54-108) drifted out of their assigned line range, started processing candidates from files that were not theirs, and wrote into .run/distill_out/D1.md instead of their assigned D1_part*.md. That path is independently in active use by a separate session in this project's fleet (its own dd/de/df-style analysis was found live there, with a different, more conservative verdict mix — see the snapshot at D1.foreign-snapshot.*.md). All three slices were re-run under an explicit "do not call the Agent tool, do not touch any file but your own" instruction and landed cleanly the second time. Two takeaways for future distill batches: (1) a forked sub-task should be told explicitly NOT to launch further sub-agents, since it otherwise infers that behavior from the parent's own actions earlier in the inherited context; (2) the shared deliverable filename (D1.md) is not a safe target for more than one concurrent session/assignment — a collision here cost roughly 1.7M tokens across three wasted full-scope re-runs before it was caught.
From part1:
- func_8018E89C (ov_SC06_033): the note states plainly that
match_onecannot run §42a's own prescribed check (splice the draft into a scratch copy of the real destination TU andcc1it before reporting MATCH) — "the check I could not run from this harness." This is a capability gap between the documented recovery procedure (§42a) and what the drafting harness actually lets an agent do; worth a maintenance look at whethermatch_one/the drafting tool could expose a cheap TU-splice-compile check. - func_8017DFE4 (ov_SC03_011): the note reports the oracle silently served a DIFFERENT overlay's same-address function (ov_SC03_124's) for part of the session, discovered only because resubmitting the identical C against the "same" card produced a different verdict. If this is more than a one-off card-metadata mix-up, it's a gate/target-resolution bug worth checking on the harness side, not a drafting idiom (adjacent to, but a possible harness-level instance of, §238's overlay-homonym trap).
- func_800CB1A0 (md_MAIN_019): the harvested note for this "byte-gate-BANKED" candidate describes an unresolved, unclosed residual ("near 3... levers exhausted") rather than a winning spelling, even though the function is committed as real C in the tree. This suggests the harvest/note-capture step may sometimes record an intermediate turn's note instead of the note from the actual successful final oracle call — worth checking the harvest tool's note-selection logic against the oracle-call sequence for this card (5 oracle calls total; unclear which one is quoted).
From part2:
func_8018F028(ov_SC06_033): describes a REPEATED silent gate-rejection (byte-correct codegen, DECL-layer only defect) caused bysig_unify'sDATA_DECL_REbeing unable to parse a function-pointer-arrayexternfor a data symbol, pertooling-audit.md:429and cookbook §28 case #1 (both cited by the note itself). Already a tracked/documented class, not new, but flagged per the assignment's request since it cost a full extra oracle round for a pure tooling gap.func_800CB1A0(md_MAIN_019): the note reports its "previous attempt" replay pointer led to a body for a DIFFERENT bank at the same virtual address (a shared-VA / wrong-overlay trap), costing a full prior session. This is the same class as §238's overlay-homonym trap and the "shared-VA trap" already flagged in this same wave's index guidance — recorded here because it recurred and cost real turns, not because the mechanism is new.
From part3:
- func_80180198 (ov_SC03_112) — MAJOR. The submitting note presents direct proof that a single card/function-name slot serves TWO DISTINCT function bodies (a 36-ins body and, per the card metadata, presumably another) on alternating replays within one session: the oracle's own printed instruction count contradicted the card header on different replays, and the live probe on this session's replay returned the FULL alternate instruction stream as the "target." This is a step beyond the already-documented §238 overlay-homonym trap (which assumes one fixed anchor per card per session) — here the SAME card slot alternates. Worth escalating to whoever operates the gate/replay harness; the submitting agent's own defense ("submit the body matching the LIVE probe, not the historically-correct one") is a reasonable stopgap, not a fix.
- Repeated pattern across this batch (≈10 candidates: func_8017EA58, func_80186408, func_8017FE74, func_801825B4, func_8017FA98, func_8018A6C0, func_8017E354, func_801E5624, func_801874E0, and others) — a byte-proven, symbol-audited, TU-decl-clean body fails the whole-binary gate repeatedly with no reproducible cause on the drafting surface. Several of these notes explicitly propose "wave contamination" (one sibling card's bad draft reverting the whole batch's gate result) as the likely mechanism. Given how many independent, otherwise-careful sessions converge on this same complaint in one wave, it is worth a maintenance look at whether the whole-binary gate is atomic per-function or per-batch.
- Possible note-attachment mismatch (func_8017FCFC, func_80181E64 9-call entry, func_80181B04 1-call entry) — MODERATE. Each describes an unresolved near-miss (not a MATCH) for a function the assignment states is byte-gate-BANKED. Either these functions eventually banked via a session not captured in this note, or the harvester is attaching a losing attempt's note to a winning function's slot. Worth checking whether the harvest step selects the note from the SUBMITTED/winning turn specifically, per the same class of concern other distill batches have already flagged for stale/superseded notes.
- func_8018C7C8 (ov_SC04_019) and func_80180228 (ov_SC06_030), each independently: match_one validated a draft as MATCH against a byte-identical WRONG overlay's copy of the same-named function (HI16/LO16 masking hid the difference in one case; a stale prior-session read in the other). Both resolve to the already-documented §238 class, but the recurrence rate in this one wave (at least 4 distinct instances across the full de batch) suggests the underlying VA-sharing/oracle-masking interaction deserves a standing checklist item rather than being rediscovered per-wave.
From part4:
- func_8017DFE4 (ov_SC03_011): mid-session the oracle silently served a DIFFERENT function (ov_SC03_124's same-address 25-ins sibling) under this card's identity before the correct target was re-established — the same overlay-homonym/wrong-VA class as §238, but worth flagging again as it nearly produced a false MATCH.
- func_80181D38 (ov_SC01_009): the replayed session had been matching a completely different
function's
.s(ov_SC02_041's 39-ins body) under this name/card before the true 17-ins ov_SC01_009 target was re-read — another instance of the recurring "replayed session carries a stale/wrong-binary.sread" defect class already flagged repeatedly across this whole batch (dd/de/df), suggesting the harvest/replay path itself (not any individual drafting session) is where this should be fixed. - func_8017FEB4 (ov_SC06_011): identical class — the original session read
asm/ov_SC03_124/.../func_8017FEB4.s(a same-named function in a different overlay) and built an entirely wrong state-machine shape around it before the correct jump-table-dispatcher target was found. Three independent instances of this exact failure mode inside one 55-candidate slice is a strong signal that whatever selects/serves the target.sfor a card is intermittently resolving the wrong overlay at the same virtual address, not that agents are careless.
From part5:
- Repeated, provable non-banking of byte-equivalent/oracle-MATCH text with zero diagnostic surface. At least eight candidates in this slice (
func_80180D84,func_8017EA30,func_8017F940,func_8018E89C,func_8017EC6C,func_800CD0CC,func_8017FCFC,func_8017FE74,func_80183528) independently report a fully symbol/decl-audited, oracle-confirmed body that the whole-binary gate refuses across 2-5 sessions, several explicitly asking for a diagnostic surface (compiler stderr, first-diff offset, or the gate's actual pass/fail state) that a drafter's sandbox cannot see. This is the single largest pattern in this slice by candidate count and matches the already-documented §42b stale-object-gate-trap and §138 declaration-plumbing classes, but the sheer repetition (drafters re-proving correctness 3-5x per function with no new information each time) suggests the gate itself is not surfacing enough for drafters to stop retrying blind. func_801916BC(df): the note attributes its prior gate rejection to a PARALLEL SHARD splicing into a SHARED TU concurrently with another shard (.run/wave_dd/shard236), i.e. two in-flight drafts racing to edit the same destination.cfile — a concrete instance of the concurrent-multi-shard-splice hazard the note says §42c's own text already predicts.func_801816FC(ov_SC01_009): four sessions were spent scheduling a 129-ins body against a seed that was stale — the live target.shad been re-extracted/redacted down to a 10-ins leaf sometime between sessions. Worth checking whether splat re-extraction runs are invalidating in-flight shard seeds without notifying whatever queued the work against them.func_8018F028(ov_SC06_033): duplicate confirmation of an already-flagged bug —sig_unify'sDATA_DECL_REcannot parse function-pointer-array data externs, silently reverting file-scope declarations of that shape (this exact bug was already flagged by this batch's own wave-dd reviewer as tracked under §28; this candidate hits the same wall independently in wave df).func_80187480(ov_SC02_000): a resume discovered the function's bank had, in fact, already landed in the tree — the "gate did not accept" state the session started from was stale/incorrect, suggesting a reporting-lag or state-sync issue between the gate and whatever tells drafters a function still needs work.
From part6:
- func_80182564 (ov_SC04_000): three separate runs each produced a standalone match_one MATCH (112/112) with every relocation symbol re-verified against the target's own lines, and the body did not bank on the first two. The note explicitly concludes "the residue is NOT in this function body" and suspects TU-level decl conflict or data-segment placement rather than instruction selection — consistent with the already-documented gate/integration classes (§42b, §270), not a new defect, but worth a maintenance look if this exact fn is still unbanked.
- func_801915A8 (ov_SC06_033): "Prior submits returned MATCH but reportedly did not persist" — a byte-proven body (69/69, reconfirmed) that the gate reported as not surviving across submissions. Same class as above.
- func_8017D12C (ov_SC06_032): two prior sessions each produced an oracle-MATCH, byte-identical standalone-compiling body that nonetheless failed the whole-binary gate; the eventual fix (adopting the card's authoritative decl-TEXT verbatim) is plausible but, per "could not verify" above, not provably the actual cause — match_one's inability to see whole-TU declaration conflicts (§273) makes this class hard to diagnose from the drafting side alone.
- func_8017D464 (ov_SC06_011): third consecutive match_one MATCH (38/38) on an unchanged body with a full Law-1c relocation audit repeated each round and no residual diff found; the note flags that if the byte-gate still rejects this, "the failure is outside the function body." Not a confirmed repeat-failure (may simply be repeated verification before a first submit), but the phrasing pattern matches the other two flagged cases closely enough to note here.
ADDENDUM to §167-40 (func_8017E2CC, ov_SC04_015 — wave dg)
THE DOUBLE-sra DBR-FILL SHAPE FOR A SIGNED /2^k IS NOT BOUNDED BY k≥16 — IT CAN FIRE AT ANY k WHEN THE DELAY SLOT HAS A SPECULATIVE-SAFE FILLER, INDEPENDENT OF THE IMMEDIATE-RANGE QUESTION
§167-40 documents a 5-instruction ori/addu shape with the shift appearing twice — once in the bgez's own delay slot, once on the biased fall-through — and ties it specifically to k = 16, where addiu's 16-bit signed immediate can no longer hold 2^k − 1. func_8017E2CC (ov_SC04_015, MATCH 134/134) shows the identical double-sra topology at k = 9, where addiu 0x1FF fits trivially — proving the double-shift shape is a separate dial from the immediate-range question §167-40 documents, not bounded by it.
Mechanism. reorg.c's fill_slots_from_thread can duplicate a side-effect-free computation (here, the UNBIASED sra k) into a conditional branch's delay slot when it is safe on both threads and no higher-priority instruction is ready — independent of whether the ordinary bias addiu would otherwise have fit there. Whether it fires depends on what else is competing for the slot; in this instance an unrelated call-argument constant (addiu $a0,$zero,0x76C) sits between the bias addiu and the second sra, giving dbr room to speculatively fill the branch's own delay slot with the shift rather than the bias.
The law. Do not assume §1-I5's plain "3-instruction, single-sra" shape whenever k ≤ 15. If the target shows a REPEATED sra (one in the bgez's delay slot, one after the bias addiu, possibly with an unrelated instruction interleaved between the addiu and the second sra), the C is still unchanged — write the division literally (x / 2^k) exactly as §1-I5/§167-40 already prescribe. Which of the two dbr-fill shapes (3-insn vs. 5-insn double-sra) appears is a pure scheduling decision, not a C dial. For register pins: constrain the DIVIDEND'S PRODUCER CHAIN (the values feeding the numerator), not the shift's own destination register — pinning the shift destination itself risks perturbing the Haifa ready-list's priority order and flipping the emission order between the sra and an interleaved unrelated instruction.
Byte evidence. Source: src/ov_SC04_015/ov_SC04_015_jr_8017AE2C.c:4776-4783 — v0 = 0x300 - v1; v1 = (v0 << 7) - v0; func_8002D4C8(0x76C, (v1 / 512 & 0x7F) | 0x1000);, with register s32 v1 asm("$3") and register s32 v0 asm("$2") pinning only the producer chain (the shift's destination, $a1, is left unpinned). Target: asm/ov_SC04_015/nonmatchings/ov_SC04_015_jr_8017AE2C/func_8017E2CC.s:8017E35C-8017E370 — bgez $v1,.L8017E370 / sra $a1,$v1,9 (delay slot) / addiu $v1,$v1,0x1FF / addiu $a0,$zero,0x76C / sra $a1,$v1,9 (second copy) / .L8017E370:.
When it applies. Any signed /2^k (k ≤ 15) whose target shows the doubled-sra topology instead of the naive single-shift-after-join shape.
Bounds. Single instance, k = 9. §167-40's k = 16 boundary for the ADDIU-vs-ORI materialization choice is unaffected — that remains a separate dial (immediate range) from this one (whether dbr duplicates the shift at all).
ADDENDUM to §20's cross-jump EXPLOIT bullet (func_8017E360, ov_SC05_007 — wave dg)
DUPLICATING A CALL-RESULT-CONSUMING STATEMENT IN BOTH ARMS — RATHER THAN NAMING A SHARED LOCAL — IS WHAT LETS THE CROSS-JUMP-MERGED TAIL CONSUME $v0 DIRECTLY, WITH NO COPY
§20 already states that duplicating the SAME call into both if/else arms lets gcc's cross-jump pass (§5a) fuse the two copies into one shared block. This addendum supplies the register-allocation reason a single named local defeats that trick even when the merged RTL looks superficially identical: a local v spanning both arms extends its live range across the arm-exit jump edge, forcing gcc to settle it into ONE consistent physical location before the jump — coloured here to $a0 — which costs an explicit move $a0,$v0 in each arm's own delay slot plus extra reload shuffling around the join. Duplicating the consuming statement in each arm instead produces TWO independent, arm-local, short-lived $v0 definitions that each die at their own use; cross-jump then merges the two identical instruction sequences into one shared block that reads $v0 straight from the call, with no register unification needed across the jump edge.
Byte evidence. func_8017E360 (ov_SC05_007, MATCH 54/54). Source: src/ov_SC05_007/ov_SC05_007_jr_8017BEBC.c:4074-4084 — the if arm writes *(u16 *)(*(s32 *)(a0 + 0x20) + 0x12) += func_8012B8E4(a0, 8); and the else arm independently writes *(u16 *)(*(s32 *)(a0 + 0x20) + 0x12) += func_8012B608(*(s16 *)(*(s32 *)(a0 + 0x20) + 0x12), *(s32 *)(a0 + 0xE0), 0xA); — no shared local. Target: asm/ov_SC05_007/nonmatchings/ov_SC05_007_jr_8017BEBC/func_8017E360.s:8017E3B0-8017E3CC — both jal func_8012B8E4 and jal func_8012B608 jump (j .L8017E3CC) to the same shared block, whose first instruction (addu $v1,$v1,$v0) consumes $v0 directly with zero intervening move.
When it applies. A call whose result feeds the SAME follow-on RMW/store from two branch arms. Write the full consuming statement in BOTH arms rather than hoisting it to one post-if statement fed by a shared local — the duplicated form is what keeps the merged register web copy-free; a shared local can defeat that even when cross-jump still fires on the call itself.
ADDENDUM to §87 (func_801815F4 ov_SC06_032; corroborating func_801840DC ov_SC05_017, func_80189C68 ov_SC03_006 — wave dg)
A CALLER'S EXTERN DECL CAN GO STALE MID-CAMPAIGN WHEN THE CALLEE ITSELF GETS BANKED INTO THE SAME TU BETWEEN ATTEMPTS — RE-READ THE CALLEE'S OWN .s TAIL, NOT AN OLD ATLAS/FLEET ROW
§87 already establishes that a stored draft's correctness claim carries a timestamp — the tree can move under it. This is a narrower, recurring instance of the same class specific to sibling functions in one TU: a caller's extern for a callee that is STILL an INCLUDE_ASM stub at drafting time is necessarily a guess at that callee's true signature (from a fleet/atlas row, or copied from another binary's version of the symbol). If that callee later gets banked into the same TU — by a different lane, session, or later pass — its now-concrete definition supplies the TU's real signature. If it disagrees with the caller's earlier guess (return type and/or parameter type), the caller's own previously match_one-clean body now fails the whole-binary gate with a fresh conflicting types error, even though the caller's own instruction stream never changed. Because match_one compiles the caller alone, it is structurally blind to this drift.
Diagnostic addition (beyond the already-documented §37/§124 asm-label-alias fix). When a function that was previously match_one-clean now fails integration with a conflicting types error naming a CALLEE (not itself), re-grep the destination TU for that callee's CURRENT definition before trusting any stale extern or fleet/atlas row — the callee's own .s tail is ground truth for its true arity/return type, and other binaries that already bank the same callee are corroborating evidence.
Byte evidence, three independent instances, all confirmed live in the current tree:
func_801815F4(ov_SC06_032, MATCH 17/17):src/ov_SC06_032/ov_SC06_032_jr_80181014.c:3001now readsextern void func_801816CC(s32 a0);, agreeing with the callee's own definition two lines later at:3034(void func_801816CC(s32 a0) {...}) — the caller's priorextern s32 func_801816CC()(a then-correct guess against a still-stub callee) is what the note describes breaking once:3034landed.func_801840DC(ov_SC05_017):src/ov_SC05_017/ov_SC05_017_jr_80182A74.c:3874,s32 aF801840DC(void *a0) __asm__("func_801840DC");— needed once neighbourfunc_80184054(banked at:3847) tookfunc_801840DC's address against its ownextern void func_801840DC();at:3845.func_80189C68(ov_SC03_006):src/ov_SC03_006/ov_SC03_006_jr_8017AE2C.c:12768-12770,s32 aF80189C68(s32 *arg0) __asm__("func_80189C68");— against the caller's still-live staleextern void func_80189C68(s32 a0);at:12255(both return AND param axes disagree — §73's diagnostic split applies to both at once here).
When it applies. An integration failure whose conflicting types diagnostic names a CALLEE, on a function whose own body already scored a clean match_one MATCH (possibly in a prior session) — before re-deriving the body, check whether the callee has since been banked in the same TU with a signature your stale extern disagrees with.
ADDENDUM to §153 / §236-5 (func_801A419C, md_SC07_003 — waves di and dl, corroborating; refutes a contradicted dj-wave card)
A GLOBAL POINTER VARIABLE (NOT AN ADDRESS-OF) IS THE DUAL OF §153'S "NOT REACHABLE BY RESPELLING" CLASS — CHANGING ITS DECLARED TYPE (ARRAY VS POINTER) DOES CHANGE THE CODEGEN, AND A BLOCK-SCOPE EXTERN CANNOT ESCAPE A HOSTILE FILE-SCOPE ARRAY DECLARATION OF THE SAME DATA SYMBOL
§153 documents an address-rematerialisation CSE class for &SYM arguments and states plainly that it is "not reachable by respelling." func_801A419C (md_SC07_003, MATCH 51/51) is the dual case — SYM itself is a pointer-typed global, read (not address-taken) at two call sites separated by an intervening call — and here the class genuinely IS reachable by respelling, but only via the DECLARED TYPE of the global, not any local launder.
The mechanism. Under the TU's canonical extern s32 D_80126B78[]; (array), a use *(s32*)D_80126B78 + 0x34 is MEM[symbol_ref] — the symbol's own address is a call-invariant pure constant, so cse hoists ONE address pseudo spanning both basic blocks; because the pseudo's live range crosses the intervening call it cannot rematerialise cheaply (local-alloc.c:1080 wants reg_basic_block < 0) and lands in a callee-saved register with a prologue save — +2 instructions versus the target. Under extern s32 *D_80126B78; (pointer), the SAME expression is instead a load of a call-KILLED memory value; cse cannot hoist it across the call, so each site independently emits its own lui/lw — exactly the target's per-site %lo-folded pair. Value-side launders (a volatile-qualified deref, or wrapping the loaded VALUE in a zero-byte __asm__ per §153) do not break the unification under the array decl, because the CSE unification happens on the ADDRESS pseudo, not the loaded value — only the declared TYPE changes which pseudo (address vs. value) exists at all.
The declaration-layer trap this creates, and its boundary. The TU's OWN file-scope decl of the symbol is extern s32 D_80126B78[]; (array); a body-level extern s32 *D_80126B78; (pointer) at FILE scope is a hard conflicting types error. Moving that same pointer decl to BLOCK scope does not escape it — a block-scope extern denotes the SAME external entity as the file-scope one, so gcc still type-checks it against the outer decl and still errors (cc1 exit 33). This is the boundary on §236/§237's "block-scope the decl so it shadows a hostile prototype" escape: that escape works for ordinary FUNCTION prototypes and for statics/tags (which create new entities), but not for a data extern, because extern never creates a new entity to shadow with. The only working escape here is the §37/§124 asm-label alias: a fresh C identifier (aD_80126B78) bound to the real symbol via __asm__("D_80126B78"), which no type-compatibility check can fire against because the C identifiers differ.
Evidence. src/md_SC07_003/md_SC07_003.c:684 (unmodified array decl, still present) and :2305 (extern s32 *aD_80126B78 __asm__("D_80126B78");), verified in-tree.
A separate, wrong card about the same function (dj-wave) argued the fix was to FLIP the file-scope array declaration itself to pointer type, and that the asm-label alias "is refused by the pipeline." Both claims are contradicted by the tree: the array decl at :684 is unmodified, and the alias IS what's banked. That card's residual observation (§37 covers the "extra la hoisted across calls" symptom generically) is still correctly COVERED; its specific proposed fix was not what shipped.
When it applies. A REGALLOC-PERM or a stray callee-saved prologue save/reload pair around a global whose value (not address) is read at two or more call-separated sites: try the alternate declared type (array vs pointer) for that global before reaching for a launder. If the TU's OWN canonical decl already commits to the "wrong" type for your target's shape, the asm-label alias — not a block-scope redeclaration — is the fix.
ADDENDUM to §263 (func_801E83AC, md_SC04_029 — wave dj)
MIXED ARITY AT TWO CALL SITES OF THE SAME UNPROTOTYPED CALLEE, WITHIN ONE FUNCTION, IS A LEGAL AND SOMETIMES REQUIRED SOURCE SPELLING
§263 establishes that a stolen delay-slot arg-copy is an arity error against the WHOLE FUNCTION's own declared arity for a callee. This sharpens it in a direction §263 doesn't cover: under a K&R (unspecified-prototype) extern, the SAME callee can legally be called with a DIFFERENT argument count at two DIFFERENT call sites inside one function, and the target can require exactly that — one site's jal delay slot carries the arg-register copy, the other site's carries a bare nop, for the identical symbol.
Byte evidence. func_801E83AC (md_SC04_029, MATCH 37/37): src/md_SC04_029/md_SC04_029.c:358,363,369 — extern s32 func_801789AC(); (unprototyped), then func_801789AC() (bare, case-0 arm) and func_801789AC((s32)param_1) (with arg, case-1 arm), both calling the identical symbol. Target asm/md_SC04_029/nonmatchings/md_SC04_029/func_801E83AC.s: the case-0 jal func_801789AC at 0x8B4 carries a nop delay slot ($a0 still holds the entry parameter, untouched); the case-1 jal func_801789AC at 0x8DC carries addu $a0,$s0,$zero.
When it applies. A callee appears at two or more call sites in one function under an unprototyped extern, and the target shows an arg-register copy in SOME of that callee's jal delay slots but not others — write each call site with its OWN, independently-correct argument list rather than assuming one arity governs every call to that symbol in the function.
ADDENDUM to §265 (func_8017D878, ov_SC03_107 — wave dj; corroborated by a REJECTED, contradicted card in wave dm — see closing)
§265's VERBATIM-ASM LANE IS ALSO THE ESCAPE FROM A TU-DECLARATION SIGNATURE DEADLOCK, NOT ONLY FROM AN UNCOMPILABLE -O0/HANDWRITTEN WALL
§265 frames the lane's trigger as "no -O2 C can ever match the codegen" (an -O0 prologue, a handwritten /* Handwritten function */ tag, a shape gcc never emits). func_8017D878 shows a DIFFERENT, genuinely distinct trigger: the C body is byte-perfect ordinary -O2 codegen (MATCH 45/45, reproduced across five sessions) — the wall is purely at the TU-DECLARATION level, and it is irreconcilable. The function's ONLY use in its destination TU is address-taken (func_801788B8(s0, (s32)func_8017D878), passed as a callback, never called directly), which forces the TU's own declaration to be void func_8017D878(void). The real body takes an argument and writes $v0 on every exit. No void(void)-compatible C spelling — register-$2 pins, volatile/non-volatile empty-asm barriers, goto-shared epilogues — reproduces the target's delay-slot addu $v0,$zero,$zero writes; each either vanishes, becomes barrier-blocked, or breaks cross-jumping.
Byte evidence. src/ov_SC03_107/ov_SC03_107_jr_801789AC.c:6126 (extern void func_8017D878(void);, the TU's forced declaration), :6137 (the address-taken use), :6161-6223 (the banked §265 verbatim-asm block, with the recovered s32(s32) semantics left in a comment per §265's own rule).
When it applies. A function is byte-perfect as ordinary -O2 C, but its ONLY appearance in the destination TU is as a function-pointer VALUE (address-taken) rather than a direct call, forcing a void(void)-shaped declaration that the real body's actual signature (arguments, $v0 writes) cannot satisfy under any C spelling. Reach for §265 here too — the lane is not restricted to bodies gcc structurally cannot emit; it also covers bodies gcc emits fine but the TU cannot TYPE.
ADDENDUM to §6 (func_801811F0, ov_SC03_102 — waves dj and dl, corroborated by a self-reported "nothing new" dm-wave card)
FRAME-GHOST PADDING ALSO WORKS AT -O2, VIA volatile, NOT ONLY AT -O0
§6's "-O0 idiom" (reserved-unstored-local sets the frame size) is stated for -O0 only, using a plain int x; declaration — at -O2 that same plain unread local is dead-code-eliminated and reserves nothing. func_801811F0 (ov_SC03_102, ordinary -O2 TU, MATCH 28/28) shows the -O2 equivalent: a volatile-qualified unread local defeats DCE and still reserves its stack slot under full optimization.
Byte evidence. src/ov_SC03_102/ov_SC03_102_jr_8017BEBC.c:4269 — volatile s32 pad;, never read or written. Target asm/ov_SC03_102/nonmatchings/ov_SC03_102_jr_8017BEBC/func_801811F0.s:4-5 — frame 0x20, $ra at 0x18 (one word above what the four remaining locals need).
When it applies. A near-match at -O2 that differs from the target ONLY by frame size and a uniform save-offset shift, with every real instruction otherwise identical (the same tell already given for -O0): add a volatile-qualified, never-referenced local of the missing size — a plain (non-volatile) local will be optimized away and buys nothing at this opt level.
ADDENDUM to §195-G (func_80183BB0, ov_SC05_001 — wave dj)
THE SAME "ARMS MERGE INTO ONE CALL" LAW APPLIES TO N-ARY switch ARMS, VIA A PER-ARM LOCAL FEEDING ONE HOISTED CALL SITE
§195-G (jump.c:1737) establishes that two IF/ELSE arms calling the same callee merge into ONE jal unless each arm's own code consumes the result. func_80183BB0 shows the same underlying "one call site, arm-specific setup" shape for a switch with MORE than two arms, where the arms share a per-arm CONSTANT argument rather than a return-value consumer.
The law. When a target's dispatch calls the same callee once, downstream of several switch arms that each need to pass a DIFFERENT constant/address to that one call, do not duplicate the call inside each case. Assign a per-arm local to the arm-specific value and let the arms break into ONE call site placed after the switch:
s32 tbl;
switch (state) {
case 1: tbl = (s32)D_801AE478; break;
case 8: tbl = (s32)D_801AE488; break;
...
}
func_8012B14C(param_1, tbl);
Duplicating the call inside each arm materializes BOTH call arguments eagerly in every arm's own j-to-join delay slot — gcc does not merge these back into one call site the way it would for an if/else pair reachable via §195-G, so the draft length-drifts against the target's single shared jal.
Evidence. func_80183BB0 (ov_SC05_001, jr_80183508, MATCH). Source: src/ov_SC05_001/ov_SC05_001_jr_80183508.c:3070-3099 — tbl assigned per arm, func_8012B14C called exactly once after the switch.
When it applies. A switch with 3+ live arms that all funnel into one downstream call taking an arm-specific constant/address argument — hoist the call after the switch and let a local carry the per-arm value, rather than writing the call once per arm.
ADDENDUM to the zero-byte-asm-slider family (§47 / §148-C / §153) (func_800CB874, md_MAIN_040 — wave dj; open tension with a more cautious dm-wave card — see closing)
AN INPUT-ONLY ZERO-BYTE __asm__ SLIDER (NO OUTPUT OPERAND) MUST BE VOLATILE TO SURVIVE TO REGISTER ALLOCATION
Every zero-byte-asm-slider instance already in the corpus (§47's live-length slider, §148-C's allocno-priority slider, §153's address launder) uses an OUTPUT-bearing re-tie form — __asm__("" : "=r"(x) : "0"(x)) — which is a genuine RTL SET of x and therefore always survives to register allocation regardless of volatile. func_800CB874 shows a DIFFERENT, INPUT-ONLY form — __asm__(... : : "r"(t)), no output operand at all.
The fix, byte-verified. __asm__ volatile("" : : "r"(t));, placed immediately after the definition whose live range needs extending (here, right after d = t - 4;).
Evidence. func_800CB874 (md_MAIN_040, MATCH 13/13). src/md_MAIN_040/md_MAIN_040.c:312-322 — register u8 *p __asm__("$4"); ... d = t - 4; __asm__ volatile("" : : "r"(t)); if (d < 0) ....
When it applies. A zero-emission asm slider intended purely to extend a value's live range or bump its allocation priority, written WITHOUT an output operand — it must be volatile.
Caution (see closing). A dm-wave card on the same function treated this as COVERED by the existing §34 "input-only dummy" entry and explicitly flagged that it could NOT independently verify the volatile-vs-non-volatile DCE distinction (no rejected non-volatile draft survives in the tree to test the negative half against). The POSITIVE case (the volatile form is what's banked and works) is byte-verified; the NEGATIVE case (a non-volatile form would be silently deleted) rests on general gcc inline-asm semantics rather than an in-tree counterfactual. Promoted as ADDENDUM on the strength of that general semantics plus the dj-wave session's own report of ~46 failed non-volatile probes, but flagged honestly rather than treated as fully closed.
ADDENDUM to §194-B (func_8017EB34, ov_SC03_117 — wave dk; distinct from the §74 co-pinning finding on the SAME function below)
AN UNCONDITIONAL PRE-HOISTED COPY BEFORE A GUARD CAN BLOCK THE COPY COALESCER FOR A NARROWING ASSIGNMENT — A DIFFERENT MECHANISM FROM §194-B'S "SECOND 16-BIT LOCAL" LAW, SAME NEIGHBOURHOOD
§194-B's law is about a second, narrower-declared local fed from a wider one, kept alive by width + ≥2 consumers. func_8017EB34 (ov_SC03_117, jr_8017BEBC, MATCH 38/38) shows a related but structurally different device: write the narrow local's copy unconditionally, before the guard that would otherwise be the only place it is set, so its live range overlaps the guarded variable's, and invert the guard's polarity to match.
Mechanism. The source is:
s16 y;
...
x = *(volatile s32 *)((char *)work + 0x94);
y = x; /* unconditional, BEFORE the guard */
if ((s16)x >= 0x11) {
t = 0x20 - x;
y = t;
if ((s16)t < 0) {
y = 0;
}
}
Writing y = x; only inside the if gives y and x disjoint live ranges at the point gcc's copy-propagation looks for a fold, and the HImode y = x copy coalesces away. Writing it unconditionally, before the branch, means y's live range now overlaps x's own live range across the guard — the copy propagator cannot fold a mode-changing SImode→HImode set whose source is still separately live, so y stays a genuine pseudo.
Evidence. src/ov_SC03_117/ov_SC03_117_jr_8017BEBC.c:4189-4221, MATCH 38/38 against asm/ov_SC03_117/nonmatchings/ov_SC03_117_jr_8017BEBC/func_8017EB34.s.
When it applies. A target's narrowing/widening copy of a value that is ALSO tested by a guard immediately afterward, where a copy naively written only inside the guarded arm folds away one instruction short. Try writing the copy unconditionally before the guard (inverting the guard's own polarity if needed) before reaching for §194-B's second-local-width lever, which addresses a different shape.
ADDENDUM to §74 (func_8017EB34, ov_SC03_117 — counter/clamp variant; wave dj, corroborated by dk/dl/dm cards on the same function)
BINDING ONE HARD REGISTER NUMBER TO TWO C VARIABLES WITH DISJOINT LIVE RANGES ALSO WORKS ACROSS A NARROW-TO-WIDE PROMOTION, AND HOISTING A COPY BEFORE THE BRANCH THAT CONSUMES IT KEEPS THE COPY A PSEUDO, FORCING A FULL MODE-MOVE INSTEAD OF AN IN-PLACE SUBREG FIXUP
§74's own text documents INCIDENTAL scratch reuse of a pinned register; a companion ADDENDUM from wave cf documents DELIBERATE co-pinning of two disjoint-lifetime C variables to one register number (func_800D0E30, resident). func_8017EB34 (ov_SC03_117, MATCH 38/38) is a second, independent confirmation of that same co-pinning law from an unrelated function, and adds a mechanism the cf-wave instance did not need: a TRUNCATE fork between a pinned hard register and an unpinned pseudo of the same value under a width promotion.
The co-pin, confirmed. src/ov_SC03_117/ov_SC03_117_jr_8017BEBC.c:4304-4308: register s32 x __asm__("$3"); and register s16 r __asm__("$3"); are two C locals sharing register $3. x is read from a volatile load and consumed in the head comparisons and the else-arm subtraction; r is defined later, after x's last use, and consumed only in the tail clamp — the same same-hard-register, disjoint-live-range pattern as §74's cf-wave addendum, on an entirely different function.
The new mechanism: pseudo-vs-hard-reg fork under promotion. The target's y (a copy of x, hoisted BEFORE the branch that also reads x) has its live range OVERLAP x's (because x is still needed in the else arm after y is copied out). That overlap defeats the copy coalescer — y cannot merge into x's register and survives as its OWN pseudo. Because y is a genuine pseudo, its later HImode→SImode promotion (y * 3) is expanded as a real fresh-register mode-move (sll v1,a1,16 twice + sra v1,v1,16) rather than the in-place subreg fixup gcc uses when the same promotion applies directly to a hard-register-pinned value. dbr then duplicates this idempotent promotion head into a mutually-exclusive branch arm's delay slot, which is what finally reproduces the target's exact instruction placement.
Evidence. Target asm/ov_SC03_117/nonmatchings/ov_SC03_117_jr_8017BEBC/func_8017EB34.s: the y=x; pre-hoist copy lands at $a1 via addu a1,v1,zero in the first bnez's delay slot; the promotion (sll/sll/sra) appears once in the main flow and is duplicated verbatim into the else-arm's bgez delay slot.
When it applies. A residual where (a) one hard register number legitimately serves two disjoint-lifetime C values (apply §74's co-pin law), AND (b) a copy of a live value, hoisted before a branch that still needs the original, shows a full-width mode-move sequence in the target rather than an in-place narrow-to-wide fixup: write the copy as an ordinary (unpinned) plain local, positioned BEFORE the branch so its live range overlaps the source's.
Corroboration. The identical function was independently re-derived and reported across three further sessions (waves dk, dl, dm), all reaching the same C recipe.
ADDENDUM §37 — merged into the §153/§236-5 entry above (func_801A419C, wave dl)
The dl-wave card's ADDENDUM proposing §37 for func_801A419C describes the same underlying mechanism as the di-wave §153/§236-5 card above (declared TYPE of a global pointer variable controls whether cse hoists its address across a call). Both are folded into the single merged entry above; the dl-wave version additionally confirmed that the alias reproduces the exact per-call-reload shape and refuted the dj-wave card's wrong claim that the alias is "refused by the pipeline."
ADDENDUM §6 — merged into the §6 entry above (func_801811F0, wave dl)
The dl-wave card independently reached the identical volatile-pad-grows-frame-at--O2 finding for the same function; folded into the merged §6 entry above rather than repeated.
ADDENDUM to §238 (func_80182CB4, ov_SC02_000 — wave dl)
A SHARED engine_core.h DEFINE_ MACRO CAN ITSELF BE THE HOMONYM ARTIFACT: A CALLER-GENERATED extern DECLARATION CAN CARRY A DIFFERENT BINARY'S SIGNATURE FOR THE SAME VA, EVEN WHEN YOU HAVE READ THE CORRECT TARGET .s THE WHOLE TIME
§238 documents four sources of a WRONG BODY — all failures of what code the agent looked at. func_80182CB4 (ov_SC02_000, MATCH 19/19) shows a fifth, distinct vector where the agent's C is correct throughout but the declaration layer itself is poisoned by a homonym, invisible to match_one's standalone compile exactly as §236 describes for ordinary decl conflicts.
Mechanism. DEFINE_func_801829F8() in src/shared/engine_core.h is a shared macro instantiated by this TU; its expansion declares extern void func_80182CB4(s32 a0); at file scope and calls it with an argument. That signature is correct for the ov_SC03_104 binary's function at the same VMA, which does read its parameter — but ov_SC02_000's own function at address 0x80182CB4 is a different, unrelated function (a 19-ins block-copy loop) that never touches $a0 at all. Every void func_80182CB4(void) draft — objectively correct for THIS binary's body — conflicts with the macro's (s32) extern the moment the definition lands in a TU that includes the header, a plain §236-1 failure, but one whose cause is the cross-binary homonym, not a local decl mistake.
The fix is cheap, not a header edit. Unlike §63's fresh-138 header-decl blocker (where the macro's signature is a simplified/narrower version of the SAME logical function and must be corrected fleet-wide), here the macro's signature belongs to a genuinely different function. The correct move is simply to accept the unused parameter in THIS binary's definition — void func_80182CB4(s32 _arg0), parameter unread — which costs zero instructions at -O2 and satisfies the macro's extern without touching shared state.
Byte evidence. src/shared/engine_core.h:61350-61374 (DEFINE_func_801829F8, extern void func_80182CB4(s32 a0); … func_80182CB4(a0);); banked src/ov_SC02_000/ov_SC02_000_jr_8018173C.c:3352-3369, void func_80182CB4(s32 _arg0) { … } with _arg0 never referenced in the body.
When it applies. A byte-perfect (void)-signature draft that repeatedly fails the whole-binary gate with conflicting types, where the TU includes a DEFINE_func_X() macro whose expansion declares an argument this binary's function never uses — before touching the header, check whether the macro's signature belongs to a different binary's same-VA function and, if so, just add the unused parameter to the definition.
ADDENDUM to §137a (func_801684B4, ov_MAIN_012; corroborated independently by func_80189E68, func_8017FF9C, func_801822B4, func_8018DA8C — wave dl)
A "DID NOT BANK" RESUME BANNER CAN BE STALE EVEN WHEN NO GATE VERDICT EXISTS TO BE STALE: submit SPLICES INTO THE LIVE DESTINATION TU EAGERLY, INDEPENDENT OF WHETHER OR WHEN THE GATE REPORTS — CHECK THE TU'S ACTUAL CONTENT FIRST, NOT JUST A VERDICT'S TIMESTAMP
§137a establishes that a gate verdict has a timestamp and must be checked against the draft's mtime before acting on it. This wave's harvest surfaced a related but distinct failure mode, independently reported by five separate cards: a session resumes a card carrying a "did not bank" status, spends real budget re-deriving or re-verifying the body from scratch, and only discovers by re-reading the destination TU that the exact submitted text is already present as a live C definition, with the INCLUDE_ASM stub already removed — meaning the submission tool spliced the text into the tree independent of, and possibly before, any gate verdict was ever recorded against it.
Byte evidence (the clearest of five instances). func_801684B4: void func_801684B4(s32 a0) { is present at src/ov_MAIN_012/ov_MAIN_012_jr_8015A3C8.c:8120, with the INCLUDE_ASM stub gone. The same pattern was independently confirmed in-tree for func_801811F0 (src/ov_SC03_102/ov_SC03_102_jr_8017BEBC.c:4267), func_8018DA8C (src/ov_SC02_005/ov_SC02_005_jr_80181D30.c:10555), and func_801827A0 (src/ov_SC05_001/ov_SC05_001_jr_8017BEBC.c:6983, aliased form).
The rule. When resuming any card whose status says "MATCH but not banked" or similar: read the destination TU fresh and grep for the function's own definition BEFORE re-deriving, re-verifying, or even re-running match_one. If the body is already resident and matches the card's submitted text, the remaining work (if any) is diagnosing why the whole-binary gate — not this function — is still failing, not re-deriving C.
Boundary. This does not contradict §137a — it is the case where §137a's own comparison is inapplicable because no fresh gate verdict exists at all to compare a mtime against; the TU's content is the ground truth in that gap.
ADDENDUM to §8c / §88d (func_8016AB6C, ov_MAIN_012 — wave dm)
A DRAFTER-FACING TELL FOR A STALE POST-RESPLIT CARD: TWO LIVE nonmatchings/<subseg>/ COPIES OF THE SAME FUNCTION PLUS A DESTINATION TU THAT NO LONGER CARRIES ITS INCLUDE_ASM ANCHOR MEANS THE CARD'S PATHS PREDATE A RESPLIT/CARVE
§8c already documents the underlying tool defect from the tool-author's side; §88d separately establishes the ordering rule (run the carve chain before banking). Neither states the fact in a form a drafting agent can use to recognize a stale card from the read surface available to it.
The tell, byte-verified on func_8016AB6C (ov_MAIN_012). The card's target .s existed simultaneously at asm/ov_MAIN_012/nonmatchings/ov_MAIN_012_jr_8015A3C8/func_8016AB6C.s (the pre-resplit location — a directory still live for ~20 OTHER functions) and asm/ov_MAIN_012/nonmatchings/ov_MAIN_012_jr_8016AB6C/func_8016AB6C.s (the post-resplit location, matching the true target). The card's own destination-TU path no longer contained the function's INCLUDE_ASM anchor.
Why it matters. Once this combination is observed, no gate verdict rendered against the card's original paths can be trusted — favorable or unfavorable — because the tree the gate evaluated may not be the tree the card's own paths describe. Gives a drafting agent a cheap two-ls-and-one-grep precondition check before spending further rounds diagnosing a "gate defect" that may just be a stale card.
ADDENDUM to §73 / §30#2 (func_800D1984, resident — wave dm)
THE RETURN-AXIS SELF-DECL CONFLICT CAN BE SOURCED FROM THE FUNCTION'S OWN PRIOR CALLER-SIDE FORWARD-DECLARATION, NOT ONLY A SHARED CALLER MACRO — AND EVERY DIAGNOSTIC SIGNAL WILL POINT AT THE CALLEE INSTEAD
§30#2/§73 already establish the return-axis fix for a def-side conflicting types wall: when the byte-true definition must return a value but the fleet/TU declares it void, widen the declaration. Both existing write-ups frame the offending declaration as living in a DEFINE_func_* macro or a caller's own decl block, elsewhere in the fleet.
func_800D1984 (resident) shows the same axis with a different, uncovered source: the conflicting extern void func_800D1984(S800D1938 *arg0); at src/resident/resident.c:1905 is not a fleet-wide macro artifact — it is a forward-declaration this function's OWN caller wrote for itself, at a point in the project's history when func_800D1984 was still an INCLUDE_ASM stub and nobody yet knew its true return type. The declaration has outlived the stub that justified it, and every available diagnostic instead implicates the CALLEE func_800D19DC func_800D1984 itself calls, because that is the symbol nearest the point cc1 reports the error against.
Byte evidence. asm/resident/nonmatchings/resident/func_800D1984.s (0x58, 22 ins): the bnez $v0 branch has two live-out paths for $v0 at the shared epilogue — the not-taken arm sets it explicitly (addu $v0,$zero,$zero, i.e. return 0), the taken arm's last instruction before falling into the epilogue is jal func_800D19DC (its own return value flows through unmodified). The function's return value is genuinely live, so a void forward-declaration anywhere in its call chain is wrong.
The rule. When a def-side return-axis wall's diagnostic implicates a callee, check the FUNCTION BEING BANKED's own prior forward-declarations before trusting the diagnostic's pointer — a stub-era decl for the function itself is exactly as capable of causing conflicting types/void value not ignored as a macro elsewhere, and is more easily missed since it names the function you're already looking at.
Not yet applied in tree (see closing): resident.c:1905 still reads extern void as of this review — this candidate's fix is a byte-verified prescription against a genuinely live return value, not yet a confirmed executed one.
What I could not verify
-
func_8017E2CC (ov_SC04_015): the winning register-pin configuration is confirmed against the banked bytes, but the full counterfactual matrix (various pinned/unpinned permutations measuring 6/10/14/16/20/24 instructions) could not be independently re-run — only the winning banked draft is on disk. Accepted the qualitative direction (pin producers, not the destination) on the strength of the one configuration that IS on disk.
-
func_80182B08 (ov_SC03_104) and func_8018D5D4 (ov_SC06_024): cite §76/§17 as the closest existing law by theme match; did not independently re-derive the specific register-flow chain beyond confirming func_8018D5D4's store-order claim against the current banked bytes (which matched exactly).
-
func_7FAD4 / func_8017FAD4 (ov_SC03_113): two independent cards (di and dl) proposed the SAME claim — an HImode
registerpin silently ignored while an SImode pin on the identical call-free value is honored. The positive SImode half is byte-confirmed in both; the negative HImode half has no surviving counterfactual anywhere in the tree. This integration sides with dl's REJECT rather than di's ADDENDUM-with-caveat, per R14/G3 ("if a claim can't be checked, reject it") — see the integration note at the top of this file. The underlying claim is plausible and specific enough to be worth a targeted probe in a future session. -
func_8017F834 (ov_SC03_093): the declaration-order/subword-scheduling claim (dk-wave COVERED, dl-wave REJECT for the same underlying claim on a different card) could not be independently re-verified against the target
.s's actual instruction sequence in the time available on either pass. -
func_800CB874 (md_MAIN_040): the dj-wave ADDENDUM promotes "input-only zero-byte asm needs
volatile" on the strength of the positive case plus general gcc semantics; the dm-wave card, working the same function, explicitly could not verify the negative (non-volatile-is-deleted) counterfactual either. Recorded as an open tension in the merged ADDENDUM above rather than silently resolved. -
func_80180A88 (ov_SC06_010), func_8017D104 (ov_SC06_027), func_800CB4E4 (md_MAIN_040), func_80183BB0 (ov_SC05_001), func_800CB458 (md_MAIN_016), func_8018CC28 (ov_SC02_005), func_8017D698 (ov_SC04_003, dm), func_800CEF00 (md_MAIN_011, dm), func_8017F180 (ov_SC04_006), func_801827A0 (ov_SC03_118, dm): each of these dj/dm-wave notes described a plausible, specific mechanism; the NEW-worthy ones (80180A88, 8017D104) were independently byte-verified and promoted above. The remainder were folded into the nearest existing COVERED family rather than promoted, because time/scope did not allow a full independent
.s-level re-derivation of each — flagged here for a future pass with more budget. -
func_80184458 (ov_SC04_005) [carried from the cf-wave precedent], func_80181600 (ov_SC01_009): not part of this batch; mentioned in no card here.
-
func_801684B4 (dm), func_800D1984 (dm): the §73/§30#2 ADDENDUM's prescribed fix (widen
resident.c:1905's forward-decl) is byte-verified as necessary but was NOT confirmed applied/banked as of this review — the underlying diagnostic fact is solid, the fix itself is a prescription, not yet an observed outcome.
ADDENDUM to §174 Law 4 (func_8017E9A8, ov_SC06_015)
LAW 4'S CURE HAS NO ANSWER FOR AN ARITY CONFLICT — ONLY A TYPE CONFLICT. THE FIX MUST LAND ON THE CALLER, NOT THE DEFINITION.
§174 Law 4 states: a standalone MATCH that gates 0 is a declaration fact, and its prescribed cure is
"keep the TU's canonical signature and narrow at the USE site" — demonstrated on a s16-vs-s32
type mismatch (func_80181724, the DEF-side wall). func_8017E9A8 (ov_SC06_015, MATCH 37/37,
banked) shows a case Law 4's stated cure cannot reach: the destination TU's existing declaration was
extern s32 func_8017E9A8(void); at two call sites (ov_SC06_015_jr_8017BEBC.c:4041,4075), while
the real function genuinely consumes one argument — pinned into $s0 across two jals in the target
.s. This is an arity conflict (0 params declared vs 1 param consumed), not a type conflict on an
existing parameter. There is no cast or narrowing you can apply inside the definition that reconciles
"the caller declared zero parameters" with "the callee reads $a0" — narrowing only works when a
parameter slot already exists on both sides.
The fix, byte-verified (src/ov_SC06_015/ov_SC06_015_jr_8017BEBC.c:4161,4195,4282, banked,
37/37): retarget the stale caller-side declarations to unprototyped (extern s32 func_8017E9A8();) instead of (void), and define the real 1-parameter signature
(s32 func_8017E9A8(s32 param_1)) below. Both call sites only test the return value (!= 0), so the
unprototyped/K&R-int linkage costs nothing at the caller (still beqz $v0). Confirmed in the current
tree: both call sites now read extern s32 func_8017E9A8(); and the definition takes s32 param_1.
Corollary for Law 4's triage table. Before assuming a DEF-side wall is fixable by narrowing at the use site, check whether the conflict is over a parameter's TYPE (Law 4's cure applies) or its very EXISTENCE / count (the cure must be on the CALLER'S declaration instead — relax it to unprototyped, or give it the true arity if a caller actually can be shown to pass the argument).
A second, narrower tell from the same card (extend-tell spelling, minor). The same function's
tail is return (s16)w == 0x800;, which emits lhu/addiu/sh + a naked sll/sra extend pair +
xori 0x800 + sltiu 1 (5 instructions after the store). The semantically identical
return ((s16)w ^ 0x800) == 0; folds the sll/sra pair away (−2 instructions) — an
equality-against-a-constant spelling preserves the promotion pair that an XOR spelling destroys. This
is a narrow addition to the extend-tell family (§172b-1/§264): when a target shows the promotion pair
immediately before an equality test against a constant, do not respell the comparison as XOR-then-zero
even though it is mathematically identical — the front end folds the two differently.
ADDENDUM — the zero-emission-asm family gains a REF-SLIDER PLACEMENT LAW and a paired RESTORER (func_800CB900, md_MAIN_026)
Context. The cookbook already documents five zero-emission-asm allocator levers under one family:
§47 (live-length slider, between two existing volatile asms), §148-C (allocno priority NUMERATOR
ref-boost), §151 (blocked-scheduler-tick absorber), §153 (address-rematerialisation launder), §158
(range-extender), §158a (the do{}while(0) region ref-multiplier, "not an asm" but the same family).
None of the six states a rule about the slider's position relative to the function's own real
calls, and none documents a second, paired slider whose job is to restore a live range the first
one shortened.
The law, byte-verified (src/md_MAIN_026/md_MAIN_026.c:348-430, banked, func_800CB900,
132/132 MATCH):
- A bare
__asm__ __volatile__("" : : "r"(p));immediately after the LAST realjalin the function is a reference-ORDER override forp, distinct from every existing family member's reference-COUNT or live-length mechanism. In this cardtwicep(a pointer parameter) is read once early, but the target keeps it live across the whole GTE block after the last call (func_800491AC). The banked C places__asm__ __volatile__("" : : "r"(twicep));right after that call and before any of the block's own reads oftwicep/oncep/vsrc/dsrc. Placed BEFORE any call instead (untried counterfactual, not claimed as measured here — flagged per the two rules earned today), the note's own reasoning is that it would extendp's liveness across the call and perturb the whole prologue register map; the placement AFTER the last call is what the banked body actually uses. - A SECOND, later slider on the SAME operand restores the value's ordinary live range once the
first slider's local re-ordering effect is no longer wanted. The banked C repeats
__asm__ __volatile__("" : : "r"(twicep));again immediately after the thirdSTSVmacro call and before the function's tail constant-store block and its own finaljal func_800174DC()— i.e. AT THE END, not adjacent to the first. Both sliders are on the exact same expression (twicep), and between them sits the entire three-vector GTE transform block.
Mechanism (as far as the note establishes it, not independently re-derived from RTL here): ordinary
zero-emission sliders (§47/§148-C/§158) buy or spend refs/live_length counted globally over the
allocno's full range. What's different here is that the FIRST slider's effect (letting an address
compute early and reference-order past a call) needs to be un-done before the tail so the OTHER five
allocnos in this dense GTE body keep their own correct colors — i.e. the pair brackets a region in
which one operand is deliberately mis-ordered relative to the rest, then restored. This reads as a
seventh member of the family, distinguished from all six others by pairing (bracket, not single
insertion) and by acting on reference ORDER at a call boundary rather than reference COUNT or live
length in general.
Two supporting facts from the same card, already fully explained by existing law (not new): the
three-way w3c/L.w3C/(u32)L.o1 variable reuse for one physical register across three sequential
roles is an application of the established "one C variable, one register identity" creation-order
discipline (§76/§162m family); reading twicep into a temp BEFORE the first L.v48[0] field read
(rather than after) is ordinary statement-order scheduling, not a new mechanism.
Assign a section number at merge (family member, not a standalone new law) — cite alongside §47/§148-C/§151/§153/§158/§158a as the "call-boundary reference-order" member.
From ck + cl + cm
ADDENDUM — §NNN — sharpens §55a / §164-37 / §165-28 (switch-vs-tree cluster): A SHAPE THAT LOOKS LIKE A SWITCH DISPATCH CAN BE PLAIN NESTED ifs WHOSE SHARED BODY WAS TRIPLICATED BY THE SOURCE AND THEN CROSS-JUMP-MERGED BACK DOWN
Function. func_801F06B0 (md_SC03_076, 33/33 ins). Reported independently in wave ck (worked out
from first principles) and wave cm (confirmed by finding the actual cross-binary twin,
md_SC04_027:func_801E8D70, already banked).
Symptom. The Ghidra seed reads as switch (a1) { case 0: ...; case 5: ...; default: ...; } — a
plausible 2-case dispatch. §55a/§163c/§165-28 already state that gcc-2.7.2 compiles anything below
CASE_VALUES_THRESHOLD (5) as a flat equality-test tree, not a jump table — but a naive C switch
with exactly this case set still compiles to a SHORTER, flatter tree (measured 26 ins: beqz/li 5/beq) than the target's 33 ins. The extra length is not a scheduling residual; it is a shape
error.
Mechanism. The target's 33 ins decode as three copies of ONE shared compare-and-load body, spread
across a slti/bgtz root and an inner a1 != 0 test, with gcc's cross-jump pass (§5a family)
merging two of the three copies back into shared code. The real source is closer to if ((a1 > 0 && a1 < 5) || (a1 != 0 && a1 != 5)) { ...shared body... } than to any switch/case construct — the
"switch" reading was a plausible-looking misdirection from the seed, not evidence of a dispatch at
all. The cm instance found the actual mechanism by locating the real twin: md_SC04_027's already-
banked func_801E8D70 at the same shape (33 ins, same D_80115158 table, same caller pattern),
findable only by grepping asm/ for the shared symbol + call shape (D_80115158[ + * 2), not by
name — which is exactly §238's documented "positive use" (grep asm/ by encoding/shape, not src/
by name) applied cross-binary to a same-family md_* module rather than an overlay.
Why ADDENDUM, not NEW. The cross-binary-twin-discovery half is already §238's positive-use case,
just with an md_* module standing in for an overlay — not a new mechanism. What is worth adding as a
sharpening of the §55a/§164-37/§165-28 cluster is the negative caution the cluster doesn't currently
carry: a case-labeled shape under CASE_VALUES_THRESHOLD that is LONGER than the flat tree its own
apparent case set would produce is a structural tell (wrong C construct entirely, not a missing
transform) — check the instruction count against the naive flat-tree prediction before trusting a
switch reading at all, the same discipline §162a3/§164-37 already apply to jump-table dispatch
shapes but do not currently state for the below-threshold tree route.
ADDENDUM — §NNN — sharpens §167-13's boundary: CHAINING TWO IDENTICAL SIDE-BY-SIDE STORES INTO ONE C ASSIGNMENT STATEMENT IS A MID-BLOCK SCHEDULING-PRIORITY DIAL, NOT ONLY A STORE-ORDER SPELLING
Function. func_80181BE0 (ov_SC03_112, 51/51 ins). Reproduced across all three waves (ck 8 oracle
calls, cl 2, cm 1) with a consistent finding each time; cl/cm's later passes re-confirm rather than
add.
Symptom. A mid-block argument-setup instruction (move $a0,$s1 feeding a call to
func_8012AD80) schedules one sra/addiu pair earlier than the target places it — a
SCHEDULE-REORDER residual that plain statement reordering, named temps, an asm barrier on the
parameter, and a decl-order swap all failed to fix (measured and rejected in the ck note).
Mechanism / fix, byte-verified against the target's own relocation lines (7 sites): the target
stores two adjacent halfwords with the SAME value (*(s16*)(b+0x18) and *(s16*)(b+0x1C)).
Writing them as two separate C statements leaves the mid-block arg-setup instruction scheduled where
it was; writing them as ONE chained assignment — *(s16*)(b+0x18) = *(s16*)(b+0x1C) = expr; — shifts
that store pair's priority/LUID enough that the arg-setup instruction sinks below it, matching the
target exactly.
Why ADDENDUM, not NEW. §167-13 already documents a related but distinct fact — "a leading p1 = a1; terminates rather than rides the bb0 parameter pin, and the direction/pass/dose were previously
misstated" — but its own scope is explicitly the FUNCTION-HEAD parameter-pin weave, not an
ARBITRARY mid-block pair of stores. This candidate's own self-citation ("not in cookbook under
'head-crack'; §167-13 covers prologue-pair weaving, not mid-block") is accurate: the corpus has no
entry for chained-assignment-as-a-mid-block-priority-dial specifically. Recorded as ADDENDUM rather
than NEW because it is best read as extending §167-13's family (LUID/priority manipulation via
statement chaining) into a region §167-13 explicitly disclaims covering, and because the underlying
compiler mechanism (why chaining changes LUID assignment) was not independently re-derived from
source here — only the C-level lever and its effect were confirmed, three times, against the bytes.
ADDENDUM — §NNN — sharpens §162a3/§60b: A sll $v0,16 / sltiu $v0,1 ZERO-TEST OF A JUST-DECREMENTED HALFWORD IS A SIGNED-TEMP WIDTH TELL OUTSIDE ANY SWITCH/RANGE-TEST CONTEXT
Function. func_80186C3C (ov_SC06_029, 19/19 ins). Reproduced identically in waves cl and cm.
Byte evidence (asm/ov_SC06_029/nonmatchings/ov_SC06_029_jr_8017C954/func_80186C3C.s):
lhu $v0, 0x10A($s0)
addiu $v0, $v0, -1
sh $v0, 0x10A($s0)
sll $v0, $v0, 16 ; NO following sra
sltiu $v0, $v0, 1
The field is loaded/stored u16 (lhu/sh — it must stay unsigned for the store), but the
post-decrement zero-test uses sll $v0,16 immediately followed by sltiu $v0,1 (true only when the
shifted 32-bit value is exactly 0, i.e. when the low 16 bits were 0) rather than the more obvious
andi $v0,0xffff / sltu $zero,$v0 masking idiom. This spelling is produced by declaring the
DECREMENT TEMPORARY s16 (signed) even though the memory access itself must stay u16/lhu/sh to
match — an u16 temp instead emits the andi-based zero test.
Why ADDENDUM, not COVERED. §162a3/§164-37/§165-28's sll 16 / sra 16 family is specifically about
a SWITCH DISPATCH index (a value about to feed a sltiu-bounded jump table or beq chain against
table edges) — none of their text or examples cover a plain post-decrement ==0 test with no
dispatch anywhere nearby, and — noted as a discriminator, not a coincidence — this instance has NO
trailing sra, unlike the switch-index family's sll 16 ; sra 16 pair (this is a one-sided shift used
purely as a cheap "are the low 16 bits zero" test via sltiu, not a sign-extension). §60b's
"sltiu==!x" observation is the nearest neighbor but does not connect it to the signed/unsigned
temp-declaration choice. Grepped the corpus for this exact combination outside switch-dispatch
contexts — no hits.
ADDENDUM — §NNN — sharpens §37/§124 (unspecified-parameter-list family): DERIVE A CALLEE'S ARITY LOWER BOUND FROM ITS OWN .s STACK-ARGUMENT READS, NOT FROM ANY SINGLE CALL SITE
Function. func_801A8DCC (md_SC07_004, 26/26 ins). Reproduced in waves cl and cm; the callee in
question, func_801A8E34, is itself still INCLUDE_ASM (no C source or documented signature exists
anywhere in the tree).
Mechanism, byte-verified against asm/md_SC07_004/nonmatchings/md_SC07_004/func_801A8E34.s: the
callee's own prologue subtracts 0x68 from $sp (addiu $sp,$sp,-0x68), and its body later executes
lw $s6, 0x78($sp) — a read at an offset ABOVE its own frame (0x78 > 0x68), which can only resolve
to a slot the CALLER set up before the call (an incoming stack argument, per §217/§217-CROSS-
CONFIRMED's "the incoming home slot is the mirror" pattern, already documented for the symmetric case
of reading a caller-relative offset before sw $ra). Since func_801A8DCC itself only ever passes 5
arguments to this callee, and the true callee reads at least one argument beyond the four
register-passed slots, the only C spelling that is safe against every caller (this one included, which
under-supplies the true arity) is the K&R unspecified parameter list — extern void func_801A8E34();
— never a guessed fixed-arity prototype.
Why ADDENDUM, not NEW. The unspecified-parameter-list rule itself ("? is compatible with any
prototype and never a conflict") is already well established and repeatedly cited by name across this
corpus (§37, §124, and the drafting notes' own "card decl law" references) — this is not new. What
was previously undocumented is the SPECIFIC TECHNIQUE for deciding, from evidence, that a plain guess
is unsafe: read the callee's OWN .s for a stack-argument reference beyond its incoming-register
budget, using its declared frame size as the zero point. Could not independently verify the exact
argument COUNT the note claims (it says "sixth argument"; by the standard MIPS o32-style layout used
elsewhere in this corpus — where the first stack argument lands at the caller's $sp+0x10 — the
offset measured here reads as the callee's fifth argument, not sixth). The count is not load-bearing
for the law (unspecified-list is safe regardless of the exact number), so this is flagged in the
"could not verify" section rather than asserted either way.
ADDENDUM to §5a (func_80181F74, ov_SC03_112, wave cn)
A CROSS-JUMP TWIN BLOCK GUARDED BY ITS OWN if NEEDS THE BARRIER IN THE ELSE-CONTINUATION, NOT AT THE BLOCK HEAD AND NOT INSIDE THE GUARD'S THEN-ARM
§5a's own recipe places the zero-byte volatile-asm barrier "after the last store, before the return" —
written for a flat twin block (a sequence of stores ending in return c;). func_80181F74
(ov_SC03_112, MATCH 196/196, banked at src/ov_SC03_112/ov_SC03_112_jr_80181F74.c) shows a twin block
that is itself an if-guarded counter (*(s32*)(p+0x1C) += 1; if (counter >= 0x10) goto C78;), and
§5a's placement instruction does not say where inside a guarded twin the barrier goes. Three
placements were probed and only one works:
- Barrier above the shared suffix (top of the block):
find_cross_jump's backward scan starts at the fall-through edge below the barrier and merges the whole suffix anyway — the barrier never gets compared because the scan never walks up into it. - Barrier inside the THEN-arm of
if (>= 0x10): kills the merge, butjump.c's block layout then re-lays the branch asbnez-into-the-guarded-arm, flipping the target'sbeqz-into-C78 polarity — a BRANCH-POLARITY residual traded for the LENGTH-DRIFT it fixed. - Barrier in the ELSE-continuation, immediately after the guard, before the shared tail goto — the only spelling that reproduces the target exactly:
case 3:
*(s32 *)(param_1 + 0x1c) += 1;
if (*(s32 *)(param_1 + 0x1c) >= 0x10) {
goto C78;
}
__asm__ __volatile__("");
goto TAIL;
The sibling twin block (case 1) has the byte-identical 7-instruction shape but carries no
barrier — adding one to only ONE of a pair of would-be-identical tails is what defeats
rtx_renumbered_equal_p's suffix comparison; both blocks do not need the barrier, only one.
Evidence. asm/ov_SC03_112/nonmatchings/ov_SC03_112_jr_80181F74/func_80181F74.s; banked source
src/ov_SC03_112/ov_SC03_112_jr_80181F74.c:2748-2864, case 3: at line ~2825 carries the
__asm__ __volatile__(""); exactly as shown above; case 1: (line ~2789) has the same
+= 1; if (>= 0x10) goto C78; goto TAIL; shape with no barrier.
When it applies. Two candidate cross-jump twins are each an if-guarded tail (not a flat
store-sequence) sharing the SAME guard shape and SAME post-guard fall-through. Place the barrier as
the LAST statement of the fall-through continuation on exactly one of the two candidates, after the
guard's own branch and before the tail jump — not at the block's head (the scan walks past it) and not
inside the guard's taken-arm (it changes which arm is fall-through and flips branch polarity instead of
just blocking the merge).
ADDENDUM to §265 (func_8017E26C, ov_SC04_016, wave cn)
THE void f() { __asm__(...); } WRAPPER FORM MUST END THE STRING AT .set reorder — A TRANSCRIBED jr $ra/nop DUPLICATES cc1's OWN AUTO-GENERATED FUNCTION EPILOGUE
§265's C-definition-wrapper recipe (form 2) already documents transcribing a handwritten .s verbatim
inside a void f() { __asm__ __volatile__(...); } body. What it does not say: for THIS wrapper form,
cc1 still owns the function's own epilogue (the jr $ra/nop pair that closes out the C function),
because from cc1's point of view the function body is a single asm statement followed by an implicit
fall-off-the-end return. If the transcribed string ALSO includes its own trailing jr $ra/nop
(copied straight from the target .s's tail, which is exactly what "transcribe verbatim" invites), the
two epilogues stack — the target's return sequence appears twice. The fix is to transcribe everything
EXCEPT the final jr $ra/nop, ending the string at .set reorder and letting cc1 emit its own
closing return.
Byte evidence. func_8017E26C (ov_SC04_016, banked verbatim at
src/ov_SC04_016/ov_SC04_016_jr_8017BEBC.c:3534-3813). The asm string's own hand-transcribed
prologue/epilogue supplies sw $ra,80($sp) / addiu $sp,$sp,-88 at the head and
lw $ra,80($sp) / addiu $sp,$sp,88 at the tail (both present, both load-bearing — this function
saves/restores its own frame inside the string), but the string ends at ".set\treorder\n" with no
explicit jr $ra/nop — those two instructions are left for cc1's own function-closing codegen to
emit. Confirmed by reading the source directly (lines 3808-3813).
Toolchain facts confirmed alongside this (§265 already states the first; the rest are additions):
- MASPSX rejects hex immediates inside the string — decimal only (already in §265).
%hi/%louse a single%; a doubled%%hiis rejected by the assembler.- A GTE control-register operand must be written by its numeric form (
$31), not the pseudo-name form ($12) —cfc2 $12,31is rejected,cfc2 $12,$31(destination GPR, numeric CP2 control reg) is accepted.
When it applies. Any §265 lane-2 (void f() { asm(...); }) transcription. Read the target .s's
own trailing jr $ra/nop as belonging to cc1's auto-generated epilogue, not to the payload to
transcribe — leave it out and end the string at .set reorder, exactly as func_800CBA44's existing
precedent already does (cited by §265, but the WHY was not stated).
ADDENDUM to §42b (func_8018247C, ov_SC07_002, waves cu + cw)
A "DID THE SPLICE ADD A NEW COMPILER DIAGNOSTIC" DIFF IS A CHEAP PRE-GATE SANITY CHECK FOR A DECLARATION-LAYER CONFLICT — DOCUMENTED ONLY AS ONE FUNCTION'S OWN BANKED COMMENT, NOWHERE IN THE COOKBOOK OR INDEX
§42b's mandatory gate shape (git checkout → splice → rm the split .o → make build with an
asserted exit code → sha1sum vs config/check.<binary>.sha) is the real, sole arbiter — that does not
change. What is missing is a pre-gate diagnostic for narrowing down WHY a byte-exact, match_one-MATCH
draft keeps failing that real gate: diff the whole-TU compiler stderr with and without the splice. If
the splice adds zero new diagnostic lines over the untouched-TU baseline, the declaration-layer classes
§236/§61a/§138 name are very likely closed (necessary evidence, not by itself sufficient — the gate is
still the sha1 check) — narrower framing than the candidate note's own "diagnostic parity IS the bank
test," which overstates a diagnostic heuristic as if it were the bank criterion itself.
Evidence this technique is real and already practiced in-tree (not merely proposed):
src/ov_SC07_002/ov_SC07_002_jr_8017C8D0.c:6737-6845, the banked comment on func_801849AC, states
verbatim: "Verified by splicing this body over the INCLUDE_ASM at TU L3917 and compiling the whole TU:
same 7 stderr lines as the untouched TU, and func_801849AC is byte-identical." This is a real, on-disk
precedent of a prior session doing exactly this diff before trusting a splice — confirmed by reading the
comment directly, not inferred.
When it applies. A card that is match_one-MATCH and byte-stable across many resubmissions but
keeps failing the whole-binary gate, where every checkable §236/§61a/§138 declaration-layer class comes
back clean by static reading. Before spending another session re-deriving the C: build the untouched TU
once, build the spliced TU once, and diff cc1's stderr line-for-line. New lines pinpoint the exact
conflicting declaration (or its absence rules out the declaration layer as the cause, redirecting
suspicion to sibling-card / batch-gating mechanics per §176b).
ADDENDUM to §199-F family (func_8017EFB0, ov_SC02_021, wave cu)
RESTRUCTURING THE ENCLOSING GUARD FROM "WRAP THE BODY IN if" TO "EARLY RETURN" CAN FILL AN UNRELATED, DOWNSTREAM CONDITIONAL BRANCH'S DELAY SLOT — A CFG-SHAPE LEVER, NOT A FENCE
§199-F's target-head fence (a bare __asm__ __volatile__("") at the head of one arm) is the documented
way to steer which thread's instruction fills a conditional branch's delay slot. func_8017EFB0
(ov_SC02_021, MATCH 25/25) shows a DIFFERENT lever reaching the same class of outcome: the outer guard
of the whole function was written as an early return (if (s0 == 0) return 1;) rather than a wrapping
if (s0 != 0) { …body… }. Both spellings are semantically identical and both pass match_one on their
own local shape, but only the early-return form reproduces the SECOND, unrelated branch's delay-slot
fill deeper in the function (beqz $v0,.L8017EFF8 picking up lui $v0,(0x80520>>16) — a load literal
belonging to the fall-through arm, not the branch's own condition).
Mechanism (as far as observable from the outside). fill_eager_delay_slots' scan of a branch's
candidate threads is gated by own_thread_p/LABEL_NUSES reachability analysis over the whole
function's CFG, not just the branch's immediate arms. Wrapping the guarded body in an if block
creates a different block/edge structure around the SAME later branch than an early-return does (the
early-return form removes one CFG edge — the "guard false" path never reaches a block that also flows
into the later branch's fall-through — changing which insns downstream own_fallthrough analysis
considers eligible). The practical result matches §199-F's own consequence (a delay slot gets filled
from a specific thread) without touching that branch's own arms or adding a fence anywhere near it.
Byte evidence. src/ov_SC02_021/ov_SC02_021_jr_8017C294.c:3818-3828:
s32 func_8017EFB0(void) {
s32 s0 = func_80029204(0);
s32 v1;
if (s0 == 0)
return 1;
func_80162760();
v1 = *(s32 *)D_80193528;
if (v1 < s0)
v1 += 0x80520;
return (v1 - s0) < 0x2D0;
}
asm/ov_SC02_021/nonmatchings/ov_SC02_021_jr_8017C294/func_8017EFB0.s: beqz $s0,.L8017F000 /
addiu $v0,$zero,0x1 (outer guard's OWN delay slot, unremarkable — the early-return constant lands
there per ordinary jump-inversion, §3-T4) — but ALSO, further down, beqz $v0,.L8017EFF8 /
lui $v0,(0x80520>>16) for the SECOND, independent test, which the note reports ~19 if/else-spelled
drafts of the SAME body left as a bare nop.
When it applies. A near-miss whose only diff is an unfilled (nop) conditional-branch delay slot
DOWNSTREAM of an unrelated guard, where every fence placement at that branch itself (§199-F) has already
been tried and failed or is inapplicable. Try restructuring the EARLIER, unrelated guard from a wrapping
if to an early return before concluding the slot is unfillable — the CFG shape of a guard several
statements earlier can gate a later branch's own eligibility.
(Declaration-layer note, not promoted: the function's sole data symbol D_80193528 is typed u8[]
per its access width, matching the card's own atlas row — ordinary practice, not a new fact.)
ADDENDUM to §195-E (func_800CFC1C, md_MAIN_003, wave cv)
A GUARD'S OWN 0/1 TRUTH VALUE CAN BE THE FUNCTION'S RETURN VALUE, NOT JUST THE BRANCH'S CONDITION — SPELL IT AS return (T)gate; USING THE SAME NAMED BOOLEAN THE if TESTS
§195-E already establishes that a 0/1 value materialised at a join immediately before a controlling
beqz/bnez proves the source named the condition. func_800CFC1C (md_MAIN_003, MATCH 121/121,
banked at src/md_MAIN_003/md_MAIN_003.c:241) sharpens this for the specific case where the guard's
failure path RETURNS EXACTLY THE GUARD'S OWN TRUTH VALUE: writing gate = cond; if (gate) return (T)gate; — reusing the SAME named boolean both as the branch's condition and, unmodified, as the
return expression — keeps the compare's own destination register ($v0, from slti) as the return
register, with no separate li 0/li 1 materialisation on that path. A semantically equivalent but
differently-spelled version (if (cond) return 1;, or a fresh return cond ? 1 : 0;) would introduce a
second definition and is NOT proven byte-neutral here — the target's exact byte pattern requires the
compare's result itself, untouched, to reach $v0.
Byte evidence. src/md_MAIN_003/md_MAIN_003.c:265-270:
D_800EC69C = D_800EC69C + 1;
gate = D_800EC69C < 2;
if (gate) {
return (u16 *)gate;
}
asm/md_MAIN_003/nonmatchings/md_MAIN_003/func_800CFC1C.s:E4C-E5C:
addiu $v0, $v0, 0x1
lui $at, %hi(D_800EC69C)
sw $v0, %lo(D_800EC69C)($at)
slti $v0, $v0, 0x2
bnez $v0, .L800CFDDC
addu $s3, $zero, $zero
slti writes its 0/1 result directly into $v0; the bnez on that same register branches to the
shared epilogue with $v0 already holding the function's return value — no intervening li/move.
When it applies. A guard whose ENTIRE failure-path body is return <the guard's own boolean> (not
some other constant, and not a value derived from the guard by more than a bare cast). Name the compare
result once, test the SAME named local in the if, and return that same local (cast to the return
type) — do not re-derive the return value as a fresh literal or a second expression, even one that is
mathematically identical.
ADDENDUM to §215 addendum (func_800CB2C8, md_MAIN_033, waves cv + cw)
A TIED WRITEBACK BETWEEN TWO ALREADY-PINNED VARIABLES FORCES A GENUINE CROSS-REGISTER MOVE FOR A MULTI-STEP COPY CHAIN — x = y; asm("":"=r"(x):"0"(x)); y = x; — WHERE PLAIN REASSIGNMENT WOULD COALESCE OR GET VALUE-FORWARDED AWAY
§215 addendum item 5 already establishes that two pins of the SAME hard register coexist when their
live ranges are disjoint BLOCKS. func_800CB2C8 (md_MAIN_033, banked at
src/md_MAIN_033/md_MAIN_033.c:146) shows a related but distinct case: TWO DIFFERENT pinned variables
(register s32 raw __asm__("$3") and register s32 t __asm__("$2")), where the target's asm shows a
genuine register-to-register MOVE from one physical register to the other partway through the function
(a move/addu d,s,$zero encoding — confirmed NOT a zero-register add). A plain C reassignment between
two pinned locals (t = raw;) is not guaranteed to survive as a real move — cse/value-forwarding can
fold it away, especially when both sides are already hard-reg pinned and the "assignment" looks like a
no-op to the optimizer. The working idiom names the SAME variable pair through an explicit tied
input/output asm with no operands changed, forcing gcc to treat it as a live use that must be
materialised:
raw = t;
__asm__("" : "=r"(raw) : "0"(raw));
t = raw;
Because raw and t are each pinned to exactly one hard register apiece (no duplicate-pin conflict,
§257 item — pins on the same register in the same scope are refused by this cc1), and the tied asm
gives raw a real def/use pair that cannot be coalesced with t's, the subsequent t = raw; is forced
to emit as an actual cross-register move rather than being value-forwarded or dropped.
Byte evidence. src/md_MAIN_033/md_MAIN_033.c:146-165:
register void *p __asm__("$4");
register s32 raw __asm__("$3");
register s32 t __asm__("$2");
...
raw = *(u8 *)((u8 *)s1 + 0x24);
t = raw - 0x10;
if (t < 0) { ... }
raw = t;
__asm__("" : "=r"(raw) : "0"(raw));
t = raw;
*(u8 *)((u8 *)s1 + 0x24) = *(u8 *)((u8 *)s1 + 0x25) = *(u8 *)((u8 *)s1 + 0x26) = t;
The note's own residual history (near-33 → near-7 (pins) → near-4 (zero-reg-add misreading) → near-2 → MATCH) records introducing FRESH variables for the same chain as the failed intermediate step —
confirming the specific requirement is REUSING the two already-pinned variables, not adding new pinned
locals.
When it applies. A target shows a plain register-to-register move/addu d,s,$zero mid-function
between two hard registers that are both independently already forced by pins elsewhere in the same
function, and a naive reassignment between the two pinned C variables does not reproduce it. Route the
value through BOTH pinned variables with a tied, unmodified "0"-constrained asm between the two
assignments, rather than adding a third, freshly pinned local.
ADDENDUM to §236 item 4 (func_8017D268, ov_SC04_006, wave cw)
A SHARED Blk8/Rec8/S8-STYLE STRUCT TYPEDEF'S NAME IS KEYED TO (THE GLOBAL'S ADDRESS, THE DEFINING FUNCTION'S OWN ADDRESS) — GREP FOR YOUR FUNCTION'S ADDRESS, NOT A TWIN'S OR A NEIGHBOUR'S
§236 item 4 lists Blk8_80126940_8017D6D0_8017F6D4 among the shared-header names a draft must not
re-typedef, but does not state the naming convention itself. src/shared/engine_types.h disambiguates
per-function VIEWS of one shared raw buffer by compounding the global's own address with the address(es)
of the function(s) that use that particular field layout — because the same 8-byte (or other small)
global is legitimately interpreted with DIFFERENT field layouts by different functions, one bare
Blk8_<addr> name is not always enough. func_8017D268 (ov_SC04_006) was drafted against a donor
struct name borrowed from a NEIGHBOUR in the same TU (Blk8_80126940_8017D344) and separately from a
banked TWIN in another overlay (Blk8_80126940_8017D6D0) — both wrong; the correct spelling,
Blk8_80126940_8017D268, is keyed to func_8017D268's OWN address and exists in the header
specifically for it.
Evidence. grep -n Blk8_80126940_8017D268 src/shared/engine_types.h → line 1465,
} Blk8_80126940_8017D268; — confirmed present, keyed exactly as described. The banked same-address
twin src/ov_SC04_003/ov_SC04_003_jr_8017BEBC.c:3342 independently uses this same spelling for the same
global, corroborating that the key is the (global, defining-function) pair, not a per-overlay or
per-neighbour choice.
When it applies. Any card whose seed or a plausible twin/neighbour offers a compound-named shared
struct typedef (Blk8_, Blk20_, Rec8_, S8_, …) for a global your OWN function also touches. Before
adopting a borrowed spelling, grep src/shared/engine_types.h for a name that ends in YOUR function's
own address; a hit there is the authoritative donor, not a twin's or neighbour's compound name (which
encodes a DIFFERENT function's field-layout view of the same raw bytes and may not match yours).
What I could not verify
- func_80181A4C (ov_SC01_001, cw), item 6 — the 24-byte copy "immovable in strict 0,4,8 order".
The note claims neither independent statements, nor a struct assignment, nor anything but a single
volatile inline-asm block through pinned registers reproduces the target's store order, and that this
contradicts §32 item 4's documented struct-assign lever. I read the banked source and confirmed the
final form does use a volatile asm block, but did not independently re-derive the two rejected
alternatives (struct assignment, plain statements) against the raw
.sin the time available — the claim is plausible given the function's size (238 instructions) but not independently re-proven. - func_8017D2A8 / func_802CC (§194-L instances, cq/cr/co) and the several other candidates marked
"restates §194-L" or "restates §176-A": these were checked by reading the self-cited section's own
text and confirming the candidate's mechanism is a described case of it, not by re-deriving each
candidate's specific instruction stream from its
.s— cheaper than full re-verification, and the standing rule (Law 2: a banked twin/an existing section gives shape, the candidate's own literals still need checking) means these are lower-confidence COVERED calls than the ADDENDUM entries above, which were checked against actual banked bytes. - func_8017F404 (ov_SC01_008, ct) — RESOLVED, not left open. A second, independent reviewer pass
read the actual banked code rather than trusting the note's self-diagnosis: the claimed fix (an
explicit cast at the call site) is not what's in the tree —
src/ov_SC01_008/ov_SC01_008_jr_8017E6F0.c:3216calls the same callee uncast, identically to another call site in the same TU. The verdict above was changed from COVERED to REJECT on this basis. This is exactly the class of error the assignment warned about (a drafting agent's self-diagnosis that does not survive checking against the bytes) — left here as a worked example of why the "trusted at face value" shortcut below has a real, nonzero error rate. - The ~40 rows self-citing "nothing the cookbook did not already cover" or an explicit existing
section number were trusted at face value once the cited section was confirmed to exist and to
plausibly match the described mechanism (per the assignment's own framing: the function is
byte-proven, the note's account of WHY is a claim) — these were not independently re-derived from the
raw
.sfiles, since doing so for ~150 COVERED rows was outside the time budget for this pass, and the risk is low (a wrong self-citation here degrades a COVERED verdict, which adds nothing to the corpus either way — it does not risk propagating a false law).
Harness-defect flags (not idioms — flagged for the operator)
- Oracle target confusion across same-named/same-address functions is the single most common
non-idiom failure mode in this batch, recurring independently at: func_80181528 (co→cp,
read_fileof a same-VMA sibling silently redirected match_one's target for three consecutive calls); func_8017DA3C (cq, an entire replayed session fought a 93-ins ov_SC06_008 function under the ov_SC06_027 card); func_8018035C (cr, wrong-overlay.spath taken from memory instead of the card); func_8017EADC (cs/ct, two DIFFERENT functions — one 36 ins in ov_SC03_003, one 92 ins in ov_SC06_024 — share the bare symbol and the oracle flip-flopped between them); func_80180558 (co, same class); func_8017FCFC (cv, the oracle cycled through three same-named functions across three different overlays in one session).match_one/the drafting harness appears to resolve a target by symbol name without confirming(binary, address)identity when a stale.sfrom a different overlay was read moments earlier in the same session. match_onecannot oracle a sibling function from inside a card seat (func_80181BE0, cn) — a drafter who wants to verify a NEIGHBOURING splice (not the card's own target) has no read-only tool for it and must audit by hand.- Carve-split staleness: a function can be simultaneously defined in a main TU (still showing
INCLUDE_ASM) and its jr-carved sibling TU (already banked), risking a duplicate-symbol link error invisible tomatch_one— func_801E8630 and func_801E8440 (md_SC04_028, cv/cw) both diagnose this independently, citingtooling-audit.mdA10's "stub/card collectors hardcodesrc/<ov>/<ov>.c+asm/<ov>/nonmatchings/<ov>/and miss_jr_<ADDR>carve families." func_80181AA0 (ov_SC03_121, cv/cw) reports the mirror case: twononmatchingsdirectories (ov_SC03_121_jr_8017E1D0and_jr_8017BEBC) both carry a byte-identical.sfor the same glabel, one of them stale. - A hypothesis worth a maintenance look, not independently confirmed: func_8017D720 and
func_8017D6E8 (ov_SC04_012, cv/cw) both suspect some gate stage may key a symbol/decl table by bare
function NAME rather than
(binary, address), which would make a same-named function in a different overlay (already known to be benign per §238/§150-B for ordinary compilation) collide at a LATER pipeline stage neither candidate could observe directly. - Symbol case sensitivity blocked a splice for a full session: func_8017EE68 (ov_SC05_010, ct) — a
body submitted as
FUNC_8017EE68(uppercase) could not bind toglabel func_8017EE68; otherwise byte-identical. INCLUDE_ASMappend-vs-atomic-replace is an open question two candidates raise independently: func_80183AF0 (ov_SC03_111, cr) and func_8018973C (ov_SC03_014, cn) — the latter observed its own resubmission racing an active fleet wave splicing the SAME function from its own queue into the SAME TU; if the harness ever appends a banked definition alongside a live stub rather than atomically replacing it, every byte-perfect draft of that function would fail identically regardless of body content.
ADDENDUM to §250 (func_8017D7CC, ov_SC03_115 — cx/cy/cz/dr)
A FUNCTION-POINTER TABLE PASSED BY VALUE (NOT INDEXED) NEEDS AN EXPLICIT SECOND DIMENSION TO KEEP ITS %hi/%lo PAIR SPLIT AROUND A jal DELAY SLOT — A 1-D ARRAY OR A BARE &SYM BOTH MATERIALISE AN EXTRA addiu.
§250 already states the array-vs-scalar dial for a symbol that is loaded or indexed: declare it
as an array and the %hi/%lo pair folds straight into an lui/addiu pair; declare it as a scalar
and gcc emits a real lw. func_8017D7CC (ov_SC03_115, MATCH 61/61, src/ov_SC03_115/ ov_SC03_115_jr_8017BEBC.c:3556) shows a byte-distinct THIRD case §250's table does not cover: the
symbol is a function-pointer table whose whole address is passed by value to a callee (never
indexed, never dereferenced by this function itself), and the number of declared dimensions on the
array — not just array-vs-scalar — changes the emitted instruction count and the delay-slot layout.
The three spellings, byte-measured:
extern void (*D_80183F48[][2])(void); /* banked — folds, splits correctly around the jal */
extern void (*D_80183F48[])(void); /* plain 1-D array — materialises an extra addiu */
extern void (*D_80183F48)(void); /* scalar + &D_80183F48 — same extra addiu */
only the two-dimensional form, used as func_8001CA1C(v0, (s32)D_80183F48);, reproduces the target's
lui $at,%hi(D_80183F48) / jal func_8001CA1C / addiu $a1,$at,%lo(D_80183F48) — the %lo half riding
the jal's own delay slot. Both the plain-array and the &-of-scalar spellings instead materialise
the address into a temporary register before the call, costing one extra addiu and displacing
everything scheduled after it. The sibling table in the same function, D_80183F70[][2], is genuinely
indexed ((s32)&D_80183F70[v1]) and folds under §250's existing scalar-array rule as expected — the
[][2] finding is specific to the by-value, whole-array-decay use, not the indexed one.
Byte evidence. src/ov_SC03_115/ov_SC03_115_jr_8017BEBC.c:3556-3589 vs asm/ov_SC03_115/ nonmatchings/ov_SC03_115_jr_8017BEBC/func_8017D7CC.s — re-confirmed across four independent
candidate sessions (waves cx, cy, cz, dr), each landing on the identical [][2] declaration
independently after first trying the plain-array and scalar forms and measuring the extra addiu.
When it applies. A function-pointer (or other pointer-typed) table whose address is taken as a
whole and passed BY VALUE to another function, where the target shows the %hi/%lo pair split
around a jal's delay slot rather than materialised together beforehand. Declaring an explicit inner
dimension that matches the table's real stride (here [2], mirroring the table's actual layout) is
what keeps gcc treating the reference as a foldable symbol_ref rather than an RTL-forced
materialisation. Does not apply when the table is indexed in C — that case already folds correctly
under §250's plain array rule.
ADDENDUM to §225 (func_8017E190, ov_SC03_115 — cx/cy/cz)
A goto CAN SKIP A GUARDING CALL ENTIRELY AND LAND ON THAT CALL's OWN CONSEQUENT, INSIDE A LATER if'S BODY — DISTINCT FROM §225's ITEM 1 (A GOTO LANDING ON A SHARED CALL BOTH PATHS MAKE).
§225 catalogues four control-flow shapes no structured spelling reaches; item 1 is "a shared call that
both failure edges jump INTO." func_8017E190 (ov_SC03_115, MATCH 36/36, src/ov_SC03_115/ ov_SC03_115_jr_8017BEBC.c:3973-3990) is a different shape from the same family: one arm makes a
guarding call and only runs its consequent if the call succeeds; a SIBLING arm's goto skips the call
altogether and lands directly on that consequent — the call is made on one path and never made on the
other, not "both paths make it."
void func_8017E190(s32 arg0) {
s32 v;
if ((*(s32 *)(arg0 + 0xDC) & 8) != 0) {
*(u16 *)(*(s32 *)(arg0 + 0x20) + 0x12) += 0x44;
} else {
v = func_8012B8E4(arg0, 8);
*(u16 *)(*(s32 *)(arg0 + 0x20) + 0x12) += v;
if (v == 0) {
goto store5; /* skips func_8012BEE8 entirely */
}
}
if (func_8012BEE8(arg0) != 0) {
store5:
*(u16 *)(arg0 + 2) = 5; /* the label sits INSIDE the if's own body */
}
}
Every structured nesting duplicates the *(u16*)(arg0+2)=5; store into both the goto-taker and the
if's taken arm (+2 ins, matching §225's own diagnostic pattern for this family: a "common" label
that must sit mid-flow, not at a block boundary, to avoid duplication). The label here sits not merely
"between two calls" (§225 item 1's phrasing) but inside the controlled statement of a subsequent
if, reached both by that if's own taken branch and by an unconditional jump that bypasses the
if's guarding call from an earlier, unrelated branch.
Byte evidence. src/ov_SC03_115/ov_SC03_115_jr_8017BEBC.c:3973-3990 vs asm/ov_SC03_115/ nonmatchings/ov_SC03_115_jr_8017BEBC/func_8017E190.s — confirmed independently across three waves
(cx, cy, cz); each session's own grep of the cookbook and its index for "goto into an if body" / "label
inside an if" returned zero hits before landing on this spelling by direct CFG reconstruction from the
.s.
When it applies. The target shows a beqz/bnez whose taken arm is reached by TWO different
routes: normal fallthrough into the branch's own test, and an unconditional jump from an unrelated
earlier arm that never evaluates the branch's condition or its guarding call. Write the shared
consequent as a label sitting inside the later if's body, and goto it directly from the arm that
must skip the guard.
ADDENDUM to §45-A (func_8017F6A4, ov_SC02_016 — cy/cz)
AN ALIAS ASSIGNMENT PLACED IMMEDIATELY AFTER A CALL (BEFORE THE CALL RESULT'S OWN NEXT USE) FORCES OVERLAPPING LIVE RANGES THAT DEFEAT COALESCING — THE SAME ALIAS PLACED AFTER THAT USE GETS RE-MERGED INTO ONE PSEUDO.
§45-A documents the MERGE direction: reusing an existing variable as a second temp buys register
priority by raising its reference count. func_8017F6A4 (ov_SC02_016, MATCH 58/58, src/ov_SC02_016/ ov_SC02_016_jr_8017DC70.c:3619-3654) shows the useful inverse — a SPLIT that must stay unmerged, and
the mechanism that keeps it unmerged is a placement timing rule, not a naming rule:
v0 = ((s32 (*)(void))func_8012C1B8)();
inst = v0; /* alias created IMMEDIATELY after the call */
*(s32 *)(param_1 + 0x20) = v0; /* raw $v0 still used here, AFTER the alias */
if (v0 == 0) { ... }
...
((void (*)(s32, void *))func_8001C214)(inst, &D_801A2D40); /* inst used much later */
v0 (the call's raw result) and inst (its alias) both remain live simultaneously across the
*(s32*)(param_1+0x20) = v0; store — inst's definition happens BEFORE that store, not after it, so
for one statement both names denote the value and both are read. That deliberate overlap is what stops
cse's make_regs_eqv/local-alloc coalescing from merging the two pseudos back into one register:
the target keeps the call result raw in $v0 for its early uses and copies it into $s1 for its late
one, which is only reachable when the two live ranges genuinely overlap for at least one statement. An
alias assignment placed AFTER the guard instead (disjoint live ranges — inst born only once v0 is
otherwise dead) recoalesces to a single pseudo and loses the register split; earlier sessions on this
same card spent several compiles on that placement before finding the pre-store position.
Byte evidence. src/ov_SC02_016/ov_SC02_016_jr_8017DC70.c:3619-3654 vs asm/ov_SC02_016/ nonmatchings/ov_SC02_016_jr_8017DC70/func_8017F6A4.s — the target's own instruction 6
(addu $s1,$v0,$zero) sits BEFORE instructions 7-8 (bnez/sw on the raw $v0), i.e. the alias copy
is created while the raw value is still live, confirming the overlap directly in the target bytes, not
merely inferred from the C.
When it applies. The target keeps one call's result live in TWO different registers across a guard/branch, with the copy instruction appearing BEFORE at least one more use of the raw value (not after all raw uses). Name the alias immediately after the call and let at least one further raw use follow it in source order; placing the alias only once the raw value's own uses are exhausted recombines the two into one pseudo.
ADDENDUM to §249 (func_80182ED4, ov_SC04_004 — dp/dr/dt)
COMPOSING THE +zr OPAQUE COPY (RC-12) WITH A REGISTER PIN NEEDS THE PIN ON BOTH THE SOURCE AND THE DESTINATION OF THE LAUNDERED COPY — AN UNOPPOSED DESTINATION-ONLY PIN LETS COPY-PREFERENCE DRAG THE SOURCE VARIABLE ONTO THE SAME REGISTER TOO.
§249 point 3 already shows the RC-12 +zr opaque copy composed with pins (func_801AB54C: v=$2,
c=$3) as a working recipe, but does not say why an unopposed single pin fails. func_80182ED4
(ov_SC04_004, MATCH 42/42, src/ov_SC04_004/ov_SC04_004_jr_8017AE2C.c:7843-7876) isolates the reason
across nine falsified single-pin/no-pin variants before landing on the working double pin:
register s32 t __asm__("$2");
register s32 u __asm__("$3");
register u32 zr __asm__("$0");
...
} else {
u = t + zr; /* RC-12 opaque copy: t -> u, laundered through $0 so cprop cannot re-link it */
t = u - 1;
}
Mechanism, read off the three measured cells. (a) A plain u = t; copy is deleted by fwprop
(pure copies fold away); the RC-12 +zr form survives specifically because it is not
REG_EQUIV-linkable back to t. (b) Pinning only the destination (u = $3, t unpinned) lets
find_reg's copy-preference (the same hard_reg_copy_preferences mechanism §164-22/§195-A already
document elsewhere in this corpus) drag the WHOLE t variable onto $3 too, because t's own
allocno now has a $3 copy-preference from the u=t+zr set and nothing opposes it — the entire
variable class inverts ($v1-vs-$v0 swapped from the target). (c) Pinning BOTH ends removes that
degree of freedom entirely: t cannot drift toward u's register because t already has its own
hard-register assignment, and the allocator has no remaining preference to act on.
Byte evidence. src/ov_SC04_004/ov_SC04_004_jr_8017AE2C.c:7843-7876 vs asm/ov_SC04_004/ nonmatchings/ov_SC04_004_jr_8017AE2C/func_80182ED4.s — the banked pin triple is on disk exactly as
shown; the candidate notes (independently re-derived in three separate waves, dp/dr/dt, identical
text each time) report the falsified single-pin cell landing on the wrong register class ($v1 > $v0
instead of $v0 > $v1), consistent with the copy-preference mechanism above.
When it applies. An RC-12 +zr opaque-copy launder is being used to keep two logically-related
values (a value and its "processed" successor) in two DIFFERENT registers, and a single destination
pin is not holding the split. Pin the SOURCE too — the launder's whole point is defeated if only one
end is anchored, because copy-preference still has a free hand on the other.
ADDENDUM to §199-A (func_801816FC, ov_SC02_005 — dp/dr/dt)
ROUTING OTHERWISE-UNRELATED SMALL IMMEDIATES THROUGH AN ALREADY-PINNED SCRATCH REGISTER CREATES A GENUINE RAW DATA-DEPENDENCE CHAIN THAT ANCHORS THEM BETWEEN A SPLIT CONSTANT's lui/ori HALVES — A THIRD LEVER BEYOND §199-A's TWO (KILL THE INTERLOPER's BIRTHING BOOST, OR MOVE ITS DEFINING STATEMENT).
§199-A explains WHY unrelated single-set constants land between a split literal's lui/ori halves
(sched1's backward list scheduler and the birthing-boost priority rule) and names two available levers
to control it: kill the interloper's birthing boost, or move the interloper's own defining statement.
func_801816FC (ov_SC02_005, MATCH 129/129, src/ov_SC02_005/ov_SC02_005_jr_8017CF90.c:5201-5260)
uses a third lever neither §199-A entry states: routing the interlopers' VALUES through an
already-pinned register variable so that each interloper becomes a genuine RAW-dependent read of
that same physical register, rather than an independent single-set constant competing on scheduler
priority alone.
register s32 v0 __asm__("$2");
...
a0r = 0xE000E0; /* split-constant half held in a pinned reg */
v0 = 1;
*(u8 *)((s32)s0 + 0xC0) = (u8)v0; /* interloper 1 — routed through the SAME v0 */
v0 = (s32)D_80196218;
*(s32 *)((s32)s0 + 0xBC) = v0; /* interloper 2 */
v0 = 8;
*(u8 *)((s32)s0 + 0x75) = (u8)v0; /* interloper 3 */
...
*(s32 *)(v1 + 0x28) = a0r; /* a0r's real use — the lui/ori pair folds around here */
Six card sessions across three waves report every §199-A-prescribed lever (birthing-boost kill via a
§30#3 re-tie, moving the interlopers' defining statements, two-step constant splits, pin subsets on
a0r alone) measuring null or wrong before this composition worked. The mechanism differs from
§199-A's: instead of relying on sched1's priority contest to place three independent constants in a
gap, EACH interloper becomes a genuine SET of the same pinned register $2 that the allocator must
honour by construction (a register __asm__ binding is a hard placement, not a scheduling
preference), so their positions are pinned by data dependence rather than won on priority.
Byte evidence. src/ov_SC02_005/ov_SC02_005_jr_8017CF90.c:5201-5260 vs asm/ov_SC02_005/ nonmatchings/ov_SC02_005_jr_8017CF90/func_801816FC.s — re-confirmed identically across three waves
(dp, dr, dt); the candidate notes explicitly record having tried §199-A's own two prescribed levers
first and falsified both before finding this one.
When it applies. Several small, otherwise-unrelated immediate stores must land, in the target, between the two halves of one split literal constant, AND that constant is already held in (or can be moved into) a pinned register. Route the unrelated immediates through the SAME pinned register (or another already-pinned scratch) rather than leaving them as independent locals — this is a stronger, more reliable lever than chasing sched1's priority rules directly, at the cost of needing a free pin.
What I could not verify
-
func_8017EB34 (ov_SC03_117), waves dp/dr/dt: the typedef/extern decl-conflict diagnosis (§236-4) is sound and independently confirmed by direct source reading. The more elaborate claims layered on top of it — a specific "hard-reg-vs-pseudo TRUNCATE fork" register mechanism, and (in the dt-wave version only) an inline-
__asm__-store escape hatch used instead of the block-scope typedef shadow — could not be confirmed against the current tree. The function still has draft copies scattered across roughly a dozen.run/sweep_*directories and its.sis still present undernonmatchings/with no clear signal from the readable surface of which (if any) submitted body is the one that ultimately banked; the livesrc/file currently uses a plain file-scope typedef shared with a sibling function, a spelling none of the three candidate write-ups describe. Not promoted. -
func_8018043C (ov_SC03_031), waves cy/cz: the note's specific claim — that the fleet-canonical spelling
extern void (*D_80186478[])(void);(no parameter) is required over a locally-invented(void*)form — does NOT match what is currently banked atsrc/ov_SC03_031/ov_SC03_031_jr_8017DB5C.c:3923, which declaresextern void (*D_80186478[])(void*);and never calls through the table as a function pointer at all (only address-arithmetic indexing). Downgraded from a considered ADDENDUM to plain COVERED §236-1/law 2 — the general "fleet wins when the TU has no prior decl of the symbol" principle is sound and unremarkable, but this specific instance's byte claim did not survive checking. -
func_8018C000 (ov_SC03_001), waves dp/dr/dt: the "keep the TU's
(s32,s16)signature, not(s16,s16)" fix is confirmed verbatim against the live tree (src/ov_SC03_001/ ov_SC03_001_jr_8018B8DC.c:3168and:3521). The dr-wave session's more specific claim — that the switch operand needs(s16)(param_1 - 1)with cases rebased 0..13 to reproduce the minval fold — does NOT match the current tree, which uses the plainswitch ((short)param_1)with cases numbered 1..13 (no rebase). Either that intermediate theory was superseded by a later session's correction (visible in the dt-wave candidate, whose skeleton is what is actually banked) or the two candidates describe different points in an evolving multi-session card; either way the-1-rebase claim specifically could not be verified and was not promoted. -
func_8017F1F0 (ov_SC03_001), wave db: the
register s32 rv __asm__("$2")return-value pin under avoid-declared definition is a plausible, self-consistent alternate implementation of §138's DEFINITION-alias escape row, but I did not independently confirm it against the target.s(the candidate reports 16 verified MATCHes across sessions; I read only the mechanism description, not the bytes). Filed as COVERED under the closest existing class rather than promoted, out of caution. -
func_8017D6E4 (ov_SC07_011), waves dr (×2)/dt (×3): five near-identical write-ups of what turned out to be one root cause (a session-1 path typo that bound the wrong overlay's same-named function for several subsequent sessions). I trust the self-diagnosis — it is internally consistent and the final wave-dt/dr versions cite the correct
.spath and correct symbol set — but did not independently re-derive the 15-instruction dispatcher's semantics against the corrected target.
Harness-defect flags
Several recurring patterns in this batch describe harness/tooling behavior rather than compiler idioms. None of them are new to the project (each maps to an already-named class), but their volume in this specific batch is worth surfacing to whoever owns the gate/oracle, separately from the per-candidate verdicts above:
-
Persistent, unexplained whole-binary-gate refusal despite an exhaustively excluded integration surface.
func_8018CD5C(ov_SC06_033, wave dr) andfunc_8018CEA8(ov_SC03_001, wave dt) are the two sharpest instances: both reportmatch_oneMATCH across THREE-plus independently-written declaration layouts, all byte-identical, with every §58/§61a integration wall (jtbl drift, self-decl conflict, type-lift, arity, link-miss, concurrent-splice corruption) individually excluded by direct evidence from the readable tree — andfunc_8018CEA8additionally reports that submitting an ALREADY-BANKED sibling's exact text (in a different overlay) also failed to bank. Both resolve (correctly) to §270/§273/§259 cluster 4 as a verdict, but the sheer persistence — 6-10+ sessions each, every documented recovery lever tried — suggests these two cards specifically merit a real gate-log read (.run/wave_*shard directories, cc1/link stderr) rather than another drafting pass. This is the strongest actionable signal in the whole batch. -
Oracle/target binding drift — the oracle serves a stale or wrong-overlay body for part or all of a session. At least seven distinct instances across this batch:
func_80181F80(cx/cy, served a 201-ins body for a 61-ins target),func_8017F8C0(cx/cy/cz, served an unrelated same-named counter function instead of the real GTE routine),func_8017E190(cx/cy/cz, cross-binary same-address twin scored as the wrong overlay mid-session),func_80181C38andfunc_8017EBA0(da, oracle identity churn / wrong comparison target),func_801E30C8(cx/cy, replay-audit needed to re-establish ground truth), andfunc_8017D6E4(dr/dt, though this one self-diagnoses as an operator path typo rather than an oracle fault). Individually these are all §238-adjacent and already-anticipated, but the recurrence rate this session (7+ of 213 candidates, concentrated in ov_SC02_016/ov_SC03_115/ov_SC03_001) suggests target-binding reliability for those specific overlays is worth a maintenance look. -
A bogus lever label ("head-crack") appears in card metadata pointing nowhere. Confirmed absent from both
docs/matching-cookbook.mdanddocs/cookbook-index.md(zero grep hits) by two independent candidates (func_8017FF20, waves cy and cz;func_8017D39C, wave da makes the same complaint about a different, unrelated lever). Whatever atlas/card-generation step assigns lever labels should be checked for a source of these placeholder/broken names — each occurrence cost the drafting agent a disproving turn. -
.sfile persistence after banking is not a reliable non-bank signal, and this has now cost multiple sessions independently rediscovering it.func_80183694(wave da) andfunc_8018CEA8(wave dt) both independently found already-banked C functions whosenonmatchings/*.sfiles still exist on disk, and both explicitly warn future sessions not to treat.spresence/absence as a banking oracle. This is a pure tooling-cleanup-lag fact, not a compiler idiom, but it is clearly costing real turns. -
The dn/do waves' broken-tree outage (
config/overlays.mkcommitted empty, 0 banked across both waves) is real and visible in the current tree (config/overlays.mkis 0 bytes at HEAD, though clean/uncommitted at time of review). Per the assignment's caution, gate-blame claims from these two waves specifically (func_8017D878,func_80184908,func_8017DE14,func_8017D8DC) were treated with extra suspicion and mostly filed as REJECT/harness-observation rather than promoted, even where the underlying C-level diagnosis looked sound — the outage is sufficient explanation for the non-bank on its own and cannot be distinguished from a genuine residual using only the readable surface. -
GTE-macro silent-default substitution in
match_one.func_8018012C(waves dp/dr/dt, all three identical) reports that omitting agte_*inline-asm macro from a draft makes the oracle silently substitute its own default implementation, which in this case lacked a"memory"clobber the real destination TU's macro carries — producing a clean-looking small LENGTH-DRIFT that reads as a scheduling problem rather than a missing clobber. This is a real, checkable oracle behavior with game-codegen consequences, but it is a fact aboutmatch_one's macro-substitution fallback, not about gcc-2.7.2 — filed as a harness-defect rather than a cookbook addendum for that reason.
§284 — COMBINE CAN REASSOCIATE TWO SEQUENTIAL BITWISE-AND MASKS INTO ONE AGAINST THE PRE-MASK VALUE; AN ASM IN/OUT FENCE RIGHT AFTER THE FIRST MASK BLOCKS IT (P31, wave dd, func_8018087C, ov_SC04_020, byte-proven) (P31 S60; waves #, byte-proven)
§285 — PRE-INITIALIZING A VARIABLE WITH A SHARED CONSTANT BEFORE A BRANCH MAKES BOTH ARMS OF THE FOLLOWING if/else DESTRUCTIVELY REUSE THE SAME DESTINATION REGISTER FOR THEIR BITWISE RESULT (byte-proven; func_801811F0, ov_SC03_102, independently rediscovered across waves di/dj) (P31 S60; waves #, byte-proven)
§286 — FOLD A STATEMENT'S SIDE EFFECT INTO A COMMA-EXPRESSION IN AN ARGUMENT POSITION TO PLACE ITS RTL RELATIVE TO A CALL'S OWN DELAY SLOT (P31 S60/dj; func_80180A88, ov_SC06_010, byte-proven 411/411) (P31 S60; waves #, byte-proven)
§287 — A STACK-FRAME HOLE BELOW TWO ADDRESS-TAKEN AGGREGATE LOCALS IS A LEADING PAD MEMBER OF ONE COMBINED STRUCT, NOT SEPARATE LOCALS OR A REGISTER PIN (P31 S60/dj; func_8017D104, ov_SC06_027, byte-proven 47/47) (P31 S60; waves #, byte-proven)
§288 — A REGISTER PIN DECLARED UNINITIALIZED AND ASSIGNED ONLY AT ITS LATE, SOLE USE STILL FORCES THE FIXED REGISTER'S SAVE/RESTORE, AND THE ASSIGNMENT'S SOURCE POSITION CONTROLS WHERE THE VALUE MATERIALIZES (P31; func_80186530, ov_SC02_017, byte-proven, match_one MATCH re-verified) (P31 S60; waves #, byte-proven)
§289 — an array local's address-taken base keeps every element's store alive, even though only one pointer escapes (func_80189EFC, ov_SC04_011) (P31 S60; waves #, byte-proven)
§290 — A SINGLE STRENGTH-REDUCED GIV CAN DRIVE STORES TO SEVERAL DISTINCT RELOCATABLE SYMBOLS, EACH KEEPING ITS OWN %hi/%lo ANCHOR (P31 S60; waves #, byte-proven)
§291 — THE DELAY-SLOT FALSE-VALUE: A CONDITIONAL BRANCH'S ZERO ARM MUST BE A FALL-THROUGH-ADJACENT BLOCK ENDING IN AN EXPLICIT JUMP, OR REORG CANNOT MATERIALIZE IT INSIDE THE BRANCH'S OWN DELAY SLOT (P31 S60; waves #, byte-proven)
§292 — DECLARING A SYMBOL UPSTREAM OF AN ALREADY-BANKED SIBLING THAT RELIES ON THAT SYMBOL'S IMPLICIT (K&R) DECLARATION CAN SILENTLY REPROTOTYPE THE SIBLING'S OWN CALL SITE (P31 S60; waves #, byte-proven)
§293 — THE "WHOLE-OBJECT GATE NEEDS EVERY SIBLING" LAW, WRITTEN PROPERLY: IT IS THREE LAWS, AND THE LOAD-BEARING ONE IS "THE BASELINE, NOT THE SIBLINGS" (P31 S61; byte-proven on 15 binaries in one night)
Eight cards across four waves independently re-derived "the whole-object gate needs every sibling
matched" (§283 flagged it as the strongest missing-section signal). Taken literally it is FALSE —
--chunk 1 gates one draft per build and a correct draft banks with every sibling still a stub —
but the agents were observing something real. It decomposes into three separable laws:
(1) The batch-revert policy artifact (historical, already ended). §12/§14-L1144/§20: a chunked
gate or an all-or-nothing slate reverts every co-spliced draft when ONE fails, so a byte-perfect
body reads as refused. --chunk 1 + stage-0-first ended the mechanism; the folklore survives in
cards because the SYMPTOM (my perfect draft won't bank) persists via (2) and (3).
(2) The structural coupling that is real: a bank moves rodata. A stub's .s carries the
function's jump tables and migrated rodata islands at their RETAIL positions; banking the function
makes the COMPILER emit them into the TU object's .rodata instead. Retail layout then survives
only if the carve chain is complete and current: the splat yaml .rodata subsegment (§8), the
object's entry at the right position in <bin>_JTBL_INTERLEAVE (§8b), and a correct per-object
JTBL_PADS spec (§8e). ONE missing or stale piece shifts every later rodata/data/text reference —
byte-witnessed tonight: a single absent ov_MAIN_012_jr_8016AB6C.o interleave entry displaced
1,671 symbols by +0x20; a stale 2-entry pad spec (its switch function had been reverted, taking
a table with it — the R51 stored-derived-property class) held ov_SC01_006 RED; ov_SC04_000's wave-
committed carve left the order list one object short (+0x2944 from tail19 on). None of this is
about siblings — it is about the OBJECT's rodata bookkeeping.
(3) The load-bearing law: A GATE VERDICT IS A MEASUREMENT OF THE DRAFT ONLY WHILE THE BINARY'S
BASELINE IS GREEN. When (2) breaks, the BINARY fails its locked SHA with no draft spliced at all
— and then every draft gated against it is refused regardless of quality, which reads exactly like
"my sibling is blocking me." Measured tonight (S61 fleet audit, 214 binaries): 15 binaries were
baseline-RED at HEAD, and they accounted for 174 of the 182 refusals of the resolver's
doubly-verified drafts (245 staged; rtu-MATCH at the real TU AND reloc-AGREE on symbols). They
went red DURING S60's evening incremental banking — §61c's "incrementally valid, clean-invalid" —
and the fleet sweep that would have caught them had been guard-skipped since 12:54 (R54). The gate
now refuses a red-listed binary's drafts as class BASELINE-RED before any build (gate_stage,
.run/baseline_red.txt ∪ .run/fleet_red.txt), so a broken baseline can never again be billed to
the drafts. Corollary, one night, same class: an INSTRUMENT'S OWN WRITE PATH IS PART OF THE
INSTRUMENT — jtbl_pads_fix.write_pads split its target line on ':' (but := contains one),
poisoned the registry into a make parse error ("target pattern contains no '%'"), which failed
EVERY build of EVERY binary, voided its own candidate search (0 winners: its writer broke the
build it was measuring), and was adopted by a blanket pre-gate commit (R52's second instance).
mk_write now refuses parse-poisoned lines.
Recipe when a "wall" function won't bank and the body is proven: (a) make build BINARY=<bin>
on a CLEAN tree first — if the baseline is red, stop blaming drafts; (b) locate the shift, don't
guess: cmp -l the built image against the retail bytes and map offsets through the linker map —
name-encoded symbols (D_/func_/jtbl_<addr>) make "first misplaced symbol, delta, section, object"
a two-minute derivation; (c) the object it names tells you which carve piece is missing/stale
(yaml subsegment / interleave entry / pad spec); repair THAT, byte-prove, commit; (d) only then
re-judge the drafts — tonight the resolver's ledger re-opened them automatically (BASELINE-RED is
not a final verdict).
§294 — ADDENDA HARVESTED FROM THE S61 GEN0/MAXTOK/MAIN/DEEPSEEK BATCH (waves ab8/ab16/ab24/ab32 · g0a–g0f · m0a · ds1/ds2): 99 candidates, 52 covered, 15 notes → 10 addenda, 29 notes → 5 new laws (§295–§299), 3 refuted
Provenance. The night the ox window closed: the MAXTOK ABCD arms (ab8/ab16/ab24/ab32, same 10
functions per arm), the gen0 overlay sweep (g0a–g0e) plus its feedback-armed redraft wave (g0f), the
main-EXE gen0 push (m0a — 57 notes, the richest ore: boot/-O0, the libapi stub cluster, and the
splitter-fragment families), and DeepSeek's first two batches (ds1/ds2 — a NEW source; calibration in
§300). Every claim below was verified against the banked C in src/ and, where the stale .s
survives, the target bytes; three claims died at that check and are named in §300. 52 of 99 notes
were already owned by §§1–293 — the healthy majority, mostly honest "nothing new — §X carried it"
self-reports.
ADDENDUM to §127 — the -O0 constant-offset fold keys on the MEMBER-ACCESS tree shape, not on the index being constant
§127's dichotomy (p->f folds the offset into the displacement; p[i] computes the address first)
reads as constant-vs-variable. func_800D1B00 (md_MAIN_011, MATCH, wave g0d) sharpens it: even a
COMPILE-TIME-CONSTANT subscript through a cast-indexed form — ((s32*)p)[3] — materializes
addiu $v0,$v1,12 + sw $v1,0($v0) at -O0, 2 instructions per statement (a 72-insn drift over the
~49-store block, per the drafting A/B). The fold fires on the COMPONENT_REF tree shape only. The
banked spelling (src/md_MAIN_011/md_MAIN_011.c:552) is a FUNCTION-LOCAL typedef with exact field
offsets plus member stores:
void func_800D1B00(void) {
typedef struct {
s32 pad00;
s32 f04; s32 f08; s32 f0C; /* ... exact offsets through f94 */
} St78;
St78 *p = (St78 *)D_80078E78;
p->f04 = 0;
p->f08 = 0x258;
...
}
At -O0 only offsets matter, so the local typedef name is free (no §236-4 risk at block scope). When a
target's -O0 store run shows sw rX, K(rBase) with folded displacements, the source MUST be member
lvalues; when it shows per-statement addiu + 0-displacement stores, it must be the indexed/cast
form — §127's law, now stated for stores and for constant subscripts.
ADDENDUM to §261a — FOUR MORE -O0 DIALS BEYOND THE RELOAD COUNT (boot.c + md_MAIN_011/003, wave m0a/g0a/g0f, byte-proven)
(a) The register-local RMW three-spelling dial. func_80011A3C (boot, MATCH 40/40): banked
src/boot.c:671:
register u16 x;
x = *(u16 *)(p + 0xA3C0);
x = x + 1;
*(u16 *)(p + 0xA3C0) = x;
→ target (asm/nonmatchings/boot/func_80011A3C.s): lhu $v1,-0x5C40($at) / addiu $a0,$v1,0x1 /
addu $v1,$a0,$zero / sh $v1,-0x5C40($at) — the PLAIN-ASSIGNMENT pair loads into the variable's
own pseudo, adds into a FRESH pseudo, and copies back. Per the drafting A/B: compound += folds the
copy-back away (−1 ins); single-statement x = *mem + 1 loads into a temp first (REGALLOC-PERM).
§219 is this dial's -O2 counterpart; §261a's reload rows are the through-pointer face — this is the
register-local face.
(b) Scale-by-4: spell the SHIFT, not the multiply. func_80010A08 (boot, MATCH): banked
src/boot.c:255:
q = (arg0 + 3) / 4;
old = D_800A5E60;
D_800A5E60 += q << 2;
q * 4 routes through the mul-style expansion (frame reload into $v0 + a move); q << 2's
ashift expansion reads the frame slot straight into the shift's operand register — the target's
shape. One token closed a 16-mismatch residual. The compound += is also load-bearing (puts the
sll before the second global load; a plain a = b + c emits the load first). The dead
register u8 *p = D_800AF630; above these lines is the §127-family unused-register-local orphan
hi/lo pair — transcribe it, don't clean it.
(c) The register-pointer far-offset base lands CALLER-saved when the body has NO calls. The
banked boot family (func_80011A3C/func_80011ADC/func_800119F0/func_80011818/func_800118AC,
all register u8 *p = D_800AF630;) keeps the base in $v0/$v1 — NOT a callee-saved register —
with each >0x7FFF member offset assembling to the lui $at,(0x10000>>16) / addu $at,$v0,$at /
sh -0x5Cxx($at) split. The TU comment (src/boot.c:203-207) attributes callee-saved placement to
register; with zero calls nothing forces a saved reg — register's load-bearing effect is
suppressing the frame SPILL (drop it → base spills, frame grows), and a bare-symbol spelling instead
folds each far access into its own single %hi/%lo anchor (the 11-ins short form) even when
§261a's reload grammar is silent (no reloads exist). Promoted from the source comment per §127b's
standing rule.
(d) Routing note — the fleet's TERMINAL state for an -O0 function stranded in an -O2 TU is the
§265 verbatim-asm block IN THAT TU, not (yet) the §18/§261 _o0 carve: src/md_MAIN_003/ md_MAIN_003.c carries five banked instances (func_800D0174, func_800D0204, func_800D0440,
func_800D09A0, func_800D3204-family). §116/§126/§261 prescribe the build-graph move; until an
_o0 object exists for the TU, bank via §265 form 1 with the recovered C preserved in a comment
(waves ab16/g0f). corpus.stubs() counts these as matched though they are transcription, not decomp
— §265's accounting caveat stands.
ADDENDUM to §264-3 — the explicit entry copy's BOUND: required exactly when the RAW value must outlive the call that consumes only the PROMOTED value
§264-3 warns that a self = arg0 local emits double moves and says to use the parameter itself.
func_80012ABC (main, MATCH 18/18, wave m0a) is the bound: the raw parameter feeds the RETURN sum
while only its (s16) promotion feeds the call. Banked src/800.c:613:
s32 func_80012ABC(s32 a0, s32 a1, s32 a2)
{
s32 s0 = a0;
return (s0 + func_80012B04((s16)s0, (s16)a1, (s16)a2)) & 0xFFF;
}
The explicit copy is required AND free here — it survives as the target's addu $s0,$a0,$zero
because $a0 is immediately re-consumed by the promoted first argument. Typing the parameters s16
instead promotes into $s0 and adds a move $a0,$s0 (near-3). Single-basic-block functions obey the
same law as §264-3's multi-path exemplar: the decider is raw-value liveness past the call, not
control flow.
ADDENDUM to §172b-1 (counter-type dial) — the sll's SOURCE REGISTER is the placement discriminator
The §172b-1 addendum's dial 1 (declare the counter s16) and §264-4 (mirror a memory slot) both
produce a naked sll/sra pair — same count, different SOURCE register. Read which register the
sll consumes: func_80014588 (main, MATCH 25/25, wave m0a) shows the pair reading $v0 — the
addiu increment RESULT — not the counter's allocated home ($s1), with no memory slot in sight ⇒
dial 1: the counter itself is s16 (banked s16 i; at src/800.c:1241), keeping gcc in HImode
across the increment so the promotion applies to the increment pseudo. An s32 counter with (s16)i
casts emits the pair reading $sN — right count, wrong source register, unfixable by placement.
Pair reads a register that mirrors a store ⇒ §264-4's multi-def variable. Pair reads a call result ⇒
§264 recipe 1 / return-narrowing. One register read replaces a four-placement search.
ADDENDUM to §276 — the SHARE direction: spell the second adjacent symbol RELATIVE to force ONE anchor
§276's law covers the SPLIT direction (mix spellings so cse cannot unify repeated uses).
func_80018918 (main, MATCH 36/36, wave m0a) is the same dial's other end. The target folds two
adjacent-symbol call arguments onto ONE lui/addiu anchor plus addiu $a1,$a0,0x4C; two
independent externs (&D_80078DA0, &D_80078DEC) can never fold — no pass discovers that two
SYMBOL_REFs are numerically adjacent. Banked src/800.c:5098:
func_8005F0C8(&D_80078DA0, &D_80078DA0 + 0x4C);
Write the second symbol AS an offset off the first and the shared anchor is forced (this was the
whole 5-insn residual). Target shows one anchor + register-relative addiu for what the seed calls
two symbols ⇒ relative spelling; target shows independent %hi/%lo per use ⇒ §276's split rules.
ADDENDUM to §31's density-dummy dial (L2460/L2497) — the dose is TWO refs, and the dummy must sit where the loser is live-through
func_800348A8 (main, MATCH 29/29, wave m0a): a genuine allocno tie (i: 6 refs / long range vs
q: 5 refs / short range — either loses on density) did NOT flip with one extra ref anchored outside
the loop. TWO operand refs of the same variable, placed as the FIRST statement of the loop body —
inside the region where i is live-through — flipped the allocation (12 → 4 residual, class became
permuter fuel). Banked src/800.c:19141:
do {
__asm__ ("" :: "r"(i), "r"(i));
if (p[0] == five && q[-1] == lo) {
L2497's one-ref lift was measured on a floor_log2 power-of-two crossing (3/10→8/11); off such a
boundary, budget two refs and place them in the contested live range, not at the def.
ADDENDUM to §282 — the ADDRESS-GIV face: write the walked address INLINE so strength reduction births the induction register at loop.c's own insertion point
§282's mechanism (gcc's own late loop transformation places its new register's init AFTER the hoisted
movables — a position no source statement can reach) also governs a strength-reduced ADDRESS.
func_80034650 (main, MATCH 32/32, wave m0a), banked src/800.c:19069:
pm = (param_2 & 0xFF) << 16;
for (i = 0; i < 8; i++) {
if (*(param_1 + i + 0x3A) != 0) {
func_80030D80((Ent30D80 *)(D_800A4988 + i * 0x54), pm >> 16);
}
}
Writing the entity address INLINE as D_800A4988 + i * 0x54 lets strength reduction create the $s1
induction register itself and schedule its %hi/%lo materialization AFTER the pm computation,
matching the prologue order. An explicit e = D_800A4988; pointer-local init is a source statement
whose LUID precedes the movables — it always schedules first (6-mismatch plateau, unfixable by
statement reordering). The masked product is held in a named local (pm) and the >> 16 spelled at
the call site. Same law as §282's reversed counter; the tell is a loop-carried address register whose
init sits after other pre-loop work.
ADDENDUM to §253 — SECOND byte-proven card (upgrade from single-observation), and the placement face: postfix-in-condition parks the RMW store in the branch delay slot
§253 asked for cross-confirmation. func_800CB168 (md_MAIN_018, 112 ins, wave ds1 — DeepSeek's
first distilled card) supplies it, on the PLACEMENT axis: banked
src/md_MAIN_018/md_MAIN_018.c:128-207:
if ((*(s32 *)(obj + 7))++ & 3) {
goto L288;
}
The postfix-in-condition spelling tests the OLD value and leaves the incremented word's store free to
sink into the bnez delay slot — the compound/two-statement forms pin the store before the branch.
Same card, two more tree-verified spellings: a call result needs its OWN fresh local
(r = rand(); ...= r & 0xFFF; ...= (r & 1) + 2; — assigning into the live dispatch variable forces a
move v1,v0), and the L288 store chain D_80126BC8 = D_80126BCA = D_80126BCC = 0x1000; confirms
§145(c)'s descending store order word-for-word (stores BCC, BCA, BC8). §165-06 owns the expand face,
§253 the allocator face, this card the reorg/placement face of one axis: postfix vs compound is a
THREE-pass dial.
ADDENDUM to the L1800 anchor-steer bullet — a DERIVED-POINTER local silently flips the merged giv's anchor END
func_8019131C (ov_SC06_033, MATCH 97/97, wave g0f): carrying q = p + 1 as its own local made
combine_givs rebase the merged giv set on the LARGEST offset (base p+0x20, negative displacements
−0x18/−0x12 — LENGTH-DRIFT/2); deleting q and spelling every access off p in ELEMENT units —
banked src/ov_SC06_033/ov_SC06_033_jr_8018D98C.c:5030:
if (*p == 0x350) {
*(s32 *)(p + 4) = *(s32 *)(arg0 + 8);
*(u32 *)(*(s32 *)(p + 0x10) + 4) &= 0x7FFFFFFF;
*(p + 1) += 1;
}
p += 0x86;
— restored the target's smallest-offset base (p+2, positive displacements +6/+0x1E/0), which is what
L1800's own anchor law (last-recorded giv in program order — the *(p + 1) RMW is the final access)
predicts once there is a SINGLE base. Composition of §31-L2397 (collapse to one base pointer) with
L1800; the new fact: the derived pointer doesn't just risk a spurious IV — when the merge still
happens, it re-roots the anchor at the WRONG END, visible as sign-flipped displacements at the right
instruction count.
ADDENDUM to §31's asm→layout inference — TWO WIDTH-PAIR DISCRIMINATORS (same offset, different widths)
(a) lw feeding sh = a u16/s16 DESTINATION assigned from an s32 LVALUE. func_8017D3E4
(ov_SC04_006, MATCH, wave g0c), banked src/ov_SC04_006/ov_SC04_006_jr_8017BEBC.c:3394:
sp30[2] = *(s32 *)(self + 0x10); — the load carries the SOURCE's width (lw), the store the
DESTINATION's (sh). An lh+sh pair would mean an s16 source. Found by diff; no earlier section
names the pair.
(b) sw-of-result + lh-of-argument at the SAME offset = a WORD field read through an s16 cast,
not an s16 field. func_80180B80 (ov_SC03_111, MATCH 115/115, wave g0f), banked
src/ov_SC03_111/ov_SC03_111_jr_8017EC58.c:3688:
temp = func_80012C6C(*(s16*)&arg0->unk8, *(s16*)&arg0->unkC, 4);
*(s32*)&arg0->unk8 = temp;
The wide store + narrow read on one offset is what disambiguates the struct layout when either access
alone is ambiguous — the seed's packed s16-stride layout shifted every later offset by ±2..8.
Fields whose OTHER reads are lhu stay u16 with explicit (s16) casts at the narrow call sites.
§295 — THE KERNEL-TRAP STUB: §81's jr-DETECTOR WITHOUT A TABLE IS A PsyQ SYSCALL TRAMPOLINE — ROUTE TO §265, NEVER INTO THE CARVE CHAIN (P31 S61; wave m0a, 9 cards byte-proven; resolves §182's held cluster)
§81's detector is a CONJUNCTION: a mid-function jr on a non-$ra register AND a jtbl_<addr> in
the object's data. NINE m0a cards fired the jr half with NO table anywhere (grepped fleet-wide) and
each independently re-derived the same resolution — the §283-class signal of a missing section. The
shape:
addiu $t2, $zero, 0xA0|0xB0 # kernel A/B dispatch vector
jr $t2
addiu $t1, $zero, <fn#> # in the jr's DELAY SLOT
(pad nops)
A live constant in an indirect-jr delay slot with no epilogue is unreachable from any C body
(§179-C's mechanism) — the lane is §265 form 1, file-scope verbatim asm. docs/psyq-worklist.md:72
names the population ("libapi 800c3 cluster — ~22 4-ins BIOS syscall stubs"); §182/§188 had already
proven this cluster unreachable by the §177 epilogue lever (six members are prebuilt SDK objects) —
this is the lane that finally banked it. Banked exemplar (src/800c3.c:66):
__asm__(
".text\n" ".align 2\n" ".globl WaitEvent\n" ".ent\tWaitEvent\n"
"WaitEvent:\n" ".frame $sp, 0, $31\n" ".mask 0x00000000, 0\n" ".fmask 0x00000000, 0\n"
".set\tnoreorder\n"
"addiu $t2, $0, 176\n"
"jr $t2\n"
"addiu $t1, $0, 10\n"
".set\treorder\n" ".end\tWaitEvent\n"
"nop\n");
Family facts, each byte-proven this wave:
- The pad nops are LOAD-BEARING OUTPUT. The stubs sit at a fixed slot stride (0x10/0x18 bytes);
omitting the trailing nops shifts every later stub (TestEvent's first compile came back
LENGTH-DRIFT/−1 until an explicit
nop;_96_removecarries THREE, banked). Placement relative to.endis byte-inert — WaitEvent banks its pad AFTER.end,_96_removeINSIDE the block; both green.match_one'sinsns_from_sreads pastendlabeland attributes the pad FORWARD, so a "MATCH (4 ins)" verdict on a 3-ins stub is the tool counting the pad — expected, not a defect. - Spell
addiu $tN, $zero, K, neverli— the retail opcode is addiu (0x24);lileaves the expansion to the assembler (TestEvent). - The decimal rule's TRUE scope: maspsx int()-parses MEMORY DISPLACEMENTS — those must be
decimal; ALU immediates may stay hex. TestEvent is BANKED with
addiu $t2, $zero, 0xB0/addiu $t1, $zero, 0xB(src/800c3.c:85) while its neighbours use 176/10 — both assemble byte-identically. The blanket "maspsx rejects hex inside__asm__strings" repeated by several cards is REFUTED (§300-R2); keep offsets decimal, immediates as you like. - Zero relocations ⇒ Law-1c's symbol walk is vacuously clean; ship NO externs (§265 rule).
- One grep rules the class in: no
jtbl_in the object's data + a$t2 = 0xA0/0xB0constant.func_800626C8is the pointer-typed cousin (lw $t1from a single data slot,jr $t1— a slot the handwritten bootstrap writes is not a switch table); same §265 verdict.
Cards: WaitEvent · TestEvent · OpenEvent · DeliverEvent · DisableEvent · FlushCache · InitHeap ·
_96_remove · func_800626C8 (EnableEvent/CloseEvent banked as the same one-immediate edit).
Remaining members of the ~22 fall to the identical template.
§296 — THE FRAME CHECK OUTRANKS THE ATLAS LEVER: READ PROLOGUE/EPILOGUE BEFORE DRAFTING ANY C — A REAL TELL CAN LIVE INSIDE AN UNREACHABLE FRAGMENT (P31 S61; wave m0a, 16 cards byte-proven; extends §179-C)
SIXTEEN m0a cards carried a genuine atlas lever — mostly extend-tell, and the sll/sra pairs ARE
in the targets — on symbols that are splitter FRAGMENTS: no prologue, no epilogue, exits by bare
j <sibling> with live-out $t registers, or entry state arriving in registers no C calling
convention expresses. Every C draft gains the phantom teardown (§179-C: no sibcall pass;
expand_function_end always appends), so the drafts plateau at +2..+7 instructions REGARDLESS of how
well the lever is played — the lever is real and unreachable at once.
The routing law: before spending one compile on any card's lever, read the target's FRAME.
Greppable in seconds: no trailing jr $ra; an exit j <named sibling>; endlabel abutting the next
glabel; a frame adjust riding a delay slot; a *_OBJ_NN-suffixed symbol. Any of these ⇒ the
§179-C/§265 file-scope blob lane, and the lever's knowledge banks as a COMMENT in the blob, not as C.
Fragment chains bank in link order so shared-tail fallthroughs resolve
(func_8005AC24 → SYS_OBJ_1A30 → SYS_OBJ_1A70; RotMatrixYXZ → FGO_02_OBJ_68 → _CC → _160). The
banked comment src/800b_5.c:44-46 states the class exactly: "FGO_01_OBJ_64 ... is NOT a callable
function: it is the second code FRAGMENT of one larger routine (func_8004978C -> FGO_01_OBJ_64 ->
FGO_01_OBJ_CC -> FGO_01_OBJ_160)."
The liveness-proof pattern (before believing a computation trio is dead and letting DCE eat it):
read the SIBLING fragment's opening instructions — FGO_02_OBJ_CC opens multu $t4,$t0 and stores
$t6, values only the upstream arms wrote; liveness crosses two fragment boundaries
(RotMatrixYXZ card).
THE BOUNDARY, byte-proven the same night: a fragment that legitimately returns via jr $ra is NOT
in this class. SYS_OBJ_1AF0 banked as PLAIN C with register pins (src/800c.c:1873):
s32 SYS_OBJ_1AF0(void) {
register s32 r_v0 __asm__("v0");
register s32 r_v1 __asm__("v1");
r_v0 |= 0xE5000000;
return r_v1 | r_v0;
}
— the constant-OR kept as its own statement (fusing it lets gcc commute the pair), the tail spelled
r_v1 | r_v0 for operand order. The frame check, not the fragment-looking label, decides the lane.
Cards: RotMatrixYXZ/RotMatrixY/RotMatrixX · func_8004978C · func_80049CAC · FGO_01_OBJ_64 · FGO_02_OBJ_68/_CC · FGO_03_OBJ_64 · SetDrawEnv · SYS_OBJ_1A30/1C28/2F7C · func_8005AC24 · VM_NO1_OBJ_31C (all banked as §179-C/§265 blobs) + SYS_OBJ_1AF0 (the C-side boundary).
§297 — ONE GIV SERVING MIXED-WIDTH LOADS AND A STORE NEEDS STRUCT-MEMBER SPELLING; CAST/INDEX SPELLINGS RE-SPLIT THE PSEUDO (P31 S61; wave m0a, func_800347C8, main, byte-proven 31/31)
The target shape (asm/nonmatchings/800/func_800347C8.s): an 0x54-stride record walk with TWO
givs — $a1 = base+0 (lhu 0x0($a1), the state read) and $v1 = base+0x16 with NEGATIVE
displacements: lhu -0x12($v1) (id @0x04), lhu -0x10($v1) (group @0x06), sb $t0,0x0($v1)
(active @0x16). The $v1 giv carries two u16 LOADS and one u8 STORE.
What re-split it, measured: a raw u8 *q walker made gcc spawn a THIRD giv rebased at +6 while
the store kept its own; casting the store back through the pointer re-split it again even after the
loads had unified. Only the MEMBER spelling held all three accesses on the single pseudo — banked
src/800.c:19086:
typedef struct {
u16 state; /* 0x00 */
u16 unk02;
u16 id; /* 0x04 */
u16 group; /* 0x06 */
...
u8 active; /* 0x16 */
u8 pad17[0x3D];
} Sl; /* 0x54 */
e = (Sl *)D_800A46E8;
for (i = 0; i < 8; i++, e++) {
if (e->state == 5 && e->id == (s16)arg0 &&
((arg0 >> 16) == 0 || (arg0 >> 16) == e->group)) {
e->active = 1;
}
}
The law. Mixed-width accesses (loads AND a store) that must ride ONE strength-reduced induction register must be spelled as MEMBER SELECTS off one record pointer. Pointer-arithmetic and cast spellings build distinct address trees per width, and combine_givs re-splits the pseudo.
Two companion dials on the same card, both banked: (1) BOTH latch increments belong in the
for-header (i++, e++ as the comma expression) — written as the last body statement, the pointer
bump emitted on the wrong side of the counter bump (a 2-insn SCHEDULE-REORDER the residual class
files as permuter fuel; the header spelling fixes it for free). (2) The packed parameter's halves:
(s16)arg0 promoted ONCE inline in the condition; arg0 >> 16 used RAW in two basic blocks by
naming the parameter itself — no local, since there is no cross-bb CSE to defeat.
§298 — THE SHARED-TAIL POSITION DIAGNOSTIC: A FOLDED TAIL BETWEEN SWITCH ARMS MEANS PER-ARM DUPLICATED STATEMENTS, NOT A HOISTED POST-SWITCH STATEMENT (P31 S61; waves g0e/g0f, func_80181B68, ov_SC06_016, byte-proven 68/68)
Ghidra renders arms 0–3 jumping into ONE lhu/addiu/sh increment tail with arm 4 skipping it —
which seduces every drafter into a single state++ AFTER the switch. That spelling emits 70 ins with
the tail physically after arm 4. The target's tail sits at .L80181C24–C34, BETWEEN case 3 and case
4 (case 3's arm ends j .L80181C24; .L80181C38 — case 4 — follows immediately;
asm/ov_SC06_016/nonmatchings/ov_SC06_016_jr_801816DC/func_80181B68.s). gcc produces that layout
only when the SOURCE duplicates the increment per contributing arm — banked
src/ov_SC06_016/ov_SC06_016_jr_801816DC.c:3125: the increment statement appears INSIDE case 0's
guard, as case 1's LEADING statement (before falling through into case 2), at case 2's end, and
inside case 3's guard; case 4 early-returns with no increment. Cross-jump then folds the copies into
one block placed after the LAST CONTRIBUTING arm — which is before the non-contributing arm 4.
THE DIAGNOSTIC. A folded tail placed AFTER the final switch arm ⇒ a hoisted post-switch statement in the source. A folded tail anywhere EARLIER ⇒ the statement is duplicated inside each contributing arm, and the fold lands after the last arm that contributes. Composes with §224 (write the duplicate — this is its switch-arm face) and bounds the §162-region merged-tail law (L11268's write-ONCE-at-the-last-arm form is for goto-shared tails; here every contributing arm carries its own copy and jump2 does the folding).
Also byte-confirmed on this card: READ THE JUMP TABLE before reaching for §161a/§162a's empty-case
constructions — all 5 entries of jtbl_8019E2C0 are real bodies; neither edge equals the epilogue.
The negative control generalizes: the table's own words, not the arm count, decide whether an
empty-case construction exists.
§299 — TWO INDEPENDENT EXTRACTION CHAINS EMIT CONTIGUOUSLY INSIDE ONE EXPRESSION; ONLY A STATEMENT BOUNDARY MAKES THE SCHEDULER INTERLEAVE THEM (P31 S61; wave m0a, func_8003A404, main, byte-proven 8/8)
Target (asm/nonmatchings/800/func_8003A404.s) INTERLEAVES the byte-swap's two chains:
srl $v0, $a0, 8 # high chain, step 1
andi $a0, $a0, 0xFF # low chain, step 1
sll $a0, $a0, 8 # low chain, step 2
andi $v0, $v0, 0xFF # high chain, step 2 (deferred)
addu $v0, $v0, $a0
sll $v0, $v0, 16
jr $ra
sra $v0, $v0, 16
Any single-expression spelling emits one chain COMPLETE, then the other (contiguous — 4 compiles
measured). The banked source (src/800.c:21027) splits the HIGH chain across a statement boundary
with its mask DEFERRED to the last use:
s32 func_8003A404(u32 arg)
{
u32 h = arg >> 8;
u32 l = (arg & 0xFF) << 8;
return (s16)((h & 0xFF) + l);
}
haifa's priority-then-LUID tie-break then yields the interleave. Companion spellings, all probed
wrong: (arg & 0xFF00) >> 8 strength-reduces to the wrong opcodes; | emits or where the target
has addu; a u16 parameter adds andi 0xFFFF (keep it u32); the (s16) cast on the RETURN
supplies the final promotion pair with the sra in the jr delay slot (§264 recipe 1's fill). Kin
of L2425's ascending-LUID zero-dep order — a statement boundary is the ONLY source-level handle on
within-expression chain order.
§300 — S61 DISTILL BATCH NOTES (waves ab8/ab16/ab24/ab32 · g0a–g0f · m0a · ds1/ds2 · m0b)
Counts, with denominators. 99 candidate notes across 13 files reviewed (MAXTOK arms ab8/ab16/ab24/ab32 = 11, gen0 g0a–g0f = 30, main m0a = 57, DeepSeek ds1 = 1, ds2 = 0 of 42 noted verdicts; m0b filed 0 of 0 — an honest refusal, zero verdict lines after its 29 agents 404'd at the ox shutoff). Dispositions: 52/99 COVERED by §§1–293 (the m0a "nothing new — §X carried it" self-reports are the healthy majority), 15/99 → the 10 ADDENDUM blocks in §294, 29/99 → the 5 new laws §295–§299 (9 kernel-stub cards → §295; 16 fragment cards → §296; 2 → §298; 1 each → §297/§299), 3/99 REFUTED outright, plus 1 refuted DETAIL inside an otherwise-sound card. Every quoted spelling above was re-read from the banked source, not from the agents' notes.
The refutations, each settled against the tree:
- R1 (g0b,
func_80180154): "a zero-insn__asm__memory clobber blocks a hoist into a PRECEDING delay slot — §45-B lacks that form." The banked body (src/ov_SC02_016/ov_SC02_016_jr_8017DC70.c:4105) contains NO asm barrier — the shipping fix is thempointer local (the L2425 law); the barrier belongs to a superseded intermediate draft. An attempt-path narration is not a claim about the banked mechanism. - R2 (m0a WaitEvent, detail): "maspsx rejects hex inside
__asm__strings — 0xB0 must be 176." Byte-refuted by the SAME TU: TestEvent is banked green withaddiu $t2, $zero, 0xB0(src/800c3.c:85). The real constraint is decimal MEMORY DISPLACEMENTS; §295 fact 3 carries the corrected scope. - R3 (ab32,
func_80181490): "a short-lived arg-local (a0 = arg0) pins$a0and frees the jal delay slot — not in the cookbook." The banked body (src/ov_SC05_017/ov_SC05_017_jr_801808D4.c:3521) has NO such local — plainarg0throughout; the delay-slot fill fell out of statement order, exactly as the ab8 sibling note reported. - R4 (ab32,
func_800D09A0): four -O0 C-spelling dials ((s16)x & 4vs(s16)(x & 4), bare-preincrement copy, dead register local,*(s16*)&u16var) claimed for a function that banked as §265 VERBATIM ASM — there is no C artifact in the tree to verify against. Oracle-only; recorded here, not as law. If the §261_o0lane ever re-banks md_MAIN_003 as real C, re-harvest.
False-novelty (claimed "not in the cookbook"; the INDEX failed, not the corpus):
func_800CD288's frame-pad-by-address-taken-locals (covered 4× — L1833, L2921, §162i1/L11383,
L24544; the agent grepped "frame size"/"stack size", the corpus says "frame-pad induction"/
"frame_pad" — grep bait for next time: frame size pad, stack size pad, grow the frame,
address-taken pad local); ds1's chained-assignment store order (§145(c)/L9958, word-for-word — its
third such rediscovery); func_8019131C's "no section covers giv base selection" (L1800's
anchor-steer bullet); the constant-naming claims of func_80018918/func_800348A8 (ADD-7→§229).
DeepSeek vs ox calibration (first two-source campaign). DeepSeek's notes are far TERSER and cite almost no sections — ds1's single note names ZERO cookbook §s where ox notes cite 3–8 each and self-classify. Predicted consequence, observed: more false-novelty per claim (1 of its 3 "the cookbook did NOT document" items was §145(c) verbatim). But its byte-level specifics — the chain direction, the postfix-in-condition, the fresh rand local — were ALL present in the banked code exactly as stated: trustworthy on facts, weak on prior-art. ds2 parsed 51 verdicts, 42 with notes, and filed ZERO novel candidates — either excellent restraint or a filter mis-fire; spot-check that wave's banked functions at the next distill before trusting the zero. ox's systematic failure mode this batch is the mirror image: attempt-path narration presented as the banked mechanism (R1, R3) — always verify the FINAL source, never the story.
Label and citation hygiene for future distills. Atlas lever labels tiny-direct, head-crack,
frame-172 are card-tracking strings with NO cookbook section behind them — multiple cards burned a
disproving grep each (head-crack was already called bogus at L28955). Agents cite cookbook LINE
numbers as §N — this batch: §3047 (=L3047's §263 material), §2425 (=L2425), §14052, §5583; treat any
§ above ~300 as a line cite. One card misquoted §18 as "match_one is WRONG for -O0 functions" — §18
says no such thing; the auto --o0 oracle is §261's and is trusted, with whole-binary gating still
the arbiter as always.
MAXTOK-arm duplication. The four ab tags drafted the SAME 10 functions, so their candidates
overlap: func_8017DBA0 was independently cracked 4× (ab16/ab24/ab32/g0f), func_80181E0C 3×,
func_801854D0/func_800D09A0/func_80181B68 2× each. The converging derivations cross-confirm
§160a/§224/§37 but inflate the raw candidate count; a distill after any A/B test should expect one
crack per arm and de-duplicate before counting.
Procedural keepers, not worth sections: the -O0 store-census size oracle (nstores × 3 + 11 for a
pure field-init body — func_800D0964 predicted 158/158 before compiling); "diff-count ≠
defect-count" (func_801879B0: ONE arity defect manifested as 2 mismatched instructions); read the
raw STORE OFFSETS before believing a scheduling story (func_80188620's off-by-one slot numbering
was indistinguishable from a schedule residual until the offsets were diffed).
§301 — AN INTERNAL j CARRIES R_MIPS_26 .text: rtu/match_one "MATCH" COULD NOT SEE WHICH LABEL A j TAKES — FIXED (jrel), AND THE TWO DRAFT SHAPES IT HID (P31 S62 T1; byte-proven 2/2, negative-controlled over 3,297 stubs)
The instrument fact (supersedes §195's premise). objdump -drz on an rtu whole-TU object shows
j 4604 <func_801831F4+0x88> followed by R_MIPS_26 .text: the assembler does NOT resolve an
in-section j — it emits a section reloc with the section-relative target in the field. §195 had
recorded "the assembler resolves it itself, emitting NO relocation"; that was false for this
toolchain (maspsx → GAS), so mask_for("26") zeroed the 26-bit field and any j .Lx compared
equal to any j .Ly. Two resolver drafts sat in the ledger as MATCH (47 ins) / MATCH (148 ins) with closeness 0 and were refused by the whole-binary gate on exactly one byte each
(ov_SC02_027/func_801831F4 @0x80183250, ov_SC06_000/func_80182E38 @0x80182E90).
The gate-side tell. Whole-binary DIFF, sizes equal, ONE differing byte, and the containing word's
top six bits are 000010 (j) inside the function → the draft's CONTROL FLOW is wrong, not its
codegen. (tools/diff_autopsy.sh <binary> <fn> <tu.c> <draft.c> reproduces the gate's splice,
cmp's against the byte-good binary, decodes the words and restores the tree.)
Shape A — goto to a recheck vs a cross-jumped direct call (func_801831F4). Retail arm:
ori $v0,0xCC00; j .L_jal; sh $v0,0x5C($a0) — the store in the delay slot and the jump landing ON
the shared jal func_80182AA4 (the tail-merged call+epilogue). The draft wrote goto tail; where
tail: re-tests the condition, so its j landed on the recheck block (.L+0x88, not .L+0xA4).
Write the call directly in the arm; gcc's cross-jump produces the j to the shared jal. A goto to
a label that begins with a re-test is the smell.
Shape B — a shared negation after the join vs an else-arm-only negu (func_80182E38). Retail:
bnez .L_else; addiu $v1,$v1,0xC0 / j .L_join; addiu $v0,$zero,-0x1A0 and .L_else: … addiu $v0,$v0,0x1A0; negu $v0,$v0; .L_join: sh $v0,0x34($sp). The if-arm's CONSTANT sits in the j
delay slot and the jump lands PAST the negu. The draft computed v0 in both arms and stored
-v0 after the join — same instruction count, j one word short (onto the negu). Rule: when one
arm of an if/else yields a constant, retail has already folded the sign into it —
if (c) v = -K; else v = -(expr); — never v = -v after the join.
The fix (tools/masked_diff.py). Every object insn now carries its section offset and every .s
insn its vaddr; _annotate_jrel stores, for a j whose reloc is against .text (object side) or
for every j (.s side, already resolved), the target RELATIVE TO THE FUNCTION START —
(field << 2) − fn_offset / (field << 2) − (fn_vaddr & 0x0FFFFFFF) — and structured_diff,
diff_object_s, diff_object_object count a jrel disagreement as a mismatch. rtu_match now
calls structured_diff instead of its own copy of the loop (R33). jal stays masked on purpose: a
static callee's position legitimately differs under rtu's neutralized siblings. Controls: the two
original drafts → DIFF 1, the fixed ones → MATCH; tools/stub_invariant_audit.py (INCLUDE_ASM
pastes the original bytes ⇒ diff_object_s must be 0 for every stub) over 3,475 stubs: 1,097 carry
internal j relocs (4,043 instructions exercised), 0 new mismatches (the 181 nonzeros = 178
red-binary NO-OBJ + main's 2 data blobs + func_80062808, all with jrel = 0, pre-existing).
Ledger consequence. Every historical rtu/match_one MATCH and closeness-0 was blind to this
class. The resolver's 54 CARVE-REFUSED drafts are "byte-correct at rtu" only modulo j targets —
re-judge them under the new comparer before the carver spends builds on them; the whole-binary gate
was, and remains, the sole arbiter (G3/P9).
§302 — A RED BINARY IS A DRIFTED SPEC, NOT A MYSTERY: THE THREE CARVE-STATE INVARIANTS AND HOW TO DERIVE EACH FROM THE BYTES (P31 S62 T2; five reds healed in one session, 5/5, +39 held banks)
The overlay jtbl carve (§8/§8a/§8e) keeps THREE stored descriptions of one fact — where each
matched switch's jump table sits in the rodata island: (1) the splat yaml's [.rodata, <TU>]
subsegs with the data tail split into tailN pieces around them; (2) the <ov>_JTBL_INTERLEAVE --order list in config/overlays.mk (the ld sandwich); (3) the per-object JTBL_PADS (one entry per
.align 3 the TU emits, 0 or 4). They are written together by jtbl_carve and by the gate's §8a
carve, and they drift apart the moment ANY of them is restored, reverted or edited alone. Every one
of the five S61 reds was such a drift; none was a codegen or tool mystery:
| red | symptom | invariant broken | cause |
|---|---|---|---|
| ov_SC03_015 | ld_interleave: expected 1 tail21.data.o, found 0 + "more tables than the 2-entry spec" |
order ≠ yaml; pads ≠ tables | the 17:43 "RESTORE overlays.mk" resurrected a 06:58 order after wave de had merged two carves in the yaml |
| ov_SC02_005 | expected 1 jr_8018EA04.o(.rodata), found 0 ("+0xAE8 shift") |
order ≠ yaml | a gate wrote the order for a TU split that a later yaml restore removed |
| ov_SC03_024 | +4 rodata shift, 1,041 sparse diffs, binary +4 | pads MISSING | no JTBL_PADS line at all → the pads stage never ran → default .align 3 padded between two back-to-back 5-entry tables |
| ov_SC06_022 | consumed 1 but 2 (two objects) |
pads ≠ tables | a rival lane flip-flopped the yaml between one and two carved tables while the restored mk kept 0,4 |
| ov_SC04_018 | consumed 3 but 4 + a TU that never compiled |
pads ≠ tables; TU text | a function reverted to a stub (its table still in the carve); plus four text defects from an 08-13 propagation |
Derive, never search (R33/R37). Each invariant is computable from the bytes:
tools/interleave_check.py <ov>— order list vs the yaml subseg sequence, position by position.tools/pads_audit.py <ov>— for each object with a pads line: compile the TU (cpp|cc1|maspsx, pads stage removed) and count its.align 3tables + entries; walk the retail words inside the yaml carve with those sizes: a table either follows immediately (pad 0) or after exactly one zero word (pad 4), every entry must be an in-range code address, the walk must end at the carve end. Result classes:ok,SPEC-DRIFT(the derived spec is the fix),CARVE-DRIFT(the walk misses the carve end — a table was added/removed in the TU: a banked switch not yet carved, or a stub whose table is still inside the carve; the fix is a carve edit, not a pad value).- A stub's table inside a carve is fine and self-placing: its
.scarries the jtbl and lands AFTER the C tables in the object's.rodata(ov_SC04_018's 4th table), so the pads spec counts only the C tables. - S61's "value searches found no unique winner" were searches over a wrong ORDER (SC03_015) or a NONEXISTENT line (SC03_024) — R40: exonerate the instrument, then derive.
The gate-side tells. ld_interleave: expected exactly 1 X, found 0 ⇒ order ≠ yaml. jtbl_rodata_pads: consumed N but M ⇒ pads ≠ compiled tables. A SHA mismatch with the same size and sparse diffs from
early code onward ⇒ a data-island shift (missing/extra pad word); cmp the built binary against the
retail file and decode the first differing words (tools/diff_autopsy.sh for a draft; plain cmp -l
- the
.mapfor a config defect). Two differing bytes inside one function ⇒ a text defect in that function (SC04_018: offset/value swapped in one store).
Process rule that falls out (R59 candidate, sharpened): a blanket restore of overlays.mk or a
splat yaml is a carve-state edit; it must be followed by interleave_check on every overlay whose
lines it touched, and pads_audit on every object whose spec it carried.
§303 — MODULE ISLAND TABLES: DERIVE THE PADS AT BUILD TIME, PEEL NOTHING — THE §154-A/§260 "island-pads"/"island-blocked" WALLS DISSOLVE (P31 S62 T3a; byte-proven md_SC03_076 func_801F0A9C + func_801F0F28, sha 9a165e36…)
The shape. A module's rodata island (offset 0, [0x0, .rodata, md_X]) packs every switch's
jump table in source order, each table followed by 0–n zero words; the island is bound to the main
TU object, and every stub's .s carries its own table (+ its trailing pad) at the stub's position
in the stream, while a matched body's cc1 emits .align 3 + $L + N entries — the real entries only.
§260 peeled only END-adjacent tables into their own object; interior tables ("island-blocked") and
padded tables ("island-pads") were refused: 16 + 9 fns in the S62 resolver stock alone.
The law: emission order == island order, so the object can emit the whole island in place.
cc1 places each table at its function's position; INCLUDE_ASM'd .s blocks interleave in place;
C const data (D_…:) lands where the source puts it. The ONLY missing bytes are the pads a matched
body no longer supplies — and they are computable from the retail island. tools/jtbl_rodata_pads.py:
- is now table-aware: it acts only on an
.align 3that a$Llabel follows; const data (.align 2,D_…:,.byte), includes and string pools pass through untouched; - takes trailing pads: token
0t1= no leading pad, one zero word after the table (4t2etc.); --derive <binary> --tu <tu>(modules; wired in the Makefile for every md_ object):* parses the maspsx stream's rodata items in order —.included.sblocks (span from their/* off vaddr */comments, sized by directive when the comment carries no hex), C data objects (start = the address in theD_XXXXXXXXname, size from the directives), C tables — then walks the retail island: a.sspan must start exactly at the running position, C data at its aligned address, and a C table takes lead 4 iff the retail word at the position is zero, N in-range code words, then trailing zeros up to the next anchor (or the next non-zero word); an anchorless stream (an isolated §260 object) is framed by its yaml.rodatapiece. Any anchor miss refuses with the offset — a CARVE-DRIFT verdict AT BUILD TIME, attributable, never a silent shift.- Nothing is stored (R33): no modules.mk spec, no yaml edit, no isolation — so nothing can drift (contrast §302: every S61 red was a stored carve spec drifting).
Gate side. harvest_verify._jtbl_prep_one: a module island wall now just leaves the splice
in place ([jtbl] module island table … handled by --derive); the build judges. The old §260
peel stays valid for already-peeled pieces (--tu frames them).
Controls. Positive: the derive reproduces the hand-proven 0t1,0 and its filtered output is
byte-identical to the stored-spec output. Negative: every module binary rebuilt through the derive
stage must stay byte-identical (the S62 module sweep — count quoted in the phase log).
§304 — SELF-DEFINING RODATA: WHEN A FUNCTION'S .s IS THE ONLY OWNER OF THE DATA IT REFERENCES, THE C BODY MUST DEFINE IT (P31 S62 T3; byte-proven md_MAIN_011/func_800D04F4)
The refusal. PLUMBING: undefined reference to D_800CEE3C at link, for a draft that rtu
called MATCH. The module's rodata island holds a string table ("ON ", "OFF", "PLAYER DMG", …)
that ONLY this function references, so spimdisasm migrated those blocks into the function's own
.s (§154-A). Banking the C body deletes the .s — and with it the only definition of the strings
the body still externs.
The fix (byte-neutral by construction). Define the blocks in the draft at FILE scope, in
address order, one per dlabel in the .s: const char D_800CEE3C[] = "ON "; (cc1 emits
.section .rodata / .align 2 / label / .ascii "…\000" — the exact shape spimdisasm wrote), a
.word block as const u32 D_800CEE60[] = { … };. The definitions land at the function's position
in the emission stream, which is where the .s emitted them, so the island bytes are unchanged
(§303's order law); keep const (drops it → .data), cast at a void * use if a prototype
demands it. The block-scope externs for those symbols go away (a file-scope definition after a
block-scope extern of another type is gcc's "used prior to declaration", §302's SC04_018 lesson).
Tell + generalization. Undefined-reference at link for a D_ the target .s carries as a
dlabel block = this class; the .s block is the literal source of the initializer. Candidate
resolver transform (not built yet): rodata_from_s — for each undefined reference to D_x where
the fn's .s has dlabel D_x, emit the C definition from the block. One instance in the S62
resolver stock; expect more as module drafting resumes.
§305 — "CARVE-REFUSED" AT GATE TIME IS THREE NAMED, DETERMINISTIC CLASSES — NONE OF THEM A CARVE (P31 S62 T3; 28 resolver drafts autopsied 28/28)
The gate's CARVE-REFUSED verdict (harvest_verify §59(3)) is the label for "jtbl_carve declined
after the isolate branch", and the isolate branch's own output never reached a log (the worker's
stdout is swallowed — fixed in spirit here by naming the classes). Reproduce any instance by hand:
splice the draft, jr_isolate_all <ov> --only <fn> --dry-run, then the real isolate + make extract + jtbl_carve --func; restore with git checkout -- src/<ov> config/ + git clean -f src/<ov>/ + re-extract. The S62 census over all 28:
- A mangled definition in the TU (1 TU, 7 drafts):
s32 FUNC_80180a08(...)— a matched body committed under a case-mangled name resolves to no address, sojr_isolate_allrefuses to partition the whole TU ("construct(s) with no resolvable address"). Fix: rename to the canonicalfunc_XXXXXXXX(byte-neutral; the sig spells IMAGE-derived names lowercase, the project uppercase — both are the same address). Fleet census: exactly one instance. - A TU-local naming type the region cannot carry (10 drafts): "carry the naming type
(file_scope_types) or add it to src/shared/engine_types.h" —
Block4,Blk4_E960,Rec80182744,MATRIX_80188114,D80190448_t. Fix:tools/lift_types.py --types … --apply(dry-run first: it reports divergent per-overlay variants and keeps those local), then R22 — a shared-header change recompiles the fleet. - Tables in BOTH the data asm and the fn's own .s after isolation (9 drafts, 3 of them the
func_8015444C family): the fresh small c-segment makes spimdisasm migrate the table into the
function's new
.swhile the un-carved tail still emits it;jtbl_carve's R32 guard read the overlap as an unprecedented half-migrated layout. When the data side covers EVERY table the function references it is the ordinary tail state: carve it, and the post-carve re-extract resolves the duplicate (jtbl_carvenow says so and proceeds; a partial overlap still refuses).
Denominators: of the 90 drafts the S62 resolver staged, 54 banked at the gate, 28 were
CARVE-REFUSED (this section), 2 DIFF, 6 CC1/PLUMBING decl conflicts. The tell that a "carve wall"
is plumbing: jtbl_carve --probe says tail (a standard §8a carve) yet the gate refused — the
carve was never the problem.
§305 addendum (same session, after the fixes above shipped). Three more deterministic
sub-classes surfaced once classes 1–3 were cleared, each a one-line tool fix:
4. A carried decl line that GLUES code-emitting invocations — extern s32 aF…(…) __asm__(""); DEFINE_func_8014C4AC() DEFINE_func_8014C568() … on ONE line in the TU. jr_isolate_all's decl
carrier copied it whole into the new region and re-instantiated the shared bodies there
(nameless .globl/.ent → "junk at end of line … :"). Carry only the ;-terminated
declaration segments; carry each glued macro's implied prototype instead.
5. The lifted typedef the isolator could not see — typedef struct {…} __attribute__((packed, aligned(1))) Block4; puts the attribute between } and the name, so _engine_types() never
listed Block4 and every extern Block4 D_… was "a decl that could not be placed" even after
the lift. Pattern added.
6. A jr function that LEADS its object (ov_SC06_029_jr_8017C954 starts at func_8017C954):
the cut at its address equals the object start → an empty region 0 at the same offset →
"code subseg … out of order". A cut equal to the object start is a no-op; drop it.
And the shared-file race behind §302's drifts, now closed at the root: mk_write.write_overlays_mk
takes base= (the text the caller read) and merges only the caller's changed # --- <ov> blocks
into the CURRENT file under the lock; a block that moved on disk since the caller's read is a
conflict and is refused (R43). harvest_verify snapshots/restores only its binary's block. The
gate's parallelism (-j N) was the amplifier: every whole-file write or restore of overlays.mk
by one binary's gate clobbered another's.
§306 — A HAZARD nop IN FRONT OF A DIV-RESULT STORE IS A STATEMENT-ORDER DEFECT: THE INDEPENDENT TRAILING STATEMENT MUST BE WRITTEN BEFORE THE DIVISION-CONSUMING ONE (P31 S62 T4; byte-proven func_8017E7D0)
THE TELL. A LENGTH-DRIFT +1 whose extra instruction is a bare nop sitting immediately before the sw that stores a division result, while an adjacent, data-independent global store (lui $at,%hi(G) / sh $zero,%lo(G)($at)) appears AFTER that sw in your draft and BEFORE it in the target:
mine: … mflo $a0 ; nop ; sw $a0,0x48($s0) ; lui $at ; sh $zero,%lo(G)($at)
target: … mflo $a0 ; lui $at ; sh $zero,%lo(G)($at) ; sw $a0,0x48($s0)
Do not touch registers, pins, barriers or the permuter — the whole residual is one statement in the wrong place.
THE MECHANISM (two halves; the second is source-verified). (1) cc1 sees div as ONE insn carrying the long imuldiv latency, so its consumer store is not ready for many cycles and the scheduler fills the shadow — but only from insns already available in LUID (= source) order. An independent statement written BEFORE the division statement sinks into that shadow; one written AFTER it is not pulled back into it. (2) The visible nop is not gcc's — maspsx splices it: after the --expand-div expansion, _handle_nop_before_next_instruction (tools/maspsx/maspsx/__init__.py:642-675, called at :1137) emits nop # DEBUG: Reuse of '<rd>' whenever the instruction following the expanded mflo reads the quotient register. So an empty shadow costs exactly one instruction, and any insn that does not read the quotient kills it. Corollary (source-read, not A/B'd): that same rule exempts a next instruction which uses $at while nop_at_expansion is false — which our pinned --aspsx-version=2.56 gives (maspsx.py:99-104) — so a division result stored straight to a global through lui $at / %lo(SYM)($at) never pays this nop at all.
THE C SHAPE. Swap the two independent trailing statements so the one with NO dependence on the division comes first:
/* target order — shadow filled, no nop (54 ins) */
D_8019F70C = 0;
*(s32 *)(a0 + 0x48) = -D_80188A34 / ((s16)D_80188A2A[0] / 2);
/* wrong order — empty shadow, maspsx nop (55 ins) */
*(s32 *)(a0 + 0x48) = -D_80188A34 / ((s16)D_80188A2A[0] / 2);
D_8019F70C = 0;
BYTE EVIDENCE. func_8017E7D0 (ov_SC06_016, _jr_8017C8D0, 54 ins). Target tail asm/ov_SC06_016/nonmatchings/ov_SC06_016_jr_8017C8D0/func_8017E7D0.s:50-53 = mflo $a0 / lui $at,%hi(D_8019F70C) / sh $zero,%lo(D_8019F70C)($at) / sw $a0,0x48($s0), under the signed --expand-div form (break 7 / break 6, §228-3). v1 (division store first): mine=55, target=54, 10 mismatched, class LENGTH-DRIFT [structural], first mismatch idx45: 00000000 nop | 3c01801a lui $at,%hi(D_8019F70C). v2 = the same file with ONLY those two statements swapped: MATCH (54 ins); --json verify {"status":"match","closeness":0,"nins":54,"residual":[]}. One function, both directions measured.
(Extends §16Xb (L13142) — the mult→mflo window read at the div's shadow: §16Xb's tell is a ZERO-drift reorder and its lever is "move the statement that FOLLOWS the multiplying statement"; here the drift is +1 and the filler comes from the statement BEFORE, so §16Xb's prescription points the wrong way. Instance of §2-T2 (L78). Kin of the §253/§165-06 note (L25420: postfix (*p)++ keeps the store after the mfhi/bnez pair, bare ++ sinks it before the div) and of §50-E (L3616), which prices global-store REORDER the other way (an assembler-merged lui $at). §179-B rule 4 (L17393) owns this same maspsx nop splicer for hand-written asm; this entry is its C-side face.)
§306a — T4 DISTILL ADDENDA (P31 S62; four byte-proven refinements to existing laws, verified against the book by an independent verifier; each names its parent §)
→ parent: §255 "AND CASE-BODY PLACEMENT"
Addendum (P31 S62 T4, func_800CAF9C): third measured exemplar, and it lands at the smallest tree that has a root — the regime this paragraph's DFS sentence was written from. func_800CAF9C (md_MAIN_015, 105 ins, byte-gate green) dispatches {0x41, 0x53, 0x73}; balance_case_nodes' i == 3 arm roots on the median, so beq $v1,0x53 is the FIRST test (asm/md_MAIN_015/nonmatchings/md_MAIN_015/func_800CAF9C.s @800CAFD4) — yet the root's body sits SECOND: case 0x41's body is .L800CB008, the shared case 0x53: case 0x73: body is .L800CB034. DFS-root-first is false here, and §199-G (L21011) says why it must be: expand_end_case takes before_case = get_last_insn() after every body is already in the stream (stmt.c:4749) and reorder_insns (stmt.c:5054-5056) hoists only the header in front of them — no pass ever permutes bodies. Read ADD-9's "two regimes" as one law plus misread ascending-order exemplars: body order is source-clause order, always; what survives of §255's sentence is its other half, that physical arm order does not name case VALUES.
Byte evidence. Draft 1 spelled case 0x53: case 0x73: before case 0x41: — the compare chain (lbu/beq 0x53/slti 0x54/beq 0x41/j) was byte-identical through index ~9, then a total cascade from the first body onward: near, closeness 71, 47 of 104 residual. The ONLY edit was moving the two clauses (0x41 first); draft 2 → MATCH, 105/105, residual: []. Diagnostic tell: a residual that begins exactly at the first case body while the dispatch chain is already byte-clean ⇒ reorder the case CLAUSES in source; do not touch polarity, empty cases, or the tree. ⚠ Bound: this instance alone cannot separate source order from ascending order (0x41 < 0x53) — ADD-9's func_8017F328 swap probe is what discriminates; this card's contribution is killing root-first at 3 nodes.
→ parent: §16Xy
Addendum (P31 S62 T4, func_8017FB38): Fourth byte-instance, and a new cell — the cast is narrower than the load: lh $v0,0x2C($a0) ; beqz ; addu $a0,$v0,$zero ; andi $a0,$a0,0xFF ; jal func_80016450 (0x8017FBC8, ov_SC07_000). §16Xy's table carries (u16)-on-s16 and (u8)-on-s8; the mixed (u8)-on-an-lh cell behaves identically, so read the law as narrow memory load + narrower-or-opposite-signed cast at an SImode use, not as a width match.
The crack re-derived §16Xy's ablation row for row without finding it — v0 & 0xFF → andi $a0,$v0,0xff, no copy (closeness 10, residual [36]); u8 t = v0; → neither instruction, residual nop; only the two-instruction form matches. It then reached MATCH by the expensive road: register s32 a0r __asm__("$4") plus __asm__("" : "=r"(a0r) : "0"(a0r)) between the copy and the mask. That launder is §165-04/§165-35's instrument (the "=r"/"0" pair forces the value through an SImode register operand, blocking combine's fold into the lh) — whose "honest scope: ONE A/B pair, body later abandoned" note now has its second byte-proven instance. So the transcript's "mechanism unknown at the RTL-pass level" is answered by §165-03: the stranded SImode extension is a conflict-free orphan pseudo whose preferred class converges to ST_REGS, and alter_reg slots it.
Route to the declaration first, not the pin. s16 s = *(s16*)(a0+0x2C); if (s != 0) func_80016450((u8)s, 1); buys the same pair with no register __asm__ — which forfeits the family (§37/§162p3) and, per §164-49, can sell you a schedule. The pin here was never solo-ablated against the barrier (§266), so cite the barrier and treat $4 as an unproven rider.
Frame reconciliation — first function needing §16Xy's and §167-10's counters SUMMED. Target .frame is 0x30 with sw $ra,0x28 and args=16 ⇒ vars = 24, with zero $sp references anywhere in the body: three narrow-copy sites × 8 — two §167-10 compare-then-re-read-and-store-back copies (addu $v1,$v0,$zero at 0x8017FB58 and 0x8017FBA0) plus this §16Xy cast-to-call copy. The draft hand-shipped frame_pad[6] for exactly those 24 bytes. Falsifiable prediction: spell all three sites as s16 locals and the 24 bytes mint themselves — the pad then over-shoots to 0x48 and FAILs, the §162i1 footgun §167-10 already names.
Index gap: cookbook-index.md L19 routes "andi folded away in your output but present in the target" to §1/I2 + §12 only. When the missing instruction is the copy and not the mask, the route is §16Xy → §165-02 → §167-10.
→ parent: §176-F row 3
Addendum (P31 S62 T4, func_8017FB38): row 3's tell reproduced verbatim on a second, independent function — ov_SC07_000:func_8017FB38, IMM-OFFSET/6, closeness 1, again 44 instructions, again a lone beqz $v0 whose only difference is the local-label immediate (mine 0x10400008 vs target 0x1040000E; target asm asm/ov_SC07_000/nonmatchings/ov_SC07_000_jr_8017BEBC/func_8017FB38.s:30, the beqz $v0,.L8017FBD8 at 8017FB9C).
The dropped conditional edge has a SECOND C-level cause, and its fix is the mirror of row 3's. Row 3's cause was a trailing statement written after a guard's closing brace (fix: move it in). This one is a missing early return;: the t == 0 path was left to fall through into a later, logically-redundant if (field2C != 0) { func_80016450(field2C & 0xFF, 1); } instead of terminating, so that shared guard — not the epilogue — became what the beqz reaches. Spelling the bail-out explicitly, } else { t = field2C; if (t == 0) { return; } t -= 0x10; … }, gave MATCH 44/44 with every other byte unchanged.
Read the SIGN of the immediate; it names the defect. match_one emits residuals as [i, mine, target] (tools/match_one.py:222), so the two displacements are directly comparable. Mine NEARER than the target's ⇒ my C falls into a downstream block the target's C skips — add the return;. Mine FARTHER ⇒ I terminate a path the target lets fall through — drop the terminator, or move the trailing statement inside the guard (row 3). And the delta is a ruler, not noise: it is the instruction distance between the two candidate landing points (here 6, +0x88 inside the shared guard → +0xA0 the epilogue), so you can count straight to the block in question instead of searching the .s.
⚠ Do NOT bank the strong form "gcc-2.7.2 never merges or elides a later guard whose earlier predecessor would also fail it." The bytes refute it here: the fall-through draft did not land on the shared guard's label, it landed one instruction inside it (+0x88, past the lh $v0,0x2C($a0) reload jump1 knew was redundant given $v0 == 0) — which is exactly why the delta is 6 and not the guard block's full 7. The law is about which block the edge enters, not about guard elision. (Complements §225-2 / §225-8, which put early-return-vs-if-block on the prologue/epilogue and j-vs-branch axes with large residuals; this axis is the count-neutral one. §176-G's bound still applies: the same edge error is silent when the arm exit is a j.)
→ parent: §211
Addendum (P31 S62 T4, func_8017F608): §211's half (b) — "hoisting it above the guard lengthens the pseudo's live range across the guard block, flipping the local-alloc contest" — fires with no loop, no induction pseudo, and no instruction-count or delay-slot change at all, so §211's own BOUNDARY ("it only bites when the guard/branch actually has a fillable slot and the two induction pseudos actually contend") is too narrow: the contest can be a single constant against local-alloc's generic-temp default. func_8017F608 (ov_SC02_028, jr_8017D898, 99 ins) sat at closeness 2, REGALLOC-PERM/$v0>$a0, on exactly one pair — mine li $v0,0x600 / sh $v0,0x10($sp) vs target addiu $a0,$zero,0x600 / sh $a0,0x10($sp) — with §211's half (a) already satisfied (both builds put the addiu in the guard's own delay slot; only the colour differed). The fix: hoist the clamp constant into a named local above the guard and make the guard test the local, not the literal — s16 clampval = 0x600; if (sp10.v[0] < clampval) { sp10.v[0] = clampval; } → match, 99/99, residual []; the stuck form declared the local inside the taken arm and tested the raw literal (closeness 2).
THE TELL IS IN THE TARGET, NOT IN YOUR DIFF. Read the neighbouring guard's delay slot: asm/ov_SC02_028/nonmatchings/ov_SC02_028_jr_8017D898/func_8017F608.s holds 8017F680 addu $a0,$v1,$zero (outer slti …,0x700 slot) and 8017F68C addiu $a0,$zero,0x600 (inner slti …,0x600 slot) — the constant re-defines the same register an earlier guard's slot already loaded, which is one named local with two defs spanning both guards, never a store-time temp. A fresh temp takes $v0 by local-alloc's copy-suggestion default (§186c, as corrected by §194-F). This is §208's pseudo-set principle run in the merge direction (§208 splits one expression into two locals to buy two registers; this merges a compare operand and a store source into one local to buy one register), and it is a second counter-example to §137's "source-level levers are a dead end for REGALLOC-PERM": the edit changes the pseudo SET, which §137's R/L arithmetic cannot see.
BOUNDS (read these before quoting the recipe). (1) The A/B moved two things at once — declaration position and the compare's operand — so per §266 neither half is independently citable; probe decl-above-with-literal-test before treating "test the local, not the literal" as the load-bearing clause. (2) The rematerialisation story is the drafter's hypothesis, not read out of a -dl/-dg dump; what is byte-proven is the source edit → MATCH. (3) The drafter's side claim that the same hoist "also flipped several BRANCH-POLARITY residuals earlier in the same draft" is unquantified and unverified — treat as a lead, not a law. (4) Tried and failed, worth as much: register s32 clampval __asm__("$4") on the same body REGRESSED 2 → 62 with +1 ins (99 → 100) — another row for §257/§268's pin ledger, and a straight confirmation of §176-B: on a 2-instruction REGALLOC-PERM, reach for the naming/scope lever first and the pin never.
Harness note banked with these (P31 S62 T4). 22 of 60 probe agents reported that the pack's
warm-start body was a DIFFERENT function: drafts are stored by function name, and the same name at the
same address in another overlay is usually unrelated code. api_agent.prior_draft now accepts a
candidate only when the symbols it references overlap the target .s's relocations (law 1c); the
haiku arm re-run on its misses with the filtered packs banked 2 more. Read "warm start" as "a body
whose symbols are this .s's symbols", never as "a body with this name".
§307 — THE BRUTE-FORCE-THE-STATEMENT-ORDERS LEVER HAS A BOUND: A FAN-OUT COPY'S PRIORITY IS SCHEDULER-INTERNAL, AND NO SOURCE ORDER REACHES IT (P31 S63; byte-evidenced NEGATIVE result, main wave)
Why this is here. docs/cookbook-index.md L22 sends every "same instruction multiset, one contiguous
window, loads/registers ROTATED inside it" residual to the cheapest lever: brute-force the N independent
statement orders (gcc-2.7.2-map/sched.md §S1/§S4). That lever is real and cheap — but it is NOT
universal, and until now nothing recorded where it stops. Two functions in the S63 main wave each
exhausted it and stayed NEAR, which is worth more banked than re-discovered.
The shape it cannot reach. A value in a register is (a) COPIED to preserve it across an imminent
reuse of the same hardreg, and (b) also the source of a sibling one-shot derivation. The target computes
the derivation directly from the original register (the two chains run parallel from $v0); gcc-2.7.2
emits the preserving copy first and then chains the derivation through the copy.
target: addu $t0,$v0,$zero / addiu $t7,$v0,-0xA0 <- both read $v0
gcc: addu $t0,$v0,$zero / addiu $t7,$t0,-0xA0 <- second chains through the copy
Byte evidence — func_8001BC6C (main, 69 ins). The agent hard-pinned every temp to its exact target
hardreg ($2–$15), got the first 9 instructions byte-exact, then ran all 12 permutations of the two
independent-statement groups through match_one: every permutation produced byte-identical output,
closeness pinned at 61. An opaque-__asm__ barrier around the first group moved closeness 61 → 58 but
never removed the extra instruction. The same swap recurs at a second, independent site in the same
function (the D_800B9A02/D_800A6610 pair) — i.e. it is a rule of the scheduler, not an accident of
one window. Corroborating instance — func_80021284 (main, 220 ins, closeness 25): 7 head
instructions where no source order wins both the %hi/%lo pair placement and the lw position
(12 orders measured), plus 18 in a trailing if-block where local-alloc swaps two splice masks
$a0↔$a1 (6 statement orders AND bitfield-vs-explicit-mask all byte-identical).
THE LAW. When the residual is a fan-out copy scheduled ahead of a sibling derivation from the same
source register, rank_for_schedule's priority — not the LUID order the C statement sequence controls —
decides it. Statement order is then a constant function of the output: the permutation sweep is
provably inert, and each permutation still costs a compile.
WHAT TO DO INSTEAD. (1) Recognise the tell — permutations that all return the SAME closeness are the
signature; stop after the first 2–3, not after N!. (2) Register pins do not help either: pinning fixes
which register, not the scheduler's ready-list priority (both functions were already fully pinned).
(3) Route it to the permuter/T6 wall track, which searches a space source order cannot express. This is
the §3-The/rank_for_schedule family, and it is the case those entries did not yet cover.
Cross-link: cookbook-index L22 (the brute-force lever) now has a documented BOUND — check §307
before spending a permutation sweep; §5a/§34's __asm__("") fence moves closeness here but does not
close it, which is itself the discriminator between a delay-slot-filler residual (fence works) and a
ready-list-priority residual (fence only perturbs).
§308 — A NAIVE FINAL goto DELETES THE TARGET'S j+nop: SPELL THAT EDGE AS A TAUTOLOGICAL if SO cse FOLDS IT TO AN UNCONDITIONAL JUMP AFTER jump1's WINDOW HAS CLOSED (P31 S63 t5e-t5i; byte-proven func_80180808)
(the CONSTRUCTIVE inverse of §165-03 (L13762) and §197-B (L20604): both own cse's dominated-compare fold and both prescribe how to prevent it — the re-tie, the front-end-opaque mask — and §165-03's own tell already records that the fold's residue in your output is "a plain j". Neither says that residue is a j+nop pair you can deliberately buy. Distinct from §194-N (L19618), whose jump1 deletion needs the label to be the next active insn; from §164-55 (L13048) + its S63 t5a addendum (L13070), where a missing/extra j is bought with arm ORDER; from §195-N (L20404), a +1 j owned by reorg block placement; and from §256 / ADD-7 / the §225-§256 addendum (L24052 / L25126 / L26270), which spell goto layouts but never price a goto edge that the compiler deletes.)
THE TELL. The target has a lone j .LABEL followed by a genuine nop, hopping forward over exactly ONE block that is itself self-terminating (that block ends in its own j/return), and every structurally-plausible C spelling of that edge compiles exactly 2 instructions SHORT. The second half of the tell is the discriminator: when you "simplify" that edge to a bare goto, the masked closeness explodes rather than shrinks — a residual that reads as a whole-function cascade, not as a 2-instruction drift. Read a j+real-nop over a single self-terminating block as a conditional test in the source, never as a bare goto.
THE MECHANISM (one half source-anchored, one half HYPOTHESIS — cite accordingly). Source-anchored: the fall-through of if (v != K) goto skip; puts you on the EQ side, where record_jump_cond merges v and K into one class (cse.c:5944-5990, §165-03 / §165-43 / §167-41), so a following if (v == K) goto body1; is folded to a constant-true and fold_rtx rewrites the branch as an unconditional jump. And the pass order is checkable, not inferred: jump1 runs at toplev.c:2827, cse1 at :2865 (already banked at L13116 / L13546). Hypothesis, NOT read out of jump.c this session: jump1 will delete/thread away an unconditional j whose only job is to hop over one self-terminating block, but it only ever sees such a jump when the source wrote the edge as a bare goto. Written as a redundant test, the edge is still a real conditional branch while jump1 runs and is only collapsed to a j by cse afterwards — too late for the deletion — so the j+nop survive as real bytes. Treat the deletion half as a place to look; what is byte-proven is the C dial and its ±2.
THE C SHAPE. Keep the body identical and re-spell only the last edge:
if (v1 < 5) goto body0;
if (v1 != 0x294) goto skip;
if (v1 == 0x294) goto body1; /* tautological given the line above — and that is the point:
it stays a real conditional through jump1, and cse's later
fold to an unconditional `j` lands after the deletion window */
body0: *(s16 *)(a0 + 0x70) = 1; goto common;
body1: … /* self-terminating sibling block */
common: …
skip: …
BYTE EVIDENCE. func_80180808 (ov_SC01_006, _jr_8017ED5C, 64 ins), banked at src/ov_SC01_006/ov_SC01_006_jr_8017ED5C.c:3526-3560, re-read out of the built object here: 8018085C move $a0,$s2 (the bne $v1,$v0,.L801808DC slot) / 80180860 j .L80180870 / 80180864 nop / 80180868 j .L801808C0 + 8018086C sh $v0,0x70($s1) (body0, self-terminating) / 80180870 = body1's first insn. Ladder, one axis per round: v0 (prior draft) near, closeness 2, nins 64, residual [[20,"beq…","bne $v1,$v0,.L801808DC"],[22,"j d4","j .L80180870"]], class BRANCH-POLARITY. v1, the textbook §3-T4 polarity flip → near, closeness 40, nins 62 — the flip deleted the edge. v2–v20 (if/else nesting, do/while(0), the §256 goto-dispatch template, a §162-shaped cross-jump goto, an explicit early-return split) plateau at 62–63, e.g. v18 near 41 63 LENGTH-DRIFT/-1?, v20 near 40 62 LENGTH-DRIFT/-2?. v21 = v20's body with the bare goto body1; replaced by if (v1 == 0x294) goto body1; → match, closeness 0, nins 64.
⚠ THE §3-T4 TRAP, and it is the expensive part. Closeness went 2 → 40 on a textbook polarity flip. A closeness that EXPLODES after a polarity edit means the edit removed a control-flow edge, not that the polarity was wrong — back the flip out and diff the js before spending another round. (Same read-direction warning as §165-23 / §167-27 / the S63 t5a addendum at L13070; and §194-K's bound applies — nothing between the draft and the whole-binary gate can see which block an unconditional j targets, L19885.)
BOUNDS. (1) The fold needs ONE cse path: per §164-52 / §197-B bound 3, a label with ≥2 jump references between the guard and the redundant test resets the table, the test survives as a real branch, and you are +2 instead of exact. (2) It needs the EQ channel — you must be on the fall-through of a != (or the taken side of an ==); a range/slti guard is §164-46 / §197-B territory and does not fold. (3) The redundant test must be spelled against the same quantity — mask it, launder it, or re-tie it (§165-03, §197-B) and you get a real compare+branch back, which is exactly the +2 failure. (4) One function, both directions measured; the jump1-deletion attribution is unproven (R14) — if a second instance disagrees, keep the dial and rewrite the mechanism paragraph.
§308a — A TWO-INSTRUCTION ARM STUB LAID BEFORE THE OTHER ARM AND REACHED ONLY BY A TRAILING j SURVIVES ONLY BEHIND A SHORT-CIRCUIT && GUARD PLUS A SEPARATE RE-TEST; EVERY PLAIN if/else-if, GOTO-LADDER, NESTED-INVERTED-if AND 1-/2-NODE switch LETS jump1 INLINE IT (−2 ins) (P31 S63 t5e-t5i; byte-proven func_80180808)
(BOUNDS §256 — its flat goto-in-target-block-order dispatch is the right family and the WRONG spelling here (closeness 2, never 0); supplies the first byte-measured instance of §195-G BOUND 5's "put a surviving CODE_LABEL in the way and the two spellings are no longer guaranteed equal" — measured price 2 instructions, where §195-G's ⚠ headline says the branch sense is free; complements §164-46/§165-03, which license the surviving RANGE re-test but never say it must be PAIRED with the guard that already decided it; sits beside §199-G, whose 2-node-switch and nested-inverted-if rows were both probed and refuted for this shape.)
TARGET SHAPE / THE TELL. A three-way dispatch on one call result where the target lays the SHORT arm first:
bne $v1,$v0,.default <- the "not the special constant" exit
nop
sh $v0,0x70($a0) <- armA: a 1-2 instruction self-terminating stub…
j .common <- …ending in its own `j`
nop
j .armB <- and a LONE `j` immediately before armB's own body
i.e. a 2-instruction armA block placed BEFORE armB and reached only by a trailing unconditional j. On the naive draft match_one files this as BRANCH-POLARITY / beq!=bne at closeness 2 with nins EXACT (64/64) — a near-match. The index (L14) routes that class to §3-T4 "invert the source condition". Do not. Inverting that one branch's sense collapses nins 64 → 62 and closeness 2 → 40: gcc's jump1 deletes the stub's j+nop, inlines the block and folds the guard. A count-EXACT BRANCH-POLARITY residual sitting on a short stub is a structure diagnosis, not a polarity one (same trap family as §165-23, §167-27 and §164-55's S63 addendum).
THE C SHAPE THAT MATCHES — the guard's range conjunct is written TWICE: once inside the short-circuit &&, once as the standalone dispatch test.
if (v1 >= 5 && v1 != 0x294) goto skip; /* union guard: leave for the default */
if (v1 < 5) goto armA; /* re-test the SAME range the guard already decided */
goto armB;
armA: *(s16 *)(a0 + 0x70) = 1; goto common;
armB: /* the long, call-bearing arm */ …
common: /* shared tail */ …
skip: *(u16 *)(a0 + 2) += 1;
MECHANISM — INFERRED, NOT TRACED. Cite it as a place to look, never as a proven pass behaviour (§164z's standard). The reading is jump.c:1737-1747's invert-a-cond-jump-that-jumps-over-an-uncond-jump (invert_jump/delete_jump) plus the swapped-inequality fold at :1540-1640: any single-test spelling leaves the stub as a lone self-terminating block landing right after an unconditional jump, which jump1 then inlines. The && form is claimed to survive because do_jump's TRUTH_ANDIF codegen (expr.c:8970-8990, §194-M/L19957) aims the positive branch at the drop-through and thread_jumps folds the duplicated slti into it, leaving the j armB un-collapsed. Not verified against an RTL dump — the "what" is byte-proven, the "why" is a hypothesis.
BYTE EVIDENCE. func_80180808 (ov_SC01_005, 64 ins, target asm/ov_SC01_005/nonmatchings/ov_SC01_005_jr_8017ED5C/func_80180808.s), banked at src/ov_SC01_005/ov_SC01_005_jr_8017ED5C.c:3515 (commit commit:3162, wave t5i, whole-binary gate green). Ladder on the pinned triple: v0 = §256's flat goto ladder (if(v1<5) goto body0; if(v1==0x294) goto body1; goto skip;) → near, nins 64, closeness 2, residual = target 10620005 beq v1,v0,+0x68 vs mine bne $v1,$v0, and target 08000035 j +0xd4 vs mine j .L80180870. v1 = §3-T4 applied literally, single-axis from v0 (second test's sense inverted, the two goto targets swapped — exactly §195-G ⚠'s "free" pair) → near, nins 62, closeness 40; the j/nop pair is gone. v2 = full if/else restructure → 63 ins, closeness 38. Nine further hand-probed spellings via direct cc1 .s dumps (v4, v6-early, v8, sw1-sw4 switch forms, w1-w4, y1-y4 with __asm__("") fences per §42/§73/§37/§5a/§34/§194-H, z1/z4/z5) all either kept the wrong beq/bne at 64 or collapsed to 62/63 — none reached 0. Winner = the z1 shape above (.run/t5i/opus/scratch_func_80180808/v6.c, submitted verbatim as .run/t5i/opus/func_80180808.c): MATCH, closeness 0, nins 64/64, residual [].
BOUNDS. (1) n = 1 function. The refuted-alternative list is broad (13 spellings) but all inside one body — treat the &&+re-test prescription as a first thing to try on this tell, not a proven universal. (2) The re-test here is a RANGE compare, which §164-46 says survives free; an EQUALITY re-test in the same position would additionally need §165-03's zero-byte __asm__("" : "=r"(t) : "0"(t)) tie, untested here. (3) §199-G's 2-node-switch tell does not fire — this header leads with a bne to the default, not the all-positive beq header — so the switch probes failing is consistent with §199-G, not evidence against it. (4) Do not read this as "always duplicate the guard": the extra conjunct costs nothing only because the guard and the dispatch test are the same range on the same value.
INDEX DELTA. cookbook-index.md L14 (BRANCH-POLARITY → §3-T4) needs the exception: count-EXACT BRANCH-POLARITY where the disputed branch is followed by a 1-2 instruction stub that ends in its own j ⇒ read §308a before inverting anything; the invert costs 2 instructions.
§309 — A FRAME-ADDRESS EQUIVALENCE RIDES THROUGH A SKIPPABLE CONDITIONAL: POST-if STORES FOLD ONTO $sp UNLESS THE if SURVIVES jump1 AS A BARRIER-PRECEDED DIAMOND (P31 S63 t5e-t5i; byte-proven func_801812AC)
(The FRAME-ADDRESS analogue of §164-52 (L12946), whose fold law and ">=2-jump-ref label" dial are byte-scoped to a SYMBOL base and do not transfer — a frame address is a PLUS, never CONSTANT_P (regalloc.md [A23]), so none of §164-52/§48-C1's struct/offset escapes exist for it. RESOLVES the probe §162m files at L11525 as "UNTRIED, and it is the next probe" — the balanced-diamond EBB split works, and this is its first byte-proof; it also promotes gcc-2.7.2-map/cse_expr.md §H-1/:387 from a pass-map antidote for global/derived-pointer folds to a banked law for &local. SAME C EDIT as §48-A4 (L3458) for a DIFFERENT pass and a different residual — do not merge them. NOT §193-D (that re-bases a block onto $aN via optimize_reg_copy_1, and its dial is a surviving CODE_LABEL). NOT §195-L (a join whose label has NUSES>=2 always ends the cse block; this law is about the ONE-use join that does not).)
THE TELL. A run of sw <reg>, <imm>($sp) / sw $zero, <imm>($sp) immediately downstream of a jal that itself sits just after an if/else or a ?:, where the target has the same opcodes, same source registers and same field order based on a callee-saved pointer register instead — sw <reg>, <imm2>($sN), with imm2 = imm − frame_offset_of_the_local. Same shape, same values, wrong base register and a correspondingly shifted immediate, instruction count identical. That is not regalloc and not scheduling: nothing in your register file is wrong — the pointer variable was never materialised at all. Do not open §47/§158/§136 or a register __asm__ pin on it.
THE MECHANISM. pr = &prim enters cse's table as pseudo == (plus (reg virtual-stack-vars) K), and find_best_addr/fold_rtx rewrite every downstream (mem (plus pseudo disp)) into (mem (plus fp K+disp)) — a legal MIPS address — so the pointer loses its last user and is deleted. Whether that fold reaches past the if is decided in cse_end_of_basic_block (cse.c:8091-8185), which has two arms, both taken only when LABEL_NUSES (JUMP_LABEL (p)) == 1:
- the branch's target label is preceded by a BARRIER (
:8116,-fcse-follow-jumps) — a real two-armed diamond. The scan resumes at the ELSE label and terminates at the join CODE_LABEL;new_basic_block(:8430) flushes the table,pris re-materialised, and the stores address$sN. This is the target. - the branch's target label is not barrier-preceded (
:8146,-fcse-skip-blocks) — a one-armed conditional branching around a block. The scan jumps to that label and keeps going past it to the next CODE_LABEL, carrying the whole table with it.invalidate_skipped_block(:7843-7866) then invalidates only what the skipped arm SET; thepr == fp+Kequivalence is untouched, so every post-join store folds onto$sp.
⚠ CORRECTS THE ORIGINATING NOTE'S MECHANISM. It is not "cse ends its per-arm scan at each arm's own call". invalidate_skipped_block handles CALL_INSN explicitly (invalidate_for_call() + skipped_writes_memory = everything, :7856-7860) and keeps skipping; a call kills hard regs and MEM entries, never a pseudo's (plus fp K) equivalence (cse_expr.md §1). The call matters only because of what it does to jump1: per §136d-2/§164-57/§164-74, an if/else whose arms are plain REG SETs is collapsed by jump.c:728-760 into t = B; if (c) t = A; with the jump around the set deleted (jump.c:821) — leaving exactly the barrier-free skippable block. A call (or a MEM store) is not a simple SET, the collapse cannot fire, the j+BARRIER survives, and the diamond stands. Duplicating the call is one way to buy the barrier, not the mechanism itself.
THE C SHAPE.
// FOLDS TO $sp (wrong) — the conditional collapses to a skippable block
Prim *pr = &prim;
void *src = (a3 != 0) ? (void *)D_8018633C : a2; /* or if/else assigning one shared var */
func_80017DC4(src, &mtx);
pr->code = 0x50000000; /* emitted sw …($sp) */
pr->c[0] = pr->c[1] = a4;
// MATCHES — duplicate the CALL into both arms so jump1 cannot collapse the diamond
Prim *pr = &prim;
if (a3 != 0) { func_80017DC4((void *)D_8018633C, &mtx); }
else { func_80017DC4(a2, &mtx); }
pr->code = 0x50000000; /* now sw …($s0), matches */
pr->c[0] = pr->c[1] = a4;
jump2's cross_jump merges the two now-identical jal suffixes back into one site, so the length is unchanged — 129 ins both ways; only the store base differs. (In-tree bound on §88a: a repeated call-shaped tail was merged here, as in §224; §88a's "left UNMERGED" is a suffix-equality/§50-B-floor observation, not a call exclusion — find_cross_jump has none, §193-C.)
BYTE EVIDENCE (func_801812AC, wave t5e-t5i, 129 ins). Warm-start draft: near, closeness 10, OPCODE-MIXED, residual [36] afa20060 sw v0,96(sp) vs target ae020030 sw $v0,0x30($s0) … [40] afa00058 sw zero,88(sp) vs ae000028 sw $zero,0x28($s0) — unchanged across a statement reorder (v1.c) and a register pin (v2.c); neither reaches the fold. a1.c (drop the local hdr/src copies) → closeness 5, residual is the [36..40] $sp run alone. b1.c (shared src var written as an explicit if/else) → closeness 30, worse — the §136d-2 collapse puts the skippable block straight back; this is the negative witness that the diamond, not the if keyword, is the lever. b5.c (call duplicated into both arms + TU-authoritative void * signature, inline casts) → MATCH 129/129, residual []. b6.c (same duplicated-call structure, hdr copy reintroduced) → closeness 5, SCHEDULE-REORDER — the prologue-order and $sp-fold defects are independent and both fixes are required.
FLAG ABLATION AS A PASS ORACLE (reusable). Recompiling the folding source with -fno-cse-skip-blocks gives nins 129 129 diffs 5: the entire $sp run disappears and only the unrelated prologue arg-copy cluster remains. One recompile named the pass and separated two co-resident residual classes — cheaper than a -da dump read. Bank the technique, not just the law: when a residual is suspected to be a cse EBB-extension effect, ablate -fcse-skip-blocks / -fcse-follow-jumps first (both are -O2-only, toplev.c:3389-3390).
BOUND. 1. -O2 only — both flags are off at -O1/-O0, so this class cannot occur in an -O0 file (§116/§127). 2. Requires LABEL_NUSES == 1 on the branch target. A join reached by two or more jumps ends the block on its own (§195-L, §164-52) and there is nothing to fix. 3. Scoped to a frame address whose uses are all DEREFERENCES. §164-52 LAW 1a's clause carries over: a use of the pointer VALUE (call argument, compare, store of the pointer itself) has no absolute form to fold into and keeps the base alive on any path. 4. The refund is TAIL-only (§193-C) and has a floor of 2 insns in the merged block (§50-B) — duplicating a call whose arms differ after it buys you nothing and costs the duplicate. 5. Not length-neutral in general; it was here because the duplicated tail merged. Read the count before assuming. 6. Untested: more than two arms, a switch dispatch, and whether the same ride-through folds a frame address across a skipped block that itself contains a call (the source says it should — :7856 skips on — but I did not compile it; R14 applies to that clause).
§310 — A TWO-OPERAND subu's DESTINATION TIES ONLY TO THE OPERAND THAT IS BLOCK-LOCAL: SHARE ONE SIDE ACROSS BLOCKS TO CHOOSE WHICH OPERAND'S REGISTER IT LANDS IN (P31 S64 t5j-t5m; byte-proven func_8017F578)
(COMPLETES §197-C (L20637) — its bound 1 measured a block-local commutative destination, found Fix A1 equally inert on it, and stopped at "state the law by the conflict set"; it never found the dial that DOES move a block-local destination. This is that dial, and it runs on the tie §197-C's bullet 1 declares out of scope for a global destination. INVERTS §48-A3 (L3438) / §76 (L6038) / §162b1 (L11078), which all read the same local-alloc.c:472 predicate in the SPLIT direction — split a shared temp to BUY the tie; here you deliberately share one operand to PUSH the tie onto the other. Distinct from §10 Residual A / Fix A1 (L856), which is source-operand ORDER on a commutative op and cannot apply to a subu at all. Second counter-example this phase — after the S62 T4 addendum at L29871 — to §137's (L9344) "source-level levers are a dead end for REGALLOC-PERM": the edit changes a pseudo's BLOCK COUNT, which §137's R/L arithmetic cannot see.)
THE TELL. match_one reports REGALLOC-PERM with a clean cyclic 2-register swap (REGALLOC-PERM/$v0>$v1>$v0) whose residual is whole lh …; lh …; subu triads, all three instructions of each triad, repeated once per block — here 3 blocks × 3 insns = closeness 9, everything else byte-identical. Read the two encodings, not the mnemonics: mine 00431023 = subu $v0,$v0,$v1 (dest ties rs, the minuend); target 00621023 = subu $v0,$v1,$v0 (dest ties rt, the subtrahend). The loads swap BECAUSE the subu's tie swapped — that is the discriminator against a schedule or polarity residual, where the loads would keep their registers. If only the subu differed you would be looking at something else.
THE MECHANISM. block_alloc's per-insn operand scan calls combine_regs (usedreg = input, setreg = dest) to tie a two-operand insn's destination quantity to one of its inputs. local-alloc.c:472-477 gives a pseudo reg_qty == -2 (tie-eligible) only when reg_basic_block >= 0 && reg_n_deaths == 1; anything else gets -1. combine_regs bails on reg_qty[ureg] == -1 (:1774) and only ties at :1843 — the same three line numbers §197-C already banked, used here for the opposite purpose. So of a subtraction's two operands, only the one confined to that basic block can win the destination. A C variable assigned and read at three different if-nesting depths is multi-block/multi-death ⇒ -1 ⇒ permanently disqualified, and the tie falls to the other side by elimination. subu is non-commutative, so operand order cannot express this and §10's Fix A1 has nothing to move.
THE C SHAPE — asymmetry is the whole ingredient. Name and reuse the operand whose register the destination must not take; leave the other side a fresh inline expression at every use.
/* `va` reused across ALL three tests -> multi-block life (reg_qty == -1, tie-INELIGIBLE);
the a0-side operand stays a fresh inline expression each time -> block-local (tie-eligible),
so the destination lands on IT and the triad's two `lh`s swap with it. LOAD-BEARING:
collapsing either half to match the other returns closeness 9. */
s32 va;
va = *(s16 *)(a2 + 0xA);
if ((u32)(va - *(s16 *)(a0 + 0xA) + 0x30) < 0x51) {
va = *(s16 *)(a2 + 0x6);
if ((u32)(va - *(s16 *)(a0 + 0x6) + 0x70) < 0xE1) {
va = *(s16 *)(a2 + 0xE);
if ((u32)(va - *(s16 *)(a0 + 0xE) + 0x70) < 0xE1) { goto found; }
}
}
BYTE EVIDENCE (func_8017F578, 67 ins). Ladder, one axis per round: v1 fully inlined → near, closeness 13 (the three triads plus a li/li swap at [8]/[9]); v3 an explicit t1 = 0x12C local → 11 (drops [8],[9]); v4 a2 += 0x10C moved into the for-increment → near, closeness 9, residual only the three triads at [15,16,18] [23,24,26] [31,32,34], verdict REGALLOC-PERM, map {"$v0":"$v1","$v1":"$v0"}. Then the two-point ablation that is the finding: v5 names both va and vb, each still reused across the three nested ifs → identical closeness 9, identical verdict — naming per se does nothing; v6 names only va and returns the a0-side to an inline per-use expression → match, closeness 0, nins 67, residual [], byte-gate accepted. Three points, one variable moved: both-inline = 9, both-shared = 9, exactly one shared = 0.
BOUNDS. (1) No dump was read. The reg_qty story is taken from the pinned source, not from a -dl/-dg dump of this function — §137/§158 set that bar and §197-C cleared it with real .greg conflict lines. What is byte-proven here is the C dial and its 9→0; the free confirmation nobody ran is §197-C's own test, the ;; N regs to allocate: line, which prints whether each operand is in local-alloc's scope at all. (2) v5 is explained, not measured. Under the mechanism, disqualifying both operands should leave the destination untied and free to take $v0 by ordinary first-fit — which is mine's failing form — so v5's identical 9 is consistent but reconstructed. It is equally consistent with the tie never having been the decider. Treat "share exactly one" as the proven recipe and the elimination story as the leading hypothesis. (3) n = 1 function, one insn shape (three lh/lh/subu triads on a hit-box compare). Untested on addu/and/or (where §10 Fix A1 competes), on operands of different widths, and on any destination that survives its block — a global destination is §197-C's territory, where this tie never fires and the lever is the split accumulate instead. (4) Sharing is not free elsewhere: §162b1 prices the same edit in the other two currencies — a shared multi-block temp costs you combine's known-zero-bits record (surviving andis, §162b1's LAW) and can win or lose a callee-saved pin. Ablate the share per operand, never sweep it. (5) Per §176-B, this is the right class of lever for a small REGALLOC-PERM and the pin is still the wrong one; no register __asm__ variant was tried here, and on this evidence none is owed.
§311 — A JOIN-BLOCK STORE CAN SWALLOW AN ARM'S LOAD-DELAY nop: WHEN ONE ARM'S VALUE COMES FROM A LOAD, DUPLICATE THE STORE INTO BOTH ARMS (P31 S64 t5j-t5m; byte-proven func_8017D7E0)
(THE THIRD POLE of the duplicate-vs-join store dial, and the only one whose mechanism is the LOAD-DELAY HAZARD. Bounds §165-21, whose "two computed arms want one shared trailing store; §164-73/§164-74's write-the-store-twice is scoped to CONSTANT arms" reads as a two-way split — this is a MIXED pair (one literal, one loaded value) where the duplicate wins. Extends §164-73 / §164-74 (same prescription, two literals, jump.c:728-760 collapse + regalloc mechanism) and §167-26 (fall-through case, zero length cost, saved-register order only). Same tell as §176-A2, different lever: there the −1 nop is bought back by moving the store above a call; here no statement move reaches it, because the store's position is pinned by the if/else join.)
THE TELL. LENGTH-DRIFT/-1 — exactly one instruction short — with every instruction past the if/else join read one slot early, and the target holding a bare nop your draft does not have, sitting between an arm's final load and the store:
target 143: 84220000 lh $v0,0($at) <- the else arm's last insn
target 144: 00000000 nop <- YOUR DRAFT DOES NOT HAVE THIS
target 145: ae220014 sw $v0,0x14($s1) <- the store, ONE copy
target 146: 02202021 move $a0,$s1 <- the next call's arg setup
…
target 163: 27bd0038 addiu $sp,$sp,0x38 (you read `jr $ra` here — one line early)
Residual quotes it as [144, "02202021 move a0,s1", "00000000 nop"] … [163, "03e00008 jr ra", "27bd0038 addiu sp,sp,0x38"] — a pure one-slot displacement, not a permutation.
THE LAW. maspsx emits the load-delay nop only where the final stream still carries the hazard (§176's "the LOAD-delay nop is UNCONDITIONAL — do not write it yourself"), and sched1's scope is the basic block (pass order §45-B; the same scope fact §164-69 turns into a register lever). Compute the value into a temp and store it once after the join and the store lives in the JOIN block, whose other members — the following call's move/lui argument setup — are independent of the arm's load and legally schedulable between it and the store. One of them covers the hazard, maspsx emits no nop, and you are one instruction short with the whole tail displaced. Write the store inside each arm and it sits in that arm's own block, data-dependent on that arm's load, with nothing independent left to interleave — the nop survives at the right byte. jump2's cross_jump then merges the two identical sw tails back into ONE store at the join (§50-B / §162h floor met), so the duplicate is byte-free: the emitted object holds a single sw $v0,0x14($s1), exactly as the target does.
THE C SHAPE. Broken — the temp moves the store out of the arm's block:
s32 val;
if (cond) { val = 0x4B0; } else { val = TBL[*(u8 *)(p + 5)]; }
*(s32 *)(p + 0x14) = val;
Fixed — write the store twice; cross_jump refunds it:
if (cond) { *(s32 *)(p + 0x14) = 0x4B0; }
else { *(s32 *)(p + 0x14) = TBL[*(u8 *)(p + 5)]; }
SCOPE — this is NOT "never a temp". §165-21 and §167-26 both byte-proved a temp (and a ternary) MATCHing on their shapes; the dial has three poles and the discriminator is what the arms hold:
- two literals ⇒ §164-73 / §164-74 — duplicate; tell is a literal in the branch delay slot + a later load transposed above the store.
- two single-insn expressions over the same live registers ⇒ §165-21 — one shared store; duplicating costs +2.
- one literal + one value ending in a LOAD, with a call immediately below the join ⇒ this entry — duplicate; tell is
LENGTH-DRIFT −1and a missingnop, not a register permutation.
BYTE EVIDENCE. func_8017D7E0 (ov_SC06_033, 166 ins, banked at src/ov_SC06_033/ov_SC06_033_jr_8017C24C.c:3568-3572). v3 (pointer-CSE, val temp present): closeness 22 / nins 165 / LENGTH-DRIFT/-1? at 127, explains: partial. v4a/b/c reordered the tail statements around the temp — inert, still 22, which is what rules out §176-A2's statement-move lever. v5 (temp deleted, store duplicated into both arms): closeness 2, nins 165 → 166, the LENGTH-DRIFT class gone entirely, leaving only an unrelated 2-insn operand-order residual at [127,128]. The final 2 → 0 came one step later from an ordinary source-order edit (D_801274EA before D_801274EC, inlining a (u16) cast condition) — §176-A / §165-14 territory, not part of this law. Re-read at vet time out of the banked object: objdump of the banked build shows nop at 0x240 and exactly one sw $v0,0x14($s1) at 0x244 — the merge is visible in the bytes, and 0x240/4 = 144 is the residual's own index.
⚠ MECHANISM BOUND (R14). No -dS/-dR dump was taken. The step "an independent join-block insn covers the cross-block hazard" is read off the final stream plus the pass order, not off the scheduler; -dS on the two spellings is the cheap confirmation and has not been run. What is measured is: the temp spelling is −1 with the nop absent, the duplicated spelling restores it at the correct byte, and the object still emits only one store.
§312 — A BARE RELATIONAL IN AN if GIVES THE slt NO TARGET: ASSIGN THE COMPARISON INTO THE ALREADY-ALLOCATED VARIABLE TO CHOOSE ITS DESTINATION REGISTER, AT ZERO INSTRUCTION COST (P31 S64 t5o-t5r; byte-proven func_80180DCC)
(NEW axis, not a restatement. §164-38/§164-39 and §164-39's t5j-t5m addendum own the compare's INPUT colouring — which operand got which register — and their levers are the in-out fence on operand 0 and the relational transposition; both are the wrong door when the inputs are already correct. §197-C/§164-64 own "write it IN PLACE so the destination is an existing pseudo" for */+/|/&; this extends that lever to the relational operator, where §197-C's companion note — "a pinned pseudo can never be the load's destination" — had left the impression that a pin cannot own a computed dest at all. §164-65 also names a value you were going to consume inline, but its payload is allocno REF COUNT, not SET_DEST. §195-E is the other naming law and is about whether a 0/1 materialises at a join; here naming is length-neutral, which is exactly what makes this lever free.)
THE TELL. The target's slt writes back into one of its own two source registers — an in-place compare, slt $v0,$v0,$v1 — and is immediately consumed by a bnez/beqz on that same register. Your draft emits the identical pair with every operand register already correct and only the destination one register over: match_one reports REGALLOC-PERM with a single-register cycle ({"$v1":"$v0"}, kinds:{"reg":2}), residual exactly 2 instructions — the slt and its branch — and zero length drift. That is not structure and it is not input colouring; nothing but the compare's SET_DEST is wrong.
THE MECHANISM — INFERRED, NOT TRACED (cite as a place to look, never as proven pass behaviour; §164z / §137's bar). A truth value used only inside an if reaches do_jump/expand_expr with no fixed target, so the slt's destination is synthesised from whatever pseudo is cheapest at that point, tied to neither compared operand — which is why it can land one register away even when both operands are correctly pinned. Writing the comparison as an assignment hands expand_expr an explicit target pseudo and store_expr honours it, forcing the slt's SET_DEST to that variable's hard register. No -dS/-dg/-dl/-dg dump was taken; local-alloc.c combine_regs' tie-to-a-dying-source-operand (§10 Residual A) is the other place worth dumping first if this recurs.
THE C SHAPE.
/* wrong — dest register unpredictable */
if (v0 < v1) { goto TRUE1; }
/* right — forces the slt's dest into v0's pinned hard reg */
v0 = (v0 < v1);
if (v0 != 0) { goto TRUE1; }
/* v0 is `register s32 v0 __asm__("$2")`; v1 a sibling pinned s16-cast temp,
`register s32 v1 __asm__("$3")` */
The two spellings cost the same instruction count — a bare relational has no join and no constant arms, so §195-E's "naming materialises a 0/1" does not fire and the naming is byte-free apart from the register it buys.
BYTE EVIDENCE. func_80180DCC, 66 ins, all quotes from match_one --json on the pinned triple. v0 (backlog draft, register s16 s1 pin + call-site (s16) casts) → closeness 9, STRENGTH/sll!=sra. Unpinning s1 → closeness 6, same sig (block 2 clean, block 1's sll/sra/slt/bnez still wrong). v6 (pin register s32 v1 __asm__("$3"), hoist v1 = (s16)s1; v0 = (s16)v0; as statements ahead of a still-bare if (v0 < v1)) → 6 → 2: {"status":"near","closeness":2,"residual":[[34,"0043182a slt v1,v0,v1","0043102a slt $v0, $v0, $v1"],[35,"14600013 bnez v1,…","14400013 bnez $v0, .L80180EA8"]],"verdict":{"klass":"REGALLOC-PERM","sig":"REGALLOC-PERM/$v1>$v0","detail":{"map":{"$v1":"$v0"}}}}. v7, the only change being if (v0 < v1) {…} → v0 = (v0 < v1); if (v0 != 0) {…} → 2 → 0: {"status":"match","closeness":0,"residual":[],"verdict":{"klass":"MATCH"}}.
BOUND. (1) n = 1. (2) Only the v6 → v7 delta is solo-proven (§266): the pins and the cast hoist arrived in compound edits and are co-requisites of the shape, not separately ablated — the pin is what makes "the variable's hard register" a nameable thing here, and an unpinned local would only move the dest to whatever that local got. (3) Routing: on a REGALLOC-PERM whose single wrong register is the compare's DEST, try this first — one statement, zero bytes; go to §164-39's fence or its addendum's transposition only when the wrong register is one of the compare's INPUTS. (4) Says nothing about compare-against-constant shape (slti/sltiu immediate-vs-register is §194-L's 2-D lookup) or about signedness (§280).
§313 — A RECOVERY STAGE THAT REWRITES THE DRAFT IS PART OF THE MEASUREMENT: GATE THE RAW DRAFT FIRST, THE REWRITE ONLY AS A FALLBACK (P31 S65 t5r-recovery; byte-proven func_8017BEBC, 246 ins)
(Tooling law, not a codegen idiom — the §65/§121 family. §121 established macro-externs: a draft's extern for a DEFINE_func_*-defined callee must match the macro's own definition head, because a guessed extern int f(); collides with the macro's real void f(s32) and poisons the whole-binary build. This entry does not retract §121; it bounds it. R57 is the governing rule — an instrument's own write path is part of the instrument.)
THE TELL. recover_integration.py --probe-only classifies a stranded draft MATCH in its real TU (blocker_probe compiles the draft where it will live and compares the function's bytes), yet the driver's own pass reports banked 0/1 and the classified ledger names a blocker. The two disagree because they are not measuring the same text: the probe reads the draft, the driver rewrites it and then measures the rewrite.
THE MECHANISM — PROVEN, NOT INFERRED. func_ADDR names are per-address, not per-function: the same address name exists in many overlays and denotes DIFFERENT code in each. macro_def_sig_map() is fleet-wide, so macro-externs can install another overlay's signature over a correct local one. Measured: the ov_SC03_012 draft of func_8017BEBC declared its callee extern int func_8017C338(short *, short *, short *, int); — correct for this overlay, and what the original asm's call site emits. The stage replaced it with the fleet macro's head, extern void func_8017C338(void);, and cc1 then refused the 4-argument call. The driver reported that as the DRAFT's failure and moved on.
THE COST. A byte-perfect 246-instruction body sat unbanked, and the raw text was never gated once — stage_drafts copies into the run dir, macro-externs mutates in place, and every later pass reads the mutated copy. Spliced by hand, the untouched draft built sha1 e9483e61… == config/check.ov_SC03_012.sha (BYTE-IDENTICAL) on the first try. gate_stage --drafts <raw> banked it: {"drafts":1,"banked":1,…}.
THE FIX (shipped). reconcile_and_gate(..., draft_rewrite=) gates in a ladder — pass 1a the raw drafts, pass 1b only what 1a refused, with the rewrite applied — and records per-fn which variant won so pass 2 re-gates that one. Cost is one extra gate round, and only when something failed raw. Same shape as §53's carve-law: a 0% from a tool that changed its own input is not evidence about the input.
THE SECOND DEFECT IT EXPOSED (also shipped). harvest_verify.classify_fail's §58 red-herring guard drops warning: lines but kept their orphaned note: half, so the label read CC1-FAIL: …/engine_core.h:57282: note: this is the location of the previous definition — a note whose antecedent was the benign fleet-wide "DEFINE_func_80181538" redefined warning, on a draft that had in fact built. Notes are now dropped with their warnings; the honest CC1-FAIL(no-diagnostic) label surfaces the gap instead (R32). Negative-controlled over five diagnostic shapes (real error + note, plumbing conflict, gcc-2.7.2 no-error-word hard error, undefined reference, warning+note only): only the last changed.
BOUND. (1) n = 1 for the bank; the same raw-first gate over the other three CC1-FAIL strandeds of the same wave banked 0 — they are genuine near-misses, so this is a real-but-narrow class, not a general "the rewrite is wrong" verdict. (2) macro-externs keeps its §121 warrant: it is now a fallback, and it still fires for the drafts a raw gate refuses. (3) The deeper hazard — a fleet-wide signature map applied to a per-overlay name (R48's collision class) — is NOT fixed here, only ordered around. A per-binary "is this DEFINE_ actually instantiated in THIS TU?" guard is the real repair; it is unbuilt because it is a new refusal-check and needs an R39 negative control over every draft the stage ever helped.
(14 novelty-flagged transcripts distilled, each adversarially verified against this book before banking: 7 COVERED (rediscoveries), 6 ADDENDUM, 1 NEW. The COVERED majority is the expected signal and it points at RETRIEVAL, not missing law — see the §5a findability addendum earlier this session. Entries marked ⚠ UNPROVEN come from drafts the whole-binary gate REFUSED: the lever moved match_one's closeness but no byte-equality is claimed (R14/G3).)
→ addendum to §52a (from func_80188A10)
Addendum (P31 S65, func_80188A10): §52a's RC-3 lever generalizes past "pre-call argument, prologue save-order tie." A bare extern const s32 D_8019609C; — no pointer cast, no call-site expression, just the declaration qualifier — sets RTX_UNCHANGING_P on every load of that global, and true_dependence drops the edge against an unrelated, variable-address sh store sitting between two statements, not just against the prologue's sw $sN/sw $ra. Effect here is a straight LENGTH-DRIFT fix, not a save-order stagger: extern s32 schedules lhu $a0,0x12($v1) / nop / addu / sh $a0,0x12($v1) / lui %hi(D_) / lw %lo(D_) (2 unfilled load-delay nops → 211 ins, closeness 147); extern const s32 lets the scheduler hop the global's lui/lw pair up into those two nop slots and defer the store into the following jal's delay slot instead — lhu $a2 / lui %hi(D_) / lw %lo(D_) / addu $a2,$a2,$v0 / jal func_8012B178 / sh $a2,0x12($v1) (209 ins, closeness 12). Verified in-TU spliced (0 mismatched) and all 32 relocations identical in order.
→ addendum to §220's P31 S64 addendum (func_8018AAE4), clause (2) at L22815 (from func_800CAEC0)
Addendum (P31 S65, func_800CAEC0): §220's S64 addendum states the DELAY-SLOT/1 polarity mine=nop / target=addu $aN,$sM,$zero is categorically an aliasing defect — "check which side the copy is on before flipping any callee's arity." func_800CAEC0 (22 ins, MATCH) is a counter-example with no pin, no named alias, no copy-prop anywhere in the draft: the card's atlas fleet consensus (n=1223 call sites) gave func_80165910 as ('s32', ()) no-arg, but THIS call site's target carries addu $a0,$s0,$zero right in the jal's own delay slot — {"status":"near","closeness":1,...,"residual":[[8,"00000000 nop","02002021 addu $a0, $s0, $zero"]],"verdict":{"klass":"DELAY-SLOT","profile":"schedule","sig":"DELAY-SLOT/1"}}. Declaring func_80165910(s32 a0) and passing s0 closed it in one A/B: {"status":"match","closeness":0,"nins":22,...} — a plain §263-style under-declared-arity fix, not an alias to delete.
The bound that was missing. The S64 addendum's polarity rule holds only under ITS OWN precondition — a pinned/aliased local whose value copy-prop can forward-substitute for the incoming parameter pseudo (that draft used register s32 s0 __asm__("$16"); s0 = a0;). Absent a pin or a named alias, nop-vs-copy at a jal's own delay slot is the ordinary under-declared-arity signature, and a large fleet-consensus n is a majority vote over OTHER call sites, not proof for this one — the parameter-count sibling of §165-30's "a fleet consensus is a floor, not the signature" (there for return type). Run the free match_one arity-bump A/B before reading the polarity as aliasing.
BOUND. n=1. Co-requisite: the draft under test has no pin and no named local aliasing the parameter — this does not revoke the S64 addendum's own case (func_8018AAE4), which genuinely had a pin and needed the alias deleted. Says nothing about which reading is more common fleet-wide; only that polarity alone, without checking for a pin/alias, is not a safe discriminator.
→ addendum to §211 addendum (P31 S58b) dial 4 (from func_8018203C)
Addendum (P31 S65, func_8018203C): §211-addendum dial 4 ("declaration order of two independent inits IS the scheduler's emission order") extends to a conditional's ARM CONSTANTS, not just plain variable inits — but the dial does not exist until you extract them. func_8018203C (ov_SC01_005, 47 ins): pinning the four loop pointers (d/p/dh/sh → $5/$6/$3/$4) alone closed 27→7, leaving one clean 7-instruction BLOCK SWAP — match_one residual at that point: [16] mine "addiu a0,a2,6" tgt "lui $t1,(0x1000000>>16)" … [22] mine "addiu a3,a3,0" tgt "addiu $v1,$a1,0x6" — my sh=p+6; dh=d+6; advance-addius emitted BEFORE the loop's if(*p&mask) bitmask/pointer-constant loads; target has them after. gcc's own loop-invariant motion already hoists an anonymous mask and both if/else arm literals out of the loop, but their position relative to other already-hoisted setup is unsteerable while they stay anonymous. Naming them (mask/elseVal/ifVal), pinning each to its own register ($9/$8/$7), and placing the three assignments textually before the sh=/dh= lines (still inside the if(*p!=0) guard — hoisting the block above the guard, the classic §211 dial, was tried and regressed 7→23) closed it to {"status":"match","closeness":0}. The ordering dial requires the constant to first become a named, pinned local — an inline arm literal isn't reachable by declaration order alone.
BOUND: n=1. Co-requisite with the base-pointer group already being register-pinned (untested whether extraction+ordering alone, unpinned, would have sufficed). Says nothing about whether extraction is needed when only one arm-literal is loop-invariant, or about non-do/while loop shapes.
→ addendum to §164-38 (from func_800CB304)
Addendum (P31 S65, func_800CB304): A second, independent byte-proof that §164-38's GT/LT canonicalization fires outside the section's own "two-arm oracle" setup — here on a single, standalone if, no sibling arm needed as corroboration. Target loads lh $v1,0x28($s0) then lh $v0,0x2A($s0) before slt $v0,$v0,$v1. The "obvious" transliteration if (field2A < field28) (register-order read straight off the asm) loads $v0 first instead — byte-measured {"klass":"SCHEDULE-REORDER","profile":"schedule","bucket":"permuter","closeness":2} on exactly that lh/lh pair, §164-38's own predicted signature. Respelling as the mathematically-identical if (field28 > field2A) reverses the load order and closes it to MATCH (63/63) — no register swap, no permuter search, one flipped relational operator. The trap this sharpens: SYS.md's own law 3 ("a plateaued diff whose class says [permuter] is DONE: submit NEAR") reads exactly this residual's bucket=permuter tag as a stop sign; here it was a two-character C-level fix. Before honoring a bucket=permuter/profile=schedule verdict on a 2-instruction residual that is a transposed lh/lh pair feeding one slt, grep §164-38.
→ addendum to §176-D2, docs/matching-cookbook.md:17776-17802 ("CSE-class levers used in reverse (two sharpenings of §153 and cse_expr §2)" → "D2 (from func_8018AD74)
Addendum (P31 S65, func_8018AD74; UNPROVEN, gate-refused draft — closeness numbers only, no MATCH claimed): D2's mechanism — a struct block-copy's movstrsi lowering permanently seeds its dest-address pseudo into the cse equivalence class, so a later site keeps reusing it — also surfaces as a pure delay-slot-fill reorder, not only D2's documented +1-insn address-caching symptom. On mtx = D_800AE620;, t.i.rtl/t.i.cse show (insn 23 (set (reg:SI 80) (reg:SI 69))) — the block-copy's dest address = virtual-stack-vars — and t.i.sched2 schedules that same pseudo's materialization ($s2 = sp+0x10, i.e. &mtx) as the LAST insn before an unrelated jal func_8012C658, so it wins that call's delay slot instead of the arg-register move s1=a3 the target puts there (target order jal / s1=a3(slot) / s0=v0 / beqz / s2=&mtx-after-branch, mine s1=a3 / jal(slot=s2=&mtx) / s0=v0 / beqz). Closeness sat pinned at 5/60 (match_one.py --json, klass ADDRESSING/move!=jal, same 5-insn multiset reordered) across unrelated variants ($16/$17 pins, s16-vs-s32 typing of an unrelated param).
D2's actual escape (pinned-register hand-expansion of the copy + trailing value barrier) was not reproduced here — only its unpinned half was tried, and per D2's own "pins are load-bearing" clause it made things markedly worse, not better: field-by-field copy 5→62, §2-style output-only kill (void*q=&mtx; + volatile re-tie) 5→32, §3-style input-tied re-tie 5→59, declaration-order swap 5→16, bare "":::"memory" barrier 5→32.
Bound: n=1, never byte-matched. Says nothing about whether D2's full pinned/barriered recipe fixes this reorder-shaped symptom — only that the naive partial form doesn't, which is consistent with D2 rather than a refutation of it.
→ addendum to §308 (from func_8017E6F4)
Addendum (P31 S65, func_8017E6F4): ⚠ UNPROVEN — gate-refused draft, bottomed at closeness 3/182, never MATCHed; n=1. §308 BOUND (1) already says a ≥2-jump-ref label between a guard and a redundant re-test breaks cse's fold and the test survives as real bytes — but it is silent on WHICH REGISTER that surviving reload lands in, and on the ins-count cost of two equally-natural ways to place it.
Shape: t = *(s16*)(s0+0xFE); if (t!=0) { if(cond){ *(s16*)(s0+0x5C)=0x800; } else { *(s16*)(s0+0x5C)=0; return; } } if (*(s16*)(s0+0xFE)==0) { if(guard2) return; }. Compiled with -dr/raw.s, label $L4 (2 jump refs — the early t==0 skip branch, and the true-arm's fallthrough after sh 0x5C) lands directly between the sh store and the lh 0xFE reload, and the reload allocates $v0: 183 ins vs target's 182, match_one closeness 147 (v_AD/h1Aa, .run/t6a/opus/scratch_func_8017E6F4/).
Moving the same retest inside the producing arm via goto (if (*(s16*)(s0+0xFE)!=0) goto have_flag; right after the store; the guard2 check + have_flag: land after the outer merge) removes the 2-predecessor label from between the store and the reload: 182 ins, closeness 17 in one edit (gG), and the match_one diff at that closeness carries zero residuals in the guard region — all 17 are in an unrelated 0x1a4-0x220 trig block — vs. the target's own confirmed bytes for that reload, 860300fe lh $v1, 0xFE($s0) (read off a match_one --json residual against the true target). Tell: a "natural" nested-if spelling of a guard-then-redundant-retest pair is 1 instruction long and puts the reload in the wrong register whenever the label reaching the retest has ≥2 predecessors; goto-relocating the retest into the producing arm (duplicating the address, not the register) fixes both. Mechanism is inferred, not RTL-confirmed this session — re-verify with -dr/gdb before trusting the register claim on a second instance.
§314 — AN IF/ELSE-IF ABS-RANGE CHECK'S C SPELLING IS INERT: FOUR STRUCTURALLY DIFFERENT REWRITES ALL COMPILE BYTE-IDENTICAL, AND NONE REACHES A TARGET THAT SINKS THE NEGATIVE ARM INTO AN OUT-OF-LINE BLOCK (P31 S65; UNPROVEN, gate-refused draft func_80181720)
THE TELL. match_one/masked_diff.structured_diff closeness is a FIXED POINT under this class of rewrite. On func_80181720's tail — d = K - x; if (d < 0) { if (9 < x - K) return 0; } else if (9 < d) return 0; (an abs-range guard before a call) — four structurally distinct C spellings all produced the exact same masked_diff output, digit for digit:
total diffs: 51 mine_len 83 tgt_len 87 <- v3.c (literal if/else-if, matches seed shape)
total diffs: 51 mine_len 83 tgt_len 87 <- v7.c (inverted: if(d>=0){...}else{...})
total diffs: 51 mine_len 83 tgt_len 87 <- v8.c (explicit two-label goto split)
{"status":"near","closeness":51, ...} <- v9.c (same shape, u16-typed globals + (s16) casts)
Not "close" — the SAME 51 mismatched indices, the SAME mine_len/tgt_len. Rephrasing this control-flow shape (merged-negate-then-single-compare if(x<0)x=-x; idiom, split if/else-if matching the literal source, its inverted twin, and an explicit goto two-block split) is observably inert for this residual — none of the standard "reorder the if/else", "explicit goto", "invert the condition" C dials (§3-T4, §164-55/§164-47, §167-27) move it by even one instruction.
WHAT ACTUALLY DIFFERS (read off the instruction trace, not inferred). Every draft variant places both range comparisons close together in the primary block: one slti $v0,$v0,0xA filling the bgez's own delay slot (executes unconditionally, used only if the branch is taken) and a second, freshly-recomputed slti $v0,$v0,0xA right after the fallthrough's subu. The target instead SINKS the d<0 arm into a physically separate, out-of-line block: bltz $v0,.L8018180C jumps forward past the whole 9<d test to a distant label; that label independently recomputes the difference (subu $v0,$a0,$v1 — same two already-loaded registers, opposite operand order, i.e. it reuses the loads but redoes the subtract rather than reusing -d) and its own slti $v0,$v0,0xA, then beqz $v0,.L80181868 branches all the way back to the guard function's own return-0 label rather than falling through locally:
tgt idx53: bltz $v0, .L8018180C idx54: slti $v0,$v0,0xA idx55: bnez $v0,.L8018181C
tgt idx58: addu $v0,$zero,$zero idx59: subu $v0,$a0,$v1 idx60: slti $v0,$v0,0xA idx61: beqz $v0,.L80181868
mine idx53: bgez v0,e4 idx54: slti v0,v0,10 idx55: subu v0,a0,v1 idx56: slti v0,v0,10 (dup, inline)
So the agent's own closing summary ("merges provably-equivalent arms into one shared slt") is an over-statement worth correcting for the next reader: the drafts do NOT collapse to one physical slti — they keep two, like the target — the actual, byte-visible gap is block placement: whether the d<0 arm's compare lives inline (drafts, every spelling tried) or in a genuinely separate out-of-line block reached by bltz+distant-beqz (target).
THE MECHANISM — unknown/inferred, NOT verified against gcc-2.7.2 source this session. No -dj/-dS dump or jump.c/stmt.c read was done to identify the deciding pass. Candidate, unconfirmed: some expand_expr/do_jump block-layout heuristic for a nested if whose OUTER condition's false arm is itself a full if (as opposed to jump.c:1806's documented "every arm returns" swap at §167-27/§136g-1, whose stated precondition does not hold here — the fall-through path continues into VectorNormal(...), not a return). Do not cite either pass as the cause without re-deriving it from source.
BOUND. n=1 (one function, and the whole-binary byte-gate REFUSED this draft, so even the 51-diff intermediate state is not proven correct elsewhere in the function — only the match_one/masked_diff closeness comparisons between drafts, which are a reliable local tool, are asserted here). Only four C spellings were tried, all closely related (all keep the guard as two nested nullary-side-effect ifs feeding early returns); untested and left open: whether a spelling with a REAL intervening side effect between the two tests (a §164-52-style label with ≥2 jump references, which resets cse's path-local tables and is documented elsewhere to disarm similar folds) reaches the target's out-of-line shape. Says nothing about functions where the "rare arm" contains a return from every path (that's §167-27's territory, already covered) or about any range check that is a single ||/&& expression (that's §164-56's range_test, a different, statement-boundary-sensitive fold, explicitly checked and ruled out as the mechanism here since a statement boundary already existed in every spelling tried and did not change the outcome).
S66 HARVEST (P31, 2026-08-30) — 4 NEW, 15 ADDENDUM, 1 REFUTED
Distilled from 35 wave transcripts (12 byte-proven banked, 23 drafted-but-ungated). Rows marked UNPROVEN came from drafts the whole-binary gate had not accepted — the lever may still be real (cookbook §52: a frontier model that FAILS a wall still distils the idiom that cracks its siblings), but no byte-equality is asserted for them.
§315 — ALL-CONSTANT AGGREGATE FILL: THE EMISSION ORDER IS SHARED-LITERAL GROUPS × DESCENDING INDEX, WITH PAIRED SUB-FIELDS INTERLEAVED (P31 S66; ⚠ UNPROVEN — func_80185214, gate-REFUSED draft, closeness 32→5, never MATCHed)
⚠ STATUS FIRST. This section is filed on a differential measurement, not on bytes. func_80185214 never reached MATCH; its best draft bottoms at {"status":"near","closeness":5,...,"klass":"SCHEDULE-REORDER","bucket":"permuter"}. Treat the ordering rule below as a probe worth two minutes, not a law. No gcc-2.7.2 source cite ties it to a pass — the agent grepped sched.c for birthing_insn_p (§3-C) and tried §3-G block reordering, and both named mechanisms were flat.
THE TELL. A run of li/addiu constant loads into one or two scratch regs ($v0/$v1), each immediately followed by several sh/sw stores into a local struct/array (PS1 POLY_FT4-style vertex/uv prim is the archetype), where the store OFFSETS run counter to naive left-to-right source order — e.g. one constant group hitting 0x28, 0x18, 0x2A, 0x22 = array indices 3, 1, 3, 2 — and where two sub-fields of the same element (uv[i].u then uv[i].v) land as adjacent stores rather than being split apart by field.
THE PROBE (what the sweep selected). When a local aggregate is filled entirely with compile-time constants and several sibling fields share literals, write the source as: (1) one group per distinct literal (all the 0x40s, then all the -0x40s, …); (2) inside each group, walk the array index DOWN — [3], [2], [1], [0]; (3) for paired sub-fields, interleave per index instead of writing whole fields.
prim.v[3].vx = 0x40; prim.v[2].vx = -0x40; prim.v[1].vx = 0x40; prim.v[0].vx = -0x40;
prim.v[3].vy = 0x40; prim.v[2].vy = 0x40; prim.v[1].vy = -0x40; prim.v[0].vy = -0x40;
prim.clut = 0x79;
prim.v[3].vz = 0; prim.v[2].vz = 0; prim.v[1].vz = 0; prim.v[0].vz = 0;
prim.uv[0].u = 0xC00; prim.uv[0].v = 0x180; prim.uv[1].u = 0xC7F; prim.uv[1].v = 0x180;
prim.uv[2].u = 0xC00; prim.uv[2].v = 0x1FF; prim.uv[3].u = 0xC7F; prim.uv[3].v = 0x1FF;
EVIDENCE — a 40-variant combinatorial sweep, differential only. .run/.../sweep2.py, 5 v-shapes × 4 u-shapes × 2 tails, masked-diff against the real target .s via match_one. Baseline backlog draft (source order + single-expression tail) = 32 (sig":"WIDTH/sw!=lw"). A load-hoist variant = 34 (worse). Splitting the colour expression alone with unchanged write order = 38 (worse still). Then: grp+nat+B = 16, vdsc+nat+B = 10, fdsc+nat+B = 5 (best), against fdsc+base+A = 28, fdsc+natd+A = 28, vasc+nat+A = 28. The three levers are jointly required — swap any one out and it reverts to double digits.
WHAT THE FLOOR ACTUALLY IS, AND WHY IT MATTERS. The surviving 5-instruction window is not the fill body — it is the prologue schedule:
mine: sw $v0,0x50($sp) ; li $v0,0x40 ; sw $ra,0x58($sp) ; lw $a3,0x1C($a0) ; li $v1,-0x40
target: sw $ra,0x58($sp) ; lw $a3,0x1C($a0) ; addiu $v1,$zero,-0x40 ; sw $v0,0x50($sp) ; addiu $v0,$zero,0x40
Per §234 the li 0x40 / addiu $zero,0x40 pair is the same encoding (bit-15-clear ⇒ the signedness dial is inert), so the residual is pure position: the target sinks the first frame store below the $ra save and the $a3 load, and births $v1 before $v0. So the fill-order lever appears to have done its job and the remainder is a separate prologue-window defect — which is the honest reading, and also why the section cannot be promoted: nothing byte-confirms it. From that floor, 484 statement placements of the flags store/load, 57 block permutations, §205 chained forms, §55a/§3-C re-tie fences, §220 param typings and empty-asm barriers all left the number at 5.
BOUNDS AND RELATIONS (read these before reaching for it).
- This is §213's declared blind spot, which is why it is its own section. §213's BOUNDARY says verbatim: "This is about independent stores — no aliasing, no shared value, no call between them." It then routes shared-value runs to §30/§193-D/§194-M — none of which cover an all-constant aggregate fill. §213's own three-step procedure (asm order verbatim → ascending offset → rotate-one-left) was not among the winners here, and its S58b addendum's "a non-monotonic offset sequence in the target is order-faithful" reading is exactly the trap this card fell into at closeness 32.
- Not §145(c). §145(c) is descending emission from one chained statement
a = b = c = 0;. This is descending across separate statements, grouped by literal. - Not §281. §281 groups by OPERATION KIND (copies then RMWs) in a copy-then-adjust block; here every store is a constant and there is no dependent RMW to group.
- The tail-split half of this card is a REDISCOVERY — do not file it.
t = t | (t<<8) | (t<<16); col = 0x808080 - t;beating the single combined expression is §164-80 Law 1 (L13628: an inner op evaluates into an anonymous temp that takes a scratch; make each statement's destination the variable itself) as extended by §167-46 (L16204). Cite those, not this section. - n=1, one shape. One function, one PS1 prim layout,
sh-width stores, no call inside the run. Re-measure before generalising to word stores, to non-aggregate locals, or to any fill with a non-constant term.
Grep bait: all-constant struct fill, prim vertex store order, descending array index stores, constant group store order, uv interleave, POLY_FT4 fill order.
§316 — A GUARD CHAIN THAT GATES A SUCCESS BLOCK ROUTES ITS FAILING GUARDS TO A LABEL BEFORE THE FLAG TEST, NOT TO THE EPILOGUE: THE RESIDUAL IS IMM-OFFSET-ONLY AT AN EXACTLY EQUAL INSTRUCTION COUNT (P31 S66; byte-proven func_80180284)
(BOUNDS §225-3, whose early-return ladder is the winning spelling only when the chain ends in a bare return C == D; with no success block — with a success block the same ladder is what fails here. Instantiates §225-1's "failure edges need explicit gotos landing mid-flow" for a flag test rather than a shared call. Generalises §176-F row 3 / its S62 T4 addendum from one branch to N. NOT §195-E: the named-flag-across-a-join axis is already satisfied in both the failing and the matching draft. NOT §195-N: that law's chain has no success block and its own BOUND 2 kills it when the returned constant is not 1, which is this function's case.)
THE DIAGNOSTIC TELL. match_one reports klass == "IMM-OFFSET", bucket == "structural", nins_mine == nins_tgt exactly, and every residual word is one of the early-guard branches carrying the same constant displacement delta — sig IMM-OFFSET/-9, n=3, kinds:{"imm":3}. The instruction sequence and count are already 100% right; only the beqz/bne guard-fail targets are off, by a fixed word count equal to the size of the flag-test-plus-success block your branches are jumping over. A shared delta across N branches is not a scheduling nuance and not a dropped edge — it is one wrong destination shared by N edges. Count the words between the target's guard-branch label and your own before touching polarity, pins or statement order.
THE MECHANISM (as far as it is demonstrated). Written as N independent if (guard_fail) return 0; statements, cross-jump/tail-merge collapses the N return 0 tails to one, so the count comes out right — but every guard branch lands on the function's outer epilogue, past the whole flag-test/success block. The real source instead converges all N failing guards on a label sitting immediately before the last chain term's flag test, with $v0 already zeroed from the failed comparison, so the flag-test and branch machinery is reused rather than bypassed. The last chain term must stay its own one-line statement (r = last_cmp;); folding it back into the &&/if condition re-triggers the §195-N-family value-form tax (see the caveat below). Each guard edge must also explicitly re-zero r — leaving it uninitialised on one path is a semantic bug, not a dial, and regressed this function from closeness 3 to 50.
THE C SHAPE.
/* closeness=3, IMM-OFFSET/-9, nins 50 == 50 — WRONG: every guard branch
lands past the flag test, at the outer epilogue */
if (guard1_fail) return 0;
if (guard2_fail) return 0;
if (guard3_fail) return 0;
r = last_cmp;
if (r) { success_stuff(); return x; }
return 0;
/* closeness=0, MATCH, nins 50 — guards converge on a label BEFORE the flag
test, each re-zeroing r first (this is func_80180284 verbatim) */
if (guard1_fail) { r = 0; goto chain_fail; }
if (guard2_fail) { r = 0; goto chain_fail; }
if (guard3_fail) { r = 0; goto chain_fail; }
r = last_cmp; /* last chain term: its OWN statement, never folded into && */
chain_fail:
if (r) { success_stuff(); return x; }
return 0;
BYTE EVIDENCE. func_80180284 (ov_SC04_019, 50 ins, MATCH, banked at src/ov_SC04_019/ov_SC04_019_jr_8017AE2C.c:6064-6094 — three r = 0; goto chain_fail; guards with chain_fail: immediately above if (r)). match_one --json trajectory across fifteen drafts: 53→29→44→19→53→19→19→19→53→19→18→3 (draft11, the early-return 0 ladder: {"klass":"IMM-OFFSET","closeness":3,"nins_mine":50,"nins_tgt":50,"sig":"IMM-OFFSET/-9","detail":{"delta":-9,"n":3,"kinds":{"imm":3}}}) →18 (draft12, last term folded back into &&, LENGTH-DRIFT/+1) →50 (draft13, goto shape but r uninitialised on one path) →22 (draft14, nested if/else with r = 0 in each else) →0 (draft15, {"status":"match","closeness":0,"nins":50,"residual":[]}). Note: the same vram in ov_SC03_102 is an unrelated one-line body — cite the ov_SC04_019 definition.
SCOPE, stated honestly. One function, five spellings, one overlay. The prescription is byte-proven; the gcc internals are not — no jump.c/cse.c/reorg.c line was traced for which pass fixes the displacement or why the shared label must precede the flag test, and that half is inferred from the residual shape alone. The draft12 +1 j observation is weaker than the rest: draft12 moved from the goto shape back to && and re-inlined r (closeness 3 → 18), so it is not a single-axis A/B, and it sits outside §195-N's stated preconditions (no success block; returned constant must be 1) — it is evidence that §195-N's BOUND 2 may be over-broad, not a confirmed instance of §195-N. Re-measure both on the first out-of-TU instance before treating either as settled.
§317 — A NARROW STRUCT-FIELD STORE TURNS A LIVE SImode VARIABLE INTO A li: convert.c's TRUNCATION-DISTRIBUTION MINTS A HImode COPY, AND cse REWRITES THAT COPY'S SOURCE TO THE EQUIVALENT CONSTANT (P31 S66; byte-proven func_8017EF94)
(EXTENDS §201-B (L21231) — the SAME convert.c trunc1 distribution, a DIFFERENT consequence: §201-B decides the SIGN (ori vs addiu) of a constant the source spelled as a bare literal, and its BOUND 2 explicitly scopes itself to "the constant must actually need materialisation"; this law is about a constant the source NEVER SPELLED — it is manufactured out of a variable, and it costs an instruction. BOUNDS §178-D (L17332), whose "a narrow type blocks copy elision, so the copy SURVIVES at its source position" is the BENIGN half of this same SI→HI copy: when the copy's source pseudo carries a constant equivalence the copy does not survive as a copy at all, it becomes a li. BOUNDS §167-14's asm→source read (L15435) — "li K followed by subu ⇒ it wrote K - x" — false here: the source wrote h - n, two variables. Same cse constant-equivalence family as §164-02 (L11934) / §164-52, run through the SET_SRC of a copy instead of a commutative swap.)
THE TELL. li $vN,K immediately followed by the arithmetic op that consumes it (li $v0,0x28 ; subu $v0,$v0,$s0) where the TARGET computes straight off a callee-saved register that has carried that value since much earlier in the function (subu $v0,$s5,$s0). Two extra reads: the consumer of the result is a narrow store (sh into an s16 field), and the literal is usually one you can find in your own source as a named local assigned statements above (h = 0x28;). A constant standing where a register should be, on a narrowing store, is this — not §167-14's K - x spelling and not a scheduling residual.
THE LAW. The s16 destination makes the assignment a SI→HI truncation of a MINUS, so convert_to_integer's trunc1 (tools/reference/gcc-2.7.2/convert.c:269-317, the PLUS/MINUS/BIT_AND/BIT_IOR/BIT_XOR/BIT_ANDTC label) redoes the arithmetic in typex, converting each operand (:311-313) instead of the result — narrow(a - b) becomes narrow(narrow(a) - narrow(b)). Each operand therefore reaches RTL as its own HImode copy pseudo of the SImode variable rather than as the variable itself. fold_rtx's cheapest-operand replacement loop (cse.c:5222-5250) then picks between the folded operand and its equivalent constant by COST (cse.c:481): COST(const_int) = rtx_cost*2 = 0 against COST(pseudo) = 1, so validate_change swaps the pseudo for the constant and the copy's source is the literal. A li is not a register-register copy, so nothing downstream can coalesce it back onto the register that still holds the value — you pay a whole instruction and lose the target's register reuse. Routing the subtraction through a fresh s32 temp keeps the MINUS in SImode, where subsi3's reg_or_0_operand predicate makes the same validate_change reject the fold, and the variable survives in its callee-saved register; the temp's extra reference also lifts that variable's allocno rank back over its neighbour's, restoring the target's $s4/$s5 order.
THE C SHAPE.
/* fails: closeness 73, LENGTH-DRIFT/1 — the store narrows, and h folds to `li 0x28` */
s32 n, h, w;
h = 0x28;
D_STRUCT.h = h - n; /* s16 field: SI->HI truncation distributes into the MINUS operands */
/* fix: closeness 0, MATCH — a FRESH s32 temp keeps the MINUS SImode */
s32 n, h, w, hh;
h = 0x28;
hh = h - n;
D_STRUCT.h = hh;
BYTE EVIDENCE — func_8017EF94 (ov_SC06_025, 292 ins, MATCH; banked src/ov_SC06_025/ov_SC06_025_jr_8017EEC4.c:2905, note at :2864-2877). With the h-placement residual already fixed and D_801B1B58.h = h - n; stored direct (v2.c): near closeness 73 nins 293 LENGTH-DRIFT/1. The same file with only that one statement respelled hh = h - n; D_801B1B58.h = hh; (v6.c): match 0 292 MATCH.
THE CONTROL — the temp must be FRESH, and this inverts §164-24's imperative. v7.c reused an existing scratch local t for the identical purpose: near 63 292 OPCODE-MIXED/addressing,width. It gets the mode right (the length drift is gone) and loses on allocation. So §164-24's closing "when you use the variable-K form, SPEND AN EXISTING VARIABLE" does not transfer here; this agrees with §165-18's fresh-pair result (L14181) and gives it a second, differently-caused instance — there the discriminator was store-target-vs-load-target, here it is that the reused variable already has a live range the narrow value must not join.
BOUNDS. (1) Only trunc1's opcodes — PLUS, MINUS, BIT_AND, BIT_IOR, BIT_XOR, BIT_ANDTC (convert.c:269-276); MULT takes the :253-264 path and division/shifts never reach trunc1 (§201-B bound 4). (2) The destination must actually narrow — a wide store keeps the MINUS SImode and there is nothing to fold (§201-B bound 3). (3) The operand must have a constant equivalence in cse's table at that point; a variable whose value cse cannot prove constant is not at risk. (4) Untested: QImode fields (s8), the PLUS direction, and whether an already-narrow (s16) source variable behaves the same.
Honest scope: n=1 function, one site; the lever, the failing spelling and the reused-variable control are all byte-measured against the target. The RTL narrative (HImode copy pseudo → li) is read out of the pinned convert.c/cse.c and is NOT dump-confirmed — no -dr was taken. Ship the precondition and the lever; if this recurs, dump it.
(NEW; evidence: byte-probed, 3 spellings incl. a negative control; from func_8017EF94)
Symptom lines for the index: "a li K where the target has a subu off a callee-saved register" · "the constant I assigned to a local reappears as a literal at the narrow store" · "LENGTH-DRIFT +1 on an sh of a subtraction" · "routing a narrow store through an s32 temp changed the register allocation"
§318 — A UNARY MINUS STORED BACK INTO THE SAME HALFWORD IS COMPUTED IN HImode, SO ITS LOAD IS lhu; AN INLINE (s32) CAST IS PROVABLY INERT AND ONLY A NAMED SImode LOCAL BUYS lh (P31 S66; func_8018568C, ov_SC03_028 — match_one-only, NOT byte-gated)
⚠ STATUS — UNPROVEN LEVER. The whole-binary byte-gate refused this draft; the three states below
were measured with match_one's isolated per-function compile+mask check only. The C→asm delta is
tool-measured and reproducible, and the mechanism below I re-read out of the pinned gcc-2.7.2
source myself — but nothing here is a byte-match. Re-gate before quoting it as law.
THE TELL. A single opcode-only residual, count-neutral (72/72 both sides, closeness 1, class
WIDTH, sig=WIDTH/lhu!=lh): your lhu $v0,N($rB) where the target has lh $v0,N($rB),
immediately feeding a negu whose result goes straight back to sh at the same address. No
length drift ever warns you. Target, verbatim
(asm/ov_SC03_028/nonmatchings/ov_SC03_028_jr_80184914/func_8018568C.s, 80185764–80185778):
80185764 lw $v1, 0x20($a0)
8018576C lh $v0, 0x18($v1) <- SIGNED load: the source did NOT negate in HImode
80185774 negu $v0, $v0
80185778 sh $v0, 0x18($v1)
THE MECHANISM (source-cited; this is the front end, not an optimizer pass). *(s16*)p = -*(s16*)p;
is convert_to_integer (short, NEGATE_EXPR<int>(…)), and convert.c:320-346 is a separate arm from
the trunc1 arm §201-B owns: it distributes the truncation down through the negate unconditionally
(no get_unwidened operand guard, no operand-precision test), rebuilding it as
NEGATE_EXPR<short>(convert (short, operand)). So the negate is done in HImode, the load is a plain
HImode copy, and LOAD_EXTEND_OP == ZERO_EXTEND (mips.h:1163) emits lhu (§172a's copy-vs-promotion
tell, seen from the authoring side). Why the inline cast cannot help: -(s32)*(s16*)p builds the
same tree the implicit promotion already built, and the recursive convert (typex, operand) above
lands on case NOP_EXPR at convert.c:352-353, which is literally
return convert (type, get_unwidened (…)) — the cast is stripped on the way back down. A cast written
inside an expression the assignment narrows is not a lever; it is a no-op. The only escape is to take
the load out of the narrowed expression, into a genuine SImode local, where it is a real promotion
(extendhisi2_internal) and emits lh.
THE C SHAPE.
/* FAILS — both spellings identical, closeness 1, sig=WIDTH/lhu!=lh */
*(s16 *)p = -*(s16 *)p;
*(s16 *)p = -(s32)*(s16 *)p; /* the cast changes NOTHING — convert.c:352 strips it */
/* WORKS — closeness 0 in match_one */
s32 val;
val = *(s16 *)p; /* a real SImode local: this is the promotion -> lh */
val = -val;
*(s16 *)p = val;
THE EVIDENCE. match_one --json, three states, one edit apart: attempt A
{"status":"near","closeness":1,"nins":72,"residual":[[56,"94620018 lhu v0,24(v1)","84620018 lh $v0, 0x18($v1)"]],"verdict":{"klass":"WIDTH","sig":"WIDTH/lhu!=lh"}};
attempt B (inline (s32)) — byte-for-byte the same JSON, the cleanest possible negative control;
attempt C (named s32 local) — {"status":"match","closeness":0,"nins":72,"residual":[]}.
WHERE IT SITS vs THE NEIGHBOURS (all four checked; none states this).
- §201-B owns the same
convert_to_integernarrowing family but is bounded totrunc1's binary opcodes (its BOUND 4: "ONLYtrunc1's OPCODES: PLUS, MINUS, BIT_AND, BIT_IOR, BIT_XOR, BIT_ANDTC") and to the constant'sori/addiuopcode. Do not carry its truth table over: the NEGATE arm'stypexsignedness isTREE_UNSIGNED (TREE_TYPE (expr))alone (:340-341) — there is no OR over the unwidened operands, so the operand's*(s16*)⇄*(u16*)cast has no vote here. This entry also answers §201-B's own BOUND 6 on thelh/lhuaxis: in the unary arm the load width is visible. - §242 #1 is the same residual class from the other side (
*4keepslh,<<2giveslhu, sink is a narrowsh) and is the nearest prior art — but its dial is a spelling of the scale. Here there is no spelling escape at all; the statement must be split. - §261a states the observation (
s16fields: "lhat a plain read, butlhuinside a read-modify-write") but scoped to -O0, with no mechanism and no cure. That it holds at both -O0 and -O2 is expected —convert.cis the C front end — and it corroborates §201-B's untested -O0 bound. - §172a is NOT contradicted. Its corollary (
s32 x = shortmem≡s16 t = shortmem; s32 x = t;) is scoped to a value that really is promoted, i.e. an SImode destination. In a store-narrowed expression nothing is promoted, so the corollary never applies and the cast never had a chance.
BOUNDS / UNTESTED. n=1, and not byte-gated. Untested: ~x (BIT_NOT_EXPR shares the identical arm —
predicted the same, unmeasured); QImode (*(s8*)p = -*(s8*)p); whether a volatile or a second SImode
consumer of the same load changes the answer; whether the cure survives when the named local is s16
(predicted no — HImode local, still a copy). ABS_EXPR is explicitly excluded by the comment at
convert.c:322-323 ("we must test the sign before truncation") and is a different section (§267-ADD-5).
Addendum — §148-A2 addendum — the empty-asm insn_count dial (N bare __asm__("") = exactly +N real i (func_801835B0)
Addendum (P31 S66, func_801835B0): a third insn_count dial, and the cheapest one — N bare __asm__(""); statements at the top of the loop body buy exactly +N to insn_count and restructure nothing. A colon-less empty asm is ASM_INPUT (stmt.c:1340, per §194-A's predicate table at L19008), so it is a real insn to loop.c's count, no pass before loop can fold it (satisfying this section's BOUND 6), and it emits zero bytes. §148-A2's two dials each buy a fixed +2 and each demand a semantic rewrite (duplicate the back-edge; route a constant through a multiply-set variable); this one is arbitrary-granularity and semantics-free, so compute N from the dump and spend exactly that: N = threshold·savings·lifetime + 1 − insn_count.
The savings-1 / lifetime-1 band, which is the common one and worth memorising: a plain literal sentinel (-1) in a call-containing loop has savings 1 and lifetime 1, so the whole test collapses to 29 ≥ insn_count — it hoists iff the loop is ≤29 real insns, all-or-nothing, no staircase (only one movable). Here insn_count was 23, so N = 29+1−23 = 7; six barriers would have landed on the >= edge and still hoisted, exactly as this section's 104-both / 105-split boundary did twice.
THE +3 TELL (the mirror of §164-35's −3). Draft carries one extra callee-saved register, nins = nins_tgt + 3 (the extra sw/lw pair plus the preheader materialisation), frame -40 vs target -0x20, one extra .mask bit, and a li $sN,-1 sitting before the loop label while the target computes addiu $vN,$zero,-1 inside the body just ahead of the terminating branch. match_one files this as klass: LENGTH-DRIFT, sig: LENGTH-DRIFT/3?. §164-35's identical-looking −3 cascade is the opposite fault (an invariant that should hoist and did not) — read the sign before routing.
BYTE EVIDENCE. func_801835B0: two intermediate drafts both {"status":"near","closeness":28,"nins":37,"verdict":{"klass":"LENGTH-DRIFT","sig":"LENGTH-DRIFT/3?","detail":{"delta":3}}}; with the 7 barriers at the loop top, {"status":"match","closeness":0,"nins":34,"residual":[]}. Confirmed independently by standalone cc1 -dL runs (v5.c/v6.c): with the barriers the emitted .s loses the pre-loop li $19,-1 and the sw $19,28($sp), and the frame drops 40 → 32, matching the target's .frame $sp,32,$31 / .mask 0x80070000,-4.
BOUND. N=7 is arithmetic, not a constant — never transplant it; re-read Loop from A to B: N real insns for the new loop. The minimum was derived from the formula, not bisected by ablation (§266): 7 is proven sufficient, and 6 is predicted-insufficient by the >= edge but was not compiled. Seven barriers is also seven §194-A scheduling fences stacked at the block head; here they sat above an otherwise empty region so nothing competed, but on a loop whose top statements matter that side effect is real and must be attributed separately.
Addendum — §312 addendum — naming the compare's dest is the vehicle, the hard-reg pin on that name is (func_80184A68)
Addendum (P31 S66, func_80184A68): §312's bound (2) — "an unpinned local would only move the dest to whatever that local got" — is now measured, not reconstructed, and it splits §312's one-step recipe into two when there is no already-coloured variable to assign into. Same tell exactly: both of the compare's INPUTS already pinned correctly (register s32 iVar __asm__("$2") on the call result), zero length drift, residual exactly 2 — [[17,"0062102a slt v0,v1,v0","0062182a slt $v1, $v1, $v0"],[18,"14400003 bnez v0","14600003 bnez $v1"]], REGALLOC-PERM/$v0>$v1, map {"$v0":"$v1"} (closeness 23 → 2 on the input pin alone, nins 33 = target). draft7 applied §312's statement verbatim with a FRESH, UNPINNED name (cond = threshold < iVar; if (!cond)) → closeness unchanged at 2, byte-identical residual — naming alone buys nothing here. draft8, the only change being register s32 cond __asm__("$3") on that same name → match, closeness 0, nins 33, residual []. So the assignment is the vehicle (it gives the slt's SET_DEST an addressable C name where a bare relational has none); the pin on that name is the payload — §312 got the payload for free because its v0 was already pinned, and its "already-allocated" title clause is load-bearing, not incidental. Also ablated inert: draft6 pinned the OTHER operand (threshold → $3, not the compare's LHS) and left closeness 2 — a second confirmation that no INPUT pin reaches the dest, which is §165-47's composition law ("pinning only the operands leaves the dest on $v1"; a register relationship needs every member named) showing up on the relational operator instead of a three-address subu. BOUNDS. (1) n = 1; mechanism still un-dumped, so §312's do_jump/store_expr inference gains no evidence — what is proven is name+pin → MATCH. (2) This narrows §306a's T4 bound (4) ("on a 2-instruction REGALLOC-PERM reach for the naming/scope lever first and the pin never"): that holds when the wrong register is an INPUT; when it is the compare's own DEST and no coloured variable is at hand, name and pin. Pay §37/§162p rung-3's family cost knowingly. (3) Polarity is free — if (!cond)/beqz behaves exactly as §312's if (v0 != 0)/bnez. (4) ⚠ The func_80184A68 at L23136/L23959 (ov_SC06_000, 16 ins, the update_equiv_regs anti-remat card) is a different body at the same address (§313/R48's per-address-name collision); this one is 33 ins and the transcript did not name its overlay.
Addendum — subu/sh dest reuses a pinned operand's dying register (func_8017F498)
Addendum (P31 S66, func_8017F498): This is the banked exemplar §167-12's own scope note asks for — its eight A/B pairs were mismatch counts on a still-5-off func_8017F3C8, and it closes with "Re-confirm on a banked exemplar before treating the position rule as exact." Here the identical construct — a single-operand __asm__ __volatile__("" :: "r"(mid)); on a register s16 mid __asm__("$5") pin — drives a gated MATCH (match_one --json: status match, closeness 0, nins 99, residual []), so the volatile-anchor form now has a byte-proven instance and not only a directional one.
Three refinements the parent sections do not carry. (1) The tell generalises off unary ops. §167-12's shape is a destructive one-operand sll $s1,$s1,16 and §164-62's an andi $a0,$a0,0x1; here it is a three-operand non-commutative subu plus its consumer — draft subu $a1,$a1,$a0 / sh $a1,0x2E($s0) against target subu $v0,$a1,$a0 / sh $v0,0x2E($s0), i.e. the dest ties the pinned source while the other operand is untouched, and the residual is a 2-line REGALLOC-PERM/closeness-2 tail, not a whole triad (contrast §310 (L30086), whose subu tie is between two unpinned operands and whose dial is share-exactly-one — its BOUND 5 explicitly leaves the pinned case untested; this is that case, and the lever is the keepalive, not the share).
(2) A new legal position: after an if/else join, before the next if. §167-12 prescribes materialising the compare into a named boolean so the asm has a statement boundary to sit on; here the boundary is the join after the arm that consumes mid (if (x < mid) result = mid - x; else result = 0; → keepalive → the next if), which is enough to hold the death and hand result a fresh $v0.
(3) Two fresh negatives, one A/B apart, at the same closeness-2 plateau: pinning the result instead (register s32 d __asm__("$2") block-scoped, h5) and a plain unpinned s32 d temp (h6) both regressed 2 → 72. So the lever is the keep-alive use, not "give the result its own variable" and not a second pin — §176-B's "right class, wrong pin" reproduced on a 99-ins leaf.
⚠ Bound (unchanged from the parents, plus one). No -dl/-da dump was taken on this function: the qty_phys_sugg/find_free_reg death-guard story is inherited from §80/§164-62/§167-12, not re-derived here, and the agent's own reload narrative across g1→g5→h1 is a reading of the emitted registers. The free confirmations nobody ran are §164-62's two greps (';; Register N in H.' in .lreg, REG_DEAD on the consuming insn) and grep -n '#APP' t.s to prove the block sits below the subu. Also untested here: §164-62's competing two-operand anchor (asm("" :: "r"(mid), "r"(result))), which on this shape has a value defined at the consuming insn and should work — the volatile form was taken first and never ablated against it.
Addendum — Addendum to §261a — the -O0 increment-operator copy also fires on the POINTER LOCAL itself (func_8001212C)
Addendum (P31 S66, func_8001212C): §261a's second bullet gives this law for a struct FIELD through a pointer (++p->f ⇒ 3 reloads, the first dead, plus an addu rd,rs,$zero copy after the add; p->f += k ⇒ 2 reloads, no copy). It also fires on the auto POINTER LOCAL itself — at -O0 a non-register local IS the memory lvalue, so q++ on a $fp-slot pointer emits the four-instruction window lw $a0,0($fp) / addiu $v1,$a0,0x18 / addu $a0,$v1,$zero / sw $a0,0($fp). Tell: a draft 2 instructions short at a pointer advance with no copy before the store-back — the same dead-reload + copy pair §261a counts. An A/B sweep of one ADVANCE line in boot.c reports q++ and ++q at match_one closeness 0 / 177 ins, while all four non-increment spellings (q += 1, q = q + 1, q = (T*)((u8*)q+0x18), q = (T*)((s32)q+0x18)) fold the copy away at 87 / 175 — consistent with §164-XX's proven axis (INCREMENT-OPERATOR vs everything else, not post-vs-pre) and with its note that at -O2 the copy coalesces away by final, which is why this is count-visible only at -O0. Do not read this as flipping ADDENDUM (a): (a) is a register (pseudo-resident) local where x = x + 1 copies and += folds; residency picks the dial — memory local ⇒ only the increment operator copies. ⚠ UNPROVEN — not byte-proven. The whole-binary byte-gate refused this draft (or never ran on it); closeness 0 is match_one's isolated masked-diff verdict only. One function, one offset (0x18), mechanism inferred from §164-XX rather than re-read from RTL here. Try the lever, re-gate, and promote this to byte-proven on the next confirming target.
Addendum — Addendum to §164-XX/§165-06 — at -O0 a bare local IS a memory lvalue, so the increment ope (func_8001212C)
Addendum (P31 S66, func_8001212C): §164-XX's law reaches a bare local at -O0, and there the extra copy is emitted, not coalesced. At -O0 every local lives in a frame slot, so a pointer local is a memory lvalue: q++ / ++q route through expand_increment (expr.c:8482 → copy_to_reg :8648 → expand_binop :8657 → emit_move_insn :8660) and, with no regalloc/cse pass to coalesce it, that copy survives into the .s as a real instruction; q += 1, q = q + 1 and the cast-add forms go through build_modify_expr's plain MODIFY_EXPR and store the add's result straight back. Tell: a 4-instruction advance triad against a $fp slot — lw $a0,0x0($fp) / addiu $v1,$a0,0x18 / addu $a0,$v1,$zero / sw $a0,0x0($fp) — twice in func_8001212C (boot, -O0 prologue 21F0A003, 177 ins) at 0x80012290 and 0x800123C0; a draft emitting only load/add/store is 1 insn short per advance site. Evidence: a six-spelling A/B on the single advance line against match_one — q++ → 0/177 and ++q → 0/177; q += 1, q = (T *)((u8 *)q + 0x18), q = (T *)((s32)q + 0x18) → near 87/175 (exactly the two copies short); q = &q[1] → cc1 fail. So §164-XX's "the copy coalesces away by final, so the emitted COUNT is identical" is an -O2-only statement: at -O0 the same dial is a countable length tell, which makes the increment-operator spelling the first thing to check on an -O0 LENGTH-DRIFT that is an exact multiple of the advance-site count. It also bounds ADDENDUM to §261a (a): that card's register-local polarity (x = x + 1 copies, += folds) is a different axis, not a mirror — for a frame-slot local both += and q = q + 1 fold and only the ++/-- operator copies. Read the lvalue's class (REG vs MEM) before picking between the two dials. ⚠ UNPROVEN — not byte-gated. The whole-binary byte-gate refused this draft (or never ran on it); the 0/177 is match_one's isolated masked-diff verdict only. The target's 4-insn triad is ground truth from asm/nonmatchings/boot/func_8001212C.s, but the C→asm attribution is one function at one offset (0x18) and awaits a gated confirmation or a second target before it is treated as law.
Addendum — Addendum to §312 — the compare's named destination must itself carry a hard register: nami (func_80184A68)
Addendum (P31 S66, func_80184A68): §312's tell and lever reproduce exactly on a second function — same 2-instruction zero-drift slt+branch REGALLOC-PERM with both operands already correct ([[17,"0062102a slt v0,v1,v0","0062182a slt $v1, $v1, $v0"],[18,"14400003 bnez v0","14600003 bnez $v1"]], sig REGALLOC-PERM/$v0>$v1, map {"$v0":"$v1"}, nins 33 = target) — and it splits §312's lever into two clauses, only the second of which is load-bearing. §312's BOUND (2) guessed that "an unpinned local would only move the dest to whatever that local got"; that is now measured, not guessed. Ablation on the pinned triple, one axis per draft: draft5 (register s32 iVar __asm__("$2") on the call-result input alone) → 23 → 2; draft6, additionally pinning the other operand threshold to $3 → inert, still 2, byte-identical residual; draft7, §312's assignment shape into a fresh unpinned name (cond = threshold < iVar; if (!cond)) → also inert, still 2 — naming alone does nothing; draft8, the only change being register s32 cond __asm__("$3") on that same name → {"status":"match","closeness":0,"nins":33,"residual":[]}. So restate the recipe as: (i) give the comparison a named destination so expand_expr has a target at all, and (ii) make that name carry a hard register. §312 got (ii) for free because it assigned back into an operand that was already pinned; when neither operand's variable is pinned to the register you need, declare a fresh register s32 cond __asm__("$N") and pick $N by reading the target's own slt dest (here $v1, the LHS's register — still §312's in-place-compare tell). Routing (a bounded counter-example to §176-B and to the S62-T4 addendum's bound (4), which say reach for naming/scope on a 2-insn REGALLOC-PERM and for the pin never): on the compare's DEST the pin is required — but only on the assigned boolean; pinning an input was inert here, and §268 is why it is honored at all (the boolean's live range crosses no jal). BOUND. n = 2 for the law, n = 1 for this clause. §312's own self-assign form (threshold = (threshold < iVar)) was never tried on this body, so "fresh pinned bool" vs "reuse an already-pinned operand" is unablated — treat them as one lever with two spellings. No -dS/-dl/-dg dump was taken; the drafter cites no gcc-2.7.2 source line, so §312's do_jump-has-no-target story remains the inferred mechanism (§164z / §137's bar) and this entry adds only that the synthesised dest is also free of any preference from its pinned operands.
Addendum — Addendum to §167-12 — the single-operand volatile keepalive, re-confirmed at a gated MATCH (func_8017F498)
Addendum (P31 S66, func_8017F498): §167-12's closing ask — "re-confirm on a banked exemplar before treating the position rule as exact" — is answered. The same single-operand __asm__ __volatile__("" :: "r"(mid)); drove a gated MATCH here (match_one --json → {"status":"match","closeness":0,"nins":99,"residual":[]}), not §167-12's 5-off plateau, and it widens the entry on three axes. (1) Insn class: the consuming insn is a subu and the sh that stores its result — draft 55 subu $a1,$a1,$a0 / 57 sh $a1,0x2E($s0) vs target subu $v0,$a1,$a0 / sh $v0,0x2E($s0) — so read the tell as any 3-operand ALU op whose destination equals one of its own live sources, with its dependent store dragged along, not just §167-12's sll. (2) Pin class: the dying value is a caller-saved pin, register s16 mid __asm__("$5") whose range crosses no call (so §268's honour condition holds), not §167-12's callee-saved $s1. (3) Position: the keepalive that worked sits after the whole if/else whose arm contains the consuming insn, ahead of the next if (if (x < mid) result = mid - x; else result = 0; … __asm__ __volatile__("" :: "r"(mid)); … if (sp10.v[2] >= 0x1241)). §167-12 measured "inside either arm" as a regression on its shape, so state the position rule as "below the consuming insn, at the nearest statement boundary the value still reaches" — the if/else join qualifies. Routing, byte-measured at the same closeness-2 plateau: pinning the destination instead (register s32 d __asm__("$2") around the result) and a plain unpinned s32 d temp both regressed to 72 — so on this residual do not reach for §312/§197-C's "name/assign the destination" lever, and do not read it as §310's block-local-operand dial (which picks which operand the dest ties to, not whether it ties at all). Bound (§266 / R14): this run ablated only the lever choice; __volatile__-vs-plain and the exact placement were not solo-removed here — that axis still rests entirely on §167-12's eight controlled pairs — and no -dl/.lreg dump was taken, so qty_phys_sugg stays the cited hypothesis for this function, not a traced fact.
Addendum — A §5a BARRIER SITS IN REORG'S FILL WINDOW TOO, SO ITS SIDE OF THE INNER goto IS CHOSEN P (func_80181D08)
Addendum (P31 S66, func_80181D08): §283's ADDENDUM-to-§5a (L28359, func_80181F74, same overlay ov_SC03_112) prescribes the guarded-twin barrier "in the ELSE-continuation, … not inside the guard's taken-arm (it changes which arm is fall-through and flips branch polarity)". That prescription is function-scoped, not general. func_80181D08 (ov_SC03_112, 96 ins, banked src/ov_SC03_112/ov_SC03_112_jr_801817E0.c:3043-3062) carries both placements in one body, one per sign-arm, and neither is optional. The discriminator is §164-09's ⚠ bound (L12085) turned into a rule: the barrier does double duty — it denies find_cross_jump (its job) and stands in reorg's fill window on whatever path you typed it on — so read the target's two delay slots at the join and put the barrier on the side that does not deny the fill you need. When the join block's head insn is stolen into the conditional branch's own slot (here lui $v0,(0xB60B60B7>>16), the divide magic at the vy join), the barrier must go BEFORE the inner goto, inside the taken arm; left after the goto on the fall-through it halts the eager scan of that arm and you get beqz TAIL / nop / j CONT / lui against the target's bnez CONT / lui / j TAIL / nop. When the target leaves both slots nop (the sibling vx arm here; §164-09's and §283's own exemplars) the after-the-goto placement is free and correct. THE TELL — klass=DELAY-SLOT, sig=DELAY-SLOT/2, a 4-row residual at a §5a barrier site, not the usual single stolen-load row: the conditional branch's polarity is inverted with its two labels swapped, and one instruction has moved between that branch's slot and the following j's slot. Do not reach for §3-T4's polarity invert, a register pin, or a second fence (§163f-2) — move the existing barrier across the goto, and expect to keep the sibling arm's placement unchanged. Byte evidence (single-axis A/Bs on the pinned triple; nins already pinned at 96 by an unrelated s16-clamp-temp fix): d3, both barriers after their goto → near, closeness 4, nins 96, residual [54] beqz $v0,158 ↔ bnez $v0,.L80181E00, [55] nop ↔ lui $v0,0xb60b, [56] j f8 ↔ j .L80181E60, [57] lui $v0,0xb60b ↔ nop; d5, identical except the vy barrier moved before its goto → match, closeness 0, 96, residual []; control d4 (placement moved, clamp fix withheld) held at closeness 40 / LENGTH-DRIFT, so the two fixes are independent and both required. Honest scope: the "barrier halts the eager scan" half is §5a/§199-F's already-established stop_search_p mechanism (reorg.c:674-704, fill_slots_from_thread); this card did not dump -dd/re-derive which thread reorg would otherwise have picked, so treat the causal story as observed, and the prescription (read the slots, place per arm) as the byte-proven part.
Addendum — Loop sentinel wrongly hoisted out of a call-containing loop (func_801835B0)
Addendum (P31 S66, func_801835B0): A third insn_count dial — N × __asm__(""); at the loop-body top — and it is the only one that needs no later pass to delete it. Both dials in "HOW TO STEER IT" above (the doubled back-edge, the multiply-set returned constant) are +2 each and are free only because jump2/cross-jumping removes them afterwards, which is what BOUND 6 states as a requirement ("any construct you add to raise the count must be deleted by a pass that runs AFTER loop"). That bound is too strong. A colon-less __asm__("") expands to an ASM_INPUT insn (stmt.c:1340, §194-A's predicate table), loop.c counts it in Loop from A to B: N real insns like any other real insn, and it is never deleted — it simply emits zero bytes at final. So the dial is +1 insn_count per statement, arbitrarily many, zero bytes, and the correct form of BOUND 6 is: the construct must be invisible to cse1/jump1 (which run before loop) and must emit nothing — being deleted after loop is one way to satisfy that, not the only way.
The instance, and it runs the arithmetic in the refusing direction. func_801835B0's loop calls a function ⇒ threshold = 29; the movable was a plain -1 sentinel literal (savings 1, lifetime 1), and the draft's real body was only 23 RTL insns, so 29·1·1 = 29 ≥ 23 hoisted it into an extra callee-saved register with a preheader li $s3,-1. Seven __asm__("") at the top of the loop body took the count 23 → 30, 29 ≥ 30 is false, the hoist is refused, and the sentinel re-computes in-loop exactly as the target has it. Confirmed by reading move_movables/scan_loop and by cc1 -dL on standalone v5.c/v6.c.
THE DIAGNOSTIC TELL — the exact mirror of §164-35's. §164-35 routes "one callee-saved register short, frame 4 smaller, −3 ins" to a missing hoist. This is the same cascade with every sign flipped: nins_mine == nins_tgt + 3 (the extra sw $sN / lw $sN / preheader li), one callee-saved register too many, frame larger (addiu $sp,$sp,-40 vs the target's -0x20), an extra .mask bit, and a li $sN,-1 sitting before the loop label where the target computes addiu $vN,$zero,-1 inside the body ahead of the terminating branch. match_one calls this klass: LENGTH-DRIFT, sig: LENGTH-DRIFT/3? — do not read that sig as a scheduling or regalloc residual; it is an insn_count reading, and it is arithmetic.
BYTE EVIDENCE. func_801835B0: before, {"status":"near","closeness":28,"nins":37,"verdict":{"klass":"LENGTH-DRIFT","sig":"LENGTH-DRIFT/3?","detail":{"delta":3}}}, identical across two intermediate drafts; after inserting the seven barriers at the loop top, {"status":"match","closeness":0,"nins":34,"residual":[]}. Standalone cc1 -dL A/B: with the barriers the .s loses the pre-loop li $19,-1 and its sw $19,28($sp), and the frame shrinks 40 → 32, reaching the target's .frame $sp,32,$31 / .mask 0x80070000,-4.
BOUNDS (two, both already law elsewhere — check them before spending this dial). (1) §164-36 still governs the slot: an asm at the head of a block whose first insn the target steals into a delay slot costs +1 ins. The loop-body top was safe here; a loop head that is also a branch target may not be. (2) §47/§158/§194-A-6 collateral: N extra static insns add +N reg_live_length to every pseudo spanning them, so any exact-tie allocno pair straddling the barriers can flip — re-verify the whole function, not just the loop. Note also that this dial is a counting instrument only when the barrier is colon-less/volatile; a non-volatile __asm__("" ::: "memory") is a different animal per §194-A's table.
Addendum — Reversed-operand sltu then xori 1 means unsigned less-or-equal (func_8017D948)
Addendum (P31 S66, func_8017D948): the GT, LE, GTU, LEU row's reg-path cell reads "reverse_regs puts the constant on slt's LEFT … li CST ; slt K,x verbatim + uninverted branch at every -O". That "uninverted" clause is the GT/GTU half only — LE/LEU carry invert_reg = 1, and in VALUE context (a return, not a branch) that inversion is emitted as an explicit xori rD,rD,0x1. BOUND 1's value-context probe only ever showed the const-path xori (slt $2,$2,31 ; xori 1); this is the reg-path cell it never measured. Byte evidence — func_8017D948 (ov_SC02_026, 36 ins, MATCH on the first compile, banked at src/ov_SC02_026/ov_SC02_026_jr_8017C180.c:3491): tail ori $v0,$zero,0x8FFE ; sltu $v0,$v0,$v1 ; xori $v0,$v0,0x1 ⇐ return (data[0] + data[1] + data[2]) - 0x10001U <= 0x8FFE;. CST+1 = 0x8FFF > 32766 ⇒ out of window ⇒ force_reg + reverse_regs, so here the constant-FIRST operand order proves a bare literal, not a named local — the reading rule's second bullet is guarded by CST ≤ 32766 and must not be read as a naming signal above the window. READING RULE (value context). A register load of K (ori rC,$zero,K, or lui/ori) immediately followed by sltu rD,rC,rX — constant first, variable second — and then xori rD,rD,0x1 before the return ⇒ x <= K unsigned, K bare. The same pair without the xori is K < x (GTU). The C dial for the unsignedness is one U suffix inside the additive expression: a single unsigned operand drags the whole usual-arithmetic-conversion result unsigned even when every base term is plain int — the third member of the "what makes a compare unsigned" family beside §280 (pointer vs integer cursor) and §199-D (narrow unsigned object). Not a one-off: the identical construct is already banked in six sibling overlays in its GTU/branch spelling if (0x13FFE < s1 - 0x10001U) (ov_SC06_018/020/022/024/032/033).
Addendum — Chained 22 array index folds to one copy;sll, not two (func_80011380)
Addendum (P31 S66, func_80011380): the -O0 ARRAY-INDEX face of (b) — COUNT the copy;sll pairs, each one is one multiply STEP in the source. ⚠ LEVER UNPROVEN (the byte-gate REFUSED this draft; best result 6/192 mismatched, never a MATCH).
(b) says a power-of-2 scale spelled as a MULTIPLY routes through the mul-style expansion (a move) while the SHIFT spelling reads its operand straight. In an index that is the addu $X,$Y,$zero ; sll $Y,$X,k pair, and func_80011380 (boot, -O0) carries both counts on the same index global, three instructions apart in the C:
80011420 lw D_80074784 ; addu $v1,$v0,$zero ; sll $v0,$v1,2 <- ONE pair -> lhu off D_800629D4
80011488 lw D_80074784 ; addu $a0,$v1,$zero ; sll $v1,$a0,1
addu $a0,$v1,$zero ; sll $v1,$a0,1 <- TWO pairs -> lbu off D_800629D6
So two copy;sll 1 pairs before an array's lui/addiu/addu base add mean the source did the ×4 as two separate multiply STATEMENTS, not one *4 and not a chained i*2*2 written inline. ~15 inline respellings of the chain (parens, ({…}) statement-exprs, u32/s32/long casts, <<1, +0/|0 no-ops, __asm__-pinned regs) all plateaued at closeness 9 or worse — that is §164-16 (L12228) working exactly as written: MULT_EXPR (fold-const.c:3897) reaches the associate: label, split_tree strips same-mode casts and takes the constant from either side, and only a DECL leaf breaks it. The reported lever — D_800629D6[({ register s32 t = D_80074784; t *= 2; t; }) * 2] — moved closeness 9 → 6 (residual idx 71-76), reproduced by five sibling spellings; register matters here per §261a addendum (c) (a plain -O0 local would live in the frame and add lw/sw traffic the target does not have).
⚠ Do not re-buy the submitted mechanism. The card attributes this to expand_mult special-casing a power-of-2 under EXPAND_SUM — unsupported; fold runs before expand, §164-16 already owns the chain-collapse and §164-13/§164-03 own the real EXPAND_SUM laws (which are about (v+C)*K and operand order, not this). The card's own "t <<= 1 did NOT work, only t *= 2/t = t*2" is (b) confirming itself — the shift spelling drops the copy — not a separate mystery. Untested and left open: whether a plain, non-register local reaches the same shape (§164-16 predicts it does at the fold level), and whether anything closes the final 6-mismatch $a0/$v1 coloring residual.
Addendum — REGALLOC-PERM: one memory-clobber barrier insufficient — stack two (func_80182D1C)
Addendum (P31 S66, func_80182D1C) — ⚠ UNPROVEN: gate-refused draft, match_one closeness only, no -dl/-dg on the winning variant. §137 step 4 places one zero-byte barrier at the source point that lands L in the priority window, and its five-placement probe teaches placement as the only dial. On this card the placement sweep read as inert: draft4 (no barrier), draft11 (__asm__ __volatile__("" :: "r"(r1))) and draft12 (one __asm__ __volatile__("" ::: "memory")) all returned the byte-identical residual — near, closeness 9, REGALLOC-PERM/$s0>$s1>$s0, same residual list. A second identical barrier stacked back-to-back (draft13) took it to match, 0/66, residual []. So the barrier COUNT is itself a dial, not just its position: per §47 and §194-A bound 6 each zero-byte asm is +1 static insn at global-alloc time, +1 reg_live_length to every pseudo spanning it, and a single +1 can leave the contenders on the same int(numerator/L) plateau §47's step 3 describes. Sweep N = 1, 2, 3 at a fixed point before concluding the class is barrier-inert — the same fixed-point compose §158 step 4 already does with two different zero-emission asms, here with two identical ones for one relation.
The sub-tell that says the barrier reached global-alloc but did not cross the boundary: draft12's -dg header reordered its allocnos (5 regs to allocate: 78 75 77 74 73 → 75 78 77 74 73) while the emitted bytes did not move. Ranking motion with zero byte motion = plateau, add another slot; no ranking motion = wrong placement, per §137 step 4.
What is NOT established, and what to run first if this recurs. (1) No -dl/-dg was taken on draft13, so which allocno's R/L crossed is unconfirmed — the §137/§158 dump bar is unmet and the plateau story is the leading hypothesis, not a re-derivation. (2) The "memory" clobber is almost certainly not the ingredient: §194-A's byte-discriminated predicate table (sched.c:1953) shows the bare __asm__ __volatile__("") is an equally good barrier and does not drag in §178-G2's address-chain sinking or §21's prologue pin — retest the stack in the bare form, and per §47's placement rule sit it next to an existing volatile asm so no new cse/sched perturbation rides along. (3) draft11's null is predicted, not anomalous: §158's placement rule puts the input-tied extender after the variable's natural last use, and here it sat above the compare that kills r1, extending nothing. (4) Per §266 the pair is only jointly citable — removing either barrier reproduces draft12 — so cite "two", never "a barrier".
Addendum — Last if-arm's nested if un-fuses the whole preceding chain (func_8017FE88)
Addendum (P31 S66, func_8017FE88; ⚠ UNPROVEN — match_one isolated masked diff only, the whole-binary byte-gate refused this draft): §312's own scope sentence — "a bare relational has no join and no constant arms, so §195-E's 'naming materialises a 0/1' does not fire and the naming is byte-free apart from the register it buys" — has a measured counter-case, and the price is not a register but the whole block layout. Inside a chain of constant-assigning arms (if (v1<0x140) idx=0; else if (…) idx=1; … else idx=4;) the target form is one fused pair per arm: slti + bnez <shared merge> with the arm's constant sitting in that branch's own delay slot — no separate move, no trailing j — i.e. §136d-2/§164-57's jump.c:728-760 collapse firing at every arm because every arm is a lone simple REG SET (the N-arm face of the two-arm family at §164-73/§164-74/§13114). Naming the LAST arm's condition and nesting it — register s32 c __asm__("$2") = (v1 < 0x17C); if (c) { idx = 3; } else { idx = 4; } — gave that one arm a non-simple-SET body and the chain de-fused end to end: every comparison reverted to slti + beqz <next block> with each arm ending in its own j <merge>. near, closeness 31, LENGTH-DRIFT/2?, nins 45 vs 43, detail {"delta":2,"at":12,"explains":"partial"} — and residual[0] is at index 12, the FIRST comparison (v1<0x140), which the edit never touched: mine 10400003 beqz v0,40 vs target 1440000c bnez $v0,.L8017FEEC. Flattening the last arm back to } else if (v1 < 0x17C) { idx = 3; } else { idx = 4; } → match, closeness 0, 43/43, residual []. ROUTING / TELL: a LENGTH-DRIFT +2 on a constant-select chain whose residual starts at the chain's first comparison means an arm downstream stopped being a flat assignment — keep every arm, including the terminal else, a single flat assignment, and treat §312's naming lever and any register __asm__ pin on an arm's condition as contraindicated here (§72/§176-B's "the pin is the wrong tool", and §80's unconditional qty_phys_sugg cost, both apply). ⚠ BOUNDS — confounded, n=1, unproven. (1) The pin, the naming and the nesting all moved in ONE edit; no single-axis ablation was run, so §266's solo-proof bar is not met and the cause is not separated between "a pinned/named condition" and "a nested block in the arm". (2) The mechanism is asserted from the before/after only: the cascade is consistent with each arm's collapse being the precondition for the next one outward (an inner arm that keeps its j denies the enclosing arm the simple-SET shape jump.c requires), but no -dj/-dS dump was taken. (3) closeness 0 is per-function masked diff, not a banked byte-match, so the RIGHT form is validated only at isolated-compile level.
Addendum — A plain global-RMW triplet is a fillable scheduling window too — zero drift, and the fille (func_800CB058)
Addendum (P31 S66, func_800CB058): The fillable window needs no long-latency instruction. Here it is an ordinary global counter RMW — cnt = D_800CB984++; expanding to lui/lw %lo → addiu $v1,$v0,1 → lui/sw %lo — whose interior sched1 fills with an unrelated lw $a2,0x20($a0) (r3000 load ready-delay 2, mips.md:153-163, quoted at L20967). So a statement's multi-insn expansion is not atomic to the scheduler even with no mult/div in the block, and §306's mechanism half (1) should be read as a property of any dependence gap, not of the imuldiv shadow. Direction agrees with §306, not §16Xb: the independent statement must be written BEFORE the RMW to sink into it — fptr = *(s32 *)(p + 0x20); then cnt = D_800CB984++; → MATCH (closeness 0, 78 ins); the reverse order, everything else identical → closeness 16. New tell for this family — the drift is ZERO. nins is equal both ways, so match_one classes it ADDRESSING/li!=addu over a ~15-19-instruction span rather than §306's LENGTH-DRIFT +1 (no maspsx nop: that splicer is --expand-div-only), and ADDRESSING-class churn misroutes a drafter to §164-02's addressing laws instead of to statement order — route it here when the churn straddles a global's read and its store-back. Two levers, both load-bearing (§266): the §278/§74 co-pin of fptr onto maska's $6 was necessary but by itself left closeness 16; a zero-byte __asm__ __volatile__("") fence at the same spot (§194-A family) regressed it to 45 at +1 ins — one more row for §257/§268's barrier/pin dead-end ledger. Provenance: distilled from a wave whose byte-gate refused the draft (match_one-masked evidence only), but the identical body has since banked byte-gate-green at src/md_MAIN_033/md_MAIN_033.c:93-131 (commit commit:3304, x1 wave, authoritative make clean && make extract-all && make check-all = 213/213) — read this addendum as byte-proven, not unproven.
Addendum — Shared arithmetic that CONSUMES the selected value cannot be hoisted above the if — narr (func_80183DA0)
Addendum (P31 S66, func_80183DA0): ⚠ UNPROVEN — this draft was REFUSED by the whole-binary byte-gate and never reached MATCH (best 27 mismatched, nins 83 vs target 82). Only the relative match_one closeness deltas below are asserted; the pass attribution is inference, with no -dj/-dS dump taken. §193-C's cure — "write a shared statement ONCE, above the if" — is unreachable whenever the duplicated work CONSUMES the value the arms disagree about, which is the common face of this class in BFM's scaling arithmetic. On func_80183DA0 the drafted arms were if (out.vx < 0) val = base - (-out.vx * 0x7F / 0x140); else val = base - (out.vx * 0x7F / 0x140); — nothing is hoistable above the if, because the multiply's operand is exactly what the if selects. The tell is §193-C's own, one level in: the magic-divide head (lui $v1,0x6666 / ori $v1,$v1,0x6667 / sll $v0,$a0,7 — the *0x7F reciprocal-divide setup, §16N+2's ladder) appears twice, once inside each arm, while the target carries it once below a single bltz merge; the deeper mult/sra/mfhi tail had already merged, exactly as §164-09/§50-B predict — the backward walk breaks at the first rtx_renumbered_equal_p failure, here the sll whose source register differs per arm. Nothing was "un-CSE'd": the two arms compute different values, so no common subexpression ever existed — do not write this up as a CSE failure. The cure is §165-21's, generalised from a shared trailing STORE to a shared trailing arithmetic CHAIN: narrow the arms to the differing value alone and write the shared computation once below the select — val = base - (out.vx < 0 ? -out.vx : out.vx) * 0x7F / 0x140;. Evidence: match_one closeness 53 → 39 at an unchanged nins 81 on that single edit (agent-ad9135a44913f81b5.jsonl lines 103 → 109), the largest single-edit drop in the run (62→56→53→39→27). Per §193-C, the length did not move at all — read the duplicated micro-sequence, never the count. Bounds: n=1, gate-refused, and the ?:-vs-if/else axis was not separately ablated against §195-F/§167-09's arm-order folds (§267-ADD-5 measured every select spelling byte-identical for a bare halfword abs, so the load-bearing edit here is plausibly the narrowing, not the ternary itself — untested).
REFUTED claim — Dead call-arg register address-fold: no C-level lever found (func_8017ECAC)
⚠ UNPROVEN — REFUTED AT VET TIME. File under §167z-style "do NOT re-derive"; nothing here is byte-proven (no MATCH, closeness floor 16/31, never sent to the whole-binary gate).
func_8017ECAC— "Dead call-arg register address-fold: gcc-2.7.2 picks the just-dead$a1to materialize the address of the FIRST of three sequential global stores; a genuine compiler-internal register-allocation artifact with no C-level lever." Why refuted: (1) The headline contradicts byte-proven law. §48-C1 (L3519, 14 micro-probes) states the opposite as a positive rule — "the C type literally selects the addressing mode … target showsla+ offsets → declare a struct; target shows plainlui/%lo→ declare a scalar" — and §165-10 (L13990) byte-measures both directions of this exact two-form choice on one symbol (−9 / −10 ins A/Bs off a MATCH baseline), while §167-45's PER-SITE, NOT PER-BLOCK law (L16187) is banked on precisely this shape: one global routed through a materialized base while its neighbours keep the plain%hi/%lofold. The class is steerable; L1396-1401's R14 caveat forbids the verdict on this evidence. (2) The prescribed lever was never run. Every probe in the set attacks the use site (*(Blk8*)&D_x,s16 *p=&D_x,u8 D_x[],*(s16*)&D_x, a$5pin, anasm("")barrier, store reordering) or converts all three symbols to one struct. §48-C1's construct — a struct-TYPED file-scope declaration for the FIRST symbol only,D_801274EA/D_801274ECleft bare scalars — is §48-C1 crossed with §167-45's per-site discipline and is absent from the list. A struct cast at the use site is a different construct, and L3046-3049 already byte-proves the cast form is address-neutral for a write-only global (lui $at,%hi ; sh $v,%lo($at)), i.e. that probe was predicted dead before it ran. (3) The 8-way plateau is a rediscovery of documented negatives, not a wall. §48-B (L3508-3510: "s16 *p = &G;is constant-folded straight back by cse'sfind_best_addr"), its offset-0 corollary (L3515), and §164-52 (L12990: init and uses on ONE cse path with no ≥2-jump-ref label ⇒ thelaloses its last user and is deleted) predict that the pointer-local,&-cast, array-decay and plain-scalar spellings all collapse to the identical direct-macro form. Eight spellings landing on the same 16/31 is exactly that prediction, not evidence of an unsteerable residual. (4) The "dead call-arg register" framing is over-specific and unsupported. A scan of all 11,681 stub.sforaddiu $rD,$rD,%lo(SYM)immediately followed by a store at0($rD)returns 4 instances:func_8017ECAC($a1),ov_SC07_007/func_8017DFB8($s0), andov_MAIN_012+ov_SC03_107func_8016D1D8($v0). Infunc_8016D1D8the base is explained outright by §48-C1 — it serves two MEMs (sb $v1,0x0($v0)at 8016D2E0 andsb $v1,-0x1($v0)at 8016D2F0) while five neighbours (D_80184892/94/95/96) keep the$atfold, textbook §167-45. The register identity is an allocation outcome; it is not a tell, and the previous call's argument register is coincidence at n=1. What survives and is worth keeping: the target reading itself (asm/ov_SC02_005/nonmatchings/ov_SC02_005_jr_8017CF90/func_8017ECAC.s:19-27— 3-insn base forD_801274E8, 2-insn$atfolds forD_801274EA/D_801274EC, LENGTH-DRIFT −1 at idx 15) and the negative-control table (8 spellings, identical 16/31). Next agent: run §48-C1's struct-typed declaration onD_801274E8alone; there are three banked in-tree references to this record (src/ov_SC07_009/ov_SC07_009_jr_8017AE2C.c:3933,:4038-4040,:4070) to read the fleet-canonical typing off before touching the file-scope decls.
Why refuted: Checked §48-C1 (L3519), §48-B + its offset-0 corollary (L3505-3517), §48-C2/§48-C3, §164-52 (L12990), §20's scalar-global-RMW and volatile-reload bullets (L1907-1946), the *(T*)&D_sym write-only note (L3046-3049), §165-10 (L13990-14009), §167-45 LAW 1/1a/PER-SITE (L16163-16190), §167-18 (L15496), §167-08 (L15242), §153/§165-26 pointers, §176-A/§176j-2 and §177 (the refs the agent cited), plus §167z's refuted-list format and the R14 discipline line at L1396-1401. Verdict is REFUTED, not NEW/ADDENDUM, on four grounds. (a) Not byte-proven: no MATCH, floor 16/31, never gated — so ADDENDUM's "byte-proven refinement" bar is not met. (b) The submitted law contradicts byte-proven sections: §48-C1's 14 micro-probes make the base-vs-$at addressing mode a C-TYPE choice, §165-10 measures both directions of that same choice on one global, and §167-45 banks the exact per-site shape (one global through a base, neighbours on the plain fold). (c) The prescribed lever was never tried — a struct-TYPED file-scope declaration for D_801274E8 ALONE (§48-C1 × §167-45's per-site rule); the agent tried a struct CAST, which L3046 already byte-proves is address-neutral for a write-only global. (d) The 8-way 16/31 plateau is predicted by §48-B/§164-52 (an all-dereference pointer-to-global on a single cse path folds back to the direct macro), so it is a rediscovery of documented negatives rather than a new wall. I also verified the target myself (asm/ov_SC02_005/.../func_8017ECAC.s:19-27 — the tell is real) and scanned all 11,681 stub .s for the shape: 4 hits, bases $a1/$s0/$v0/$v0, and the ov_MAIN_012 hit (func_8016D1D8:71-75) is a clean §48-C1 counter-example (base serves two MEMs at 0x0 and -0x1 while five neighbours keep $at), which falsifies the "dead call-argument register" half of the tell at n=1. The entry_markdown is written as a §167z-style refutation note that preserves the honest target reading, the negative-control table, and the untried lever plus the three banked in-tree references to the same record.
S66 HARVEST, ROUND 2 (P31, 2026-08-30) — the M1/O1 resume transcripts
2 ADDENDUM, 1 REFUTED, 1 COVERED from 7 mechanism-advertising transcripts of 32.
Addendum — Truncating assign must be the lazy || operand, not hoisted (func_80012B58)
Addendum (P31 S66 round2, func_80012B58; ⚠ UNPROVEN — match_one isolated masked-diff only; the whole-binary byte-gate refused this draft, so read the RELATIVE closeness movement, never the "MATCH" label): §252's construct has a || face, and the guarded expression need not be an RMW — a truncating assignment behaves the same way, and this is a THIRD, independent reason to keep such an expression in the controlling position (§252's own guarded-vs-unconditional reading rule is the first; the L27538 addendum's postfix old-value semantics the second; basic-block placement is this one).
TELL. The parameter's promotion pair (sll $aN,16 / sra $tN,16) sitting after a beqz/bnez that tests an unrelated prior value — i.e. inside that branch's guarded block, not in the straight-line entry code above it. Written as its own statement (dv = (a2 << 16) >> 16; before the if) the def is unconditional and lands in the entry block; written as the short-circuited second operand — if (h == 0 || (dv = (a2 << 16) >> 16) == 0) — it is reachable only on the h != 0 edge and the pair moves into the guarded block. The wrong-placement residual is diffuse, not local: scattered extra/reordered sll/sra/addu plus shifted .L targets across the whole body.
BOTH HALVES ARE REQUIRED, AND ONLY TOGETHER. Ladder on one draft, match_one closeness: dv = a2; hoisted → 18; (dv = a2) == 0 embedded in the || → 18, byte-identical residual (placement is inert on a bare copy — cse/copy-prop eats it, so the truncation is what gives placement anything to move); dv = (a2 << 16) >> 16; hoisted → 67 (a regression worse than having no dv at all); the identical double-shift moved into the || → closeness 0, nins 69/69, residual []. The 67 → 0 step is single-axis — placement was the only delta — so it clears §266's solo-ablation bar at isolated-compile level, which is exactly as far as this evidence reaches.
COMPOSES WITH. §28's coalescing-pin note (L2355, "keep the load lazy inside the &&") states the same non-hoisting property for a load under a register pin — this is its truncation face, pin-free, on ||. §172b-1's placement addendum (L26100 dial 2) dials the same promotion pair against a loop (cast inside the body ⇒ loop.c hoists it to the preheader through a fresh intermediate); this dials it against a guard. §211's hoist-above-the-guard lever runs the axis in the opposite direction — A/B both, per its own "init placement has no default".
BOUNDS. (1) n = 1, unbanked; nothing here is byte-proven. (2) Route only a diffuse multi-block residual here — a localized 1-2 instruction promotion-pair diff is §164-37/§163b (extension between a bias subtract and an sltiu), §164-48/§167-44 (declared-width tells) or ADD-6's shift-as-test, not this. (3) Mechanism untraced: no -dj/-dS dump was taken. do_jump's TRUTH_ORIF path (expr.c:8970-8990, cited at §194-M/L19957 and §308a) is the place to look, cite it as a place to look and never as a proven pass behaviour (§164z's standard).
Addendum — WIDTH sh!=lhu residual: hoist a sibling field's load first (func_80182538)
Addendum (P31 S66 round2, func_80182538): §167-05's site is the mirror image of this one — same address class (§16Z row 3: one base pseudo, two disjoint u16 offsets, memrefs_conflict_p = 0), literally the same two offsets 0x6/0xA, and the same match_one presentation — so its headline sentence "no /s grant, no statement order, no register pin and no scope/reuse edit can create the edge" is the thing a future agent will mis-apply here. It is true and polarity-scoped: you need the edge only to keep the load BELOW the store (§167-05's direction, where the scheduler hoists and only the volatile pair stops it). When the target wants the load ABOVE an unrelated sibling store, no edge is required at all — both orders are legal, rank_for_schedule falls through to INSN_LUID (§49), and plain statement order is the whole dial, for free. Do not reach for the volatile pair, and do not conclude "statement order is inert on this cell" from §167-05's 20-variant plateau.
THE TELL. match_one verdict WIDTH/sh!=lhu, detail.pairs == [["sh","lhu"],["lhu","sh"]], kinds opcode-only, zero length drift and no register or operand change — an independent store and an independent load off one base have traded places by exactly one slot. Nothing is the wrong width. This is not cookbook-index.md line 27's WAR/§76 story (that routes the opposite direction — a load hoisted too early), and it is not worth §66d-5's "run the three permuter profiles" route: read it as an emission-order question first, it is a one-line edit.
THE FIX is §136g rule 15's RMW split (v = *p + K; … *p = v; "so its load rises") with the intervening blocker being a sibling STORE rather than rule 15's pointer chase: take the re-read out of the compound RMW and give it its own statement, physically above the store.
/* closeness 3 (WIDTH/sh!=lhu) — the re-read inline, inside the later RMW */
*(u16*)(p + 6) = t;
*(u16*)(p + 0xA) = *(u16*)(param_1 + 0x52) + (*(u16*)(p + 0xA) + 0x80);
/* closeness 0 (MATCH) — the sibling load hoisted, ABOVE the unrelated store */
f = *(u16*)(p + 0xA);
*(u16*)(p + 6) = t;
*(u16*)(p + 0xA) = *(u16*)(param_1 + 0x52) + (f + 0x80);
BYTE EVIDENCE (func_80182538, 53 ins, single-line delta on the pinned triple, the §164-24 r = 0x30 fold fix already in both arms). draft6: near, closeness 3, residual [26] sh a1,6(s1) ↔ lhu $v0,0xA($s1) / [27] lhu v0,82(s2) ↔ sh $a1,0x6($s1) / [28] lhu v1,10(s1) ↔ lhu $v1,0x52($s2); draft7, the only change being f = *(u16*)(p + 0xA); moved above the p+6 store → {"status":"match","closeness":0,"nins":53,"residual":[]}. An operand-order swap (draft6 vs draft5) was inert, confirming the position is the dial and not the expression shape.
BOUNDS. (1) n = 1, and the mechanism is unestablished — no -dS/-dR dump was taken, so source-order RTL emission (§49's LUID tiebreak), sched1 priority and same-base alias conservatism are all still live explanations; state "statement order moved it" and stop. (2) The A/B confounds two axes: draft6 had the re-read inline and after the store; draft7 made it a statement and put it before. "Own statement" vs "physically before" is unablated — if the cheap edit fails, try each half separately. (3) Discriminate against §167-29 (a), which prescribes the exact opposite (delete the block-head local, re-read the field at each use site) when your load lands too EARLY and the target has it mid-block. The direction of the residual picks the entry: your load below the target's ⇒ here; your load above ⇒ §167-29 (a). (4) Only free where no edge is needed; if the target wants the load after the store, you are back in §167-05 and only the both-volatile pair works.
REFUTED claim — REFUTED — the j-slot store is an ASPSX macro-expansion hop, not a cse split; maspsx hard (func_8005DBD8)
§NNN — REFUTED: A sw %lo(SYM)($at) IN A JUMP'S DELAY SLOT WITH ITS lui $at STRANDED ABOVE THE JUMP IS ASSEMBLER MACRO-EXPANSION, NOT A cse SPLIT — OUR PIPELINE CANNOT EMIT IT FROM ANY C, AND THE +1 IS A maspsx GAP (P31 S66 round2, func_8005DBD8, byte-probed)
The claim. That func_8005DBD8's LENGTH-DRIFT +1 is the first live occurrence of sched.md §D2's "theoretical" cse-split store form, and that dereferencing a local pointer twice (s32 *p2 = &D_800729A0; if (*p2 >= 5) *p2 = *p2 - 1;) is the lever that gets the store into the j's slot. The lever never reached MATCH (best: near, closeness 8, nins 50) and the mechanism is misattributed. Both halves are refuted below on the pinned triple.
What is true and already banked. eligible_for_delay needs dslot==no AND length==1, so a -G0 macro-form global store (sw $r,SYM, length 2) can never occupy a slot at the cc1 level. That is sched.md §D2 verbatim, including the exact triage row the agent used ("Store-to-global in target slot, nop in yours → your store kept -G0 macro form (length 2)"), and the cookbook already restates the same ineligibility for la/large-li in §201-C BOUND 1. Nothing new.
BYTE PROOF 1 — the baseline draft is already correct at cc1; the whole +1 is the assembler layer. Probe p1.c = the agent's own baseline shape (extern int G; if (c>=0) { if (G>=5) G=G-1; } else h();) through the pinned triple (cc1 -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float → maspsx 2.56 → as -O1 -G0) disassembles to bnez v0,L / addiu v0,v1,-1 / lui at,%hi(G) / sw v0,%lo(G)(at) / j L / nop — byte-identical to the target's 8005DC68-8005DC78 except that the j and the sw are swapped and a nop is appended. cc1's raw .s shows sw $2,G then j $L4 inside .set macro / .set reorder: cc1 left the slot to the assembler. tools/maspsx/maspsx/__init__.py:1054-1057 then appends nop # DEBUG: branch/jump after every branch/jump while is_reorder, and GNU as expands the macro in place. The original's ASPSX 2.56 instead expanded the macro and hopped its second half into the slot. The $at base is the give-away — the cookbook index's own §0 tell ("$at in any pair ⇒ assembler macro expansion, not cc1"); reorg moves whole insns and never mints $at.
BYTE PROOF 2 — the proposed lever is byte-explained and byte-dead. Probe p2.c (same body through int *p = &G) makes cc1 itself emit .set noreorder / j $L4 / sw $2,0($5) — a length-1 store, genuinely eligible — but it pays la $5,G (lui+addiu) and turns the read into lw $3,0($5). That is exactly the reported ADDRESSING/addiu!=lw profile=cse residual at nins 50, reproduced from first principles. The target keeps the per-access macro form on both sides (lui $v1,%hi ; lw $v1,%lo(D_800729A0)($v1) — base = destination register, i.e. the gas load macro), which is what the bare-global spelling gives. The lever is also a rediscovery of §21's L1928 scalar-global-RMW bullet, whose banked exemplar (func_80186938) is this same C shape; §21 predicts precisely the shared-base form the target does not have.
BYTE PROOF 3 — scope kills the "C idiom" reading. The shape exists at exactly 6 sites in all 4,395 remaining asm/**/*.s, and all 6 are in one TU: asm/nonmatchings/800c3/ (func_8005D734, func_8005DBD8, func_8005ED4C ×2, func_8005F450 ×2). Three of them sit in conditional branch slots (beqz/blez/bgtz) with the lui $at stranded immediately above the branch — e.g. func_8005D734 @8005D748-8005D750. A macro half cannot get there through reorg at all. One blocked TU, one signature, no C variation ⇒ toolchain, not source.
WHAT TO DO INSTEAD (and what NOT to do). Do not spend attempts respelling the store's address on any of these 6 — no pointer local, no volatile, no §153 launder, no polarity flip reaches it. The open item is a maspsx fix: in reorder mode, when the insn immediately preceding a branch/jump is a -G0 global load/store macro, expand it and hop the second half into the slot instead of unconditionally emitting nop. ⚠ Blast-radius first (R14): 3,009 genuine nop slots exist game-wide and 213/213 binaries are currently green — the reason this is rare is that dbr normally fills the slot itself and only leaves it free when nothing is eligible, so the patch should be narrow; prove that by full-gate A/B before shipping, not by argument.
Status: UNPROVEN lever, REFUTED mechanism. No byte-exact match was reached on func_8005DBD8 in the originating transcript or here. sched.md §D2's "cse-split form … no occurrence found yet" flag stands — this is not that occurrence, and D2 should be annotated with the assembler-hop case as a distinct third row.
Why refuted: Checked §153 (address-remat launder — the agent already rejected it correctly; it is about call-arg addresses, not stores), §21/L1928 (scalar-global-RMW pointer bullet — this IS the proposed lever, already banked and byte-gated on func_80186938), §18/§20 (single-store %lo fold), §5a/§34 (asm fence, the opposite direction), §176-A/A2 (store vs a following unconditional jump's slot — closest symptom in the book, but its cause is statement order around a jal and its drift sign is opposite), §194-M + BOUND 1/2 (store in a conditional slot ⇒ dominance; BOUND 1 explicitly excludes j slots), §194-H, §201-C BOUND 1 (restates the same length-2 -G0 macro ineligibility, cited to sched.md D2), §291, §263, §223, §224, §211, and grepped cookbook-index.md ("delay slot", "macro form", "split"). The mechanism half of the claim is already stated verbatim in docs/gcc-2.7.2-map/sched.md §D2 (the agent read it and quotes it), so it is at best a restatement — but the claim's specific offer, "a live occurrence of the cse-split store form", is byte-REFUTED: I compiled the agent's own baseline shape through the pinned triple and it reproduces the target's five instructions exactly, with only the j/sw swapped plus a trailing nop. cc1 emits sw $2,G under .set macro/.set reorder and leaves the slot to the assembler; maspsx (tools/maspsx/maspsx/__init__.py:1054-1057) then hard-codes nop after every branch/jump. The target's split is ASPSX expanding the macro and hopping its second half into the slot — proven by the $at base (the index's own §0 tell for assembler expansion) and, decisively, by three of the six in-tree sites sitting in conditional branch slots with the lui $at stranded above, which reorg cannot produce. I also compiled the submitted lever: cc1 does put sw $2,0($5) in the j slot, but pays la $5,G and rewrites the read as lw $3,0($5) — reproducing the exact ADDRESSING/addiu!=lw residual the transcript reports, i.e. the lever's nins-50 is real and structurally wrong. Scope corroborates: 6 occurrences in 4,395 asm files, all in one TU (asm/nonmatchings/800c3/). Weighing the unproven gate: the draft never matched, closeness never reached 0, and every restatement that preserved foldability reproduced +1 — consistent with a source-unreachable residual. Banking this as a C idiom would have sent future agents to grind a toolchain gap; the value is the redirect (a narrow, blast-radius-gated maspsx patch) and the correction to sched.md D2.
S66 HARVEST, ROUND 3 (P31, 2026-08-30) — the M2 (main) + O21 (overlay) lanes
5 ADDENDUM, 2 COVERED, 1 REFUTED from 11 mechanism-advertising transcripts of 30.
Addendum — Static local_type blocker survives typedef removal — it's textual (func_801588CC)
Addendum (P31 S66 round3, func_801588CC): ⚠ UNPROVEN as a C lever — the whole-binary gate refused this draft, so no byte-equality is claimed; the TOOL defect below is source-verified, not inferred. §184's own closing instruction ("when you find one instance, grep for the others in the same session") did not reach a fourth member of the class, and it is the one that gates the recovery pipeline: blocker_probe.draft_types (tools/blocker_probe.py:105) runs re.findall(r'\btypedef\b[^;]*?\b(\w+)…', text) over the raw draft, never cdecl._mask, and its result feeds static_verdict's dup line (:150) — the local_type T0 blocker that recover_integration.py's static prescan reports. So a typedef quoted inside a // comment is counted as a definition, exactly §184's pregate_check._typedefs row, 13 sessions later.
The authoring rule this sharpens (generalizes §167-02 from #include "common.h" to any declaration text): deleting the offending construct from the code is necessary but not sufficient if the comment justifying the deletion reproduces its literal syntax. On func_801588CC the code-side typedef int s32; was removed and grep -n typedef returned exactly one hit — a // comment quoting the old declaration verbatim — and the prescan still reported local_type: draft redefines type 's32' the TU already defines, with blockers.json carrying "static":[["local_type", …]]. Rewording that comment, zero change to compiled code, cleared it to -- none MATCH 122 ins yes, agreement=1 STATIC-ONLY=0. Paraphrase the removed construct; never quote it.
Diagnostic tell — the closeness number is structurally blind to this whole class. match_one reported {"status":"match","closeness":0,"nins":122} on all three variants (buggy typedef in code, typedef removed, comment reworded); the only signal that moved was recover_integration.py's real-TU pair (cc1 verdict + static class), which went CC1-FAIL tu:2768: redefinition of 's32' → MATCH 122 ins STATIC-ONLY → none … yes. A stalled bank whose closeness never budges across decl-plumbing edits is a §184 signature, not a codegen residual — read the probe's verdict pair, not the diff.
BOUND. n=1, no bank. Fix site is named but unshipped: mask with cdecl._mask inside draft_types (the struct|union regex on the same lines has the identical hole, and whence's ^[^/\n]* guard only catches a // at line start, not mid-line). Says nothing about whether func_801588CC is byte-correct — the gate refused it for a separate reason.
Addendum — Orphan sh triplet to $sp is a write-only local array (func_8017F274)
Addendum (P31 S66 round3, func_8017F274): the aggregate half of this law does not need the escape. ADD-9 hangs element-store survival on an address that escapes into a call (&sp18), and §289 (L29094, title-only) on an address-taken base; func_8017F274 (ov_SC04_003, 127/127 MATCH, one-shot) carries an s16 tmp[3] whose address is never taken, whose elements are never read, and whose three sh at $sp+0x10/0x12/0x14 all survive -O2 regardless — because an aggregate is memory from its declaration (§82-3-2, L6423) and the only store-side DSE in this compiler is flow's back-to-back last_mem_set window (§194-J, L19433/L19447 "aggregates, which flow does not touch"; §27's parenthetical at L2334, "no DSE for aggregates"). Reading rule — the negative form of ADD-9's: a stride-2 run of sh to consecutive $sp offsets where no lh/lhu anywhere in the function reads those offsets is a write-only local array. Declare it and assign every element from the source fields; do not hunt for the missing read, do not treat the stores as a scheduling artifact, and do not reach for §258's pad forms — the array is the frame contribution. Declaration order is still load-bearing: tmp must precede the s32 local[2] it shares slots with (§136-6/§258). Honest scope: positive direction only. The one-shot MATCH proves the C shape emits the three stores; the negative control (drop tmp, expect LENGTH-DRIFT/-3) was never run, so the "gcc would not have removed them" half is inference from §194-J's bound, not a measured ablation on this function.
Addendum — REGALLOC-PERM: the store reads the narrow copy's register — split the one narrow local by (func_801838CC)
Addendum (P31 S66 round3, func_801838CC): ⚠ UNPROVEN — the whole-binary byte-gate REFUSED (or never ran on) this draft; closeness 0 is match_one-only, relocation-masked. Try the lever, re-gate, promote on the next confirming target. §194-B's bound list owns the copy's existence (width, bound 1/2) and its position (bound 4, the two-arm select); it says nothing about which register the STORE reads when ONE narrow local plays both roles. Target, byte-read from asm/ov_SC03_024/nonmatchings/ov_SC03_024_jr_8017DF84/func_801838CC.s (0x80183A28-0x80183A68): addu $v0,$v0,$v1 / addu $v1,$v0,$zero — the copy is emitted BEFORE the store, not after it as the submitted asm_tell transcribes — / sh $v0,0x18($s0) (reads the ORIGINAL) / sll 16 ; sra 16 ; slti 0xB1 ; bnez on $v0 / else-arm sh $v1,0x1C($s0) (the only consumer of the copy). A single s16 w serving store + compare + arm compiles byte-identical except at idx 89 = 0x80183A30 exactly: mine a6030018 sh $v1,0x18($s0) vs target a6020018 sh $v0,0x18($s0), REGALLOC-PERM/$v1>$v0, closeness 1, nins 128 = target. The dial is a role split: s32 t2 owns the store and the (s16)t2 >= K compare, and a second s16 w = t2 is consumed ONLY inside the else arm → match_one MATCH 128/128. This is §162j1's optimize_reg_copy_1 firing at a MEM use where §165-33's prescribed exit ("hoist the surviving use above the copy") is structurally unreachable — the copy is compiler-minted by the narrow DECL_MODE, not a C statement, so there is no copy to hoist above, and the store already precedes it in source order. It also explains why exactly one instruction moved: the scan breaks at the in-place sll $v0,$v0,16 (local-alloc.c:732 reg_set_p, tested before the death test), so only the single insn between the copy and the shift is rewritten onto the copy's register. Mechanism for why the split blocks it is a HYPOTHESIS, not dumped: with two locals the copy's SRC is SImode and the arm's use HImode, so validate_replace_rtx cannot rewrite the SImode store — the same unverified mode-mismatch story §194-B already flags in its FALSIFIED-HALF note, one pass later. Two free riders: (a) w has one surviving consumer and the copy appears anyway — a further witness for §209 Direction A against §194-B bound 2; (b) ablated dead on this body, §36/§249-3's + $0 opaque-copy pin on w regressed closeness 1 → 44 (+1 ins), a new row for §257/§268's pin ledger and a further confirmation of §176-B ("on a small REGALLOC-PERM reach for the naming/scope lever first, the pin never").
Addendum — REGALLOC-PERM store in a cross-jump shared tail defeats §137's barrier (func_8017EDE8)
Addendum (P31 S66 round3, func_8017EDE8): ⚠ UNPROVEN — gate-REFUSED draft, match_one closeness only, floor 3, never MATCHed; the negative result below is what is citable, not a fix. §137 step 4 says to place the zero-byte asm "at the source point that makes L land in the window" and treats placement as a free dial. It is not free, and §137 never inherits the bound its own primitive already carries: §164-09's ⚠ (L12085) — the barrier sits inside reorg's fill window, so if the target FILLS a nearby delay slot the barrier costs you that fill (sharpened by this session's func_80181D08 addendum, L30489: read the join's two delay slots and put the barrier on the side that does not deny the fill you need). func_8017EDE8's contended pair is inside that window: the target is lw $vA,K1($sN) / addiu $vB,$zero,LIT / j .Lshared_tail / (slot) sw $vB,K2($vA), where .Lshared_tail is also reached by the sibling if/else arm — i.e. the pair's only placement sites lie between a filled j slot and a cross-jump-merged call-arg block. THE TELL: you ran §137's two-compile arithmetic, the -dl/-dg dumps confirmed the swap (here pseudo 82 = ptr L=6 vs pseudo 83 = const L=4, "pri favors const first", backwards from the target's $v0=ptr), and then every barrier moved nins instead of the register pair — v8 closeness 41 / nins 62 LENGTH-DRIFT, v12 closeness 39 / nins 61 LENGTH-DRIFT, v10 closeness 6 at an unchanged nins 59 but with the class flipped to ADDRESSING over insns 26–32 (the merged call-arg block, not the pair), v9 and v11 cc1-fails on the "+r" constraint. Ranking-motion-with-length-motion is the inverse of the func_80182D1C sub-tell (L30523): it says the asm is acting as a scheduling/merge fence first and an L dial second, not that the class is barrier-inert. ⚠ BOUNDS — the agent's conclusion ("a pair flush against a cross-jump boundary has NO placeable room") is NOT established, and two documented levers were never run: (1) barrier COUNT is a dial, not just position — this session's func_80182D1C addendum (L30523) took an identically-plateaued REGALLOC-PERM/$s0>$s1>$s0 from closeness 9 to match by stacking a second identical zero-byte asm at one fixed point; sweep N = 1, 2, 3 at the one site whose -dg allocno order moves before declaring no room exists; (2) §176-B (L17704) still owns the routing — a 3-instruction, zero-drift perm is usually not register allocation, and a -dl/-dg dump confirming the priority order does not falsify that (the priority is downstream of whatever mis-shapes the pair), so re-read the callee's arity and the wide/narrow symbol alias (§176-B1/B4, §231.5) before spending another placement. Also measured null here and worth the ledger row (§257/§268): the §30 anonymous-struct /s grant (L23263), which flipped exactly this const-vs-pointer $v0/$v1 colouring on func_801824D4, is inert on this shape, as are 7 statement/decl-order permutations (byte-identical residual — §137's own "invariance under permutation is information", pointing away from the source).
Addendum — LENGTH-DRIFT nop clears when offset math precedes the symbol launder (func_80039B20)
Addendum (P31 S66 round3, func_80039B20) — ⚠ UNPROVEN: the byte-gate refused this draft, match_one masked closeness only (floor 16, MATCH never reached), and no .i.loop / -dL / -dS RTL diff was taken between the two drafts, so no gcc-source cause is owned for the count change. §194-A's BOUND 3 states this failure mode in ONE direction only — a peer left ABOVE the fence hoists ahead of what you wanted first — and its DIAGNOSTIC TELL commits the residual to "count-neutral". This card is the mirror, and it is not count-neutral. With a §153 launder in §194-A's prescribed AFTER position on a symbol base (s32 base0 = (s32)D_800C6DD0; __asm__ __volatile__("" : "=r"(base0) : "0"(base0));), folding the loop index's scale arithmetic into the consuming expression below the fence — p = (s16)i * 0x60 + base0; — measured nins 80 vs target 79, "sig": "LENGTH-DRIFT/1?", closeness 52: one stray nop whose PC-relative cascade re-scores every later branch. Extracting that same arithmetic into its own statement sequenced above the base's declaration and its fence — s32 off2 = (s16)i * 0x60; first, then base0, the launder, then p = off2 + base0; — restored 79/79 exact and collapsed the class to OPCODE-MIXED/addressing,width, closeness 16 (seed was 81 at nins 89). Read BOUND 3 both ways: the region above the fence must contain everything the target computes first, and a peer left below it can cost an INSTRUCTION, not merely a slot. ⚠ The nop's provenance is the open question and it decides the retrieval route: §306's splicer is --expand-div-only and the S66 §306 addendum (L30535) states the general dependence-gap version of this defect is zero-drift, so a +1 here is either §46/L2516's #APP-kills-the-ASPSX-slot-hop (with the load-delay nop re-inserted) or §164-36/§164-36b's fill-window denial — take -dS/-dR and grep -n '#APP' before citing any of the three. The residual this does NOT reach is a local sched1 interleave: the target finishes the whole 5-insn sll/sra ; sll ; addu ; sll (×0x60) chain before starting lui/addiu %hi/%lo(SYM), while the draft still slots the symbol pair after the chain's first insn — and that floor is flat (pin off2 to $3 → 17; a second launder on off2 → 16; nesting the block → 16; pinning p to $4 → 51 at nins 80), i.e. §167-17's statement sweep is exhausted and no further fence placement moved it. Per §266 the winning edit moved two axes at once (extraction AND placement) and neither was solo-ablated. Stub still open at src/800.c:21456.
REFUTED claim — REFUTED: a block shared across TWO switch statements is ordinary forward cross_jump, not a (func_8001B0D4)
§NNN — REFUTED: A CODE BLOCK SHARED BY TWO DIFFERENT SWITCH STATEMENTS IS ORDINARY FORWARD cross_jump, NOT A SHAPE C CANNOT REACH — §164-11 ALREADY BANKS IT FROM PLAIN NESTED SWITCHES (P31 S66 round3, func_8001B0D4; ⚠ UNPROVEN — gate-REFUSED draft, closeness 68→45, never MATCHed)
The claim. That func_8001B0D4's persistent ADDRESSING/j!=lui profile=cse residual at nins 86=86 comes from a cross-switch shared tail — the same physical block reached from two separate switch statements' jump tables — and that "ordinary case A: switch(t){…} case B: switch(t){…} nesting, however ordered, keeps each inner switch's codegen self-contained in gcc-2.7.2's normal (non-cross-switch) path." The observation is real and the ground truth is quoted correctly; the negative half is refuted, and the mechanism is already law.
What is true (and re-verified here). asm/data/6324C.data.s does hold the overlap: jtbl_80072B24 (outer) slot 4 = 0x8001B208 reappears as jtbl_80072B64 slots 0 and 8; 0x8001B1D8 is jtbl_80072B3C slot 1 and jtbl_80072B64 slots 1/2/4; 0x8001B218 is in both inner tables. All confirmed byte-for-byte.
REFUTATION 1 — §164-11 is exactly this, byte-proven and banked. §164-11's exemplar func_80183324 (ov_SC01_077, 324 ins, banked, src/ov_SC01_077/ov_SC01_077_jr_80183324.c:3115) spells the inner switch four separate times in outer cases 0/1/2/4 as ordinary nested switches; cc1 emits five tables and $L34/$L48/$L63 — three different inner switches — all point at one copy of the code living inside case 4's arm. That is a cross-switch shared body produced by plain nesting, whole-binary gated. §164-11 even states the prescription: k tables sharing entries ⇒ write the body out k times and let cross_jump merge; do NOT collapse them into one switch reached by goto.
REFUTATION 2 — the target's own bytes show a plain SUFFIX merge, and §162g predicts every survivor's position. In asm/nonmatchings/800/func_8001B0D4.s, case 0 keeps its own lui/lw %hi/%lo(D_800747E4) at 8001B114/B118 and its own addiu $v1,$zero,1 in the j delay slot, and joins only at .L8001B18C — cross_jump merged the common suffix (D_800747E8=$v1; D_800747E4=…+1; return 0) and nothing else, exactly §193-C/§224. And every survivor is the later-emitted copy, exactly §162g: B1D8 (case-1-inner t==1 merged into case-2's region), B208 (case-2-inner t==0/8 merged into outer case 4, the last arm), B184/B18C (case 0's tail merged forward into case 1's arm).
What to do instead. Read the tables element-wise across tables, not just vector-vs-vector: any address appearing in ≥2 tables — or in an outer table and an inner one — names an arm whose statements must be written longhand in every arm that reaches it, with the merged copy landing in the last-emitted arm. Here that means case 0 gets D_800747E8 = 1; D_800747E4 = D_800747E4 + 1; return 0; written out in full (not factored, not goto-ed), case-2's inner t==0/t==8 get outer case 4's body written out in full, and case-1-inner t==1 gets case-2-inner t==1/2/4's body. The transcript never tried this; it only permuted outer case-label order, which moves which single-switch pairing wins and can never mint a duplicate that was not written.
Status: UNPROVEN lever, REFUTED mechanism. No byte-exact match was reached (best closeness 45 of 86 ins). The two levers that did move the number — register s32 t __asm__("$3") for CdQueueBusy()'s return and outer arm reordering — are §249/§268 and §222.1 verbatim; nothing new. Do not re-bank the "cross-switch merge is unreachable from C" reading: §164-11's banked counter-example already exists.
Why refuted: Checked §164-11 (L12111 — THE JUMP TABLE IS PER-SWITCH AND SURVIVES TAIL-MERGE), §162g (L11300 — cross-jump direction / survivor is the LATER copy), §162h (L11336 — the CALL veto is a COUNT law), §193-C (L18588 — scheduled common SUFFIX only, no head merge), §224 (L22939 — CROSS-JUMP: write the duplicate), §225 (L22975 — three control-flow shapes no structured spelling reaches, incl. the goto-landing-mid-flow case the distiller says is untested), §298 (L29542 — shared-tail position diagnostic), §222 (L22835 — arm order in source IS arm order in asm), §223, §249 (L23931), §268 (L25285 — register pin honored when the pinned range crosses no call), §88a, §50-B, §163c, §165-28, §206, §8/§8a-pad/§8e, §163z (L11868, which flags this exact func_80183324 tell), plus a cookbook-index.md grep of the "jump tables & switches (46)" cluster and "cross-jump / tail merge / shared tail". VERDICT REFUTED, on two independent grounds. (1) The claim's distinguishing content — "gcc-2.7.2's normal path keeps each inner switch's codegen self-contained; no ordinary nesting aliases one inner switch's arm onto a sibling outer case's inner switch arm" — is contradicted by a byte-proven, whole-binary-gated §: §164-11's exemplar func_80183324 spells the inner switch FOUR times as plain nested switches in cases 0/1/2/4, and cc1's $L34/$L48/$L63 — three DIFFERENT switches — all point at ONE copy of the code inside case 4's arm. Cross-switch sharing from ordinary source is already banked, and §164-11 already gives the cure (write the body out k times; never collapse to one switch reached by goto). (2) The target's own bytes show a textbook §193-C suffix merge, not an exotic shape: I read asm/nonmatchings/800/func_8001B0D4.s (0x158 = 86 ins) and case 0 keeps its OWN lui/lw of D_800747E4 at 8001B114/B118 and its own addiu $v1,$zero,1 in the j delay slot, joining only at .L8001B18C — i.e. only the common suffix merged, exactly §224's "write the duplicate". I also verified every survivor's position against §162g's LATER-copy law from the real tables in asm/data/6324C.data.s (jtbl_80072B24 slot4=0x8001B208; jtbl_80072B3C = B154,B1D8,B184,B218,B184,B218,B218,B218,B168; jtbl_80072B64 = B208,B1D8,B1D8,B218,B1D8,B218,B218,B218,B208): B1D8 survives in case 2's region (later than case-1's copy), B208 in case 4 (the last arm), B184/B18C in case 1 (later than case 0). Three for three forward merges into the last-emitted copy — §162g exactly, no new pass behaviour. Weighing the UNPROVEN gate: the draft never reached 0, so the residual's cause is inferred, not measured; the only measured facts are that a §249/§268 pin took 68→45 and a §222.1 arm reorder took nins 84→86 — both already banked levers. The genuinely new observable — element-wise entry overlap ACROSS sibling tables, including an outer table's entry reappearing inside a sibling case's inner table — is a real sharpening of §164-11's identical-vector tell, and I folded it into the entry as the corrective prescription rather than as a new law, because §164-11's mechanism (expand_end_case emits one vector per switch; do_cross_jump only redirects and deletes insns) already predicts it and the transcript never tested the prescription it implies (duplicate the statements longhand in each arm). Banking this as written would have told future agents that a shape we have already matched and gated is unreachable from C.
RETRIEVAL FAILURES this round (not new knowledge — a RETRIEVAL defect)
These transcripts re-derived a law the book ALREADY states, by grinding rather than by grepping. Per the book's own standing guidance, a high COVERED count is the signal to fix RETRIEVAL, not to write more prose:
- Standalone bitmask statement scrambles register-copy/move order around a call —
func_800CDBA8. Covered by: §165-19 (primary — THE BIRTHING BOOST IS A REGISTER LEVER TOO, L14191) + §162d1 (THE ANONYMOUS TEMP IS A SINGLE-SET PSEUDO, L11135) + §30 #3 (THE "BIRTHING-BO. Refs the agent actually grepped: NONE — ground it out empirically - Block-scope callee decl defuses a later-in-TU definition (order-dependent solven —
func_800CB00C. Covered by: §41 point 3 (L2806-2812) — the lever verbatim; with §55a (L4150-4154) + §56 tactics 2/3 (L4244-4258) for the order half, and §136f-1 (L9220-9231) + §56 tactic 4. Refs the agent actually grepped: NONE — ground it out empirically
S66 HARVEST, ROUND 4 (P31, 2026-08-30) — the O31 overlay lane (hardest band, first auto-bucket wave)
1 NEW, 5 ADDENDUM, 1 COVERED from 9 mechanism-advertising transcripts of 19.
§319 — N TEXTUALLY DUPLICATED return v; TAILS PUT THE LAST or AND THE RETURN-REGISTER MOVE IN ONE BASIC BLOCK, SO combine FOLDS THEM AND YOU ARE EXACTLY −1; A goto done; JOIN WITH REAL INCOMING EDGES RESTORES THE SEPARATE addu $v0,$a0,$zero (P31 S66 round4; byte-proven func_801809F0)
(COMPOSES two already-banked laws onto an object neither of them names — a value-carrying return tail. The mechanism half is §36's CROSS-BB COMBINE LAW (L2508, flow.c:2087), whose three banked instances are a loop BACKEDGE (§36), a call-argument copy preference (§50-D, L3645) and an andi mask on a pinned value (§179-G, L17601) — none a return. The C-dial half is §194-N's SURVIVING-CODE_LABEL law (L19669): a bare done: or an adjacent goto done; done: is deleted by jump1 and buys nothing. SIBLING, OPPOSITE PASS: §45-Lever-D (L3343) and §17's shared-ret0 note (L1364) already use the same goto-to-a-shared-return dial, but both govern a return CONSTANT and both price it in sched1 hoisting / cross-jump clustering / branch polarity; this one governs a computed return value and is decided in combine. Distinct from §277 (return-tail spelling → delay-slot topology), §164-25 (occupy $v0 to keep the return insn out of a load-delay slot; a pin), and ADD-4/§225-3 (which constant lands on which edge).)
THE TELL. match_one says LENGTH-DRIFT/-1 at low-to-mid-20s closeness, every branch and j target downstream of the missing word is off by one, and the one absent instruction is a move into the return register: the target carries or $a0,$a0,$vN immediately followed by a separate addu $vD,$a0,$zero just before a shared epilogue / lw $ra sequence, while your draft shows the OR writing the return register directly (or $v0,…) with no move at all. Read a −1 whose missing word is a return-register copy as a control-flow verdict, not a scheduling one — do not route it to the permuter.
THE MECHANISM. With the tails written out as N independent return v; statements, each arm's final v |= … and that arm's own load of the return register sit in one basic block, so combine has a LOG_LINK to hang on (LOG_LINKS(y) is created only when BLOCK_NUM(y)==blocknum, flow.c:2087) and merges the two insns into a single or $v0,$a0,$v0, deleting the copy — one instruction shorter than the target, per arm that survives cross-jumping. Route the arms to one done: label instead and the OR stays in its arm's block while the return-register load lives in the join block; there is no link across the boundary, the fold cannot fire, and the explicit addu $v0,$a0,$zero survives ahead of the shared epilogue — which is exactly the shape the target's own cross-jumping left behind. Per §194-N the label must have a real incoming edge: the gotos must be non-adjacent (arms above it), or jump1 deletes the jump and the label and you get a false negative against a correct law.
THE C SHAPE.
/* WRONG — three independent return sites; combine folds each OR into $v0 (−1) */
if (c1) { v |= f1(); storeA(); return v; }
if (c2) { v |= f2(); storeB(); return v; }
v |= f3(); storeC();
return v;
/* RIGHT — one shared label with real multi-way incoming edges */
if (c1) { v |= f1(); storeA(); goto done; }
if (c2) { v |= f2(); storeB(); goto done; }
v |= f3(); storeC();
done:
return v;
BYTE EVIDENCE. func_801809F0 (ov_SC05_005 family, 135 ins). .run/O31/opus/scratch_func_801809F0/w_AA_nopin.c → near 24 134 LENGTH-DRIFT/-1; j1.c → match 0 135 MATCH, residual [], klass MATCH. The two files differ by five lines and nothing else (verified by diff on disk): two return v0; → goto done; in the inner arms, plus a done: label above the function's last return v0; — i.e. three return SITES collapsed to one join. Nothing else changed between the 24-closeness draft and the byte-exact one: same declarations, same s32 v0, no pins, no barriers, same statement order.
BOUND / HONESTY. (1) The A/B is n=1 and pure-C; no -dc/-dj dump was taken for this function, so the fold is attributed to the already-banked flow.c:2087 law rather than re-derived here — if you have a counterexample, dump .combine before trusting the attribution. (2) The target .s is not in the asm/ tree for this symbol; the or/addu tell above is read off match_one's residual, not off a hand-read listing. (3) The fall-through arm needs no goto — the label's edges come from the other arms; adding an adjacent goto done; immediately above done: is a zero-byte no-op (§194-N). (4) This is not a licence to prefer the join everywhere: §162g/§164-69/§88a still decide duplicate-vs-join for a tail the compiler will merge for you, and a call-bearing tail is a different regime. The discriminator for this section is narrow — a −1 whose missing instruction is the return-register copy and whose target keeps the OR in a non-$v0 register.
Addendum — Masked-OR field-merge plateaus at close=10; bitfield store clears it (func_8017FD64)
Addendum (P31 S66 round4, func_8017FD64): Second byte-proof, different binary — src/ov_SC01_080/ov_SC01_080_jr_8017AE2C.c:5594 (133 ins, sprite-billboard OT emitter, MATCH), banked with exactly this bullet's prescription (typedef struct { u32 addr : 24; u32 len : 8; } P_TAG_FD64; then ((P_TAG_FD64 *)pkt)->addr = ((P_TAG_FD64 *)otp)->addr; ((P_TAG_FD64 *)otp)->addr = (u32)pkt;). The routing tell for when you cannot read the preheader: a five-variant match_one sweep over the merge plateaus at closeness 10–12 with {"klass":"REGALLOC-PERM","sig":"REGALLOC-PERM/$v0>$v1>$v0,$t2>$t3>$t2"} — a clean two-cycle swap of the ior destination and of the paired lui/ori mask registers, everything else byte-identical. Both operand orders of the hand-written merge land there (natural → 20, swapped → 22, → 10 once unrelated diffs cleared), and a fourth arm this bullet does not enumerate — hoisting *otp & 0xFFFFFF into a plain temp — is strictly worse: {"klass":"OPCODE-MIXED","pairs":[[114,"and","sw"],[115,"sw","and"]]}, closeness 12. The bitfield form goes to 0. BOUNDS §137: a REGALLOC-PERM verdict sitting on a two-mask read-modify-merge block is NOT §137's two-compile allocno_compare window problem, and no zero-byte __asm__ placement reaches it — the register pairing is downstream of the bitfield store evaluating its RHS bitfield-extract first (which is what frees loop.c to hoist 0xFFFFFF ahead of 0xFF000000), so change the C shape and do not go compute a priority window. Mechanism unchanged and still owed to expmed.c:556 above; this session opened no -dl/-dg dumps and re-derived nothing at the RTL level.
Addendum — Integer-space address arithmetic is a THIRD no-movable spelling, and a distant SYM[0] re (func_8017D89C)
Addendum (P31 S66 round4, func_8017D89C): §164-26 closes with "It is the only spelling that reaches it" (the 2-D extern T SYM[][2] + SYM[i][0] form), and §185b hardens that into a recorded blocker — a ((s16 (*)[2])SYM)[i][0] cast and a typed pointer local "each hoist a base load", so a function whose TU forbids the retype is written off. A third spelling reaches the same no-movable state and needs no retype: do the address arithmetic in INTEGER space and cast the result — v = *(s32 *)((s32)D_801AA37C + (j << 2));. This is §164-01's lever ("do the addition in INTEGER space and cast" — two integer operands go through build_binary_op untouched) aimed at the movable-existence question rather than at operand order: the array never decays, expand_expr's case ARRAY_REF is never entered, no ADDR_EXPR base pseudo is emitted, and scan_loop has nothing to record. §164-26's own byte-refuted control is the discriminator and must not be read as covering this: *(char**)(D_8010F468 + b*8) still hoists because pointer_int_sum rebuilds it as a pointer add with the pointer at operand 0 (§164-01) — a cast to s32 before the add is not the same edit as a cast to char** after it.
A SECOND TELL, visible without counting the frame. §164-26 keys its tell on "your frame is one callee-saved register heavy" (frame 0x34 vs 0x30). Here the fingerprint shows up at a distant, non-loop site instead: a straight-line read of element 0 of the same array, several blocks earlier, comes out as 3 instructions — lui $rX,0x0 / addiu $rX,$rX,0 / lw $rY,0($rX) — where the target has the clean 2-instruction lui %hi(SYM) / lw %lo(SYM)($rX) fold. That is the §42c-lever-3 / §165-27g held-base-vs-%lo axis (L3089, L20153), but fired by the loop's subscript form, not by that read's own spelling or its extern decl. Do not fix the outer read — do not flip its decl per §42c lever 3, do not reach for §165-27g's D[0] dial. Fix the loop; the outer read repairs itself.
Byte evidence (single-axis A/B, pinned triple). v1, both sites written D_SYM[idx] → {"status":"near","closeness":37,"nins":129}. v2, the only change being the in-loop read respelled *(s32 *)((s32)D_801AA37C + (j << 2)) with the outer read left verbatim → {"status":"near","closeness":4,"nins":128}. The 37→4 collapse is the whole fix; the residual 4 → match 0 came from an unrelated reorder (hoisting o = i << 3; above j = i + 1; so the right statement duplicates into the preheader/delay slot — §162e2's body-order law, not this one).
⚠ MECHANISM NOT ESTABLISHED, and the one control is weak — cite the fix, never a cause. Repointing the outer read at a different symbol with the loop form unchanged scored 38 (worse than 37), which was offered as ruling out shared-symbol CSE; but swapping a symbol moves relocations and the entire allocation, so it isolates nothing and the "not CSE" conclusion does not follow. Why a subscript inside the loop perturbs a straight-line read of the same array upstream of the preheader is open — no cc1 -dL / .i.loop dump was taken, which is the two-minute close (it names every movable, moved / not desirable, and would say directly whether the outer read's address became a related value). n=1, one function, no replication.
Addendum — LENGTH-DRIFT −1 on a coalesced-away copy: a CALLER-SAVED SOURCE pin can resurrect it, boun (func_8017D72C)
Addendum (P31 S66 round4, func_8017D72C): ⚠ UNPROVEN — match_one standalone only; the whole-binary byte-gate REFUSED this draft (docs/backlog.md row 24 already ledgers this function as "match_one MATCH but the whole-binary gate rejected — CAUSE NOT DETERMINED"). Do not cite it as byte-proven, and do not spend the lever before the cheaper alternatives below.
The target (asm/ov_SC01_080/nonmatchings/ov_SC01_080_jr_8017AE2C/func_8017D72C.s, 8017D80C-8017D818) is RC-12's own symptom in its purest form: lbu $v0,%lo(D_80186F8C)($at) / addu $a3,$zero,$zero / addu $s0,$v0,$zero / sh $v0,0xFC($s1) — the copy's DEST is the callee-saved survivor and the store consumes the SOURCE register, exactly the "consumer reads the LOAD, not the COPY" shape item 1 is written for. Three adjacent 1-mismatch near-misses are the same root cause: sh $s0 vs sh $v0 (REGALLOC-PERM/$s0>$v0), andi $s0,$v0,0xff vs addu $s0,$v0,$zero (OPCODE-MIXED), and, when one temp is used twice, the copy vanishing entirely (LENGTH-DRIFT/-1?, 131 vs 132).
The counter-instance. A single-axis A/B on the same C — one temp t loaded once, stored to +0xFC and also assigned to a callee-saved-resident local — went -1 unpinned → MATCH (132 ins), closeness 0 with nothing added but register s32 t __asm__("$2") on the SOURCE. That contradicts item 1's flat "do NOT pin the copy's source", and it falsifies §164-22's BOUNDS §42c-6 discriminator ("does the pinned source have a def insn inside the function? If yes, a source pin is dead weight") — here the source's def is the lbu, in-function, and the pin was load-bearing. It corroborates §136-3 ("v1 = f(); rnd = v1; with v1 pinned") at a LOAD source rather than a call result.
Bounds, and what was never tried. RC-12's own pin-free register s32 zr __asm__("$0"); n = t + zr; was not run on this function, nor was §194-B/§193's width lever — and §193 is explicit: "Read §136-3's pin as sufficient, never necessary; try the width lever first," while §194-B byte-proved a register s32 result __asm__("$2") pin inert on its own exemplar. The pin also fails dedup_propagate.compiles_standalone and forfeits the family (§37 / §162p rung 3). Finally, the submitter's mechanism — "pinning only the surviving side does not stop the coalescing" — is unmeasured: no survivor-pin arm was run (the $s0 residuals came from structurally different C, not one-axis ablations). Carry the source-pin arm; do not carry the survivor half.
Addendum — Return-0 statement order: extra j+move vs delay-slot fusion (func_8018BB50)
Addendum (P31 S66 round4, func_8018BB50): a second byte-proven instance of the mirror row, this time with a CALL in the taken arm — ADD-4 was n=1 (func_8017EF64). func_8018BB50 (ov_SC04_019, banked at src/ov_SC04_019/ov_SC04_019_jr_80188E1C.c:5082-5085) ends if (func_80184BAC(2, 0x12) == 0) { return func_80184E44() != 1; } return 0; — computed predicate in the taken arm, bare return 0 trailing → MATCH, 59/59, closeness 0. The early-return spelling if (func_80184BAC(2, 0x12) != 0) { return 0; } return func_80184E44() != 1; gives 61 vs 59, match_one LENGTH-DRIFT +2, closeness 16; the two extra instructions surface here as an unconditional j <epilogue-label> followed by a move $vN,$zero (addu $vN,$zero,$zero) tail block wedged between the last real computation and lw $ra/addiu $sp/jr $ra — the same 2-instruction pair ADD-4 describes from the other side ("the beqz slot empties to nop and a trailing move v0,zero block appears"). Two riders for the next agent: (a) the taken arm is a value return of a call comparison (return f() != 1;) and that spelling costs nothing here — §195-N's value-form tax requires N≥2 chained tests (its BOUND 1 makes N=1 byte-identical), so at N=1 only the ARM PLACEMENT moves bytes. (b) Do not generalise this to "always write the constant return last": §167-27's law is the arm you write FIRST is the epilogue-adjacent one, and §3-T4's CdReadRequest (305 → 0 by flipping to if (busy) return 0; … return cdReq_result;) is the byte-proven opposite-direction instance — read the target's block order and put the target's no-j arm inside the if. The same function's mid-block v0 = v1 < 5; if (!v0) goto ret0; v0 = 0; ret0: return v0; is §16's shared-ret0-goto / §178-B lever (route return-0 exits through one goto'd shared tail), not a new law, and its own transcript evidence is self-contradictory — the isolated cc1 repro reports frame −24 for both spellings of that instance, so nothing there is byte-proven beyond what §16/§178-B already state.
Addendum — Counter zero in the DECLARATION slot, empty for-init — the prologue SHIFT-DRIFT/+K face of (func_8018275C)
Addendum (P31 S66 round4, func_8018275C): §211's S58b addendum item 4 ("declaration order of two independent inits IS the scheduler's emission order") and item 3 (func_801824E4, splitting i = 0; out of the for-init) have a prologue face with its own residual signature, and a second, cheaper spelling: leave the for-init EMPTY and give the counter a declaration initializer — s32 ret = 0; s32 i = 0; u8 *base = …; then for (; i < N; i++).
THE TELL — an equal-count SHIFT-DRIFT/+K whose shifted run is the counter's own save+zero pair. nins_mine == nins_tgt, no LENGTH-DRIFT, registers already correct, and a contiguous prologue run offset by a small constant: the target carries sw $sN,off($sp) / addu $sN,$zero,$zero for the loop counter sandwiched between the other locals' save+init pairs, while your draft emits that pair K slots later. That is not an allocator tie and not §264-2's save-order-only residual (L24985): when the shifted run contains the counter's own zeroing insn, do not spend §264-2's bb0 control-flow surgery or its S63 def-move — the counter's write is simply in the wrong source position.
MECHANISM — no new pass; this is the LUID = source-position law already banked. §165-14 (L14062) states the operative half verbatim: "the for-init is emitted where the for statement sits … so it carries the same LUID position" — i.e. after every declared local's initializer. A declaration's initializer is expanded at the declaration's textual position, so moving the zero into the decl block moves its insn up to that slot, and rank_for_schedule's final INSN_LUID (tmp) - INSN_LUID (tmp2) fallback (L2491 / §25) keeps the tied zero-dependence sets in that order through sched2's prologue weave. Reject the distiller's framing that a declaration-with-initializer is zeroed by a separate DECLARATION-LIST/block-entry pass: that is unsourced, and the ordinary source-position law already predicts the observation. No RTL dump was taken; the position sensitivity is byte-proven, the attribution is inherited, not re-verified here.
BYTE EVIDENCE — func_8018275C (ov_SC01_001, 134 ins). v4c with s32 i; + for (i = 0; …): DIFF mine=134 / target=134, 14 mismatched, SHIFT-DRIFT [structural] sig=SHIFT-DRIFT/+2, target idx3-4 = sw $s5,0x2C($sp) / addu $s5,$zero,$zero. Single-axis edit — s32 i;→s32 i = 0; and for (i = 0; …)→for (; …), nothing else — → MATCH 134/134, closeness 0, residual []. Shipped shape banked at src/ov_SC01_001/ov_SC01_001_jr_8017D2DC.c:5731 (s32 ret = 0; s32 i = 0; … for (; i < 0x14; i++), with the $2/$17 pins unchanged across the A/B).
SCOPE. This is the second witness for item 4 (which was n=1 on func_801AAA6C, md_SC07_004) and the position-only face of item 3 — on func_801824E4 the same edit bought a register change ($s1 vs $s0); here the colours were already right and only the emission slot moved. Item 5's negative still stands and does not conflict: an initialiser-list i = 0 was measured inert on func_801ACD4C where the problem was a constant floating above an address materialisation, not a decl-order slot.
S66 HARVEST, ROUND 5 (P31, 2026-08-30) — M3 (main) + O41 (overlay) lanes
4 ADDENDUM, 3 COVERED from 12 mechanism-advertising transcripts of 26.
Addendum — The dying-pinned-register reuse on a sign-extend chain: route every later read through a s (func_80186868)
Addendum (P31 S66 round5, func_80186868): §164-62's tell fires on a sign-extend chain, and this body pays it off with a third cure beside §80-R7's keepalive: pin the CONSUMER and route every later read through it. Draft sll $a0,$a0,0x10 / sra $a0,$a0,0x10 / slti $a0,513 / beqz $a0 against target sll $v0,$a0,16 / sra $v0,$v0,16 / slti $v0,0x201 / beqz $v0 — match_one REGALLOC-PERM/$a0>$v0, map {"$a0":"$v0"}, zero length drift — is exactly §164-62's andi $a0,$a0,0x1 ; beqz $a0 / §167-12's sll $s1,$s1,16 picture with an sll/sra pair as the consuming insn: the abs result is pinned (register s32 dx __asm__("$19"), §40's abs-pin, needed so cse keeps negu $19,$v1), its inline (s16)dx read is its LAST, and local-alloc hands that about-to-die hard register straight back out as the extend's own scratch. The lever used here is neither the keepalive nor another abs pin: it is register s32 v0 __asm__("$2"); plus rewriting EVERY later read as v0 = (s16)dx; ahead of its use (v0 = (s16)dx; p = v0*v0;, never p = (s16)dx * (s16)dx;) — and the pin and the routing are ONE lever, because a pin whose read stays inline REGRESSES. Byte path, all at nins = 142 on the pinned triple: d7 closeness 3 (three negu regalloc residuals) → d8, pinning all three abs results (dx=$19, dy=$4, dz=$18) with their range/square reads left inline, regressed to 15, klass REGALLOC-LOCAL, residual [52,"sll a0,a0,0x10","sll $v0, $a0, 16"] → d9 (unpin dy) 12 → d10 (route only the dx/dz square-multiply reads through v0) 1, residual [[38,"negu a0,a0","negu $a0, $v1"]], i.e. the one still-unpinned dy → d11 (re-pin dy, range-check read still inline) regressed to 4, sig REGALLOC-PERM/$a0>$v0, residual exactly the sll/sra/slti/beqz block at lines 52-55 → d12 (route the dx/dy/dz range checks through v0 as well) {"status":"match","closeness":0,"residual":[]}. Read this as §165-47's composition law applied at the read site rather than at a three-address insn, and let it bound the S66 func_8017F498 addendum's routing note (L30470): that card measured "pin the destination instead" as a 2 → 72 regression and routes this residual to the volatile keepalive — the discriminator is the CONSUMER, not the symptom. A 3-operand subu plus its dependent sh, with one consuming site, takes §167-12's keepalive; a sign-extend/compare chain off a pinned value that is read at several later sites takes the pinned-temp routing, applied at every one of them (leave one inline and you are back at d11's 4). ⚠ Mechanism un-dumped. The transcript's combine_regs-ties-the-dying-input story is §80's own law re-derived from the emitted registers, not a new trace: no -dl/-da/-dr was taken here (unlike the §195-H bullet the same agent cited), §164-62's two free confirmations (';; Register N in H.' in .lreg, REG_DEAD on the consuming insn) were not run, and the two cures were never A/B'd against each other on this body — so ship the recipe, not the attribution.
Addendum — Volatile zero-byte asm slider scrambles the prologue schedule (func_801814B0)
Addendum (P31 S66 round5, func_801814B0): §47's Placement rule offers exactly ONE escape from the barrier — "next to an existing volatile asm (GTE-heavy functions are full of them). A bare asm("") elsewhere is a cse table-flush + sched barrier + a maspsx #APP hop-killer (§42) — the classic perturbation trap." That escape does not exist in a call-bearing, GTE-free function with no volatile asm to hide beside. There is a second escape: spell the slider as the register-tied NON-volatile re-tie __asm__ ("" : "=r" (x) : "0" (x));. It is still one extra static insn inside the live range (the §47 denominator dial), but it is a non-volatile ASM_OPERANDS, so by §194-A's predicate table (sched.c:1953) it is not a scheduling barrier — §164-62 already says the quiet part ("a bare asm("") with no colons is ASM_INPUT … and IS a barrier — that is why §47/§153's barrier language does not carry over"); this card supplies the constructive use.
THE TELL THAT YOU REACHED FOR THE WRONG SPELLING — the fix for the REGISTERS visibly breaks the ORDER. Inserting the volatile/colon-less form flips the residual from a small saved-register pair swap (verdict.klass="MIXED", $s3↔$s4 over 7-9 lines) into a whole-prologue SCHEDULE-REORDER — sw $ra, sw $s2, sw $s1, sw $s0 and the counter addiu $sN,$a1,-1 all land in the wrong relative order (span [3,9]) — and the numeric closeness drops (9 → 6) while the residual class gets strictly worse. Read the class, not the number: a slider that changes the class from register-assignment to instruction-order has fenced sched2, not mis-slid the priority.
Byte evidence. func_801814B0 (ov_SC06_029, banked at src/ov_SC06_029/ov_SC06_029_jr_8017C954.c:4764): param_1's and i's pseudos tie exactly in global.c:594 at pri = 14·10000/120 = 1166, so creation order gives param_1 the $s3 the target wants for i; the re-tie makes it 14·10000/121 = 1157 and splits the tie the right way — {"status":"match","closeness":0}, the only point closeness reached 0. Controls: v1 straight port = 9 (MIXED, $s3/$s4); v2 bare __asm__ __volatile__("") at the same point = 6 (SCHEDULE-REORDER); v3/v4 the p = param_1; copy dial in both statement orders = 7 (ADDRESSING/addiu!=addu) — i.e. §211/§208's "second named local" lever does not reach this fork, and the $s3/$s4 problem resurfaces under it.
⚠ Honest scope. The re-tie is also a second RTL SET of the pseudo, so §34's reg_n_sets==2 birthing-boost kill (L2409 — itself a documented cure for a wrong prologue/init ORDER) is not separated from the +1-live-length story by this evidence; the operational rule holds under either attribution, but do not cite the arithmetic as proven mechanism without a -dl dump read. Composes with §257-8 / §269 ADD-1: the volatile-ness of an interposed asm is a per-shape ~20-second A/B, never a rule carried between functions — this is one more card where the non-volatile polarity wins, on the same prologue-save-order symptom.
Addendum — Register-pinned helper pointer local regresses closeness; use plain local (func_80013CFC)
Addendum (P31 S66 round5, func_80013CFC): Item 6's A/B now has a numeric tell and a placement half. The tell: your draft's nins is exactly +1 over the target's and the extra insn is an unexplained register copy (move $aX,$vY) sitting inside a SHIFT-DRIFT/regalloc residual — that is a function-scope pin colliding with another value's natural occupancy, not a missing or extra statement, so re-read the pin before re-reading the C. Measured, solo-ablated: closeness 19 (nins 102 = target, after a §194-A zero-byte fence) → add register s16 *rotp __asm__("$2"); register s16 *matp __asm__("$16"); → 80, nins 103, residual led by [17,"00402021 move a0,v0"] — cos_val/sin_val naturally want $v0/$s0, and per §215-add-5 the pin's whole-function scope (§72: a preference, not a reservation, but priced function-wide via §80's unconditional qty_phys_sugg) shoves one of them out through an extra copy; reverting the pin alone returned it to 19. The placement half: replacing the pins with plain unpinned s16 *rotp; s16 *matp; and passing those — never rot/matrix — to every gte_SetRotMatrixX/gte_ldclmvX site closed it to {"status":"match","closeness":0,"nins":102,"residual":[]}, because the local's assignment statement is itself the scheduling anchor: putting rotp = rot; and matp = matrix; at the source positions where the target hoists addiu $v0,$sp,0x30 and addiu $s0,$sp,0x10 reproduces the target's register choice and its position for free. This is §162e2's "the local exists only to order the hoist" (L11199) and §162o2's statement-distance law (L11716) read in a straight-line, non-loop body, on an sp-frame address handed to inline-asm GTE macros — neither parent covers that. BOUND: the pin regression is a clean one-axis A/B; the winning edit is compound (two locals + call-site rewrites + placement, 19 → 0) and was not solo-ablated per §266, so "the assignment position is the anchor" is inferred from the target's hoist sites, not isolated.
Addendum — §194-A addendum — the bare/colon-less fence measured NULL and only the "memory"-clobber fo (func_8017E464)
Addendum (P31 S66 round5, func_8017E464): the "prefer the bare fence" default has a measured counter-instance — ⚠ UNPROVEN (byte-gate refused the draft; match_one masked closeness only, floor 3/144, MATCH never reached; no -dS/-dR taken, so no gcc-source cause is owned for the differential). At §194-A's own AFTER position (immediately below nn = (s32)buf;) the two spellings this section tells you to prefer measured byte-identical to no fence at all: v7 fence-free, v9 __asm__(""); and v13 __asm__ __volatile__(""); all returned closeness 10/144, ADDRESSING/andi!=addiu, the same residual list. Only v14 __asm__ __volatile__("" ::: "memory"); moved it — closeness 4/144, class flipping to REGALLOC-PERM/$a0>$a1>$a2. This does not refute the predicate table: I re-read sched.c:1943-1985 and it is exactly right — ASM_INPUT takes the barrier path because code != ASM_OPERANDS, so all three forms fence identically in sched_analyze_2. The equivalence is a sched-dependence-graph equivalence and nothing more. The clobber form additionally emits (clobber (mem:BLK (scratch))) (stmt.c:1656-1663), which is live in every pass before sched — cse's memory-table flush (§21 L1835-1845, §193-E L20377) and §189-C's write-memory pin path (sched.c:2035-2042 → :1736-1790). So BOUND 4's list of what the clobber "drags in" is not only a cost: on a residual whose cause sits outside sched's dependence graph it is the entire lever. Operational rule: a NULL from the bare/colon-less form at a correct after-position is not evidence the fence class is dead — sweep the "memory" form before filing the card barrier-inert. This is the direct counter-measurement to the S66 func_80182D1C addendum's prediction at L30527 ("the "memory" clobber is almost certainly not the ingredient"), which was itself unproven. Why this card still did not close, and where to go next: the steered value is a call argument (func_80015978(aa, nn), target addiu $a0,$sN,K / addiu $a1,$sp,K), which is §190-C's case, and §190-C already says the fence is "both halves together — arg registers pinned above a __volatile__ memory clobber", because plain s32 locals get folded into the call's own arg setup and sink below a lone fence. The missing pins are the leading explanation for the stall at 3/144 (SCHEDULE-REORDER/3). Run §190-C's -fno-schedule-insns2 attribution and add the arg pins before spending anything else here — and note §190-C never A/B'd bare-vs-clobber, so this instance is also the first evidence that its clobber spelling is load-bearing rather than incidental.
RETRIEVAL FAILURES this round
- match_one MATCH but gate rejects: self K&R decl conflict —
func_8017D8C8. Covered by: §174 Law 4 (L16706/L16748, "the DEF-side prototype is a wave-prompt law too") — with the K&R/default-promotion half owned by §14e (L1319) and §99 (L7201), and the same fix stated as the PARAMS axis of - Chained-assignment direction sets which same-value store emits first —
func_800139C8. Covered by: §145(c) — "Chained assignment emits stores RIGHT-TO-LEFT" (docs/matching-cookbook.md L9990); reinforced by §205 — "THE CHAINED ASSIGNMENT IS ITS OWN SCHEDULING DIAL" (L22098) and its §205 addendum (P3 - Struct field needs opposite alias regime per switch-case, not per-struct —
func_80181F0C. Covered by: §162q1 (L11758) — "THE/sGRANT IS A PER-SITE EDIT, NOT A SHAPE REWRITE (bounds §30 / §135-2)"; supported by §30 #1 (L2405, both grant AND deny directions), §30a-1 (L2415, the flag is set by the ACC
§145(c) has now been rediscovered FOUR times (func_800CBE28 at L21953, the ds1 note at
L29636, and now func_800139C8). The verifier checked and cleared the index — §205 is indexed
three times and its title spells *b = *a = v;. The gap is VOCABULARY: agents grep the
symptom ("stores in the wrong order", "SCHEDULE-REORDER") and the owning sections are titled
in mechanism language. Grep bait added below.
S66 HARVEST, ROUND 6 (P31, 2026-08-30) — the CAST-AT-USE RECONCILE lanes
27 agents over two lanes (7 main + 20 overlay) on drafts the gate had DROPPED for in-TU declaration conflicts — not codegen failures. 24 reconciled to MATCH; 19 banked. Distilled from the agents' own byte-evidenced notes.
§320 — THE §43 "RETURN-TYPE FLIP PAIR" IS NOT TU-EDIT-REQUIRED: THREE DRAFT-ONLY ESCAPES (P31 S66; byte-proven func_800CCBC0, func_800D30D0, func_800D2A24)
(BOUNDS §43 and §183 item 4, which both record this shape as an immovable needing a TU edit. It is not. Three independent agents broke it three different ways in one lane, each byte-proven.)
THE SHAPE. The destination TU declares extern void f(...) — often through a DEFINE_ macro in
engine_core.h, often with the sole caller discarding the value — while the target's own asm
plainly materialises a result in $v0 on every exit (addu $v0,$zero,$zero in a branch-delay slot,
addiu $v0,$zero,1 before the epilogue). Compile the byte-true s32 body and cc1 rejects the TU
with conflicting types. Compile it as a true void and gcc-2.7.2 dead-codes exactly those two
materialisations — you lose 1-2 instructions and the diff is all downstream of them. §43 called
this a pair with no draft-side move. It has three.
ESCAPE 1 — the §202 DEF-SIDE-RETURN alias (cleanest; prefer this). Bind the byte-correct s32
definition to the real link symbol and leave every TU declaration untouched:
s32 aF800CCBC0(void) __asm__("func_800CCBC0");
s32 aF800CCBC0(void) { ... } /* the byte-true body, unchanged */
Byte-proven twice: func_800CCBC0 (md_MAIN_047, MATCH 138/138 first try) and func_800D30D0
(md_MAIN_003, MATCH 76/76 — which ALSO needed the TU's old-style extern void func_8005C604();
adopted verbatim, because a varargs prototype is incompatible with a no-prototype declaration under
C89 6.5.4.3).
ESCAPE 2 — hold $v0 live across a bare return; (when the alias is unavailable). Keep the
void signature the TU demands, and materialise the return value into a pinned $v0 that an
input-only asm barrier keeps alive:
void func_800D2A24(...) {
register s32 v0 __asm__("$2");
...
if (early) { v0 = 1; goto ret; } /* NOT inline in the if body — see the bound */
v0 = 0;
ret:
__asm__ __volatile__("" :: "r"(v0)); /* zero bytes; stops the dead-code elimination */
return;
}
BOUND, measured: the early-exit's v0 = must sit behind a goto to a SHARED exit label. Setting
it inline inside the if body bloats that block and flips the branch polarity — NEAR closeness 10
inline vs MATCH closeness 0 via the shared exit.
ESCAPE 3 — adopt the TU's old-style declaration. Where the TU's own forward decl is K&R
(extern void func_800CB428();), match THAT rather than writing a full prototype: MIPS passes
word-sized args in $a0-$a2 identically, so no cast-at-use is needed (func_800CB1CC, MATCH 47/47;
sibling callers in the same TU already call it with 3 args under that same decl).
WHEN A TU EDIT IS STILL THE RIGHT ANSWER — and how to prove it safe. If every call site discards
the result, widening the declaration void -> s32 is byte-neutral, and you must MEASURE that, not
argue it: apply the edit with NO draft substituted and rebuild — main must still produce
143dbb89f34491258bbc27810d0a12ec8b43a8dd. Done twice for func_8002A544/func_8002A2D4/
func_8002A7B4 in src/800.c; 2 of the 3 then banked. Prefer escapes 1-3 where the TU is shared —
the alias has the smaller blast radius.
PROCESS TRAP THAT COST A PASS. gate_main SNAPSHOTS AND RESTORES the destination TU between
passes, so an UNCOMMITTED TU edit is reverted before the gate ever sees it. Measured: the first
attempt banked 4 of 7 and left all three return-type targets as stubs; committing the (proven
neutral) decl change first and re-gating banked 2 of 3.
§321 — FILE-SCOPE DUPLICATE ANONYMOUS-STRUCT TYPEDEFS ARE A HARD ERROR; THE SAME TEXT AT BLOCK SCOPE IS A WARNING (P31 S66; byte-proven func_80180728, func_8017F9F8, func_8017E07C)
THE TELL. conflicting types for 'D_xxxxxxxx' or a redefinition error naming a struct typedef,
where your draft's declaration is TEXTUALLY IDENTICAL to one already in the TU. Two anonymous struct
definitions are two DISTINCT types to gcc-2.7.2 even when spelled the same, so a file-scope
duplicate is fatal.
THE DIALS, all three byte-proven in one lane:
- Move the duplicate to BLOCK scope. A block-scope redeclaration of the same extern symbol is
only a warning here, and is codegen-neutral (
func_80180728, MATCH; verified both standalone and spliced viartu_match). - Or hoist to file scope and delete the local copy — the mirror move, when the TU's own
declaration already sits at file scope right before your INCLUDE_ASM slot (
func_8017F9F8, MATCH 117 ins, valid against BOTH ov_SC01_005 and its ov_SC01_006 twin). - Name a local typedef for its OWN address, never a neighbour's.
SVEC_8017E158belongs tofunc_8017E158; reusing that tag insidefunc_8017E07C's draft is a real two-anonymous-structs conflict once spliced. UseSVEC_<own addr>(func_8017E07C, MATCH).
Do not reach for a cast-at-use here — the shape already matches; the conflict is type IDENTITY, not type WIDTH.
§322 — A PROBE THAT ANSWERS A NECESSARY BUT NOT SUFFICIENT QUESTION WILL PRICE BLOCKED WORK AS FREE: RUN THE REAL PLANNER WHEN THE PLANNER IS PURE (P31 S67; measured, 96 of 159 open jtbl functions)
THE SHAPE. jtbl_carve --probe called only island_probe, which answers where does this
table live — a table in the data tail returns tail — standard §8a carve at gate time, and that
reads as "carveable". It is necessary and not sufficient: build_carve, the real planner, ALSO
refuses a plan whose two same-subseg .rodata carves would be non-contiguous, because one
object cannot leave a hole for an uncarved neighbour's table. island_probe cannot see that — it
looks at one function, and the refusal is a property of the SUBSEG's whole carve set.
WHAT IT COST. The S66 free-wins audit priced 32 jtbl functions as "free now, run them through
the serial gate" on this probe. Re-probed with the planner: ov_SC03_010:func_8017F6C0 probes a
clean tail and the gate books CARVE-REFUSED; ov_SC02_000:func_8017F950, explicitly on that
free list, refuses too. Fleet-wide the honest numbers are 159 open functions reference a jtbl
(30% of the 530 frontier, 57 binaries, 72 (binary,subseg) hosts) → 96 plan-refused · 43 carveable ·
16 main curated-name ERR · 4 main-manual, with 75 of the refusals non-main across 38 subsegs.
A whole session's lane was scoped off a probe that had never been asked the blocking question.
THE FIX, AND WHEN IT IS AVAILABLE. build_carve is a PURE planner — it reads the config and
the payload and writes nothing (verified: no open(...,'w'), no subprocess, no os.rename in
its body), and it signals refusal by sys.exit(msg). So the probe can simply CALL it inside
try/except SystemExit and report the message as plan-refused. Cost: nothing. Blast radius: none.
Generalise: whenever a cheap probe and an expensive applier disagree about feasibility, check
whether the applier's DECISION half is separable from its MUTATION half. If it is, the probe must
call it. A probe that models the applier loosely is a second oracle that silently disagrees (R34),
and the direction of its error — optimistic — is the expensive one, because optimistic probes
create work plans, and pessimistic ones only lose opportunities.
THE TELL THAT YOU HAVE ONE. A batch whose verdicts are uniform (13 of 13 near) is a harness
smell, not a subject signal. Real codegen residuals scatter.
§323 — CARRYING FILE-SCOPE TYPES BETWEEN SPLIT TUs: DEDUPE BY NAME, SKIP HEADER-PROVIDED, AND THE GUARD TRAVELS WITH THE BLOCK (P31 S67; ov_SC02_000, two defects fixed, one open)
When a TU is cut into regions (jr_isolate_all, o0_subsplit), each region becomes its own
translation unit and needs the file-scope types its carried prototypes name. overlay_src_split. file_scope_types() returns those blocks per ITEM, and the carried layer unions over every item
feeding the region. Three ways that goes wrong, all measured on one overlay:
- N copies of one definition. Several items each define the same tag at file scope — legal
while they were separate TUs, fatal in one. Measured:
struct sprite8emitted FOUR times intoov_SC02_000_jr_80187B40.c→redefinition of struct sprite8, the whole overlay's build down. FIX: dedupe by declared NAME with bodies normalised of comments and whitespace (the copies here differed only in hand-written field comments). Two DIFFERENT bodies under one name are a real conflict a rename must resolve — refuse loudly (R43), never merge: emitting either one silently decides what the region compiles against. Anonymous typedefs are exempt — §321 says two identically-spelled anonymous structs are DISTINCT types, so there is nothing to dedupe against. - Re-emitting a type the shared headers already define. Every region
#includes engine_core.h → engine_types.h at its top, so carrying a definition of a type that lives there is an unconditional redefinition.struct sprite8is engine_types.h:417, and_engine_types()has recognised body-defined tags since the SESSION-19PW8017E6D8fix — nothing on the carry path was consulting it. FIX: skip a block whose names are all header-provided. - OPEN — the preprocessor guard does not travel. With 1+2 fixed the overlay compiles and links
and then fails
make check(3ef423b5… vs the locked 5ece4bca…).file_scope_typesextracts the block by brace-scanning from a col-0 type keyword, so a definition sitting inside#if …/#endifis carried WITHOUT its guard — the originalstruct sprite8inov_SC02_000_jr_8018173C.chas its#endifon the following line. Unresolved; start here before scaling this lane.
STATUS: the isolation lane is NOT byte-validated. 20 of 35 blocked overlays dry-run clean and the tool advertises byte-neutrality by construction, but the one overlay actually taken to the byte gate needed two fixes to compile and still diverges. Do not scale on the dry-run count.
§323a — R53, TWICE IN ONE HOUR: A FAILED BUILD LEAVES THE PREVIOUS BINARY, AND sha1sum READS GREEN (P31 S67)
make build BINARY=ov_SC02_000 returned rc=2 and sha1sum build/ov_SC02_000/ov_SC02_000 printed
the LOCKED hash both times — because the failing object was deleted and the previous link survived.
Trusting the output file over the exit code would have certified a resegmentation that does not
compile as byte-neutral, and scaled it to 38 subsegs. Read the exit code; then read the errors, and
filter warnings out first — gcc-2.7.2's hard errors carry no error: prefix and the one line that
killed cc1 sits mid-stream (the tail of the log was pure warnings in both cases).
§323b — A SCRIPT THAT PARSES argv AT IMPORT CANNOT BE SHARED; EXTRACT THE PREDICATE, DO NOT COPY IT (P31 S67)
match_one.py builds its ArgumentParser and calls parse_args() at module level, so
import match_one from any other tool exits with that tool's own argv as an error. The -O0
prologue tell (detect_o0) lives there and is the oracle every drafting agent's feedback loop
steers by. Re-implementing it in the caller would be a second oracle that can drift from the first
(R33) — the exact defect class §322 describes. FIX: extract to tools/o0_detect.py verbatim and
re-export from match_one, so there is still ONE definition. Negative-control BOTH directions
after the move: an -O0 target must still auto-fire (md_MAIN_003/func_800CFEB4, 78-vs-78 regalloc
diff) and an -O2 target must still MATCH (ov_SC04_018/func_80181804, 72 ins).
Why it was worth doing: wiring that one predicate into the stranded-draft classifier turned
md_MAIN_003's verdicts from 8 NEAR (seven of them >20 mismatches) into 6 MATCH — seven residuals
that were 100% artefact of compiling -O0 code at -O2.
§324 — THE ARITY-SELF WALL NEEDS BOTH HALVES: A K&R DEFINITION AND A NO-PROTO TU DECL; EITHER ALONE STILL FAILS (P31 S67; rtu-proven ov_SC01_005/func_8017FBCC, 14 instances measured)
THE SHAPE. The draft defines void func_X(s32 arg0). The TU declares extern void func_X(void);
and calls func_X();. The definition splices in ABOVE both, so cc1 checks the call against the
DEFINITION's prototype and says too few arguments to function func_X. Measured on the S67 stranded
population: 14 of 63 residuals are this class (7 naming the function itself, 7 a callee) — the
largest MECHANICAL class left after §8d clears the data-decl conflicts.
WHY EACH HALF ALONE FAILS.
fix_arity_callers --any-proto(already in the gate ladder) no-protos the caller's declaration — but the call is checked against the visible DEFINITION, not that declaration, so nothing changes. This is why the ladder's arity pre-pass does not clear this class despite being aimed at it.- A K&R definition alone (
void func_X(arg0) s32 arg0; {…}) removes the definition's prototype, but the TU'sextern void func_X(void);IS a prototype, and C89 requires a K&R definition to be compatible with any visible prototype —(void)vs one promoteds32is not. Result:conflicting types. This is exactly the S66 measurement "mechanical K&R converts only 4/26 — many TUs declare a real PROTOTYPE, which a K&R definition cannot match". The 22 it could not convert were not a wall; they were the half-fix.
THE RECIPE (both edits, together).
draft: void func_X(s32 arg0) { -> void func_X(arg0) s32 arg0; {
TU: extern void func_X(void); -> extern void func_X();
Now the definition carries no prototype, the declaration carries no prototype, they are compatible,
and the 0-argument call is unchecked. Test it without touching the tree using rtu_match's own
pre-edit directive — that is what it is for:
//@EDIT extern void func_X(void);||extern void func_X();
as the first line of the candidate, then rtu_match <fn> --split <TU stem> --source <bin> --c <cand>.
WHAT IT BUYS, HONESTLY. It converts "cannot compile" into "measurable codegen residual", not
into a bank. func_8017FBCC lands at 97 vs 97 instructions, 4 mismatched, and the residual is
pure register allocation (move $a0,$v0 where the target has addu $v1,$v0,$zero) — i.e. it hands
the function to the §17 register-pin lever at closeness 4. Route accordingly (F20): ≤3 → pins or the
wall ledger, 8-20 → long-budget permuter. Do not price this rung as 14 banks.
BLAST RADIUS. The TU-side half edits shared-within-binary source, so it needs the same journal/revert discipline as the arity pre-pass: keep the no-proto only where it bought a match, revert it everywhere else, and let the whole-binary gate arbitrate (G3/P9).
§325 — REGALLOC: A SHARED SMALL CONSTANT STORED TWICE IN THE PRE-LOOP BLOCK IS A local-alloc $s-OCCUPANT THAT STEALS THE ARGUMENT ALLOCNO'S REGISTER — PIN THE ARGUMENT-DERIVED LOCAL, NOT THE CONSTANT (P31 S67; byte-proven ov_SC03_119/func_80184F18, 153 ins)
The twin gave the whole shape in one compile (147 -> 24 residual); the last 24 were a pure $s2/$s3
swap that no pin-free dial reaches. WHY: a CSE'd HImode constant written twice in block 0
(desc.v[0].vz = 3 ×2) is a SINGLE-BLOCK, call-crossing quantity, so local-alloc assigns it a
callee-saved register BEFORE global-alloc runs, and find_free_reg always takes the first free one —
pushing the argument allocno from $s2 to $s3 and cascading. There is no 4th block-0 call-crossing
local that could die before the $a2 copy is born, so no source-level reordering reaches it.
THE LEVER, AND ITS DIRECTION IS THE POINT: pin the ARGUMENT-DERIVED local
(register s32 u __asm__("$18") on the uv-base local), NOT the constant. Pinning the constant to
$19 only reached closeness 15. §175's caller-saved-pin correctness trap does not apply to $s2.
§326 — DEFEATING ADDRESS-CSE: SPELL TWO READS OF THE SAME HALFWORD DIFFERENTLY AND GCC CANNOT SHARE THE ADDRESS (P31 S67; byte-proven ov_SC01_077/func_8017FAAC, 154 ins)
The target keeps three separate lui/addu/l* $at groups where gcc wanted one shared base. Writing
one read as D_8018A9B2[i] and the other as *(s16*)(D_8018A9B0 + i*4 + 2) yields DIFFERENT address
rtx for the same location, so address-CSE cannot merge them and each keeps its own %hi/%lo pair.
Same function also needed §186: moving the 0x1A += before the 0x16 -= 0x40 put the if-arm's last
store in $v1 vs the else-arm's $v0, blocking the jump2 tail-merge that was costing 2 instructions
(cross-jump runs AFTER scheduling, so only a post-schedule register difference blocks it).
§327 — A RANGE TEST MUST BE HImode: WITH s32 + A (u16) CAST GCC PROVES THE MASK REDUNDANT AND DROPS THE andi (P31 S67; byte-proven ov_SC02_039/func_8017EC34, 154 ins)
s32 q + (u16)q lets fold-const derive q = 0x7F ± (s16 x >> 1) and conclude the mask cannot
change the value, so the andi never emits — a REAL −1 length drift that reads like a schedule
difference and sends you hunting the scheduler. Declare u16 q and test q >= 0x2F && q < 0x80.
Three more levers from the same function, all reusable:
- §194-H in-place AND.
v1 &= 0x2000;insideif (v1 != 0)— only the IN-PLACE form reuses the hard reg, which is what lets dbr speculate it into the guard's delay slot. - §3-T2 chain order. Put the LONG chain (
p->0xA += D_8019B5E6[i]) BEFORE the short store (p->0x34 = s1+1); sched1 re-swaps the twoshs itself, and the leadinglhforces the genuine nop delay slot (may_trap_pon a MEM blocks dbr) while freeing $a0 for the table value. - Build the 2nd argument INLINE in the call with a
(u16)cast. As a preceding statement (u16 code = ...; & 0xFFFF) its sll/ori/or chain outranksmove $a0,$s1in sched priority and steals the beqz delay slot.
§328 — THE VOLATILE ALIAS MUST BE AN OBJECT, NOT A CAST: *(volatile s32*)&sym UNFOLDS %lo INTO A SEPARATE addiu (P31 S67; byte-proven ov_SC07_007/func_80181B8C, 156 ins, NEW LAW)
Four identical lui/lw D_801C79A0; j LDDC; addiu tails needed TWO levers together:
(1) register __asm__("$2") pins on cases 0 and 3 — cc1 otherwise homes them in $v1, missing the
depth-2 merge onto .L80181DDC and deep-merging case 0 into case 3 at the shared jal; and
(2) an __asm__-aliased volatile OBJECT view of D_801C79A0 for case 3's reload only, to block
the back-merge into case 1's identical tail.
THE LAW: the volatile must be a declared aliased object (extern volatile s32 vD_x __asm__("D_801C79A0");).
Spelling it *(volatile s32*)&D_801C79A0 unfolds the %lo into a separate addiu and costs +1 ins.
§329 — fold-const NARROWS (int)s16_var & 0xFFF ONTO THE RAW HImode PSEUDO, BREAKING THE SIGN-EXTEND/MASK REGISTER TIE — A ZERO-BYTE WIDENING TEMP RESTORES IT (P31 S67; byte-proven ov_SC01_084/func_80183244, 157 ins)
gcc-2.7.2 narrows the AND onto the raw HImode pseudo (andi $s2,$a1), so the sign-extended value and
the masked value land in different registers and the difference cascades — measured 27 register rows
plus 3 schedule rows from this one cause. FIX: s32 e = t; — a named SImode temp that forces ONE
sign_extend, so local-alloc coalesces the fraction onto $s1 (andi $s1,$s1) while sh $a1,0xDC
keeps the raw register. 30 residual rows -> 0. (The neighbour §194-E gave the TU shape first: 70->30.)
§330 — THE NEIGHBOUR-SHAPE LEVER IS THE CHEAPEST FIRST MOVE, AND IT IS UNDER-USED (P31 S67; four independent instances in one 20-function wave)
Before any codegen reasoning, read an ALREADY-BANKED function in the same TU and copy its SPELLING, not just its structure. Measured this wave:
md_MAIN_031/func_800CAE0C— the neighbour 6 lines away contained the same 3-call body verbatim (same symbols, same(void*)casts). Copied its call spelling -> MATCH on first compile.ov_SC01_084/func_8017EF28— the neighbour's memory-to-memory house form (*(u16*)(p+0xE2) = *(u16*)(p+0xE2) + 0x70) replaced a reusedint vlocal and dissolved ALL 18 $v0/$v1 REGALLOC-PERM rows in ONE compile — no §137 arithmetic, no pins.ov_SC01_084/func_80183244— neighbour gave the exact TU shape, 70 -> 30, nins exact.main/func_8002A088+func_8002A7B4— mirroring the already-banked in-TU twinfunc_8002A2D4's local shape (separate i/off locals, subtraction inlined as the call arg) produced the correct 0x18 frame; the warm-start body had been 4-off on frame size alone. The corollary that costs functions when ignored: a warm-start body from ANOTHER binary is often worth less than the neighbour 20 lines away.func_8017FAAC's warm start was a different function entirely and was discarded.
§331 — OPEN GAP: NO LEVER ELIMINATES AN UNWANTED DUPLICATE COPY AT A BRANCH-TARGET BLOCK HEAD (P31 S67; main/func_80013154, closeness 12, NOT solved)
nins matches 43/43. An inline-asm-forced sy=y copy needed at the "ax!=ay" block head gets
DUPLICATED by gcc into two hardregs: $t1 in the bne's delay slot (correct) and a redundant $t3 at
the block's own head (wrong), cascading into idx10-16, a register-role swap with nx at idx25-28,
and $t3-vs-$t1 at the final subu. Tried and REFUTED: 5 sy-placement variants, register pins
($9/$8/$10), an explicit-register asm constraint (cc1-fail), and substituting (s16)sy for ay in
the later comparisons (regresses to 46 ins). ~16 iterations.
§164-36/§31-lever-2 cover asm at a branch-target block head + delay-slot fill, but every listed lever
FILLS AN EMPTY SLOT; none REMOVES A DUPLICATE. This is a genuine hole in the knowledge base — record
it as such rather than spending another wave slot on it blind.
§332 — THE maspsx la-IN-A-DELAY-SLOT GAP: gcc EMITS A SYMBOLIC ADDRESS LOAD AS ONE ATOMIC length-2 INSN, SO IT CAN NEVER FILL A JUMP DELAY SLOT — 6 FUNCTIONS FLEET-WIDE, NONE BANKABLE FROM C (P31 S67; byte-traced main/func_80062144, func_8005DBD8)
THE MECHANISM, traced to the compiler sources, not inferred. The target fills jal func_X's
delay slot with addiu $a1,$a1,%lo(D_80078CE0) — the second half of a la. gcc-2.7.2's
movsi_internal emits a symbolic address load as ONE atomic RTL insn of length 2 (the lui+addiu
macro pair; NO HIGH/LO_SUM define_split exists in this MIPS backend — verified in
tools/reference/gcc-2.7.2/mips.md, mips.c, reorg.c, and mips.md's own comment says so).
eligible_for_delay requires length == 1, so the define_delay gate categorically REFUSES it as a
filler. cc1 therefore leaves it under .set reorder/macro, and maspsx
(tools/maspsx/__init__.py:1054-1057) unconditionally emits a real nop after every branch/jump in
reorder mode. The retail split is genuine ASPSX macro-hopping — the assembler moving the la's
low half into the jal slot — which maspsx does not replicate.
BYTE-VERIFIED: running maspsx.py --aspsx-version=2.56 --expand-div over cc1's raw -dS output
reproduces the extra nop exactly. The C is not the problem.
THE COST OF NOT KNOWING THIS: func_80062144 reads closeness 29 and every one of those 29
mismatches is a single length-drift consequence of this one nop. A closeness-29 verdict looks like
a badly wrong draft and invites a redraft; it is a one-instruction toolchain gap.
POPULATION: 6 occurrences fleet-wide, NONE banked — asm/nonmatchings/800c2/func_80061FA8.s ×2,
libgte3 PopMatrix/PushMatrix, md_MAIN_034/func_800CB00C, main/func_80062144. They are
source-unreachable pending a NARROW maspsx patch (hop a length-2 macro's low half into an otherwise
empty delay slot). Until that patch exists: ledger them, never draft them.
§332a — MAIN'S RESIDUAL FRONTIER IS CONTAMINATED WITH TOOLCHAIN WALLS: 4 OF 7 IN ONE WAVE (P31 S67, measured)
Wave s67m2_1 (7 main functions, 50-68 ins) returned 1 MATCH and 6 NEAR — but the NEAR verdicts
were not drafting failures:
func_8005FA94— §188 epilogue wall.oracle_reorder.pybypass +as -O2gives 0/55 diffs: the C is byte-correct and the pinnedas -O1can never emit it.func_8005D244— same, 0/62 diffs. AND §188 Law 3 already names this exact address as libpadpdent3.o— an SDK object that belongs topsyq_integrate.py, not to hand-matching. It should never have been in a draw at all.func_80062144,func_8005DBD8— the §332 maspsx gap above. Onlyfunc_80021174(closeness 14, a scheduler/cross-jump tie) andfunc_8005F290's idx50 are genuine open matching problems.
THE LESSON FOR DRAW POLICY (R45 — never draw a card the pipeline cannot bank): main's cheap
population is spent, and what remains is enriched in toolchain walls. A draw that does not filter
oracle_reorder-proven §188 functions, the §332 maspsx set, and SDK-object addresses will spend an
increasing share of its slots on functions no model can bank. Filter before drafting, and expect
main's apparent "match rate" to be an artifact of that contamination rather than a model signal.
§333 — FRAME SIZE IS SET BY DECLARED AGGREGATES, NOT USED ONES: AN UNREFERENCED TRAILING LOCAL IS A REAL DIAL (P31 S67; three independent byte-proven instances in one wave)
gcc-2.7.2 slots every aggregate local in declaration order regardless of whether it is used, so a frame larger than the live locals explain is a DEAD trailing aggregate in the original source. This is a dial you set, not a residual you chase:
ov_SC03_121/func_8017D8D4— three SVECTORs (sp+0x10/0x18/0x20) gave 0x30, target was 0x38. Declaring an unreferenced FOURTH SVECTOR last fixed 6 of 13 residuals.ov_SC04_016/func_8017DF8C— an unuseds32 pad[2]raised var_size 16 -> 24 and reproduced the target's 0x50 frame: worth 30 of 32 residual rows, because gcc allocates that slot before caller-save's and shifts all four spill offsets 0x10 -> 0x18.ov_SC05_008/func_8017DF68— needed 16 bytes of unreferenced aggregate locals at 0x28-0x37. Read it off the frame arithmetic first: if.frameexceeds what your declared live locals require, add the missing bytes as a trailing unused aggregate before touching anything else.
§334 — A RELOAD SPILL SLOT IS ROUNDED TO BIGGEST_ALIGNMENT (8B), SO ONE SPILLED 4-BYTE PSEUDO CAN GROW THE FRAME BY 16 (P31 S67; byte-proven ov_SC05_008/func_8017DF68, 82 -> 53 residual)
sw $a1,0x60($sp) looked like a volatile-parameter home; it is a RELOAD SPILL. alter_reg calls
assign_stack_local(mode, size, -1), and the -1 means BIGGEST_ALIGNMENT — 8 bytes on MIPS — used
for both the alignment AND the size rounding. So a single spilled 4-byte pseudo took 68 bytes of
locals to var_size 80 and the frame from 0x88 to 0x90.
Companion lever from the same function: two-temp field updates
(tx = v.vx + 8; ty = v.vy - 8; v.vx = tx; v.vy = ty;) emit lhu/lhu/addiu/addiu/sh/sh instead of
serial lhu/addiu/sh, and that reshuffles global.c's log2(n_refs)*n_refs/live_length priority
enough to change WHICH pseudo is the spill victim — closing the remaining 53 rows for free.
§335 — AN extern u16 A[] READ AT A VARIABLE SUBSCRIPT ALLOCATES DEAD STACK TEMPS (~8B PER ACCESS) THAT INFLATE THE FRAME WHILE EMITTING ZERO EXTRA INSTRUCTIONS (P31 S67; byte-proven ov_SC06_025/func_8017EA74, closeness 141 -> 20)
The instruction stream is identical either way, so this is invisible in a diff of the body — it
shows up only as .frame vars being too large, which then moves addiu $sp and EVERY saved-register
offset. Spelling the same table as an 8-byte-stride struct array
(extern struct { u16 a, b, c, d; } A[];) costs zero temps and is what the original wrote.
One spelling change moved 121 of 141 residual rows. After it, the frame closed exactly at 0x68
with a plain s32 buf[6].
§336 — THE §5a CROSS-JUMP BARRIER GOES AT THE BOTTOM OF THE TWIN, NOT THE TOP: find_cross_jump WALKS BACKWARD FROM THE CONVERGING JUMP (P31 S67; byte-proven ov_SC05_001/func_80183C9C)
A zero-byte __asm__ __volatile__("") placed at the TOP of the duplicated block is simply walked
past — the seven instructions below it still merge. find_cross_jump compares BACKWARD from the
point where the two paths converge, so the barrier must sit just before the shared goto TAIL,
at the bottom of the twin. Same wave, ov_SC01_084/func_80181F80: the barrier bought −4 instructions
once placed correctly.
§337 — THE CC1-ONLY BLOCKER CLASS: blocker_probe's STATIC ORACLE REPORTS "none" AND THE WHOLE-TU COMPILE STILL FAILS (P31 S67; ov_SC04_011/func_801827DC, md_MAIN_027/func_800CB4A4)
Two byte-correct bodies this wave were rejected for declaration conflicts the STATIC analyzer could
not see, and it said so — blocker "none" — while real cc1 died:
func_801827DC— the TU declaresextern void *D_80194500[]at file scope (line 6005) and the draft redeclared it[][3]. Fixed with the TU's own §183__asm__-label alias (extern void *a4500[][3] __asm__("D_80194500");), preserving the 2-D index shape.func_800CB4A4— the draft'stypedef ... Blk8;collides with one atmd_MAIN_027.c:314, which sits AFTER the INCLUDE_ASM site. Fixed by dropping the typedef and adopting the TU's house spelling (memcpy((void*)(s3+0x154), &D_800CC298[0], 8)). The lesson is about the oracle, not the drafts: a "none" from the static analyzer is not evidence of no blocker — it is evidence the analyzer is blind here (R34). Always finish on the real whole-TU compile.
§338 — jtbl_carve._sltiu_bounds MISREADS A NON-SWITCH sltiu AS A BOUNDS CHECK, OVER-SPANNING THE TABLE (P31 S67; ov_SC06_022/func_80185B80, byte-diagnosed)
The body was MATCH 185/185 with the real-cc1 probe reporting blocker "none"; the gate still refused
it. Cause: _sltiu_bounds returned {6, 2049} because case 0's own sltiu 0x801 comparison —
ordinary program logic, not a switch bound — was read as a second bound. The len(bounds) == 1 clamp
gate then refused to clamp, leaving jtbl_801E0C3C spanning 7 words against 6 emitted, a 4-byte
.rodata under-fill. Two stale card facts were corrected in passing (the table is in tail19, not
tail20; the carve is a 2-table span needing JTBL_PADS := 0,4), and per §322 build_carve returns
PLAN OK — so this is a distinct defect from the plan-refusal class.
§339 — A 2-CASE SWITCH OMITS THE LOW-BOUND RANGE TEST, SO THE PRESENCE OF slti/bnez BETWEEN THE beqs IS A COUNT TELL FOR A THIRD CASE NODE (P31 S67; byte-proven ov_SC02_005/func_80190538, 197 ins)
stmt.c's emit_case_nodes carries the rule in its own comment — "omit the conditional branch to
default if we avoid only one right child" — so a switch with TWO case nodes emits no low-bound
check, while THREE emits one. That makes the range test a structural count you can read off the
target, not a detail to reproduce by luck:
beq $v1, 1, ... <- case 1
slti $v0, $v1, 2 <- THIS PAIR ONLY EXISTS IF THERE ARE >=3 CASE NODES
bnez $v0, default
beq $v1, 2, ... <- case 2
Seeing slti/bnez sitting BETWEEN two beqs means a case node is missing from your draft. Adding
case 0: return; supplied exactly the missing 2 instructions here. Read the case count off the
range test before you start rewriting arms.
Three more dials byte-proven on the same function, all reusable:
- A frame's "holes" can be struct padding, not scalars. Slots 0x20/0x24/0x28 + 0x30/0x38 with
gaps at 0x2C/0x34/0x3C are TWO 16-byte VECTOR-shaped structs whose
padmembers are the holes — not five independent scalars. Cf. §333: frame arithmetic is evidence about DECLARATIONS. A - (B + C)tree-folds to(A - C) - B, which flips anaddiusign. An explicit temp forB + Cblocks the fold.- Declaring temps INSIDE each if-arm instead of at function scope is a REGISTER dial. A
function-scope pseudo spans both arms (ptr=$a2 / dx=$a1 / dz=$a0); block-scope gives the target's
ptr=$a1, first-computed=$a0, second=$v1. Paired lever: putting
v.vy = 0;at the END of each arm rather than after the join keeps the join block'slw $a2,0x24($sp)a REAL load instead of letting cse turn it intomove $a2,$zero.
§340 — §194-K COROLLARY: FLIP THE FALSE EDGE YOU CANNOT DELETE. A "scheduler" residual can be sched.c's ALIAS ORACLE emitting a FALSE true-dependence; source order chooses its DIRECTION (P31 S67; byte-proven ov_SC03_107/func_8017CF48, 10 -> 0 in one compile, zero bytes)
THE MISDIAGNOSIS THIS FIXES. A SCHEDULE-REORDER/10 residual was not the scheduler's priority
heuristic at all — it was memrefs_conflict_p returning a dependence that does not exist.
THE MECHANISM, traced. The §194-K re-tie __asm__("" : "=r"(tp) : "0"(tp)) is MANDATORY here
(without it reg_n_sets == 1 lets update_equiv_regs rematerialize the address and the function is
212 ins, not 206 — §194-K bound 3's coupling, measured at exactly ±6). But that second SET has a
side effect: reg_known_value[tp] falls back to (reg tp), so canon_rtx becomes a no-op, and
memrefs_conflict_p (sched.c:614) walks PAST its frame-pointer arm (:640-664, where
CONSTANT_P(y) would have returned 0) to the terminal return 1. Result: a FALSE true-dependence
between (plus (reg tp) -8) and (plus (reg sp) 0x12), pinning sh $v0,-8($s0) ahead of
lh $a1,0x12($sp).
THE LEVER. §194-K's own bound 7 says the edge's DIRECTION is whatever the source order wrote. So do not fight the false edge — reverse it into an anti-dependence by hoisting the load above the store:
t0 = f(p[-4], buf[0], 4, 1); u1 = buf[1]; p[-4] = t0;
t1 = f(p[-3], u1, 4, 1); ...
The store then sinks past all four argument setups and reorg steals it into the call's delay slot.
Zero bytes added, 10 residual rows to 0 in one compile.
REFUTED EN ROUTE (do not re-try these):
- the symbol-base spelling — fixes the ORDER but costs +6 instructions of rematerialisation;
- the
MEM_IN_STRUCT_Proute —true_dependence's/sescape is UNREACHABLE here, because a stackARRAY_REFis always/s(expr.c:4888); - the pointer-local load spelling.
GENERALISE: when a residual looks like scheduling, ask whether the dependence is REAL before reaching for priority dials. If the alias oracle invented it, the cheapest fix is not to remove the edge but to make the source write it in the direction you want.
§341 — AN HImode STORE TEMP REWEIGHTS A sched2 TIE-BREAK THAT NO STATEMENT ORDER CAN REACH (P31 S67; byte-proven ov_SC03_006/func_801823B8, last 4 ins)
After §224 cross-jump duplication closed 29 instructions, the final 4-instruction residual was a
sched2 tie-break, and no statement-order or s32-temp spelling moved it. The lever was the MODE
of the temp: declaring the 0x34 increment's temp as u16 produces a genuine HImode store (rather
than a subreg:HI of an SImode pseudo), which reweights that chain's ALU→store edge — the 0x34 load
then leads while the stores stay 0x100-first.
The general shape: when statement order is exhausted on a schedule tie, the remaining dials are the ones that change the RTL's shape rather than its order — operand mode (§341), a widening temp that forces one named sign-extend (§329), or an in-place operator that reuses the hard register (§327's §194-H). A tie that no ordering reaches is usually asking for a different mode, not a different sequence.
§342 — A void * PARAMETER CAST TO ITS REAL TYPE IN A LOCAL IS NOT BYTE-NEUTRAL WHEN A LATER PARAMETER ALSO NEEDS A CALLEE-SAVED REGISTER (P31 S67; byte-proven ov_SC07_002/func_80181394, NEW LAW)
The banked twin's house spelling — void *a1 in the signature, then a local
s16 *param_2 = (s16 *)a1; — reads as a pure retype and is usually free. It is not free here: it
sinks a1's entry copy below a2's, inverting the prologue's copy/save order. Declaring the
parameter as s16 *param_2 directly restores the target's a1-then-a2 order.
Copy a twin's spelling, but re-derive its PARAMETER TYPES against your own signature — the twin's
void * may be load-bearing for ITS register pressure and actively wrong for yours (cf. §330: the
neighbour lever is about structure and idiom, not a blind transcription).
Companion from the same function, worth its own line because it inverts §333:
do NOT hand-add an apparent 8-byte vars gap — gcc had already padded it, and adding a dead
local produced frame 0x70 against the target's 0x68. §333 says a frame LARGER than your live locals
explain means a missing declared aggregate; the converse does not hold. Compute what gcc pads before
you add anything.
§343 — decl_prior's FLEET MAJORITY CAN BE WRONG ABOUT THE TRUE SIGNATURE — READ THE RIVALS, NOT JUST THE WINNER (P31 S67; measured on func_8012BD14 / func_8012D624 / func_80143C74)
The card's decl_prior reports what the FLEET DECLARES, ranked, with rivals. That is not the same as
what the function IS. Measured this wave:
| symbol | card's fleet winner | n | rival | truth per the asm |
|---|---|---|---|---|
func_8012BD14 |
void (s32) |
1374 | s32 (s32) ×163 |
returns s32 |
func_8012D624 |
void (s32) |
1322 | s32 (void*,s32,s32) ×74 |
3 args, returns s32 |
func_80143C74 |
void (s32,s32) |
90 | s32 (s32,s32) ×42 |
returns s32 |
Verified independently: void func_8012BD14(s32 a0) really does appear ~1427× in src/. The tool
is honest; the CORPUS is wrong. A loosely-typed engine propagated a void spelling into a thousand
call sites, and a majority vote over those call sites reproduces the error at high confidence.
HOW TO USE THE FIELD: treat the winner as the house spelling (what the destination TU probably
writes), and the RIVALS as evidence about the true signature — a rival with a materially different
return type or arity, backed by dozens of sites, usually means the majority is a propagated mistake.
The .s decides: a jal whose result is consumed returns a value however many TUs say void.
Cf. §43/§99/§320 (adopt the TU's decl) — the TU's spelling is what must COMPILE; the asm is what must
MATCH, and when they disagree you need the alias/no-proto escapes, not a corrected majority.
§344 — RAISE A BIV'S global_alloc PRIORITY WITH A ZERO-BYTE REFERENCE INSTEAD OF PINNING IT; PINNING THE COUNTER KILLS LSR ENTIRELY (P31 S67; byte-proven ov_SC03_121/func_80180E64, 222 ins)
Writing the loop in INDEX form makes the pointer an LSR giv, so strength_reduce emits its init
AFTER move_movables' hoisted constant — which is the order the target has. That leaves a
counter-vs-giv register swap ($a0/$a1), and the obvious fix is wrong:
registerpin on the counter → kills LSR entirely (the biv stops being a biv).- Zero-byte
__asm__ __volatile__("" :: "r"(i))in the loop body → correct. It adds a FIFTH reference to the biv, lifting itsglobal_allocpriority (floor_log2(n_refs)*n_refs/live_length) above the giv's, and costs no instructions.
The general move: when two quantities are competing for a register and one is loop-structural, change its REFERENCE COUNT rather than its assignment. A pin overrides the allocator; an extra reference persuades it, and only the second leaves the loop optimizations intact. Cf. §334, where the same priority formula was moved by changing which pseudo got spilled.
§345 — A VOLATILE STORE EVICTS THE MEM FROM cse AND KEEPS sh; A VOLATILE LOAD BLOCKS combine AND DEGRADES lh INTO lhu+sll+sra (P31 S67; byte-proven ov_SC01_084/func_80181A7C)
For a store→reload pair at the same address, the volatile qualifier is not symmetric and the two placements give different instruction streams:
- volatile STORE — keeps the
sh, evicts the MEM from cse so the reload is real. Wanted. - volatile LOAD — blocks
combine, so the sign-extendinglhdegrades intolhu + sll + sra(+2 instructions). Not wanted. Then the ordinary ladder finished it: splitting reused C variables into distinct pseudos moved the allocno priority (refs*log/live_lengthagain) 59 → 4, and §219's in-placeh += 4closed 4 → 0.
§346 — c ? X : -X TAKES expand_expr's COND_EXPR SINGLETON PATH (copy, then negate IN PLACE) — AN if/else STATEMENT GIVES THE TWO-ARM FORM (P31 S67; byte-proven ov_SC03_102/func_80180C38, closed the last instruction)
gcc-2.7.2's expand_expr recognises a conditional whose arms differ only by negation and rewrites it
as "copy X to the target, then negate the target in place":
c ? X : -X -> negu $a0, $a0 (singleton path, one register)
if (c) a = X; else a = -X; -> negu $a0, $s0 (two-arm, separate source)
So the ternary and the if/else are not interchangeable here — the register operands differ. Pick
the spelling that matches the target's negu form.
Companion from the same function: expand_assignment expands the LHS address BEFORE the RHS, so
p = *(s32*)(a0+0x20); followed by a re-read inside a rand()-bearing RHS produces the target's two
separate lw 0x20($s2) per group rather than one CSE'd load.
Also recorded because it saves a compile: the TU declares D_800AE620 one way and func_80181260
later declares it as Blk20_80181260. A two-body compile confirmed cc1 only WARNS (exit 0) and the
bytes are unchanged — there is no -Werror in the Makefile or gate_stage. A warning is not a
blocker; check the exit code before rewriting a declaration to silence one.
§347 — LOOP REGISTER ASSIGNMENT IS A DECLARATION-ORDER + LIVE-RANGE DIAL: FIVE COMPOSABLE LEVERS, 178 -> 0 (P31 S67; byte-proven ov_SC06_029/func_8017EF34, 243 ins)
One function exercised five independent loop/branch dials in sequence. Each is reusable on its own, and the ORDER they were applied in is the ladder:
- DUPLICATE shared tails per-case and let cross-jumping re-merge them (178 -> 53). Writing a
hand
gototo a shared label mislays the blocks and leaves the argument setup in the wrong basic block. Write the tail out in each arm; gcc merges it back correctly. (Cf. §224/§5a — this is the same law seen from the authoring side.) - Sequential guarded
ifs, NOTif/else if(53 -> 34) — gcc-2.7.2 swaps the else-if chain. - Initialise the POINTER before the counter, and order the body
p++; i++(34 -> 14). Loop register allocation follows LIVE-RANGE-LENGTH priority, so settingifirst pushes it to$a1. - ONE shared counter + a DISTINCT pointer variable per loop (14 -> 6) — that makes the counter
the top allocno, so it takes
$a0while still being written first, reproducing the target's preheader order. - Assign an AND mask to an explicit local between
i = 0and the pointer init (6 -> 0) — soaddiu $a2,-3precedes thelainstead of being appended by loop-invariant motion.
THE UNIFYING RULE: gcc-2.7.2 has no live-range splitting — one C variable is one hard
register for its whole life. So the register assignment in a loop is chosen by (a) how many distinct
variables you declare, (b) their declaration/initialisation ORDER, and (c) their live-range lengths.
Splitting one reused variable into several, or merging several into one, is the primary register dial
— not pins. Same session, ov_SC03_105/func_80182DCC: three separate range locals fixed all 39
register mismatches at once, for exactly this reason.
Companion from that function, worth its own note: sp20[1] -= 0x20; sp20[1] -= i*4; must be
SEPARATE statements on an s32 temp. Any single expression lets gcc associate 0x20 into the giv,
and loop.c strength-reduces 0x20 + 4*i into one register, while a bare 4*i giv sits below the
worth-while threshold.
§347-addendum — A THIRD INSTANCE, AND THE SHARPEST STATEMENT OF THE RULE (md_MAIN_025/func_800CB300, 243 ins)
Reusing generic v0 / v1 locals across UNRELATED blocks makes them GLOBAL pseudos, and
global-alloc pins a global pseudo to one hard register FOR THE WHOLE FUNCTION. Splitting them into
per-use locals (hv / bit / flags / st) fixed three separate register residuals at once
(idx 89-92 and 197-214) and was what flipped the chained-assignment block from near-8 to MATCH.
That is now three independent byte-proven instances of the same law in one session
(§347 lever 4, ov_SC03_105/func_80182DCC's three range locals, and this):
a Ghidra-style reused scratch variable is a register bug waiting to happen. One variable per
purpose, always — it costs nothing when it does not matter and fixes whole blocks when it does.
Also from this function, an ablation record worth keeping: §205's chained assignment
*(u16*)(s0+0x18) = *(u16*)(s0+0x1A) = hv; is the ONLY spelling that both creates the surviving
addu $v1,$v0,$zero copy AND leaves sched1 free to hoist sll/sra/slti above the two sh.
Six ablations refuted: explicit copy, re-set-source per §220-4, boolean temp, hv = (s16)hv
pre-sign-extend, +=, and the plain two-statement form (which is exactly the copy short, 242 ins).
§343-addendum — SECOND INSTANCE OF THE WRONG-MAJORITY DECL (same function)
The switch bound is sltiu, so func_801633A8 must return u32 — while the card's decl_prior
fleet row says s32. Same shape as §343: the corpus majority is a propagated spelling, and the
INSTRUCTION decides. An unsigned compare on the return value is direct evidence of an unsigned
return type.
§348 — THE BASE SPELLING PICKS THE ADDRESSING MODE: A SYMBOL GIVES THE 3-INSN lui/%lo FORM, A POINTER VARIABLE GIVES THE 2-INSN addu/lw FORM (P31 S67; byte-proven ov_SC07_007/func_80182184, 264 -> 30 on this row alone)
For a repeated read off a base (an OT base here), how you SPELL the base decides the addressing mode gcc emits, and the two differ in length:
read via the SYMBOL -> lui / %lo / load (3 insns, the FIRST read)
read via a POINTER VARIABLE -> addu / lw (2 insns, every read after)
So the target's mix of forms is telling you which reads went through the symbol and which through a held pointer. Matching that mix is a LENGTH fix, not a register fix — here it was the entire drift, 264 → 30 in one change. Then ordinary §17/§325 pins closed the rest (30 → 15 → 0).
REFUTED on the same function, worth recording: §137's zero-byte "r"(K) reference dial does
not work on constants — cse will not fold the extra reference onto the existing constant pseudo,
so every probe cost +1/+2 instructions. The §344 "add a reference to raise priority" trick applies to
a biv or a live variable, NOT to a materialised constant.
§349 — RE-ASSIGN A BASE POINTER AT THE END OF THE LOOP BODY TO MAKE n_times_set > 1 — THAT KILLS BOTH loop.c's INVARIANT HOIST AND THE ADDRESS GIV, REPRODUCING A REMATERIALISED addiu $aN,$sp,K (P31 S67; byte-proven ov_SC06_029/func_801804C8, 255 ins)
When the target rematerialises a stack-array base inside the loop (addiu $a0,$sp,0x10 appearing per
iteration rather than hoisted once), the lever is to make the base pointer set more than once:
assign it AGAIN at the END of the inner loop body. loop.c treats a pseudo with n_times_set > 1
as non-invariant, so it declines BOTH the invariant hoist and the address-giv formation, and the
address is recomputed exactly where the target recomputes it.
This is the mirror of §347 lever 3-4 (declaration order and variable count steering allocation) and of §344 (reference count steering priority): the loop optimizers are driven by counts and positions of assignments, so an extra assignment is as much a dial as an extra reference.
Supporting fixes from the same function, all ordinary once the base was right: (u8) casts on the
/5 quotient and %5 remainder (they produce the andi 0xFF), a temp n = tbl[i][0] so the lh
precedes jal rand, a row pointer temp for the inner address, and increment order
off += 10, i++, q++.
Housekeeping caught by the same agent: .run/<wave>/SYS.md does not exist for pool-style waves
(only packs/ and cards.json), so a pack that references SYS.md sends the agent to a missing
file. Harmless but wasteful — the brief should name only what the wave dir actually contains.
§350 — A ZERO-BYTE RE-TIE SETS reg_n_sets=2, WHICH KILLS sched1's birthing_insn_p LAUNCH_PRIORITY BOOST — THE MECHANISM BEHIND "MY ADDS ARE GLUED TO THEIR STORES" (P31 S67; byte-proven ov_SC04_011/func_80180B24, 215 ins)
Symptom: each add is welded to its own store instead of the target's load×3 / add×3 / store×3
interleave. Cause, read straight off a -dS trace (the insns carried priority 7f000001):
sched1 gives a LAUNCH_PRIORITY boost via birthing_insn_p to an insn that BIRTHS a pseudo with
reg_n_sets == 1. The zero-byte re-tie __asm__("" : "=r"(x) : "0"(x)) gives the pseudo a SECOND
set, so it is no longer "birthing", the boost disappears, and the block un-interleaves on its own.
This is the third distinct pass steered by the same re-tie — §194-K (alias oracle, via
reg_known_value falling back), §344 (global_alloc priority, via reference count), and now
sched1 (LAUNCH_PRIORITY, via reg_n_sets). When a zero-byte re-tie changes something, identify
WHICH pass it moved; they have different bounds and the wrong attribution sends the next agent to
the wrong dial. Together with §349 (n_times_set > 1 defeating loop.c), the pattern is: a second
assignment to a pseudo is a first-class dial across loop.c, global.c and sched.c alike.
Four supporting fixes from the same function, ordinary but worth the pattern: case 3 is a return
not a break (it jumps to the epilogue); the else arm must store 0xE0 in BOTH arms so cross_jump
merges the sh at the join, using an s16 local, which yields the un-coalesced addu $v1,$v0,$zero
and stops reorg stealing the decrement; struct-member spelling (MEM_IN_STRUCT_P) defeats a false
alias against an unrelated store; s16 buf[3] gives the 0x38 frame (§333).
§351 — /s (MEM_IN_STRUCT_P) IS A DIAL YOU CHOOSE PER ACCESS: A COMPONENT_REF GRANTS IT AND LETS cse KEEP AN INDEX ACROSS THE STORE; A PLAIN CAST DENIES IT (P31 S67; ov_SC01_000/func_8017DD04, 186 -> 5)
Two accesses to the same packet, opposite requirements — and the spelling is what decides:
- the tag word must be a plain cast,
*(u32*)pkt = (*(u32*)pkt & 0xFF000000) | ot->addr;. Spelling it as aP_TAGCOMPONENT_REF grants/s, which lets cse keep an unrelated index live across the store — −13 instructions. - the OT side must STAY a
P_TAGbitfield, or the trailingD_800A5E60 = pktstops floating.
So /s is not a property of the object, it is a property of how you wrote that one access. Cf.
§340, where /s was the escape that was unreachable; here it is the thing to avoid on one line and
keep on the next.
Two more spellings from the same function:
(u16)D_800B9A02— NOT*(u16*)&D_800B9A02. The ADDR_EXPR form makes cse hold the base in a register: 8 ×lui/lhucollapses to 1 ×lui/addiu+ 4 ×lhu.- The base-split (byte-proven, previously undocumented): spell the FIRST read off the raw symbol
(
(s32)D_800AA60C + idx→lui $at / addu / lw %lo), then assignob = D_800AA60C;and route every later reference throughob(→addu $x,$idx,$t2 / lw 0($x)). Exactly the 3-instruction shape §348 describes, applied deliberately per-reference.
OPEN, with the door named (do not re-grind blindly): the last 5 are a prologue li-block
SCHEDULE-REORDER caused by the pins that fixed the registers. Pinning 0x80 makes (set (reg 7) 128) a HARD-REG set, which sched1 ranks by successor count (12 sb uses) and hoists to idx 10;
the target has all pseudos at sched1 and emits them in source/first-use order (li $a3,0x80 at idx
14). Refuted: pinning the other 4 prologue constants (still 5), hand-writing the mask via a pinned
var (+3), a non-bitfield COMPONENT_REF (+3 and an $s0 spill), an m24/mFF pin pair (+3).
The only remaining door is the allocno-priority route — make 0xFFFFFF outrank 0x80 with NO
pin (lreg: refs 9/len 173 vs refs 13/len 320), which needs a zero-byte ref/live-length edit. That is
a §344-shaped problem and the next attempt should start there, not at the pins.
§352 — ⚠ TWO IDENTICAL __asm__ __volatile__("") BARRIERS MERGE WITH EACH OTHER — SPELL THE SECOND ONE DIFFERENTLY (P31 S67; byte-proven resident/func_800D128C, measured closeness 105 when they merged)
This corrects how §5a/§336 barriers must be USED. Identical ASM_INPUT rtx are
rtx_renumbered_equal_p, so find_cross_jump matches the two barriers to each other and folds
the very arms they were placed to separate. Measured: closeness 105 with two identical ("")
barriers. Spell the second one differently — __asm__ __volatile__("" ::: "memory") — and they stop
matching.
Two more placement laws from the same function:
- THE BARRIER GOES BETWEEN THE CALL AND THE
val =, NOT AFTER IT. Placed after,reorg(stop_search_p) can no longer stealli $s0,Kinto thej's delay slot and you get anop. - A ONE-INSTRUCTION TAIL CAN MERGE.
jump.c:2402's CODE_LABEL clause (--minimum; break) drops the usual 2-insn floor to 1, so even a barecase N: val = K; break;arm merges — it needs a barrier too. Do not assume a single-instruction arm is safe.
NEW LEVER — resurrect a switch-index copy (addu $v1,$s1,$zero before the sltiu):
switch (ret) never emits one (expand_end_case folds ret - 0), and a plain sel = ret; is
canonicalised away by CSE. This works:
sel = ret;
__asm__ __volatile__("" : "=r"(sel) : "0"(sel)); /* re-DEFINE so CSE cannot fold it back */
switch (sel)
local-alloc then gives the single-block sel a caller-saved register while global-alloc must give
ret a call-saved one — reproducing the target's copy. (A fourth pass steered by the re-tie; cf.
§350's list.)
Two ordinary fixes worth the pattern: a clamp must be if/else plus a SEPARATE val = val * 25;
because a ?: const-folds 0x14*25 to 500; and the definition needed (s32 arg0, s32 arg1) with
(u8)/(s16) casts INSIDE, because resident.c:1693 already declares the prototype at file scope
(gcc-2.7.2 rejects + asm constraints).
§353 — USE -fno-thread-jumps AS AN ORACLE TO PROVE A RESIDUAL IS thread_jumps, THEN LAUNDER THE VALUE TO KEEP A DEAD RE-TEST (P31 S67; byte-proven ov_SC01_008/func_8017EC68, 279 ins)
A surviving "dead" bne $s0,$v1 re-test in the target looks impossible to reproduce: thread_jumps
deletes the label, and cse then folds the comparison away. §308 read in REVERSE gives the lever —
launder the VALUE, not the control flow:
__asm__("" : "=r"(w) : "0"(w)); /* in place, on an s32 w */
With the value laundered, cse can no longer prove the comparison redundant and the re-test survives.
THE DIAGNOSTIC IS THE REUSABLE PART: compile once with -fno-thread-jumps. If the residual
disappears, the pass is identified and you are looking for a value/label-liveness dial, not a
scheduling or allocation one — here it took the count 4 → 1 immediately and named the target. Add it
to the same toolbox as oracle_reorder.py (proves an as -O1 epilogue wall) and cc1 -dS/-dL
(prove a sched1 priority or a loop.c threshold): a per-pass disable flag is a cheap oracle for
"which pass is doing this".
Also from this function, and it is the §347/§162 pattern again: the shared
D_801A3458 = p; beep; return 0; tail must be a source-level goto placed in case 0's LAST arm —
leaving it to cross-jumping gives 341 instructions against the target's 279. Cross-jumping
re-merges what you duplicate (§347 lever 1), but it will not INVENT the jump structure a source
goto expresses.
§354 — THE giv WORTH-WHILE TEST IS A DIAL: RE-ASSOCIATE THE ADDEND INTO THE INDEX TERM AND strength_reduce DECLINES (P31 S68; byte-proven ov_SC03_105/func_801824CC, 320 ins, 201 → 5 → 0)
Write
(i << 5) + (base - 0x10) /* NOT base + (i << 5) - 0x10 */
and the giv's computed benefit drops to 2, so strength_reduce declines to reduce it — which in
turn frees $fp for another variable. The two spellings are arithmetically identical; only the
association differs, and the benefit heuristic reads the association.
docs/gcc-2.7.2-map/loop.md §L0 documents the worth-while test itself; this is the usable spelling,
which is the part an agent cannot re-derive from the pass source in reasonable time.
Two companions on the same function: a signed s16 f1 picks addiu over ori for the
constants, and an explicit u16 *p temp for the stack walk. And a collision worth knowing —
K&R s16 params were load-bearing AND required: an ANSI s16 prototype conflicts with the TU's
existing extern (lines 4828/4837), so the K&R form is not a style choice there.
§355 — A REMAPPED SIBLING'S SOURCE BIAS IS NOT ITS EMITTED BIAS — DO NOT HAND-SHIFT OFFSETS TO MATCH THE ASM (P31 S68; byte-proven ov_SC07_007/func_8013DD68, 187/187 in 2 iterations)
Remapping a banked exemplar onto a sibling, the exemplar's C writes p + 0x18 / puVar16 + 8 while
the TARGET emits $s0 + 0x16 / $s1 + 0xE. Both are correct. gcc-2.7.2 re-anchors reduced givs,
so the emitted displacement need not equal the source displacement.
The trap: seeing the mismatch and "fixing" the source offsets to match the disassembly. That breaks a remap that was already right. When a remapped body differs only in displacement, suspect the anchor, not the arithmetic.
Also from this function: guard a draft's local typedefs with #ifndef BFM_ENGINE_TYPES_H to make it
dual-safe — emitted in match_one's standalone probe, skipped in the real TU where
engine_core.h already provides them.
§356 — MEASURE A DRAFT IN THE TU IT WILL LIVE IN, NOT IN THE STANDALONE PROBE (P31 S68; measured over 47 stored drafts)
match_one compiles a draft against common.h only. A real TU carries a whole file-scope
declaration environment. Sizing a lane by the standalone verdict undercounts it badly:
Of 47 backlog drafts reported
cc1-fail, 43 failed with`D_XXXXXXXX' undeclared. 39 of those 43 name symbols that ARE present in the draft's own target TU. The failure was the probe's environment, not the draft. Only 4 were genuinely missing.
The honest instrument is recover_integration --probe-only, which compiles in the REAL TU and
reports MATCH / DIFF / CC1-FAIL separately from the static guess. Here the standalone verdict
said 22 candidates; the real-TU population is up to 61. Extends R35 — fix the instrument before
trusting the measurement — to the specific case where the instrument is narrower than reality
rather than broken.
§357 — ONE STRUCT POINTER, NOT TWO: A SECOND SOURCE VARIABLE BUILDS A THIRD IV (P31 S68; byte-proven ov_SC06_029/func_80181DF8, 335 ins, 330 → 13)
Dropping an explicit cur = base + 4 pointer lets combine_givs produce the single +4 address
giv. Keeping cur as its own source variable makes gcc build a third induction variable at +6.
Same family as §347 (loop regalloc is a declaration-order / live-range dial) and §349
(n_times_set > 1 on a base pointer), but the opposite direction: here the lever is to remove a
variable, not add one.
§358 (sharpens §333) — AN UNREFERENCED FIXED-SIZE AGGREGATE LOCAL IS LOAD-BEARING (P31 S68; same function)
A 32-byte aggregate local declared before pos and never referenced anywhere in the body is what
makes the frame -0x58 and places pos at sp+0x30. expand_decl slots every fixed-size aggregate
whether or not it is used.
§333 says frame size is set by DECLARED aggregates. This adds that the aggregate need not be
referenced at all — which turns it from a constraint you work around into a knob you can turn.
(The remaining 13 on that function were §3-T2 statement order: call → i=0 → mtx → colp.)
§359 (§43-adjacent) — SPELL A SIGN-WIDEN AS AN EXPLICIT TWO-STEP, FUNCTION-SCOPED TEMP (P31 S68; byte-proven main/func_80012B58, 69 ins, 58 → 3 → 0)
To keep the shift IN PLACE on $a2 instead of routing it through $v0:
s32 t = a2; t <<= 16; dv = t >> 16;
with t declared at function scope (it is reused later by (u32)t >> 31). Measured on this
function: a single-statement (s16) cast failed, and a register pin failed. Only the
two-step shared temp reproduced the tell.
The polarity fix that got 58 → 3 first: goto-invert if (dv != 0) so the fallback clamp is the
fallthrough and the division block is the branch target.
§360 — THE "COMPILER FOUND A SHORTER EQUIVALENT THAN THE TARGET" PAIR (P31 S68; main/func_800241C0, 68 ins, 68 → 19)
Use when the TARGET is longer than your draft and its extra instructions look redundant: gcc found a shorter equivalent and you must spell the source so it cannot.
$0opaque-copy — writex + zrwithregister s32 zr __asm__("$0")to force a realaddu $vN,$aN,$zerowhere gcc would fuse the copy into the next instruction. It also splits ani = arg2into the target's two-step instead of gcc's one-instructionaddiufusion.- Hoist the narrowing cast to its own statement —
srem = (s16)rem;standalone, pinned to the same register as a non-overlapping-lifetime neighbour, reproduces the target's early rem-shift scheduling.
⚠ A third lever was tried here and is REFUTED — see §361. Duplicating i = i - 1; into both
if/else arms to bait cross_jump appeared to work (it is part of how 68 → 19 was reached) and was
in fact the cause of the residual that remained. It is recorded here only so nobody re-derives it.
§361 ★ — A LOOP-TAIL BYTE SIGNATURE THAT NAMES ITS SOURCE SHAPE — AND THE LAW THAT A "SCHEDULING TIE" MAY BE YOUR OWN EARLIER LEVER (P31 S68; byte-proven main/func_800241C0, fable escalation, 19 → 0 in 3 iterations)
THE LAW FIRST, because it generalises past this function. A residual you have diagnosed as a
sched1 / regalloc tie may be an artifact of your own most recent structural hack. An added
instruction changes the ready list, and the disturbance surfaces at a different instruction than
the one you added — which is exactly what makes it read as an unrelated, unreachable compiler tie.
Before escalating a "compiler-internal tie", remove your most recent structural lever and
re-measure.
Here the previous agent reported "a genuine sched1 PRIORITY tie: gcc schedules the
zero-dependency cnt++ right after the mult, the target defers it until just before the sh" and
listed three levers it had ruled out. The tie did not exist. It was §360's arm-duplication polluting
that arm's ready list.
THE SIGNATURE. When the target's loop tail reads
addiu $vX,$rI,-1 ; addu $rI,$vX,$zero ; sll $vX,$vX,16 ; bgez/bltz $vX
— a surviving COPY, with the loop test reading the temp and not the loop variable — the source decremented ONCE at the join, through a temp:
register s32 zr __asm__("$0");
...
t = i - 1; i = t + zr; } while ((t << 16) >= 0);
The + zr opaque-PLUS is what stops cse/local-alloc folding i = t back into an in-place addiu.
TWO CONFIRMING TELLS.
- A preceding conditional branch whose delay slot holds a COPY of that
addiu, with the branch retargeted past the join head, isdbr_schedulefill-from-target. It is legal ONLY becauset = i - 1is idempotent. An in-placei = i - 1there is self-modifying, cannot be duplicated into the slot, and shows up as an unfillable nop. So a filled slot is positive evidence for the temp form. - Do NOT fake the join by duplicating the decrement into both arms (§360's refuted lever). Re-derive
the join/copy structure from the branch-target shape before blaming
rank_for_schedule.
Minor second lever: a store of the constant -1 through a u16 * emits ori $v,$0,0xffff;
through an s16 * it emits addiu $v,$0,-1. The front end folds the constant by the store
lvalue's signedness, so pick the lvalue sign to match the target's li encoding.
Escalation economics (R41): sonnet spent 229,121 tokens and did not bank; fable spent 74,036, banked in 9 tool uses and 6.8 minutes, warm-started from sonnet's draft plus its ruled-out list. The escalation was cheaper than the attempt it rescued — which is an argument for escalating earlier, not for drafting less.
§362 — TWO TRAPS WHEN A CARVE MOVES A STUB INTO THE -O0 TU (P31 S68; byte-proven, 6 fns / 2,547 ins across ov_MAIN_012 / ov_SC02_037 / ov_SC03_107)
Banking an -O0 function that sits inside an -O2 object needs o0_subsplit to cut it into its own
region (§116: opt level is per FILE). Two things then bite, both measured:
Trap A — rollout_o0.py goes BLIND to exactly the stubs you just carved. Its stub_file_of()
skips any file whose basename contains _o0, because its design assumes stub-in--O2-TU plus
def-appended-to--O0-TU. A carve that moves the stub INTO the -O0 TU makes both the same file, so
the driver reports no-stub / already banked? and banks nothing. Edit <ov>_o0d.c directly.
Trap B — do not keep the generated §8b carried decl layer. The carve emits
#include "../shared/engine_core.h" plus ~1,100 carried decls, which conflict with the shared -O0
header on 7 symbols (func_80015978 void*/s32, func_800CF854 void/s32, func_801336E8,
D_801274C8/CC/D0). Replace the TU wholesale with the minimal fleet-standard form — legitimate
precisely when the carved region holds only the functions the shared header defines (here
0xD44 = 0xC08 + 0x13C, exact).
Both carve and bank are byte-gated: the carve must be byte-NEUTRAL (sha1 == check.<ov>.sha,
interleave_check ALIGNED, config/overlays.mk unchanged), and only then does the bank get gated.
Derive AND carve under .run/auto/gate.<ov>.lock — a plan derived outside the lock can describe a
tree state that never existed (the commit:2791 rule; a concurrent recovery lane was observed editing
the very TU this carve splits, mid-analysis).
§363 ★★ — THE OVERLAY-LAYOUT ASSUMPTION IS A SYSTEMIC BUG CLASS, AND main IS THE EXCEPTION THAT FINDS IT (P31 S68; six instances, four of them in one session)
Every fleet tool that composes a path from a binary NAME is encoding the OVERLAY layout
(src/<binary>/…, build/<binary>/<binary>, config/check.<binary>.sha). main matches none of
it: its sources are LOOSE FILES in src/, its asm is asm/nonmatchings/<region>, its image is
build/us/SLUS_007.26, its locked hash is config/check.us.sha, and its undefined_*_auto.txt sit
at the REPO ROOT. Measured instances, each of which presented as "the model wrote bad drafts":
| tool | wrong assumption | symptom |
|---|---|---|
parallel_gate.stage_generated |
build/<b>/<b>.ld + build/<b>/undefined_* |
worktree cannot link; main banked 0/3 |
rtu_match |
TU is src/<source>/<split>.c |
ERR, empty detail |
rtu_match (again) |
asm is asm/<source>/nonmatchings/<split> |
ERR, empty detail — identical symptom, which is what makes a one-layer fix feel complete |
gate_stage |
build/<b>/<b> + config/check.<b>.sha, then DEF_SHA |
main gated against ov_SC01_077's hash, every draft "near" |
recover_integration --auto |
--binary defaults to the literal ov_SC01_077; backlog keyed by BARE NAME (R48) |
"no candidates" fleet-wide |
twin_sweep |
family_sweep.load_sigs covers 141 of 213 |
main/resident/md_MAIN_* structurally invisible |
THE RULE. Pass the fact you already have; never reconstruct it. corpus.Stub carries .path
and .asm_dir. The Makefile declares <b>_OUT, <b>_CHECK_SHA, <b>_LD_SCRIPT, <b>_UNDEF_SYMS,
<b>_UNDEF_FUNCS — ask it (make --eval='__p-%: ; @echo "$($*)"' __p-OUT BINARY=<b>). When you
genuinely must reconstruct, assert the result EXISTS and refuse with the reason (R43): a
reconstructed path handed to cpp surfaces as a compile failure and reads as a bad draft.
THE DIAGNOSTIC HABIT. When ONE binary's lane banks far below the rest, the prior is harness, not
model. The cheapest test is to run the SAME draft through a SECOND, INDEPENDENT oracle and see
whether they disagree (R34/R40) — here rtu_match's real-TU compile said MATCH ×3 while the
whole-binary gate said "near" ×3, and the gate was the one that was wrong. And do not stop at the
first layer: two of the six were the same tool, one call deeper, with an identical symptom.
§364 ★ — THE libgpu P_TAG BITFIELD SPELLING IS OPT-LEVEL DEPENDENT (P31 S68; two functions, opposite verdicts, same session)
- At
-O0the bitfield is REQUIRED. md_MAIN_003/func_800D0D6C (345 ins): theunsigned addr:24read-modify-write is what makesstore_fixed_bit_fieldexpand the VALUE first and then the dest read, which fixes the finalor's operand order. The hand-written(X & 0xFF000000) | (Y & 0xFFFFFF)gets the operands BACKWARDS. - At
-O2the bitfield is BYTE-WRONG. ov_SC01_000/func_8017E594 (357 ins): the bitfield setsMEM_IN_STRUCT_P, so the alias oracle CSEs the secondD_800B9A02load across the tag store — -4 instructions per block, 357 → 325. The manual mask/or form is required.
Same struct, same field, opposite correct spellings, decided by the TU's opt level. This is §351's
/s dial with a concrete high-frequency victim, and the first case where the RIGHT answer FLIPS
between -O0 and -O2. Check the TU's opt level before choosing the spelling.
§365 — PIN BOTH MASKS OR NEITHER (P31 S68; ov_SC01_000/func_8017E594, 357 ins)
§137's tell (a residual invariant under every source permutation) resolved by pinning
register u32 mlo __asm__("$10") and mhi __asm__("$12"). Pinning only one does NOT work
(36/34 residual) because $t2 is legitimately reused for the last block's temp after mlo dies.
Companions: a sibling's frame_pad[2] + (void)& for the +8 stack pad (cf. §358); BLOCK-scoping the
link temporaries (one reused C variable = one long pseudo that stole $s8, +8 ins); and splitting
the array-symbol read from the q-pointer read into two locals (+7).
§366 ★★ — group_case_nodes MERGES STACKED CONSECUTIVE CASE LABELS: GIVE EVERY CASE ITS OWN BODY (P31 S68; ov_SC01_001/func_8017EC28, first-try MATCH 360/360, 96/96 relocs audited)
gcc-2.7.2 group_case_nodes (stmt.c:5281, from expand_end_case:4752) merges CONSECUTIVE case
values into ONE range node whenever next_real_insn(label_rtx(code_label)) is identical — i.e.
whenever labels are STACKED:
case 5: case 6: case 7: body; /* -> ONE node, shrinking the balanced tree */
Three stacked consecutive runs ({5,6,7}, {0x53A,0x53B}, {0x57A,0x57B}) = 4 fewer case nodes, which was exactly the observed -25 LENGTH-DRIFT.
The fix: give EVERY case its own DUPLICATED body + break so no two labels share a
next_real_insn, then let cross_jump fold them back — ordering arms so each family's fold lands
after its LAST contributing arm (§298). That reproduced the target's .L…000 -> .L…010,
.L…090 -> .L…098 and .L…0DC -> .L…0E4 fall-throughs.
Diagnostic: an unexplained LENGTH drift on a switch, equal to a small number of case nodes,
points at group_case_nodes before anything else. Same "spell it long, let a later pass re-merge"
family as §360/§361 — but here cross_jump is the intended merger, not an accident.
§367 — RECONCILING A DECL CONFLICT BETWEEN TWO DRAFTS FOR THE SAME TU (P31 S68; main)
main/func_800242D0 declared extern u16 D_80063870; the just-banked main/func_800241C0 declares
extern s16 D_80063870[] at file scope. gcc-2.7.2 rejects the conflicting redeclaration at file
scope AND at block scope — the block-scope shadow was tried and also rejected. (That lever DOES
work for a conflicting typedef, cf. resident/func_800D06E8's Struct80078E78; it does not extend
to objects.)
The fix that banks: match the ALREADY-BANKED spelling and adapt the USE SITE — here
t4 = D_80063870; (array decay) instead of t4 = &D_80063870;. Safe because only the ADDRESS is
consumed (t4 is a register s16 *), so the decl's element type never reaches codegen — and the
whole-binary SHA proves it. Order matters for a chunked gate: the first draft to bank fixes the
file-scope spelling for every later one, so a two-draft conflict is not symmetric. --chunk 1 makes
this visible one draft at a time instead of sinking the pair.
§368 ★★★ — THE RELOAD-REMAT CONSTANT: REACH A REGISTER NO PIN CAN REACH (P31 S68; ov_SC03_105/func_80187A30, 339 ins, fable escalation closed 8 → 0 in ONE edit)
THE TELL, and check it FIRST on any REGALLOC-LOCAL residual: the wrong-register rows ALSO have
swapped operands on a commutative op — target mult $a3,$v0, yours mult $v0,$a3. A
wrong-register row with swapped commutative operands is THIS idiom. It is not a pin case and not a
scheduling tie. Reading the operand order IS the diagnostic.
Why a pin cannot work, and why an in-block constant cannot either. cse.c fold_rtx (~5284)
forcibly swaps any operand with a known constant equivalent into position 2 — the reg-reg swap
always validates — so an in-block r = K can NEVER be operand 1. And register __asm__ pins
fight local-alloc instead of rerouting around it (measured WORSE here: 18 and 14, vs 8 unpinned).
The lever — a FUNCTION-SCOPE single-set local, assigned once at entry, used constant-first:
s32 cK; /* function scope */
cK = K; /* ONCE, at entry — a different basic block from the uses */
...
prod = cK * x; /* constant FIRST */
Preconditions: cK's live range must cross calls and every callee-saved register must already be
occupied, so global-alloc leaves it UNCOLORED. Reload then records reg_equiv_constant, DELETES
the init insn (instruction-count-neutral — the extra statement is free) and REMATERIALIZES
addiu <reload-reg>,$zero,K immediately before each use, picking the register by
reload1.c order_regs_for_reload preference rather than local-alloc's ascending scan. That is how it
reaches $a3 while $v1 is free.
Bonus, and why ONE edit fixed two things: the cross-block def blinds cse to the value, so the
source operand order SURVIVES into the emitted instruction. Register and operand order are fixed
together; adjacent product/mfhi pseudos follow into the same register.
§369 — REUSE THE COMPARE CONSTANT'S OWN VARIABLE FOR A MASK THAT KEEPS COALESCING (P31 S68; md_SC07_004/func_801AEC38, 365 ins)
When a mask result keeps coalescing onto a dying hard-pinned register, assign it into the variable that held the COMPARE CONSTANT:
d = 0xC000;
if (w == d) { d = raw & 0xFFF; ... } else { d = raw & 0xFFF; ... }
That moved the andi from $a0 to $v0 at ZERO length cost — cse still hoists the duplicated
andi into the bne delay slot.
A frame rule this sharpens (with §333/§358): aggregates get their stack slot at BLOCK ENTRY;
scalars only get one when &x is expanded, and slots are 8-byte aligned. So an INNER-BLOCK
u8 pad1[8] lands BEFORE later address-taken scalars — block placement of a pad is a frame
ordering dial, not only a size dial. (Here it was what hit -0xB0.)
§370 ★★ — A HARD BOUND FROM sched.c, AND THE reorg SLOT-STEAL DIAGNOSTIC (P31 S68; main/func_8001BC6C, 69 ins, NOT closed — 33 → 28 over ~45 measured compiles)
This entry is a NEGATIVE result, and that is why it is worth its space: it tells you when to stop. A fable escalation drove this function to 28 and did not close it. What it PROVED is reusable.
THE HARD BOUND. sched.c schedule_select always fronts a ready load over an equal-priority ALU
leaf (potential_hazard). Therefore no C spelling can emit an ALU chain before loads that are
simultaneously-ready, same-priority leaves. If the target shows that order, the cause is NOT your
source order — look for reorg slot-steals, hard-register dependency walls, or late in-block
consumers before burning compiles on statement permutations.
THE SLOT-STEAL DIAGNOSTIC. A dying-operand ALU op sitting in a conditional branch's delay slot
(or $a1,$a2,$a1) whose registers are untouched by the compare/address tail is
reorg fill_simple_delay_slots stealing the FIRST ELIGIBLE insn scanning back from the branch
(eligible = shares no register with anything between it and the branch).
THE PRECONDITION FOR REPRODUCING IT. The value must accumulate OUTSIDE the $v0-heavy tail, so
spell the or-tree SPLIT, not fused:
hi = c << 16; /* early */
hi |= (c << 8) | tag; /* mid */
color = hi | c; /* late */
The accumulator is then $a2 and is eligible. The fused one-statement form accumulates in $v0, is
INELIGIBLE, and reorg steals a different insn instead.
Three supporting levers that each moved bytes: (1) pin the first-loaded field to $2
(register short f0 __asm__("$2")) — every consumer then reads $v0, creating post-reload
anti-dependencies against all later $v0 writers; (2) two source reads of the same cell get two
single-set temps, never one reused multi-set tmp (alloc then reuses $v0 for both loads);
(3) fuse the colour expression into ONE late statement to fix a t5/t6 allocation swap.
REFUTED here, with measurements — do not re-derive:
- a dead-init
f0 = 0;as a boost-kill:cse delete_dead_from_cseremoves it before the finalreg_scan(toplev.c:2925), soreg_n_setsstays 1 and it is a NO-OP; - §350 re-ties on loads in a DENSE block: each re-tie is a real stream insn that re-anchors its load and delays the consumers — measured +4 to +9;
- the
-fno-schedule-insnsoracle: BOTH scheduling passes actively build the target here, so neither ablation approaches it. §353's pass-identification oracle does not discriminate when the residual is a multi-pass composition.
AND THE FRAMING LESSON (with §361). The residual is not a single sched1 tie, which is what
the cheaper tier reported. Read off -dS/-dR dumps plus sched.c, it is a four-pass
composition: sched1 birthing-boost layout → local-alloc birth order → sched2 hard-register
anti/output webs (ADJUST_COST zeroes anti/output costs) → reorg slot-steal. A split+§350-re-tie
variant (37) reproduces the target's slot, or-tree and sll 16-at-top EXACTLY, so the target is the
union of one variant's load block and another's chain block — and no single spelling produced both.
Per §361 the escalation first REMOVED the prior agent's pin and re-measured: the pin is exonerated
for the head (identical head residual unpinned) and load-bearing only for the tail. That is the
§361 procedure applied correctly, and it is why this diagnosis can be trusted where the previous
one could not.
§371 ★★ — CARVING A SINGLE-OBJECT MODULE BINARY, AND THE spimdisasm RODATA-MIGRATION TRAP THAT FOLLOWS (P31 S68; byte-proven md_MAIN_003, func_800D0D6C 345 ins)
The overlays arrive pre-split into many jr_* objects; the md_* module binaries are ONE c
subseg. Every attempt to carve an -O0 range out of one died at
jr_isolate_all: unaddressable content. That single message was three stacked causes, which is
exactly why it read as one impassable wall — fix one and the message does not change:
overlay_src_split.load_ov_symsstopped at an interior YAML comment. md_MAIN_003's yaml annotates the body of its symbol-file list, so onlysymbols.us.txtloaded andD_800D3200resolved toNone.- A trailing content chunk had nowhere to go. The verbatim-asm pair after the last addressable anchor now attaches to the LAST region when every symbol it defines resolves at/after the last cut, instead of hard-refusing.
- Bare tag forward decls (
struct S_D2394;) tripped the_file_scope_declsdedupe refusal.
THEN THE CARVE CAUSES A LINK FAILURE, and this is the reusable part.
spimdisasm migrates rodata referenced by exactly one function into that function's .s ONLY
within the same subseg. A carve that moves the function to a new subseg while the .rodata island
stays behind makes splat silently drop those blocks:
undefined reference to `D_800CEE58' / `D_800CEE80' (from the post object's .text)
INCLUDE_RODATA does NOT resurrect them — splat marks them migrated segment-wide and emits
nothing (can't open ... D_800CEE58.s). The fix is to rename the .rodata subseg to the object
where its emitters now live. Verification that you got it right: the regenerated .s for the moved
function comes back byte-identical to the pre-carve one.
THE MAKEFILE HUNK IS PART OF THE CARVE, NOT A FOLLOW-UP. The -O0 wildcard covered only
src/ov_*/ov_*_o0?.c. A module region file compiles at -O2 without widening it to
src/md_*/md_*_o0?.c — byte-neutral while the region holds only stubs (INCLUDE_ASM is verbatim
asm), but every -O0 draft banked into it then mystery-fails the gate (§362's trap class), and a
fresh clone reintroduces it. Commit the Makefile, the tool fixes and the carve TOGETHER.
DO NOT chase interleave_check ALIGNED here. md_MAIN_003 reports DRIFT on a CLEAN tree —
pre-existing, not carve-caused (verify before changing anything). It has no _JTBL_INTERLEAVE block
and must not get one: forcing ALIGNED moves the leading rodata island after .text and shifts every
address by 0xD8. The honest criterion is "the carve does not change interleave_check's output".
KNOWN, STILL OPEN: a second carve on the same binary now refuses with
jr_inventory(md_MAIN_003): committed .rodata carve ownership is not 1:1 (R32/R33) — a stranded/duplicated carve: [('UNOWNED', '0x800cedf8')]. The first carve's rodata rename left that
island unowned by the inventory's 1:1 check. Solve that before carving the remaining 7 -O0 stubs in
this binary's pre-TU.
PLANNING THE REST (§126 applied): of md_MAIN_003's 12 open stubs, 8 are -O0 (1,166 ins).
func_800D0A7C / func_800D0B1C / func_800D0C50 are ADJACENT (gap 0) so ONE region covers all
three; the other five have matched bodies between them and need their own regions — K interleaved
matched bodies ⇒ K+1 regions, which o0_subsplit derives for you from a single --lo/--hi span.
§372 ★★★ — THE COPY-CAPTURE PAIR, AND THE ONE ZERO-BYTE EDIT THAT DEFEATS BOTH (P31 S68; byte-proven main/func_8003491C, 78 ins, fable escalation closed 5 → 0)
THE TELL. A REGALLOC-PERM residual whose wrong-register rows read the destination of a nearby
MATCHING copy insn (map $dst > $src) — e.g. your narrow/compare reads $a2 where the target
reads $v0, while the addu $a2,$v0,$zero copy itself matches. This is NOT an allocation tie,
and pins make it worse.
TWO SEPARATE PASSES re-base uses of a value onto a copy's DESTINATION:
cse.c make_regs_eqv— after(set P X),Pbecomes the quantity's CANONICAL register wheneverXis a non-fixed hard reg ("prefer pseudo to hard") orPoutlives the basic block with a later last-use.canon_regthen rewrites every LATER same-EBB use of the value toP.local-alloc.c optimize_reg_copy_1(live at -O2 viaflag_expensive_optimizations) — asingle_setREG-REG copy whose source does NOT die in the copy (e.g. after sched1 hoists the copy above the source's last use) forward-substitutes dest for src up to src's death.
BOTH DIE TO THE SAME EDIT. Spell the copy as a PLUS:
register s32 zr __asm__("$0");
P = X + zr;
SET_SRC is no longer a reg-reg copy (kills #2) and cse records no register equivalence (kills #1) —
and it emits the byte-identical canonical move encoding addu $rd,$rs,$zero, so it is zero-cost
and safe to leave in the banked source.
ORDERING COROLLARY. Uses that must read the SOURCE register (a narrow, a compare) must precede
the copy in SOURCE order — canon_reg walks source order — and sched1 will still hoist the
dependence-free copy above them to match a copy-first target schedule.
AND WHERE THE TARGET KEEPS A LIVE COPY OF AN INCOMING ARGUMENT (addu $aN,$a0,$zero at entry),
the same + zr on the ENTRY def is what keeps that copy insn alive at all.
Relationship to §368 — read both, they are different animals. §368's tell is a wrong-register row
whose COMMUTATIVE OPERANDS are also swapped, and its lever is an uncolorable single-set local routed
through reload. Here the rows are pure shift/slti with no commutative operands, the tell did not
hold, and the RTL dumps named copy-capture instead. The escalation was told to CHECK whether §368's
tell applied rather than assume it, reported that it did not, and found the real cause — which is
the procedure §361 asks for, working as intended.
§373 ★★★ — THE DEAD-RESET CSE-BREAKER, THE PIN THAT BREAKS A sched2 ANTI-DEP, AND WHY AN asm CAN NEVER RAISE PRIORITY (P31 S68; byte-proven ov_SC06_010/func_8017E764, 438 ins, fable escalation closed 8 → 0)
1. DEAD-RESET CSE-BREAKER — the zero-footprint replacement for a §195-I asm re-tie
To stop cse merging two computations of the same address/expression (the classic case: &local
passed to two calls) without an asm's scheduling footprint:
p = expr; /* name it */
f(..., p); /* use it */
p = 0; /* DEAD-RESET, immediately after */
cse's forward scan invalidates the equivalence at the second set, and flow deletes the dead set
before sched1 — so it costs zero bytes and zero LUID/priority disturbance.
Why this beats the asm re-tie: an empty-asm re-tie is a REAL pre-call insn whose
def -> asm -> arg chain has an early LUID and therefore FRONTS that argument's addiu over its
siblings. That is a scheduling side effect, and on this function it was the second residual
cluster — §361 confirmed, the lever caused the bug it was later blamed on.
Proof the dead-reset is load-bearing rather than cosmetic: removing it costs +2 instructions and
+8 frame bytes.
2. A REGISTER PIN THAT DELETES A sched2 ANTI-DEPENDENCE
Symptom: the target interleaves an address materialization (an la pair) between an unrelated
load and its store; yours emits load / store / la serially in the same register.
Cause chain, in order: sched1's birthing boost sinks the single-set la to its consumer →
local-alloc reuses the just-freed scratch ($v0) → sched2 is then walled by the
store-reads-$v0 → la-writes-$v0 ANTI-dependence.
Fix: a register __asm__ pin on the address pointer to the TARGET's register deletes that
anti-dep, and sched2 reproduces the interleave. Pair it with splitting the deref from its +K
consumer around the neighbouring statement to set LUID fill order.
Note this is a case where a pin is exactly right — contrast §368, where pins were measured WORSE.
The discriminator is WHAT the pin is for: breaking a false anti-dependence (works) versus trying to
out-argue local-alloc about an allocation (fails).
3. HARD FACT FOR THE SCHEDULING MAP — an asm ALWAYS has priority 1
gcc-2.7.2 insn_cost (sched.c:1363) sets LINK_COST_FREE on any dependence whose CONSUMER is
unrecognizable — INSN_CODE < 0, which is every inline asm. So an asm can never inherit a load's
latency-2 into its priority: pri(asm) = 1, always.
Do not try to raise an instruction's priority by feeding a load through an asm. That closes off a
whole family of plausible-looking levers, which is why it is worth its space next to §370's
schedule_select bound.
CROSS-REFERENCE TO §370 — checked and found INAPPLICABLE here, which is the point
This escalation was briefed to test §370's bound (a ready load always beats an equal-priority ALU leaf) against cluster 1 FIRST. It reported that the bound did not explain this residual — the cause was the birthing-boost → local-alloc → anti-dep chain above — and then found the real one. §370's claim is unchanged and still narrow: it applies to simultaneously-ready same-priority leaves. The habit is the transferable part: check whether a recorded bound actually covers your case before concluding the residual is unreachable.
§374 — A register … __asm__("$30") RESERVATION IS NOT HONOURED BY move_movables (P31 S68; main/func_80015608, 86 ins)
Under ~9-way register pressure at -O2, gcc-2.7.2 free-registers something into $30/$fp AFTER
the pin's own assignment, silently corrupting whichever variable loses that fight (observed as
y / flags / size / tag / mode2 across configurations). Reproducible. move_movables' invariant-hoist
register SELECTION does not consult local register-asm reservations.
THE CORRUPTION IS SILENT — the build succeeds. Audit the raw objdump register uses by hand before trusting a pinned build that "compiles fine".
This is the fourth bound on the pin lever, and together they are a usable rule:
- §368 — a pin FIGHTS local-alloc when you want a different ALLOCATION (measured worse than none)
- §373 — a pin WORKS to break a false sched2 ANTI-DEPENDENCE
- §374 — a pin on
$30/$fpis NOT HONOURED at all againstmove_movablesunder pressure - §375 — a pin on
$a0-$a3has NON-LOCAL effects, backwards, through earlier calls
§375 — AN $a0-$a3 PIN USED LATE RELOCATES AN EARLIER OUTGOING-CALL USE OF THAT REGISTER (P31 S68; main/func_8005F0C8, 88 ins)
The loop body is oracle-proven byte-exact once the base is fenced and p is pinned to $4. But that
pin hoists the earlier func_8005C2C8 argument copy ~26 slots early, identically across three
structural variants — block scope and declaration order make no difference.
The rule: a hard-register pin on an ARGUMENT register used LATE relocates any EARLIER outgoing-call
use of that same register to the earliest legal point. Argument-register pins are not local the way
$s-register pins are; they reach backwards past unrelated calls.
(The other residual on that function is §188's epilogue and is toolchain-unfixable — it is now in the
walls ledger. See the playbook §1b: check wall_sweep BEFORE escalating anything.)
§376 ★★★ — A STANDALONE match_one CLOSENESS OF 0 IS A CLAIM ABOUT THE BODY, NEVER ABOUT THE TU (P31 S69; measured 0/28)
The claim. S68 closed with "32 FREE BANKS ARE WAITING", 10 of them personally verified with
match_one at closeness 0 (NOCOMPILE-UNDECLARED-FIXED), the other 22 classified
INTEG-STANDALONE-MATCH — "they already match standalone and were misfiled as failures".
The measurement. 4 had banked since the checkpoint was written. The remaining 28 gated 0/28, and not one failure was a codegen miss:
CC1-FAIL : ov_SC01_080_jr_8017AE2C.c:4365: too few arguments to function `func_8017D72C'
PLUMBING : ov_SC04_010_jr_8017BEBC.c:3714: conflicting types for `func_8017D6CC'
PLUMBING : ov_SC07_000_jr_8017BEBC.c:4406: conflicting types for `func_8012AD44'
The mechanism. match_one compiles the draft ALONE. The TU it must actually live in already
carries a forward declaration written for a call site — extern void func_8017D6CC(void); — while
the draft's true signature is void func_8017D6CC(void *a0). C rejects the pair. The body was
right the whole time; the TU would not accept the signature.
The autodecl trap. The NOCOMPILE-UNDECLARED-FIXED tier adds an extern derived from the
target's own .s so the STANDALONE probe compiles. In the real TU that extern is a SECOND
conflicting declaration, so the patched arm is strictly WORSE in-tree than the raw draft. Gate the
raw draft, never the autodecl arm.
The recipe (byte-neutral: a declaration emits no code, so the gate stays the sole arbiter):
tools/fix_arity_callers.py --funcs <FN[,FN…]> --binary <B> --any-proto --apply \
--drafts <dir> --journal .run/<id>/<B>.json # no-protos every conflicting caller decl
<gate> # then, and only then, the byte gate
tools/fix_arity_callers.py --undo-journal … --keep <banked> # revert what did not buy a match
S69: 32 caller decls no-proto'd across 10 binaries, plus 12 in src/shared/engine_core.h — that
second set makes the edit fleet-tier, so R22 is mandatory, not optional (§65a).
The law. An oracle that compiles a function in isolation cannot see the declarations that
surround it. Every "already matches, just bank it" claim must state WHICH compilation it survived.
This is the silently-narrowed-tool-scope class (accelerators #15) applied to a banking claim:
a TRUE number — closeness 0 — about a NARROWER world than the reader believes.
§377 — THREE HARNESS DEFECTS FOUND IN ONE GATING SESSION, ALL "A CONFIDENT NUMBER ABOUT A SMALLER WORLD" (P31 S69)
gater_lane's ledger arm is the draft's PARENT-DIRECTORY BASENAME. Re-staging the same(binary, fn)under a different root with the same directory name reads asalready-gated: 28 of 28 drafts skipped, exit 0, no gate ran. The arm must be explicit or the full relative path — a basename is not a key (R48).pgrep -creturned a FALSE ZERO twice for a processpgrep -aflisted seconds later. Acting on it, I removed a live run's worktrees and started a second concurrent gate. Never trust a pgrep COUNT; read the rows. Filter the rows too: the naive-afpattern counted 34 "gate processes" on a tree running 2, because the harness's ownbash -cwrapper carries the pattern in its argv — a guard that can never pass is as bad as one that never fires (R54).- A live gate makes the stub oracle transiently wrong in BOTH directions. A second gate read
ov_SC01_004:func_8017EB30asalready-bankedfrom the first gate's working tree, then the first gate was killed mid-merge and the bank was lost — leaving a phantombanked-elsewhereledger entry on a function that is still an INCLUDE_ASM stub. Classify only on a quiescent tree (triage_ladder.pyrefuses otherwise), and never blind-kill a merging gate (R42).
§378 ★★★ — THE SELF-CALLER CAST: LET A TU KEEP CALLING THE FUNCTION IT IS ABOUT TO DEFINE (P31 S69; byte-proven ov_SC04_010/func_8017D6CC)
The mirror image of §20. cast_call_sites fixes the DRAFT calling a conflicting CALLEE. Nothing
handled the opposite direction — the TU's own already-banked code calling the function the draft is
about to DEFINE — and that is the terminal blocker of the entire §376 "it already matches
standalone" pile.
The three-step failure, and why fixing step 1 only reveals step 2:
/* step 1 — the TU carries a forward decl written for its own call site */
extern void func_8017D6CC(void); /* draft defines void func_8017D6CC(void *a0) */
=> conflicting types for `func_8017D6CC'
/* step 2 — fix_arity_callers --any-proto no-protos it … and now the DRAFT'S
DEFINITION is the prototype in scope, so the SAME call site fails differently */
extern void func_8017D6CC();
void func_8017DB20(void) { func_8017D6CC(); }
=> too few arguments to function `func_8017D6CC'
/* step 3 — cast the call site to a NO-PROTO function pointer: byte-neutral */
void func_8017DB20(void) { ((void (*)())func_8017D6CC)(); }
=> VERIFIED, final SHA BYTE-IDENTICAL
Why a no-proto cast and not a typed one. The call site's ARGUMENT LIST must survive; its argument
TYPES are not knowable from the call text. A no-proto pointer accepts any list and applies the
default promotions — exactly what the site did before the draft arrived, when it called through a
no-proto extern. Inventing types is how every cheaper §20 lever produced a byte MISS instead of a
compile error. gcc-2.7.2 folds a cast of a KNOWN function symbol back to a direct jal, so the
caller's bytes do not move.
The standing recipe for the whole class (each step reveals the next; do not stop at the first):
tools/fix_arity_callers.py --binary B --funcs FN --any-proto --apply --journal … # conflicting types
tools/cast_self_callers.py --binary B --funcs FN --drafts D --apply --journal … # too few arguments
<gate> # the sole arbiter
cast_self_callers.py reads the return type off the DRAFT (never guesses it — a guessed return type
changes the call's value category, so it REFUSES instead, R43), skips declarations, definitions,
INCLUDE_ASM lines and already-cast sites, and journals every edit for an exact --undo-journal --keep <banked> revert of whatever did not buy a match.
The generalizable law. When you move a function's body INTO a TU, you change what every existing call site in that TU is checked against. A draft is not just a body — it is a new prototype imposed on code that already compiled. Expect to fix the callers, and expect the fix to be a cast, because a cast is the only edit that changes typing without changing codegen.
§379 ★★★ — MEM_IN_STRUCT_P: THE SAME LOAD, WRITTEN AS A STRUCT MEMBER, SCHEDULES WHERE A CAST CANNOT (P31 S69; byte-proven main/func_80021284 220 ins and main/func_8002D904 217 ins, found INDEPENDENTLY by two agents)
The symptom. A load sits below a store to a scalar global in your output and above it in the
target (or vice versa), and no statement permutation moves it. func_80021284 sat at closeness 7
across a 120-order statement-permutation plateau; the fix took it straight to 0.
The mechanism. gcc-2.7.2's alias machinery asks true_dependence/anti_dependence whether a
load may cross a store. Its answer turns on MEM_IN_STRUCT_P — the /s flag expand_expr sets on
a MEM. A raw *(u32 *)(arg0 + N) load is a plain MEM: it may alias the scalar global, so the
scheduler will not move it across that store. The same address read through a struct pointer
(p->field) is /s, declared non-aliasing against a fixed scalar store, and floats freely.
*(u32 *)(arg0 + 0x18) /* plain MEM — pinned below the store to D_800xxxxx */
p->attr /* MEM_IN_STRUCT_P — free to hoist to the block top */
The complement, same pass, opposite direction (func_8002D904, worth 26 of its 41 residuals):
assigning an indexed table address to a pointer variable first makes the resulting INDIRECT_REF
non-MEM_IN_STRUCT_P — gcc sets /s whenever "the address was computed by addition", so every
array/cast spelling comes out /s+varying, and the load floats to the block top. Take the address
into a local pointer when you need the load to STAY.
So the dial has two ends and both are zero-byte:
| you want | write it as |
|---|---|
| the load to FLOAT (cross a scalar store) | a struct-member access through a pointer |
| the load to STAY | take the address into a local pointer variable first |
Related store-side note at src/800.c:21942 (W16). This is the alias-analysis sibling of §41b's
"global load hoisted above the RTL prologue" wall — that one is unreachable; this one is a spelling.
§380 ★★★ — A SECOND SET OF A PSEUDO DISQUALIFIES IT FROM move_movables (P31 S69; main/func_800215F4, 465 ins, closeness 106 → 59 → 39)
move_movables hoists a loop invariant only while the pseudo has exactly one set. Give it a
second, and the hoist does not happen — a lever no register pin can reach, because a pin changes
which register, never whether the value is hoisted.
pz = &otz; /* first set */
…
pz = &dzp[i >> 1]; /* second set — pz is now ineligible for hoisting */
k = 0xC000;
…
k = 0xFFF; /* same trick on a constant */
Two loop invariants that a $v0 pin could not un-hoist came down this way. Reach for it whenever
the target computes something INSIDE the loop that your draft computes in the preheader.
The related pin form (func_8001EFE0, 468 ins): a call-used hard-reg pin makes invariant_p
return 0 when the loop contains a call, which is what stops move_movables hoisting an addPrim
address. Pin to $a3 — a call-clobbered register — precisely because it is call-clobbered.
§381 — THE insn_count HOIST THRESHOLD IS A DIAL YOU CAN READ WITH cc1 -dL (P31 S69; four independent uses in one wave)
move_movables' willingness to hoist scales with the loop's insn_count, and the edge is sharp
enough to sit on. Read the count from the -dL (loop) dump, then move ONE statement across it:
func_80040DE8— insn_count 232 hoisted(s16)param_1at the exact58×2×2 = 232edge; akey = param_1;copy inside the loop lifted it to 233 and un-hoisted it.func_80023BF0— loop 2's insn_count is 39 and needs ≥41: below the threshold gcc admits one constant hoist too many (li 0xFFFFat threshold 40), which costs a fourth callee-saved register and shifts the whole register file. Still open at 269 for exactly this reason.func_8001DA34—29×1×9 ≥ 203hoisted the0xFF000000OT mask and spilledot(frame 0xB8→0xC8); fixed with$a1/$a2pins. BANKED, 408 ins.
Read the dump before you theorise. Three of these four were found by reading -dL, not by
guessing, and the fourth is documented as an open threshold miss rather than a mystery.
§382 — TWO FOLD REASSOCIATIONS THAT NEED THEIR OWN STATEMENT (P31 S69)
gcc-2.7.2's fold rewrites these before you ever see RTL, so the fix is always "split the
expression", never "add a cast":
A * (X / 0x7F * 0x3FFF)folds to(A * 0x3FFF) * X— the 0x3FFF scaling needs its own statement (func_80040DE8).A + (B + C)reassociates to(A + C) + B— an address split into two statements blocks it (func_80021284).(x << 9) | CONST | varreassociates so that only the shift gets hoisted — the fourgetTPagebases must be explicit pre-loop variables (func_8001EFE0).
§383 — TWO TOOLCHAIN FACTS THE PACKS DID NOT CARRY (P31 S69)
- maspsx requires DECIMAL memory-operand offsets in verbatim asm.
0x40($sp)dies withinvalid literal for int() with base 10; write64($sp). Hex is fine for plain immediates. Found while bankingmain/func_80026514(239 ins) the §265 verbatim-asm way — that function is genuinely HANDWRITTEN: itsD_800B99DAfetch is a gas macro expansion oflbu $v1,0xA3AA($t2)intolui $at,1 / addu $at,$t2,$at / lbu $v1,-0x5C56($at), which gcc cannot emit. - A 4-byte struct local is BLKmode, so
assign_stack_localgives it 8-byteBIGGEST_ALIGNMENT. Declaringxy0/xy1ass32instead of a 4-byte struct is the difference between frame 0x130 and 0x138 (func_8001EFE0). convert_modesreturns x unchanged whenoldmode == GET_MODE(x), so no cast can emit the(s16)sll/srapair. Only ans16local assigned in one block and consumed in a distant one does — and the copy must sit ABOVE the guard, or it coalesces away against the target'saddu $a1,$v0,$zero(func_8002D904).
§384 ★★★ — A CARVE-CONFIG BANK IS RED UNTIL YOU RE-EXTRACT, AND THAT LOOKS EXACTLY LIKE A FALSE BANK (P31 S69; measured twice, cost one destroyed match)
The symptom. A gate banks a jtbl function, commits, and reports green. You then verify the
binary in the main tree with make build BINARY=<b> and get a SHA mismatch:
[FAIL] build/ov_SC04_011/ov_SC04_011
got 9c94d36a2cb5d8b56073d859c6e58cf396a5e750
want 8bc09c422de2c28b3d71cf382cb6241c1e39d5db
Every instinct says false bank. It is not. Both times it happened in S69, the bank was byte-perfect:
make extract BINARY=ov_SC06_025 && make build BINARY=ov_SC06_025 -> BYTE-IDENTICAL
make extract BINARY=ov_SC04_011 && make build BINARY=ov_SC04_011 -> BYTE-IDENTICAL
The mechanism. Banking a jtbl function changes CARVE CONFIG — JTBL_PADS in
config/overlays.mk and the binary's splat yaml (tables=+0x0,+0x20,+0x40,+0x58 gaining a fifth
entry). Those files are splat inputs: asm/ and the linker script are generated from them. A
worker gates inside a worktree that regenerated its own state, so it is right. The main tree, after
the merge, holds NEW carve config against OLD extracted state — and links the newly-carved C against
a stale .ld. The SHA that comes out is meaningless.
The trap inside the trap. Reverting the C alone makes it worse and changes the error, which reads like progress:
jtbl_rodata_pads: consumed 4 rodata jump table(s) but 5 pad spec(s) given — table-count drift vs the carve
That is the src and the carve state disagreeing — you reverted half a commit. A carve revert must
carry config/ too (see the carve-state-files-never-blanket-add discipline).
The law. Verification after a gate must regenerate whatever that gate changed the inputs to. Concretely:
| the gate touched | the valid per-binary check |
|---|---|
src/ only |
make build BINARY=<b> |
anything under config/ |
make extract BINARY=<b> && make build BINARY=<b> |
This is the R22 corollary aimed the other way. R22 says a reverted config needs a re-extract; a
landed config change needs one just as much. It is also R40 in its purest form — the instrument
(a build over stale extract state) was broken, and I attributed the failure to the subject, reverted
a legitimate 96-line match, and wrote a checkpoint calling it a false bank. The give-away I ignored:
the commit's own diffstat showed config/overlays.mk and a splat yaml right next to the .c.
§385 ★★★ — THE SCHED2 PRIORITY-DONOR ASM: closing the "hoisted-invariant vs IV-init preheader swap" class (P31 S69; byte-proven main/func_80038A58, 347 ins, fable escalation 2 → 0)
Closes cookbook §5's open class. Symptom: two independent one-instruction inits after a mult (or any latency chain) emit in swapped order, and the swap is invariant under source order, register pins, and zero-byte barriers.
Root cause, read out of the pinned compiler (sched.c priority():1425 + rank_for_schedule():2385):
sched2 schedules BACKWARD. The init that writes a register the mult READS carries an anti-dependence
that inherits the mult's priority (anti cost clamps to 1, so +cost-1 = +0), while the dependence-free
init stays at priority 1. Priority beats the LUID tie-break, so no C-level reordering can flip it.
The fix — donate priority to the loser via a NON-volatile single-instruction asm with a dead extra input reading the winner's destination:
c2 = 0;
__asm__("addiu %0,%1,18" : "=r"(ptr) : "r"(base), "r"(c2));
The true dep on the c2 init donates priority 2 AND delays the asm's backward release until the
donor is placed — target order, identical bytes.
Two traps. A volatile asm cannot be used: volatile is a full scheduling barrier, so it
inherits the whole chain's priority and glues itself behind the mflo. And a bare ghost asm whose
output is immediately overwritten gets deleted.
§386 ★★★ — A BYTE LOAD ON THE BIV BASE WAS BORN IN THE COMBINE PASS: SPELL IT AS A SHIFT-MASK, NEVER A DEREF (P31 S69; byte-proven main/func_80020598, 292 ins, escalation 1 → 0)
Diff tell: a one-row reg+imm residual where yours reads offset-from-giv and the target reads
offset-from-biv at the same address — and the twin expression in another arm IS giv-based in the
target.
Why no deref spelling works. A byte deref is a DEST_ADDR giv, and loop.c's combine_givs
folds it onto the reduced register unconditionally: identity or express_from, then
memory_address_p and equal MIPS ADDRESS_COST. There is no per-instance guard, so no temp, no
ordering and no spelling can split two identical (mult, add) givs.
The lever — defer the load's birth past loop.c:
b1 = (w >> 8) & 0xFF; /* NOT ((u8 *)p)[1] */
Post-loop combine then narrows zero_extend(lshiftrt(mem)) into an lbu at base+k off the
ORIGINAL register.
Two corollaries. (1) Temp-pointer escapes are impossible: cse1's find_best_addr tie-break at
equal ADDRESS_COST prefers the MORE complex address (it frees a register) and folds any same-ebb
temp back into (plus base k) before loop — and anything opaque enough to survive cse1 is equally
opaque to cse2/combine. (2) The narrowed spelling changes spill demand, so expect the §333/§334
dead-aggregate pad to need retuning (here pad[6] → pad[4] at frame 0x88).
§387 ★★ — SPLIT-FOLD DISPATCH CLOBBER: one switch case needs a reload, another must keep the fold (P31 S69; byte-proven main/func_80030F80, 343 ins, escalation 3 → 0)
When one case needs a cse-forced RELOAD of the switch-index load while another must keep FOLDING that same value into a call argument:
- Do NOT put the invalidator in the case body —
reorg.c'sstop_search_phaltsfill_slots_from_threadat ANY asm insn (asm_noperands >= 0), so branch-delay-slot hoists into that thread die (+1 length). - Do NOT volatile-cast the load —
loop.c:339init_recog_no_volatilefailsrecogon everyDEST_ADDRgiv rewrite of a volatile mem, so its address stays biv-based forever.
Instead: name the index in a local; load the dispatch byte into an s32 local (a u8 costs an
andi); place a non-volatile __asm__("" ::: "memory") AFTER that lbu and BEFORE the switch —
zero bytes, invalidates cse's memory table on every path, and sits outside every case's delay-slot
thread. Never between a load and its first use: it eats the final-pass load-delay nop. Fold-cases
then pass the named local; reload-cases re-read memory.
Side-effect law, and it is general: adding or removing pseudos flips razor-thin global-alloc ties
among pre-loop invariant pointers — which one loses its callee-saved register and gets reload-
rematerialised as inline lui/addiu. This is INVISIBLE under match_one's HI16/LO16 mask and
surfaces as a bogus "schedule" diff at the remat site. Audit with objdump -r, and flip it back by
swapping the two invariants' init order.
§388 ★★★ — THE -O0 COLOURING ORACLE: simulate stupid.c instead of grinding spellings (P31 S69; main/func_80011380 proved a C-level WALL at 6)
At -O0 gcc-2.7.2 allocates with stupid.c, not local/global-alloc. It is simple enough to
simulate exactly, so predict the colours instead of guessing at spellings:
- every insn including NOTES gets a sequential
suid; - a pseudo set at suid
sdies atmax(last_use, s+2)and occupies[born, dead-1]; - pseudos are allocated longest-first, then fewer-refs-first, then lowest regno, first-fit
$v0,$v1,$a0,$a1,…; - a
registerdeclaration emits a zero-byte head-(use) insn making that var ONE convex interval.
Two consequences you can diagnose by inspection. (1) An -O0 copy;sll multiply chain
2-colours ping-pong because the +2 rule makes each link conflict with the next — so a NOTE inside
the chain (any statement-expression's BLOCK_BEG/BLOCK_END) lapses the slack, one copy becomes a
same-register move, and final.c deletes it: a length −1 next to a ({...}) is THIS, not
scheduling. (2) A register decl recolours the whole chain via its head-use interval, so
exact-length REGALLOC rotations in -O0 code are decl-lifetime artifacts. cc1 -dr suid arithmetic
predicts the colours before you compile.
Also from the same run — the SYMBOL-ADDEND SHIELD. In a pointer index, (s32)(idx + K): the
same-mode cast hides the PLUS from pointer_int_sum's distributive rewrite, yet EXPAND_SUM still
absorbs K into the relocation, emitting la SYM+K*size with the scale applied to idx alone. Use
it when the target shows a symbol+constant base feeding a scaled index — no plain spelling
reproduces it, because the frontend otherwise distributes K into the pointer before scaling.
§389 ★★★ — h_norm IS BLIND TO INDEXED-GLOBAL RELOCS, SO FREE WORK BECOMES AN INVISIBLE SINGLETON (P31 S69; 31 stubs / 4,811 ins recovered, 8 banked same day)
The hole. sig_image.norm_stream tracks a pending lui-hi so it can neutralise %hi/%lo
pairs — but it DROPS that pending hi the moment an R-type intervenes. The indexed-global triad is
exactly that shape:
lui $at, %hi(arr)
addu $at, $at, idx <-- R-type; the pending hi is dropped here
lw r, %lo(arr)($at) <-- %lo survives into the hash
So two per-overlay copies of ONE function that differ only in a data symbol's ADDRESS hash to
different h_norm. They are byte-identical modulo relocation and they are invisible to every
hash-keyed consumer at once: seed_ref (d=0 tier), twin_sweep, dup_report,
config/dedup.us.yaml, and the family maps' structural tier.
Measured consequence (2026-09-01). Beyond the 22 stubs with a d=0 hash twin, 31 more reachable
open stubs (4,811 ins) were PURE twins of already-banked bodies at instruction edit distance 1–5 —
family_remap.classify_member seconds all 31 as PURE. Ten were clean of jtbl carve blockers;
remapping them and gating banked 8, at ~0 agent tokens. One exemplar (ov_SC06_033:0x80185f6c,
94 ins) served five open copies; ov_MAIN_012:0x8016ab6c (188 ins) serves five more.
The fix is a NEW TIER, NOT A NEW NORMALIZER. Do not change h_norm: every stored map,
ledger and calibration in the project keys on it, and a re-hash invalidates all of them. Instead read
through the hole with an edit-distance tier over reloc-normalized streams —
tools/seed_ref.py --near [--max-d N], which scans all 213 binaries, reproduces all 22 hash twins as
an R34 cross-check on every run, and ships R39 controls (positive 200/200 at d=0; random-pair base
rate 1.17%).
THE LAW, and it generalises past this project. A conservative normalizer is SAFE for a DEDUP claim and UNSAFE as a FRONTIER JOIN. Dedup asks "are these certainly the same?" — under-matching there is harmless. A frontier join asks "is there anything close to this?" — and there, every missed match silently converts free mechanical work into an apparent singleton that a future session will pay an agent to re-derive from scratch. Audit any hash you use for BOTH questions; they want opposite error directions.
Diff tell: an open stub your card calls "no banked twin — derive from the .s", whose body is a per-location copy of engine code that exists in a sibling overlay. Run the near tier before believing a singleton verdict.
§390 ★★★ — MINIMUM DISTANCE IS NOT MINIMUM WORK; RANK TWIN CANDIDATES BY EFFORT, AND FILTER LOOKALIKES BY RATIO (P31 S69; byte-proven ov_SC01_077/func_80184D50, banked)
The measurement. The near tier offered two banked candidates for one open stub:
| candidate | distance | what it costs a human |
|---|---|---|
ov_SC03_006:0x8018b868 |
d=5 — 5 scattered SUBSTITUTIONS | rewrite an expression: real thought |
ov_SC03_007:func_8018283C |
d≈6 — ONE contiguous block absent | copy the C, delete one statement |
Edit distance ranked the substitution twin first, because 5 < 6. The deletion twin was the strictly
cheaper answer and it banked: copy func_8018283C's body, drop its trailing
*(s32 *)(...) &= 0x7FFFFFFF;, rename, match_one -> MATCH, closeness 0, 98/98 ins.
The law. A deletion is free and a substitution is thought. Distance counts moved instructions;
it does not count the reasoning needed to convert one body into another. Rank by
(substitutions + regions, coverage), never by raw d — seed_ref --contained does; --near does
not, by design (it is the recall tier).
The companion filter — LOOKALIKES. A widened band promotes coincidence. Of 30 NEAR-COUSIN rows, only 13 were true cousins; the other 17 were boilerplate lookalikes. They separate cleanly on
r = d / min(nins) true cousins r <= 0.27 lookalikes r >= 0.37
Anything at r >= ~0.3 is two functions that merely share prologue/epilogue/dispatch shape. Never send one to an agent as a "twin" — a wrong twin is worse than no twin, because the agent trusts it.
THE MECHANICAL FRONTIER PAST RELOC-ONLY IS SINGLE DIGITS — three fleet-wide nulls, all controlled:
| probe | scope | result |
|---|---|---|
| skeleton join (opcodes+registers, immediates dropped) | 91,638 banked × 352 open | 0 new — hits are exactly the 46 known same-length twins. The "same shape, different constants" class DOES NOT EXIST in this corpus |
| contained / block-indel (± whole blocks, any gap) | fleet | 9 usable, 4 previously invisible; 89% of raw hits were prologue/epilogue vacuity until a min-side-25 floor was applied |
| past-the-cap cousins (d26-45, ≤30% drift, ≥100 ins) | 178 pairs | 2 — both replace-heavy seeded cracks |
Conclusion, and it is a spending decision: after RELOC-ONLY (§389) the scanner well is dry. Further similarity tooling buys single digits; the integration levers (§376/§378) bought dozens in the same session. Spend integration effort, not scanner effort, from here.
§391 ★★ — A BYTE-ALIGNED STRUCT COPIES IN FOUR INSTRUCTIONS, A WORD-ALIGNED ONE IN TWO (P31 S69)
typedef struct { u8 b[8]; } Blk8; (src/shared/engine_types.h:497) is BYTE-aligned, so gcc-2.7.2
cannot assume word alignment and emits the unaligned quartet lwl / lwr / swl / swr per copy.
A word-aligned aggregate of the same size (struct { int a, b; }) emits lw / sw.
Diff tell: an exact multiple of 2 instructions missing, scaling with the number of struct
assignments. Measured while reproducing a twin: substituting an invented word-aligned Blk8 for
the real byte-aligned one lost exactly 8 instructions across two copies (90 vs the target's 98) and
read as a plausible "near, closeness 70" — a wrong TYPE masquerading as a codegen residual.
So: never invent an aggregate type to make a draft compile. Resolve it from
src/shared/engine_types.h. An invented type does not fail loudly; it fails as a believable diff.
§392 ★★★ — THE SONNET-WAVE HARVEST (P31 S69): SEVEN TYPE/ORDER LEVERS THAT EACH CLOSED A MATCH
Seven overlay waves (105 agents, 57 MATCHes) produced these. Each is byte-proven by the named function and each is a SPELLING change, not a pin or a barrier.
(a) A same-address DUAL-SIGN read is fixed by ORDER, not by casts (ov_SC03_094/func_8017E254).
When one address is read both lh (signed, loop-invariant) and lhu (unsigned, store source),
gcc-2.7.2's cse merges both into lhu + sll + sra for EVERY spelling tried (volatile load, volatile
store, struct component-ref, asm re-tie — all identical output). The lever is source order: emit the
UNSIGNED store-source read BEFORE the signed assignment, so the intervening store consumes the cse
quantity first and forces a genuine separate lh.
(b) A narrow temp picks the narrow load (ov_SC03_028/func_8018568C,
ov_SC05_001/func_801809B8). Declaring a temp s16 vs s32 decides lh vs lhu: a 16-bit-ONLY
use needs no sign-extension, so gcc picks lhu. Widen the temp to s32 and it must sign-extend →
lh. The mirror case: a post-decrement zero-test temp declared signed yields the bare sll 16
zero-test; unsigned yields andi 0xFFFF — while the memory access stays lhu/sh either way.
(c) Fold the index, or force the subtract — the subscript decides (md_MAIN_045/func_800CCB28).
Writing tbl[idx - 2] lets gcc fold -2*4 = -8 into the lw offset (no runtime subtract). Hoisting
it — i = idx - 2; tbl[i] — forces the real addiu. The target's standalone addiu is the tell.
(d) Identical switch arms must be SEPARATE case blocks (ov_SC06_025/func_80182170). Cases with
identical bodies written comma-grouped (case 4: case 5:) merge too much (52 of 62 ins). The target
DUPLICATES the argument setup per case and cross-jump-merges only the shared jal+epilogue tail —
so write each case as its own block with its own call and return.
(e) Distinct pseudos per repeated copy (ov_SC03_031/func_8017F728). Two identical inline copies
(here rand()%160 + a field load) sharing ONE pseudo pair biases sched1's tie-break for the FIRST
copy only — copy 2 matches, copy 1 emits sra/lh swapped. Give each copy its own locals
(b1/r1, b2/r2) to remove the cross-copy WAW/anti-dependence.
(f) Split the widen to move it off a pinned register (ov_SC04_004/func_80182014). A sign-extend
chain will reuse a DYING pinned register as its own scratch. Writing wide = x << 16; x = wide >> 16;
as two statements forces the sll's target off the pinned register.
(g) The RETURN TYPE alone can close a schedule residual (ov_SC04_002/func_80182CBC). A
warm-start plateaued at SCHEDULE-REORDER/7 (arg-eval order + a delay-slot constant hoist). Changing
only s32 → void — matching the same-TU twin's declaration, parameters untouched — closed all
seven. Check the TU's own forward declaration before assuming the warm-start's return type.
§393 ★★ — THE BIRTHING BOOST: A SINGLE-SET LOCAL IS SCHEDULED LAST; GIVE IT A SECOND SET (P31 S69; byte-proven ov_SC02_017/func_8017FCFC)
A pointer local assigned exactly once (p = D_8018E204;) receives gcc-2.7.2's maximum scheduling
priority, and because sched2 works BACKWARD that pushes it LATE — emitting lui/addiu $s1 after the
jal where the target has it before. Five instructions of residual, invariant under statement order.
The lever: a zero-byte re-tie in a later block gives the pseudo a SECOND set and kills the boost:
__asm__("" : "=r"(p) : "0"(p)); /* non-volatile, zero bytes, p is now 2-set */
This is the scheduler-side sibling of §380 (a second set disqualifies a pseudo from
move_movables). Same one-line trick, two different passes, opposite symptoms: §380 un-hoists a loop
invariant, §393 un-delays a preheader constant. If a single-assignment local lands on the wrong side
of a call, try the re-tie before reaching for a register pin.
§394 ★★ — TWO ALIGN-1 ACCESSES IN ONE FUNCTION RESERVE A PHANTOM STACK SLOT (P31 S69; ov_SC02_005/func_80180610)
Using two align-1-typed accesses (the §160a Blk8 byte-aligned struct-assign family) in one
function, with a real store between them, makes gcc-2.7.2 reserve a DUPLICATE, never-referenced
temp slot: vars=8 in the frame with no instruction touching it. Declaring the second access as a
plain scalar with a u8 * offset cast — instead of routing it through the same align-1 record type —
removes the second align-1 access and the 8-byte bloat with it.
Diff tell: a frame 8 bytes larger than the target with no spill or save to account for it, in a function that copies a byte-aligned aggregate more than once. Related: §391 (a byte-aligned struct copies in four instructions, a word-aligned one in two).
§378b ★★★ — THE FOUR VARIANTS OF THE DECL BLOCKER, AND THE TWO PLACES §378 DOES NOT APPLY (P31 S69, all four measured the same day)
§378 gave the SELF-CALLER chain. Three more variants appeared within hours, and two of them break the chain rather than extend it. Identify the variant from the diagnostic before applying anything.
| # | diagnostic | who conflicts | fix | status |
|---|---|---|---|---|
| 1 | conflicting types for func_X |
the TU's forward decl of the fn being DEFINED | fix_arity_callers --any-proto → cast_self_callers |
§378, byte-proven ×8 |
| 2 | conflicting types on a promotion-affected param (s16) |
same, but C89 forbids no-proto | cast_self_callers --sync-decls |
§378a, byte-proven main/func_80036D58 |
| 3 | conflicting types — RETURN type only, decl ALREADY () |
same, and the symbol is ADDRESS-TAKEN, not called | --sync-decls ALONE |
NEW, byte-proven ov_SC04_018/func_8017F35C |
| 4 | conflicting types for <SOME OTHER SYMBOL> |
a CALLEE the draft shares with the TU | NOT the §378 chain — see the refutation | DO NOT no-proto it blindly |
Variant 3 — the two levers that BOTH no-op. ov_SC04_018/func_8017F35C matched 0/47 and still
would not gate. The TU's decl at :5366 was extern void func_8017F35C(); — already no-proto, so
--any-proto has nothing to relax; and the use at :5385 is
func_801788B8((s32)a0, (s32)&func_8017F35C) — the symbol's ADDRESS is taken, so there is no call
site for cast_self_callers to cast. The only conflict is the RETURN type (void vs s32), and a
one-token decl sync fixes it. A decl sync is safe here precisely BECAUSE the site is address-taken:
there are no arguments to convert, so nothing can move.
Variant 4 — the refutation. I documented "run the same chain on the callee the diagnostic names"
and that is WRONG at scale. Applied to func_8012AD44 in ov_SC07_000, --any-proto no-protoed
60 caller decls and the binary went RED (265b24bb vs 9dbe4241), reverted via journal.
Why the self case is safe and the callee case is not: in the self case, step 2 CASTS the call
sites, so the decl change cannot alter argument conversion. For a callee, cast_self_callers
correctly REFUSES (it reads the return type off the draft, and the callee is not the draft), so the
decl change runs unprotected — and a no-proto decl applies the default promotions instead of
converting to the declared parameter types. With 60 call sites, some argument narrowing changed and
the codegen moved. It banked main/func_80021D38 earlier only because that callee had one decl,
not sixty.
The rule: never --any-proto a symbol whose call sites you are not also casting. Count the sites
first — fix_arity_callers prints the count — and if it is more than a couple, treat the callee as a
byte-risk edit that must be build-verified per binary BEFORE the gate, not after.
Standing order for any decl blocker: read the diagnostic, name the variant, and apply only that variant's lever. The chain is not a sequence to run blindly; it is a decision table.
§395 ★★★ — FIVE NARROWING/PLACEMENT LEVERS FROM ONE 91-INSTRUCTION CRACK (P31 S69; byte-proven ov_SC06_018/func_80189E60, warm start 32 off → MATCH 91/91)
All five are TYPE or STATEMENT-POSITION changes. None is a register pin or a barrier, and four of them are invisible in the diff until you know to look for them.
(a) shorten_binary_op narrows a division to HImode, and a CAST DOES NOT STOP IT. Writing
short / 25 makes gcc-2.7.2 do the divide in HImode. An (s32) cast on the dividend does not
defeat it — get_narrower sees straight through the NOP_EXPR. The only lever that works is to
route the dividend through a real s32 TEMP, assigned in its own statement.
(b) A clamp local must be s16 to produce the target's plain MOVE. A HImode pseudo makes
b = <int expr> a plain move — which IS the target's addu $s3,$v0,$zero — and the subsequent
compare's sign-extend then reads the pre-copy $v0. All four s32 spellings coalesce that copy
away; a register pin restores the copy but cse then canonicalises the sll onto b's register and
the residual plateaus at 1. The type is the lever; the pin is a trap.
(c) Narrow the argument INSIDE the expression, not around it. (u16)(x | ((b << 8) | 0x3000))
makes convert_to_integer distribute the narrowing INTO the shift, emitting sll $a1,$s3,8. Casting
outside gives the sll 16/sra 8 pair instead. The trailing andi is then just the u16→int
promotion, not a mistake to chase.
(d) base = &D_XXXXXXXX must be the FIRST statement in the function. Anywhere else and
REG_ALLOC_ORDER hands it $a1 — costing two callee-saved registers and 50 mismatches. Note sched1
still sinks the lui/addiu into the multiply shadow afterwards, so the source position and the
emitted position differ: do not "fix" what looks like a misplaced address load.
(e) A parameter's declared width is a sched1 dial. u16 arg1 vs s32 arg1 + a mask moved
addu $s4,$a1,$zero by three slots. When a prologue copy sits a few instructions off and nothing
in the body explains it, try narrowing a parameter before touching the body.
Meta-lesson for the reach lanes: this function is a REACH EXEMPLAR (its cluster has six members). Five levers bought one crack — and the other five members inherit them for free via remap or a seeded card. Spend the deep effort on the exemplar, never on the sibling.
§314b ★★ — TWO ABS-RANGE GUARDS IN ONE FUNCTION NEED DIFFERENT SPELLINGS (P31 S69; byte-proven ov_SC04_002/func_8018691C, 111 ins)
A function bounds-checks X and Y the same way in source, and the target emits them DIFFERENTLY: one as a merged abs compare, the other with an out-of-line negative arm. Do not "fix" the asymmetry — reproduce it.
- The merged form (X here) is what every ordinary spelling produces:
if (x >= 0x78 || x <= -0x78)cross-jumps the twosltiinto one. - The out-of-line form (Y here) requires a
COND_EXPR:
y >= 0 ? y >= 0x78 : -y >= 0x78
do_jump's COND_EXPR case emits the target's separate negative arm. Every if/else/goto
spelling cross-jumps the two slti into one and cannot reach it — the ternary is not a style
choice here, it is the only construct that survives.
Diff tell: two structurally identical range guards where yours emits one slti pair and the
target emits two, with a branch to a tail arm. Reach for the ternary on the one that differs, and
leave the other alone.
§322b ★★★ — THE CARVE CLASS IS COMPLETABLE, AND EVERY WORKTREE CARVE-REFUSED WAS AN INSTRUMENT VERDICT (P31 S69, Fable-3 audit; byte-proven end-to-end)
The artifact, first — it invalidates a whole verdict class. jr_isolate_all.jr_inventory
resolves each committed .rodata carve's owner via family_remap.reloc_targets, whose nins_of
reads .run/sig.<binary>.jsonl — which is gitignored, so no parallel_gate worktree has it.
Inside a worker every carve therefore reads UNOWNED, jr_inventory R32-aborts, harvest_verify
prints isolate FAILED, and the draft is booked CARVE-REFUSED.
That verdict describes the WORKTREE, not the function. Measured on ov_SC02_000/func_8017F950
(a RELOC-ONLY twin whose body rtu-MATCHes 117/117): dry-run isolation passes in the main tree and
aborts in the worktree with 30 phantom UNOWNED carves. Fixed by symlinking the registry in
parallel_gate.stage_generated, which now also reports it in missing_generated when absent.
Re-check any CARVE-REFUSED row recorded before this fix — it is probably not one.
The refusal itself is EXACT, not conservative. One object emits one contiguous .rodata, so two
same-subseg carves with a raw table between them are genuinely unsatisfiable — build_carve is right
to refuse. The real fix already exists and harvest_verify already runs it: isolate the function
into its own subseg. Live census over all 123 jtbl-carrying reachable stubs: 71 NON-CONTIGUOUS
(main 21, ov_ 50) + 5 span-starts + 2 not-in-data-asm + 4 main curated-name parse errors. The 15
md_* "island-below-tail" rows are not blocked at all — §303 derives them at build time and the
probe label is stale.
End-to-end, byte-proven, zero tokens: family_remap draft → isolate → extract → carve → extract
→ build ⇒ BYTE-IDENTICAL in 23 seconds (ov_SC02_000/func_8017F950, a new .rodata piece with
25 relocated entries; the stub oracle drops the function).
What still blocks the automatic route: a dry run over all 57 non-main refusals passes 39 and fails
18 — 9 × unaddressed helper definitions, 8 × a file-local type not carried (§323), 1 × a
typedef-forward misread as a conflicting body. All are overlay_src_split plumbing, ≤30 lines each.
main's 25 additionally need a jr_isolate_all port (src/800.c parses except for four
#ifdef NON_MATCHING blocks; Makefile:642 hardcodes main's interleave; gate_main lacks the carve).
Reach: 21 of the 71 are RELOC-ONLY/HASH twins of ALREADY-BANKED bodies — 3,852 instructions, free at ~25 s each. Carve work is not matching work; do not send these to a drafting agent.
§332b ★★★ — THE §332 "WALLS" ARE A PER-OBJECT ASSEMBLER MODE, NOT A C LIMIT — 6 CLOSE AS REAL C (P31 S69, Fable-3)
§332 recorded the la-in-a-delay-slot class as unemittable from C at any closeness, and
oracle_reorder.py exists to prove a draft byte-correct-but-unemittable. The root cause is
narrower than that: the 800c3 / 800c2 band was assembled with reorder-mode slot filling —
a property of how those OBJECTS were built, not of the C.
Measured: a maspsx reorder-passthrough (3 lines) plus as -O2 is byte-INERT across the
whole 800c3 and 800c2 objects (P1 vs P2 .text identical) and yields 0 diffs for six walls
whose drafts already exist — func_80061FA8, func_8005FA94, func_8005D244, func_8005DBD8,
func_8005D734, func_80062144 — plus one at closeness 1. The other five never had correct drafts.
Implication: a per-object Makefile switch converts six "permanently unbankable" functions into
ordinary C banks, and retires oracle_reorder.py. Of the 15 previously-listed walls only 13 are
reachable at all (PopMatrix/PushMatrix are LINKED dead text).
The general lesson: when a class is declared unreachable by the toolchain, ask whether the property belongs to the TOOLCHAIN or to the OBJECT it produced. A per-object assembler mode looks exactly like a compiler limit from the diff.
§378c ★★ — THE FIFTH DECL-BLOCKER VARIANT: THE DRAFT REDECLARES WHAT THE TU ALREADY OWNS (P31 S69)
§378b's table has four variants, all about the TU's declaration of the function being banked or of a
callee. A fifth exists and the table does not cover it: the DRAFT redeclares a type, data symbol or
callee that the TU — or a header the TU includes — already owns. Measured instances: struct B16,
Blk8 + D_80126970, SVECTOR, callee arity (func_801811C8), callee promotion (func_8012AD44).
Fix direction is the mirror of §378: edit the DRAFT to adopt the TU's spelling (§367 banked-spelling rule), never the TU to match the draft. The levers exist; what is missing is a gate rung that applies them to type tags and callee prototypes rather than to the function's own decl.
Also measured: two "integration-blocked" rows were phantoms — one a blind worktree (§322b), one
where a fix_arity_callers --any-proto pre-pass broke a SIBLING TU (§378b variant 4 biting a second
time). Re-compile a stalled row in its real TU before believing its recorded verdict.
§396 ★★★ — SIX LEVERS FROM THE S69 SINGLETON ROUND, INCLUDING ONE THAT ONLY A COND_EXPR REACHES (P31 S69)
(a) A cross-jump tail-merge whose identical suffix is only slti; beqz CANNOT be fenced.
__asm__("") does not stop it, and neither if/else nor &&/|| avoids it — all three merge and
lose 5 instructions. The abs guard must be a ternary inside the if condition:
if (d >= 0 ? d < 0x80 : -d < 0x80) /* emits bltz/slti/beqz/j-join/negu/slti/beqz, unmerged */
This is the SECOND independent discovery of the do_jump COND_EXPR mechanism in one session
(§314b found it on a different function, in a different binary, from a different warm start). Treat
it as a first-class lever, not a curiosity: when a guard's two arms share a two-instruction tail,
the ternary is the only construct that survives cross-jump.
(byte-proven ov_SC02_005/func_8018074C; §314b ov_SC04_002/func_8018691C)
(b) Let gcc cross-jump the arms — do not hand-merge them. A warm start with a hand-written
shared tail (goto done) plateaued with 12 "register mismatches". Rewriting it as four arms, each
writing all four variables, let gcc perform the merge itself and the register diffs vanished with
it — they were a CONSEQUENCE of the join, not separate defects. §162's surviving-copy rule (the LAST
copy survives) tells you which arm's spelling to match.
(byte-proven ov_SC06_018/func_8017D4DC)
(c) Commute a comparison to change which pseudo is born first. A $a1/$a2 REGALLOC-PERM closed
in two compiles by writing the test as *(s16 *)(a0+0xDE) > -lim instead of the natural order: it
births the 0xDE pseudo BEFORE lim's last use, so the two conflict and lim is pushed off $a1.
Same slt emitted; different allocation. Operand order is a lifetime dial.
(byte-proven ov_SC03_112/func_8017EE48)
(d) UNPINNING can be the lever. §76 allocno-class: splitting val = (8-q) << 8 into two sets on
ONE variable (making it a global allocno → $a1) plus removing the pins on t and q — so the
divide's hi>>8 temp stops stealing $a3 — took 5 → 0. Reach for unpinning when a pinned register
is being stolen by an unrelated temp.
(byte-proven ov_SC03_102/func_8018139C)
(e) When a file-scope typedef collides, move BOTH the type and the extern to BLOCK scope.
Renaming the type at file scope only trades conflicting types for <Blob> for
conflicting types for <the data symbol> (§65g). Putting the shape decl AND its
extern inside the function (§100), with a unique name, clears both. Watch the §184 textual-typedef
prescan trap — paraphrase the comment.
(byte-proven ov_SC02_005/func_80189B30; this is §378c's fifth variant, solved)
(f) A K&R old-style definition is an integration lever. When the TU's existing prototype is
(s32, s32) and the body wants s16 parameters, writing the definition K&R-style lets the s16
parameters PROMOTE to the TU's prototype — no decl edit, no cast, and the gate's "conflicting types"
disappears.
(byte-proven ov_SC05_018/func_80182CDC)
§397 ★★★ — RE-RUN THE TWIN SCAN AFTER EVERY EXEMPLAR BANK; A CLUSTER SIBLING IS FREE THE MOMENT ITS EXEMPLAR LANDS (P31 S69; ~250k tokens spent proving it the expensive way)
The mistake, measured. A reach-6 cluster (ov_SC06_018/020/022/024/032/033) showed as
open-open in the family map: six members, none banked, so seed_ref reported "no banked twin"
for every one of them. I cracked the exemplar ov_SC06_018/func_80189E60 (203k tokens, five new
levers → §395), then drafted four siblings at ~60k each.
They did not need drafting. The moment the exemplar banked, seed_ref returned it as a banked twin
for all of them — and the agents' own diffs said why: "label-stripped .s diff vs the twin is
EMPTY", "an EXACT clone (asm diff = labels only, same relocation symbols)". family_remap +
the §378 chain banks those for ~0 tokens. The waste included ov_SC06_020/func_80183FD0, which
burned 257k tokens plateauing at permuter-class NEAR 4 and then remapped cleanly from the twin
that had banked in the meantime.
The law: a bank CHANGES THE TWIN GRAPH, and every open-open cluster is one bank away from being a remap job. The twin oracle answers "is there a BANKED body like this?" — so its answer for every sibling is stale the instant the exemplar lands.
The procedure, in order:
- Crack ONE exemplar per cluster (never two — see below).
- Gate it. Bank it.
- RE-RUN
seed_ref(and--near) for every remaining member of that cluster. - Remap what now has a twin (
family_remap→ §378 chain → gate). Draft only what does not.
And never draft two members of the same cluster in parallel. I drew func_80189E60 and
func_80183FD0 — same cluster — in the same round: if either cracks the other is free, so the second
agent is pure waste, and it was.
This is the crack-wave-sweep-map-regen rule ("regen the map after banking") applied one level down:
the FAMILY map is not the only stale artifact — the twin oracle is stale too, and it is the one the
cards read.
§396g ★★ — A GUARD LADDER'S RUNGS MUST STAY SYMMETRIC OR reorg.c LOSES ITS BRANCH REDIRECT (P31 S69; byte-proven ov_SC03_028/func_80184C90, 92 ins)
A chain of range checks that all jump to one shared tail (flag = 0; goto check;) only emits the
target's shape if every rung is spelled identically:
if (X >= Y) { flag = 0; goto check; } /* every rung, same form */
Reusing a temp across rungs — e.g. flag = a0->E as a scratch — writes $s1 and kills
reorg.c's redundant_insn branch redirect past the shared flag = 0, so the merged tail stops
forming and the diff blooms far from the edit.
Diff tell: a guard ladder where the target redirects several branches to one tail and yours re-emits the tail per arm, with register noise around a temp you thought was free. Make the rungs symmetric before chasing the registers.
§398 ★★★ — family_remap CARRIES THE SOURCE TU's DECL ENVIRONMENT INTO A DESTINATION THAT ALREADY OWNS THOSE NAMES (P31 S69; measured 3 banked of 22)
The measurement. 22 open stubs with a banked twin at d≤5 (no jtbl blocker) were remapped with
family_remap and gated as a batch. Outcome:
| result | n | what it means |
|---|---|---|
| banked | 3 | |
| CC1-FAIL / PLUMBING | 11 | the BODY is fine; the destination TU rejects the carried declarations |
| DIFF | 8 | genuinely not byte-identical — these were h_norm twins, an 80% class, not 100% |
The eleven integration failures are not subtle: syntax error before …, 'D_801A6964' undeclared,
conflicting types for …, parse error before ')', invalid lvalue in unary …. Those are the
signature of a decl block written for ANOTHER translation unit.
The mechanism. family_remap correctly rewrites the BODY (per-overlay symbol addresses, reloc
targets). It does not reconcile the DECL ENVIRONMENT: the source TU's typedefs, externs and
callee prototypes come along verbatim, and the destination TU frequently already owns those names
with different spellings — §378c's fifth variant, at scale and by construction.
So a remap is a DRAFT, not a bank. Expect roughly half to need the integration pass, and budget for it: after remapping, run the §378b decision table per failure, and adopt the DESTINATION TU's spelling (§367) rather than the source's.
The tooling gap, named: family_remap should emit the body with the destination TU's decl
environment — the information exists (decl_prior already computes it for cards, and
cast_self_callers/fix_arity_callers already edit it). Until it does, the remap lane's realistic
yield is ~15% straight-through and ~50% after integration, not the 76–88% the PURE-class rate
suggests. Quote the straight-through number when planning, not the class rate.
§398b ★★ — NAMING A SUB-EXPRESSION IN A LOCAL CHANGES WHICH PSEUDO SURVIVES; INLINE IT AT BOTH USE SITES (P31 S69; byte-proven ov_SC03_028/func_80183264, 93 ins)
Writing a repeated computation inline at both use sites and writing it once into a named local are not equivalent, and the difference is visible in the register file:
f( (v1 * 104) / 1024, ... ); /* inline at BOTH sites — cse builds the CSE itself */
s32 t = (v1 * 104) / 1024; /* named — expmed.c:2986 copy_to_mode_reg's temp */
Naming it lets expmed.c's copy_to_mode_reg temp coalesce with the product (an in-place
addiu/sra) and moves the surviving divide-copy onto the /16. Letting cse construct the common
subexpression itself keeps the temp a separate pseudo — which is what the target shows.
Two companions from the same crack:
- An
s16local's cse quantity differs from ans32one, and that difference alone is why anaddu $s3,$s2,$zerocopy survives. Rewriting ass32 t+ an(s16)cast letst.i.csefold three regs into one and the function loses two saved registers and two copies. The declared width is a cse-quantity dial, not just a codegen one (compare §392b). - The §136d-1
zrtrick needs to be applied to BOTH copies, not one.arg = ang;alone iscanon_reg'd away;arg = ang + zr;on both copies lets cse merge the two identicalang+$0expressions into one, reproducing the target's pair.
Banking caveat: a register … __asm__("$0") pin fails dedup_propagate.compiles_standalone
(§37), so a function cracked this way banks ×1 and never propagates to siblings. Prefer a
width/inlining lever over a $0 pin when the function has cousins.
§399 ★★★ — FOUR LEVERS FROM THE FINAL S69 ROUND (P31 S69; each byte-proven, none previously in the cookbook)
(a) A fence BETWEEN two prologue loads, where source order does nothing. Two loads at the top of
a function emitted in the wrong order, and reordering the C statements had no effect — the order
is fixed before statement order matters. A __asm__ __volatile__("") placed between the two
loads (§194-A) swapped them and closed the last 2 instructions.
(byte-proven md_MAIN_013/func_800CB56C, 94 ins)
(b) SINK A CALL INTO BOTH ARMS and let cross_jump keep only the jal suffix. A warm start sat at
88 ins / closeness 86. Sinking func_8017E3E8 into both arms of a >= 0x400 test (so
cross_jump merges only the shared jal tail, §193-C) took it to 92 ins / closeness 13; then §3-T2
field-order on the two zero-stores — deliberately NOT the neighbour's order — let each sh $zero
fill an lhu load-delay slot and closed it. Note the direction: duplicate the call in source so
the compiler merges it, rather than writing the merged form yourself.
(byte-proven ov_SC07_001/func_8017EDC0, 92 ins)
(c) A $v0 → $a0 → $s3 DOUBLE COPY IS A TWO-PSEUDO TELL. Not a scheduling artifact — it means
the source has two distinct variables: an SImode temp holding the call result (living in a
caller-saved register for the first compare) and a separate HImode variable for the tail:
s32 t = call(); /* SImode, caller-saved, feeds the compare */
s16 ret = t; /* HImode, $s3, survives to the tail */
That split alone took 70 → 37. And §195-N precondition 5: nesting return 1 inside the if-chain
with exactly ONE trailing return 0; blocks jump.c's store-flag transform (killing
sltiu $v0,$v0,1), so reorg fills both delay slots with addu $v0,$zero,$zero — 18 → 0.
(byte-proven ov_SC02_017/func_8018347C, 94 ins)
(d) THE RE-TIE AS A BIV KILLER. __asm__("" : "=r"(q) : "0"(q)) on a halfword walker gives q a
second set, so n_times_set > 1 and loop.c refuses it as a biv — which kills the combined
address giv that had biased the base to +2 and had sunk $s0's init into the preheader.
combine_givs picks the last-discovered / lowest-offset giv, so denying it the biv is the whole
lever. Measured alternatives: a volatile q was WORSE (8), a dead q[0] read stayed at 6.
This is the third distinct use of the zero-byte re-tie in one session — §380 (un-hoist a
move_movables invariant), §393 (kill the scheduler's birthing boost), and now §399d (deny a biv).
One line, three passes: when a single-set pseudo is being treated specially, give it a second set.
(byte-proven ov_SC07_001/func_8017E4DC, 92 ins)
§400 ★★ — A BASELINE CHECK THAT CONFLATES "ABSENT EVERYWHERE" WITH "CHANGED UNDER US" SILENTLY DROPS NEW FILES (P31 S69; 8 files, one session)
parallel_gate refuses to adopt a worker's file when the main tree's copy no longer matches the
pinned baseline — the guard the whole tool exists for. It compared:
base = sh(["git","show", "%s:%s" % (pin, path)]).stdout # "" when the path is NOT at the pin
cur = open(path).read() if os.path.exists(path) else None # None when absent from the main tree
if cur != base: REFUSE
A jtbl carve splits a TU, creating src/<bin>/<bin>_jr_<addr>.c that exists at neither the pin
nor the main tree. That is None != "" → refused as "main tree moved under them", and never
added. Eight accumulated in one session.
Why it stayed invisible — this is the instructive part. Nothing failed locally: the file is on
disk, the build finds it, check-all goes green. But config/splat.<bin>.yaml names the subseg and
IS committed, and 31 sibling _jr_ files in that binary are tracked — so a fresh clone gets the
config without the source. The defect only surfaced when a later run's dirty-tree guard refused to
start and listed them.
The fix: distinguish not present at the pin from empty at the pin using git show's RETURN
CODE, so absent-in-both compares equal and the file adopts. Report new adoptions explicitly — adding
a brand-new source file must never be silent (R32).
The general law: when a comparison uses two different sentinels for "nothing" ("" from a failed
command, None from a missing file), it will report a difference that does not exist — and in a
guard, a phantom difference becomes a refusal, which looks exactly like the guard working correctly.
Check what your "absent" value IS on both sides before trusting an inequality.
Corollary for carve state: the standing rule is never blanket-add config/overlays.mk or the
splat yamls (they are shared state). That rule does NOT extend to a carve's own new source file —
that file is per-binary, is named by a committed yaml, and MUST be adopted with the bank that
created it.
§401
A probe that does not model the GATE's carve is optimistic, and for jtbl functions it is wrong 92% of the time.
recover_integration --probe-only reports a per-function real cc1 verdict by compiling the draft in
its real TU and comparing that function's bytes. For an ordinary function this is an excellent
predictor. For a jump-table-bearing function it is systematically wrong, because the byte-gate does
something the probe does not: harvest_verify carves the function into its own object so the jump
table can be emitted ([jtbl] carved func_X in its log). The carve changes the translation-unit
boundary, and therefore the codegen — so a draft can match in the uncarved TU and DIFF once carved.
Measured (P31 S70), on 52 leftover standalone-MATCH drafts:
| probe verdict | n | jtbl-bearing |
|---|---|---|
| MATCH (gate says DIFF) | 26 | 24 (92%) |
| CC1-FAIL | 20 | 5 (25%) |
| DIFF | 6 | 0 |
Worked case: ov_SC01_004:func_8017EB30 probes MATCH 279 ins; the gate says DIFF in a worktree and
in-tree and via harvest_verify directly — three independent runs, so it is not a worktree artifact
and not the canon -> cast_call_sites -> sig_unify pipeline either.
Rule. The whole-binary byte-gate is the arbiter (G3). Before believing a probe MATCH, ask whether
the target's .s contains jtbl_[0-9A-Fa-f]{8} (the same predicate harvest_verify carves on, and
parallel_gate._JTBL_RE). If it does, the probe has not modelled the work the gate must do — treat the
verdict as UNPROVEN and do not use it to size a batch. This is the R34 shape: two oracles that disagree
because one is structurally blind, fixed by knowing WHICH is blind, not by trusting the friendlier one.
Corollary — a declaration fix that only turns CC1-FAIL into DIFF has bought nothing. fix_arity_callers --any-proto (382 edits to the fleet-shared engine_core.h) and cast_self_callers --apply --sync-decls
(main's 4 self-conflicts) both did exactly that in S70: every target compiled afterwards and none matched.
Revert such an edit rather than leave unverified shared-state risk standing for zero banks.
§402
A tool that resolves an input path in a DIFFERENT working directory sees an empty world and calls it success.
parallel_gate runs its worker as gate_stage ... --drafts <path> with cwd=<git worktree>. It passed
--drafts through verbatim, so a relative path resolved inside the worktree. .run/ is deliberately
not linked into a worktree, and R12 puts ALL project scratch under .run/ — so every plan following the
project's own convention pointed at a directory that does not exist there. gate_stage found 0 drafts,
banked 0, and exited rc=0.
Signature: 35 binaries / 57 drafts, all "banked 0", each in 1-2 seconds, while the same drafts gated in-tree banked 15/16 and 3/6. After the fix the same job takes 100s.
The only tell was the runtime. Every other signal — exit code, per-binary summary line, the absence of any error — said success. A wall-clock far below the cost of the work the tool claims to have done is evidence the work did not happen; a gate that "verifies" a binary faster than a compile is not a fast gate.
Rules. (1) Resolve every caller-supplied path against the REPO before handing it to a subprocess with a
different cwd. (2) Assert the input is non-empty and REFUSE otherwise (R32/R43) — a 0-input job must never
report a 0-yield result, because the two are indistinguishable downstream. (3) When a batch returns all
zeros, exonerate the harness before recording a fact about the subject (R40).
Unaudited blast radius: any earlier wave that pointed parallel_gate at a .run/ drafts dir produced
honest-looking zeros. Backlog rows marked failed from such a run may never have been gated at all.
§403
An undo-journal that restores by NAME corrupts any file whose symbol is declared twice — and reports success.
Both fix_arity_callers --undo-journal and cast_self_callers --undo-journal record the original text of
each edit and restore it keyed by function name. When a symbol has more than one declaration in the file,
the originals are restored into the wrong occurrences, silently swapping distinct prototypes.
Byte-witnessed twice in one session (P31 S70):
fix_arity_callersprintedrestored 382, kept 0, missing 0;git diffshowed 97 insertions / 97 deletions in the FLEET-SHAREDsrc/shared/engine_core.h—func_8012A828()<->(int a0, void *a1)<->(s32 a0, void *a1)rotated between three declaration sites.cast_self_callersprintedreverted 10 edit(s) across 1 file(s);src/800.cwas left with the two declarations offunc_80031988swapped ((Ent30D80 *arg0)<->(struct Ent30D80 *)).
This is R48 (never key by bare function name) applied to a journal, and R57 (an instrument's own write path is part of the instrument). The byte-gate does not necessarily catch it: the corruption is in a header, and a rebuild only differs if some binary's codegen happens to depend on the swapped prototype.
Rules. A journal records file + occurrence index (or byte offset), never just a name; and an undo
hash-verifies the restored file against the recorded pre-edit content and FAILS LOUD on mismatch.
After ANY undo-journal, run git diff on the touched files and restore the specific named file from
HEAD if it is not clean — never a blanket git checkout -- src/ (R42).
SHIPPED (S70). The occurrence->original mapping is not recoverable from either journal format,
so both tools now REFUSE (rc=2) when one (file, after) group maps back to differing before
texts — naming the file and the rival originals, and writing nothing. Journals additionally record
per-file sha_before, and a clean undo hash-verifies its own result. Negative-controlled against the
journal that caused the corruption: it refuses and leaves the tree untouched. The refusal also showed
why guessing was hopeless — extern void func_8012A828(); maps back to four distinct originals
((int a0, void *a1), (int, void *), (s32 *, s32), (s32 *a0, s32 a1)). A loud refusal costs
one git checkout; a silent swap costs a corrupted fleet-shared header nobody notices.
§404
harvest_verify invoked DIRECTLY verifies but does not BANK — gate_stage is the entrypoint that persists.
harvest_verify.py --binary <b> --drafts <dir> substitutes each draft, rebuilds, and reports
verified N / VERIFIED: <fns> with the final SHA. That output means the draft passed the whole-binary
byte gate. It does NOT mean the function is banked: run alone, the splice does not survive, and the
tree is left with the INCLUDE_ASM stub still in place.
Byte-witnessed (P31 S70): verified 1 / VERIFIED: func_8002B0B4, final SHA BYTE-IDENTICAL — and
INCLUDE_ASM("asm/nonmatchings/800", func_8002B0B4); still at src/800.c:18341. The commit made
straight afterwards captured only an unrelated comment, so a bank was REPORTED that never existed.
CORRECTED, SAME SESSION — the stated mechanism is NOT established. Later in S70 the §332b wall
drafts were gated the same way and harvest_verify did persist: three functions
(func_8005D244, func_8005DBD8, func_80061FA8) were spliced into src/ and confirmed banked
against corpus.stubs. So "harvest_verify never persists" is refuted by counter-example. What is
CERTAIN is only the observable: verified 1 / VERIFIED: func_8002B0B4 was followed by a tree whose
stub was still present. The most likely explanation is that the bank was destroyed downstream by my
own git checkout -- src/800.c while undoing the §403 corruption — i.e. the loss was MINE, not the
tool's. Recorded as unresolved rather than left as a confident wrong claim.
What survives regardless, and is the actually useful rule: harvest_verify leaves banks
UNCOMMITTED in the working tree, so any later git checkout of that file silently destroys them
(R42). gate_stage.py --drafts <dir> --binary <b> --commit wraps it with the persistence,
propagation and COMMIT steps — use it to bank. Note gate_stage will then report those functions as
failed because they are no longer stubs; that is expected, not a regression.
Reach for harvest_verify directly only as a diagnostic — for instance to A/B whether the
canon -> cast_call_sites -> sig_unify pipeline is what breaks a draft (§401).
The check that would have caught it, and the general rule: confirm a bank against
corpus.stubs(<binary>) — the address must be GONE from the stub set — never against the tool's own
success line (R40). A frontier count that does not move after a reported bank is the tell; here
355 -> 314 = 41 reconciled exactly as 15+3+3+20 with main contributing zero, which is how the
phantom surfaced.
§405 ★★★ — THE S70 WAVE HARVEST: 130 agents, 113 MATCH, and the laws they brought back
One session, three waves (50 + 40 + 40 targets), 113 MATCH / 4 NEAR, 132 functions banked across
four R22-green gates. The per-agent notes are preserved in each run's journal.jsonl under
subagents/workflows/wf_*/. What follows is the generalisable residue, grouped by lever family.
A. THE ORACLE HAS A HOLE: match_one cannot see a jump table (§405-A)
match_one compares .text only. gcc-2.7.2 emits switch case BODIES in source order while jump
table entry i points at case i, so case-value and case-order are independent and .text pins
only the order. A draft can therefore score a perfect closeness 0 while emitting an IDENTITY table
where the target's real table is PERMUTED — byte-witnessed on resident/func_800D02D0 (110/110 with
jtbl_80113F14 permuted). A MATCH on a switch function is not evidence about its table. Recover
the labels FROM the table (jtbl[i] -> block addr -> that block's delay-slot constant = case i),
then emit cases sorted by block address. This is R34's shape in the project's most-trusted oracle:
some historical "match_one MATCH but the gate rejected it" verdicts were the ORACLE being wrong.
B. THE SCHEDULER DIALS (the dominant residual family this wave)
- Birthing-boost kill (§49/§350/§393), now with its placement rule. sched1 boosts a
single-set pseudo's def; making it 2-set removes the boost and sinks the insn. Every pure-C
second-set spelling (
x = x,x += 0,x &= -1, dead-reset) is folded beforereg_n_setsis computed, so the zero-byte `asm("" : "=r"(x) : "0"(x)) re-tie is the only reachable lever — and it must sit in a LATER basic block than the boosted insn (same-block placement fixes the schedule but rotates the registers instead). stop_search_phalts reorg's delay-slot scan at ANY asm. So a laundering__asm__between a copy and a branch BLOCKSfill_simple_delay_slots. Put the frame/launder asm LAST, or move it into both arms, or you trade a schedule fix for a lost delay slot.- Comparison operand order is a LUID dial.
limit > sumvssum < limitdecides which operand is expanded first, hence which sign-extendloop.choists first (4 -> 0,func_8018088C). Also reaches conflict-driven regalloc swaps: reversing-lim < xtox > -limmoved a last-use past a copy's birth and changedfind_reg's answer (func_8017EE48) — which REFUTES §137's "source levers are a dead end" for CONFLICT-driven (as opposed to priority-driven) swaps.
C. REGISTER ALLOCATION FROM C, WITHOUT PINS
- Variable IDENTITY picks the allocator. A local whose live range spans two blocks goes to
global-alloc ($a0-class); a single-block one goes to local-alloc ($v1-class). Splitting or sharing
one variable is therefore a register dial (
func_8017FBCC). - A value flowing into a cross-arm join is a GLOBAL allocno and loses first-fit to block-local
constants. Duplicate the whole expression into every arm and let
cross_jumprefund the size (func_8017D4CC) — a suffix-only lever, consistent with §193-C. - Pass-through parameters reserve argument registers for free. Two extra params handed straight
to the callee emit ZERO instructions (self-copies deleted) but their copy-preferences hold
$a1/$a2 all function long, pushing a preference-less allocno to $a3 (
func_800CAE88). - A dying HARD reg becomes local-alloc's dest suggestion — so a pin can CAUSE the swap you are trying to fix; keeping the value live one insn longer with a use-only asm restores it.
rand() % Kin one statement vs two.expand_divmodzeroes its target only when the destination is mentioned in the dividend, so the one-statement form reuses the variable's own pseudo as the quotient temp and stretches its live range into a hard-reg conflict (func_8017FED8).
D. INTEGRATION IS STILL THE BOTTLENECK, AND THE TU IS THE AUTHORITY
Roughly a third of these functions had a byte-correct body already and failed only on declarations.
The recurring shapes: adopt the TU's own spelling verbatim (law 2) · §37/§124 __asm__("name")
definition-side alias when the TU's forward decl disagrees on the RETURN axis (which
fix_arity_callers structurally cannot reach) · block-scope the externs and typedefs when a
file-scope one would collide with a definition later in the TU (§183) · K&R the definition when the
TU's decl is already no-proto and a parameter is not promotion-stable (§324/§99).
E. WHAT THE AGENTS REFUTED
- §137's source-permutation invariance is a real law for PRIORITY-driven ties and false for CONFLICT-driven ones (see B above).
- §153's "cse2 puts the deleted def straight back" does not hold for the dead-def case: a
do{}while(0)stops cse1 atNOTE_INSN_LOOP_END, the reg goes dead, anddelete_dead_from_cseremoves it before cse2 ever sees it. - §257-8 and §16x disagree on volatility polarity by SITE: dropping a
volatileuse-barrier flips a prologue save pair in one function while the volatile spelling is load-bearing in another. Diagnose per site; do not carry the polarity across functions.
§406 ★★ — THE PROLOGUE-WEAVE CLASS IS MECHANICAL AND LARGE (134 open stubs)
sw $s0 / move $s0,$a0 / sw $ra in the prologue window. Census: 134 of 1,237 open stubs (11%)
carry the shape. The residual is always the sw $ra SLOT.
Cause, read out of cc1's own .i.sched2 dump (ov_SC02_005/func_8017F898): bb0 is all
constant-address MEMs, so memrefs_conflict_p (sched.c:614 — "frame-pointer addresses cannot
conflict with static variables") finds NO dependence; sw $ra is ready at T-2 and
schedule_select's potential_hazard (memory unit beats ALU, sched.c:2616) picks it early, sinking
it to just above the bne.
Twelve variants were measured INERT — volatile locals, arrays, structs, /s-defeating casts,
address-taken scalars, statement order, cached-global and volatile-global.
The lever: a NON-volatile __asm__("" : : : "memory") immediately after the parameter copy. The
BLK clobber gives sw $ra a successor, so it is only ready after the load is picked and lands back
at index 3.
Why this is a SWEEP and not an idiom (the project thesis): one known lever against 134 known targets is exactly the "is this MECHANICAL?" test the harvest gate asks. Build the sweep — apply the lever to the class, gate the batch — before drafting any of these individually.
§407 ★★ — LATE-WAVE ADDENDA TO §405 (the last agents in)
- An in-block
return 0;can make reorg REFUSE a delay-slot steal. Onmd_SC07_004/func_801A3798a tailreturn 0;after the call leftaddiu $v0,$zero,3unable to fill thebnez $v0slot (+1 nop, LENGTH-DRIFT/1). 15 tail respellings were probed and ALL were inert — integer-space address, pointer local, sized array, hoisted args, volatile / memory / re-tie fences. The only lever that reached it is §16/§3-B's sharedgoto ret0;. The discriminator is the bnez TARGET block's head: an indexed-global load (lh K($s0)/sll/lw sym($v0)) is what suppresses the fill — replace that tail with anything simpler and the fill returns. t <<= 16; v = t >> 16;is not the same asv = (s16)t. The explicit two-step forces thesllin place on$v0, which stops dbr sinking a followingshinto a branch delay slot.- The §47 non-volatile re-tie as an EMISSION-ORDER dial. Applied to a
$0-add opaque copy it kills that copy's sched1 birthing boost, so the copy emits BEFORE the call's return-value copy and dbr steals the right instruction for the branch slot (105 -> 83 on one function). cse.c find_best_addrfolds EVERY(plus (reg) (const))whose base has a constant equivalence back to%hi/%lo(sym+K)— only a BARE reg address is exempt. Sop = &SYM; p->fieldcan never emitlh K($s1); the zero-byte__asm__("" : "=r"(p) : "0"(p))launder kills the equivalence and keeps the reads on onela(extends §42d-2/§20).- A whole-frame 8-byte offset is read off cc1's own
.frame ... # vars=Ncomment. A plainforallocates expand-time temps that a hand-invertedif(...){do{}while();}does not — the frame delta IS the tell for which loop form the source used.
INSTRUMENT CAUTION, from the same agent (worth more than any of the above). Its first fleet-wide
scan concluded "no banked function in the fleet ever has a delay slot clobbering its branch operand".
That scan read zero files: a BANKED function has no .s under asm/ at all, so the corpus it
searched was empty by construction. It caught this itself and refuted the claim with a cc1 micro-repro.
A scan over asm/ is a scan over UNMATCHED code only — any claim of the form "no banked function
ever does X" cannot be answered there, and a 0-file scan returns a confident, empty-world answer.
§408 ★★★ — §406 REFUTED AS A SWEEP: THE SHAPE IS THE FAMILY, THE DISAGREEMENT IS THE DEFECT (P31 S71; 0 MATCH / 14 applied, 0 / 210)
§406 shipped as "the single biggest measured lever on the board — 134 targets, one known lever, no agents needed". Measured end-to-end against the real frontier it is a null, and the way it fails is more useful than the lever was.
Two counting errors sat under the 134.
- The denominator held 960 non-targets. The census scanned
corpus.stubsover every binary, andcorpus.stubs('main')counts main's 960 PsyQ LINKED library stubs as open. Partitioned withprogress.linked_subsegs()the real frontier is 210 (main 64 + non-main 146), and the class census falls 134 → 77. 28 of the "members" were library stubs nobody will ever match. - The shape is symmetric; the defect is not. The census predicate —
sw $s0/move $s0,$a0/sw $rainside the target's first 24 lines — is the shape of the whole calling family. It says nothing about WHICH WAY thesw $raslot disagrees.main/func_8002EED8carries the shape with the target's ownsw $raalready sunk to just above thebnez; there the §406 clobber pushes exactly the wrong way.
Derive the class from the disagreement, not from the target (R33/R34). match_one --json already
returns the residual as [i, mine, tgt] triples, so the direction is a two-line read: find the index
whose mine text carries sw ra and the index whose tgt text carries sw $ra.
WEAVE-SUNK mine's sw ra index > target's -> the lever pulls it back up (the §406 case)
WEAVE-EARLY mine's sw ra index < target's -> the lever is BACKWARDS
Over all 210 real-frontier stubs, scoring the best of up to 4 stored drafts each
(tools/weave_sweep.py): no-ra-residual 89 · baseline-MATCH 64 · ra-same-index 22 ·
WEAVE-EARLY 17 · WEAVE-SUNK 15 · ra-one-sided 3. So the lever's true addressable set is
15 / 210, not 134 / 1,237 — and of those 15, fourteen took the clobber (one already carried it)
for 0 MATCH, 2 improved-but-near, and the only member close enough for the lever to be decisive
(main/func_8005D734, closeness 8) went 8 → 91: the clobber is not free, it re-schedules the
whole block.
Why a real exemplar-level lever generalised to nothing. The 15 WEAVE-SUNK drafts have baseline
closeness 8, 85, 103, 104, 112, 146, 158, 158, 158, 200, 231, 302, 362, 400, 502. The sw $ra slot is
a SYMPTOM in a body that is wrong for a dozen other reasons; a scheduling lever can only ever close
the last diff. A lever is only sweepable where the residual is ALREADY the lever's own signature —
median closeness 97 over the non-matching frontier says these bodies need drafting, not dialling.
THE MEASUREMENT THAT PAID FOR THE SWEEP. The same baseline pass found 64 of 210 real-frontier
stubs (30.5%) whose stored draft is already a standalone match_one MATCH at closeness 0, across 33
binaries. Per §376 that is a claim about the BODY and never about the TU — but it means the frontier's
largest single lane is integration, not codegen (matching-is-solved-integration-is-the-bottleneck).
Every one of the 210 open stubs has at least one stored draft on disk.
The general law. Before pricing a class by its SHAPE, price it by its RESIDUAL. A shape census
over targets answers "how many functions look like this"; only a mine-vs-target diff answers "how many
are BROKEN like this", and a sweep's yield is bounded by the second number. The instrument for it is
one --json field we already emitted on every score.
§409 ★★★ — THE S71 JOURNAL-FUELLED WAVE: 100% FIRST-PASS MATCH, AND THE NINE LAWS IT BROUGHT BACK (P31 S71)
The wave. 50 workflows, one agent each, drawn from the 210-function real frontier — the hardest
functions left, every one of which had already refused at least one earlier wave. Every pack carried a
new section, PAST ATTEMPTS ON THIS EXACT FUNCTION, mined from the historical agent journals
(subagents/workflows/*/journal.jsonl) and appended per function: 52 of 60 drawn targets had at least
one prior agent's note, 131 notes in all. Result: every landed agent returned MATCH at closeness 0.
No NEARs, no failures, no agent errors.
Why that number is not a model result. Read the notes and the mechanism is explicit: agents were not solving these functions, they were skipping the dead ends someone already paid for.
ov_SC05_003/func_80181720— the pack's warm-start was a FAILED draft; the agent found attempt 3's surviving MATCH body still on disk under.run/S70x_1/opusand recovered it instead of re-deriving.ov_SC01_009/func_80182438— "the body was never wrong"; four prior MATCHes died on declaration SCOPE, whichmatch_onecannot see.resident/func_800D0488— the unlock was DELETING the warm-start'sregister s32 n __asm__("$4")pin, which was itself producing the 4-instruction residual.ov_SC03_108/func_8016AE5C— three prior MATCH attempts left the gate cause "undetermined"; this agent found it in the config (below, law 9).
The lesson for the harness: a wave's cost is dominated by re-deriving what previous waves already learned, and that knowledge was sitting unread in the journals the whole time. Mining is per-function, cheap, deterministic, and it belongs in the pack build — not in a someday audit.
The nine laws this wave produced
1. ★★★ A RELOCATION-STREAM TRANSPOSITION IS INVISIBLE TO EVERY SIMILARITY ORACLE WE HAVE.
ov_SC02_005/func_80180610: a body at match_one closeness 0 whose objdump -drz reloc stream was
TRANSPOSED against the target — D_80126962 at +0xA4 and D_8012696A at +0xAC where the target has 6A
then 62. gcc-2.7.2 emits that PAIR of sh $v1 stores in REVERSE source order while the four preceding
sh $v0 stores keep source order, so the source must read 62-then-6A to emit 6A-then-62. HI16/LO16
masking hides this from match_one, from the permuter's scorer, and from every similarity tier.
Same blind-spot class as §195-D (j/jal), but for HI16/LO16 — and it is a standing explanation for
historical "MATCH but the gate rejected it" verdicts. Audit with objdump -drz, by offset, not by name.
2. ★★★ A DRAFT'S FILE-SCOPE TYPEDEF CAN COLLIDE WITH ONE DEFINED LATER IN THE SAME TU.
ov_SC03_105/func_801829CC splices in at TU line 5267; func_801848A4 defines
typedef struct {s16 vx,vy,vz,pad;} SVEC_801BA5A8; at 6220. Two anonymous struct bodies are NEVER
compatible types, so the second copy is conflicting types for SVEC_801BA5A8 — which is exactly what
killed the prior gate. The fix is BLOCK scope, never a rename (a rename changes nothing about the
later definition, and the block-scoped form is codegen-neutral). The §376/§378 chain has no check for
this direction: it looks for declarations ALREADY ABOVE the insert point.
3. ★★ gcc-2.7.2 LAYS LOCALS AT INCREASING OFFSETS IN DECLARATION ORDER.
ov_SC03_105/func_80182BD8: declaring buf1,buf2,out1,out2,diff in ascending-address order produced
sp+0x20/0x40/0x60/0x68/0x70 exactly AND freed $fp for the hoisted %hi/%lo(D_800D3918). The prior
draft's reverse order cost that hoist — the whole +1 LENGTH-DRIFT. Frame layout is a source-order dial.
4. ★★ DECLARATION SCOPE IS RETROACTIVE: a file-scope extern at your insert point can break a call
site far BELOW it. ov_SC01_009/func_80182438: file-scope externs at line 3913 broke the
pre-existing zero-arg call func_8012C218() at 3958 (too few arguments). Block-scoping the three
externs took the real-cc1 probe from CC1-FAIL straight to MATCH. Invisible to match_one by
construction, because match_one compiles the function alone.
5. ★★ AN INHERITED register __asm__ PIN CAN BE THE RESIDUAL. resident/func_800D0488: the
warm-start's $4 pin forced move a0,zero three slots early — the exact 4-instruction
ADDRESSING/move-vs-sw rotation being chased. Deleting the pin (while KEEPING the §194-A __asm__("")
fence) matched in one iteration. A warm start is a hypothesis, not a floor — ablate its levers
before adding more (this is §17/§47's dual: the pins that win are the ones you ADD deliberately).
6. ★★ AN s16 LOCAL COSTS lhu PLUS sll/sra PER COMPARE; AN s32 LOCAL DOES NOT.
ov_SC01_005/func_8017FBCC: the +2 drift was the local's declared width, not its uses. When a loaded
value is only ever compared, widen the LOCAL, not the load.
7. ★★ §205 CHAINED STORES KILL CSE FOR LATER READS OFF A LAUNDERED POINTER.
ov_SC05_010/func_80183098: hoist ONLY the fields re-read after a store; hoisting one more
(D_80126B66) stretched its live range and bought $t0/$t1 instead of $v0. Measured v1=16, v2=37,
v3=0. §153's launder and §205's chaining are in tension — the hoist set is the dial.
8. ★★ IN gcc-2.7.2 AN OUTER FILE-SCOPE PROTOTYPE BEATS A BLOCK-SCOPE () REDECLARATION.
ov_SC01_005/func_8017F9F8: extern int func_8001AAA0(void); at TU:2585 made the direct call error
too many arguments no matter what the draft declared locally. The fix is engine_core.h's cast
idiom: ((s32(*)(s32))func_8001AAA0)(0x3D).
9. ★★★ THE SAME FUNCTION'S .rodata CARVE WIDTH CAN DIFFER BETWEEN OVERLAYS — AND THE NARROW ONE
IS A GATE FAILURE WITH NO C EXPLANATION. ov_SC03_108/func_8016AE5C: config/splat.ov_SC03_108.yaml
carves ov_SC03_108_jr_8016AB6C's .rodata 0x20 wide, while EVERY other overlay carrying this
function carves 0x40 (SC03_099, SC05_008, SC06_008, SC03_006). The missing 0x20 is this function's
own jump table, still raw at the head of asm/ov_SC03_108/data/tail14.data.s, so the switch emits a
second copy with nowhere to live. Cross-overlay carve-width comparison is a diagnosis tool: when
one binary refuses a function every sibling banks, diff the yaml, not the C.
Two more, from the same wave, on the gate rather than the code. ov_SC06_022/func_80185B80 is
§338 carve over-span: jtbl_carve._sltiu_bounds still ingests case 0's sltiu 0x801, so the
len(bounds)==1 clamp is skipped and 7 words are carved where the sltiu 0x6 says 6. And
md_MAIN_003/func_800D0C50 is an -O0 function that is the LAST of an -O2 subseg, with the _o0c
object starting at exactly the next address — bankable only by moving the yaml boundary
0x1f74 -> 0x1e58, not by any edit a drafter is allowed to make.
§410 ★★★ — COPY THEN ACCUMULATE ON THE COPY: resolving the birthing-boost vs register-allocation dilemma (P31 S71; byte-proven ov_SC04_015/func_8017EB78, 98 ins)
The dilemma, stated exactly. A value u must (a) end up in $s2 as the destination of an
addu $s2,$s2,$v0 — which wants an IN-PLACE u += w — and (b) keep its sched1 LAUNCH boost, which
requires u's pseudo to have exactly ONE set (birthing_insn_p). Those are incompatible:
u += s[0x14];— right register, but a SECOND set onukills the boost and hoists theandi/sllfour slots early (+6 SCHEDULE).t = u + s[0x14];— right schedule, buttallocates$v0, not$s2.
The lever: introduce a copy and accumulate on the COPY.
u2 = u; /* u keeps ONE set -> birthing boost intact, sll stays at idx 29 */
u2 += s[0x14]; /* u2 is an in-place RMW pseudo ... */
p[0x24] = u2; /* ... which local-alloc coalesces onto $s2 THROUGH THE DYING COPY */
u stays single-set so the scheduler still treats it as birthing; u2 is a read-modify-write pseudo
whose live range starts at a copy that dies immediately, and local-alloc coalesces it onto $s2.
Both constraints satisfied by one extra statement. This is the inverse of §350: there the fix was
to remove a copy, here it is to add one.
Its companion — the commutative-operand order dial. A direct p[0x14] = u + s[0x14]; emits the
plus with the operands the wrong way round. A named temp fixes it: tA = u + s[0x14]; p[0x14] = tA;
gives plus(u, load) rather than plus(load, u).
MEASURED INERT on this function — do not re-try them (the value of a refutation list, §406's
lesson applied): register pins on $18/$s2 (they force $s4 live, +2 drift) · statement reorder of
u = … (§3-T2 / §49 LUID — sll priority is scheduler-internal, §307) · every __asm__("") fence
placement · folding u into X. Frame came out at 0x50 via s32 pad[10] (§164-53), and the
two-u8-copy locals were read off the TU neighbour func_8017E830 (§194-E).
§411 ★★★ — THE PACK MUST CARRY THAT FUNCTION'S OWN HISTORY (P31 S71; measured 38/39 vs 124/131)
The defect, stated as a pipeline fact. Every drafting agent ends its run by writing a note — what
it tried, what it measured INERT, which lever moved the residual, where its draft sits on disk — into
subagents/workflows/wf_*/journal.jsonl. For ~30 phases nothing read them back. A function that
refused a wave went straight back into the pool, and the next wave's agent met it with no idea that
three predecessors had already burned the same four levers, or that one had left a MATCHing body on
disk. The write side felt complete because the notes were being saved.
The fix is one pack step. tools/journal_notes.py mines the journals per (binary, fn) and
appends PAST ATTEMPTS ON THIS EXACT FUNCTION; claude_wave_packs.py calls it at the end of pack
generation, so it is the default and not a step to remember. Idempotent (never appends twice) and
R48-safe: a note stamped with a DIFFERENT binary is never served, because the same name is another
function in another overlay (§238).
Measured on S71 wave 1 — 50 one-agent workflows over the 210-function real frontier, where every target had already refused at least one earlier wave:
| with notes (S71) | without (S70) | |
|---|---|---|
| MATCH at closeness 0 | 38 / 39 (97.4%) | 124 / 131 (94.7%) |
| pool | the hardest residue | strictly easier |
| cite a prior attempt as what they used | 29 / 39 (74%) | n/a |
| banked by RECOVERING an existing MATCH body | 4 / 39 (10%) | 0 |
| matched on the FIRST compile | 11 / 39 (28%) | n/a |
The two failure modes it removes are the expensive ones.
- Re-testing a measured-inert lever. §406 lists twelve, §407 fifteen, §410 four — every one of those lists was paid for by an agent, and is worthless to the next agent who never sees it.
- Re-deriving a body that already exists.
ov_SC05_003/func_80181720's pack shipped a FAILED warm-start while attempt 3's MATCH sat under.run/S70x_1/opus. The agent found it because the note named the path.
The general law, past decomp. If your agents produce a per-item report, the next agent on that item must be handed it. An archive of your own verified outcomes is training data for your own tooling — and a corpus you write but never read is indistinguishable, from the outside, from one you never wrote.
§412 ★★★ — §323 CARVE BLOCKER 2 WAS A REGEX THAT COULD NOT SEE PAST __attribute__ (P31 S71)
The blocker as it presented. jr_isolate_all — the named remedy for the dominant CARVE-REFUSED
class (§322: "subseg <ov>_jr_<addr> would host NON-CONTIGUOUS .rodata carves") — refused two
overlays outright with a file-scope-decl coverage abort:
0 are function PROTOTYPES … base types: {'Blk4_9B4': 2, 'Blk4': 1}
e.g. ['extern Blk4_9B4 aD_801CE7A4[] __asm__("D_801CE7A4");', …]
Fix: carry the naming type (file_scope_types) or add it to src/shared/engine_types.h.
The advice was wrong, and the refusal was honest about the wrong thing: file_scope_types() did
return the typedef. What failed was the NAME extraction downstream of it.
The cause, one regex. Every type-name scan in the layer builder is
re.findall(r'\}\s*([A-Za-z_]\w*)\s*;|\b(?:struct|union|enum)\s+([A-Za-z_]\w*)', block)
and the type is written
typedef struct { u8 b[4]; } __attribute__((packed, aligned(1))) Blk4_9B4;
The attribute sits between the closing brace and the name. The first alternative captures
__attribute__ and then fails on the following ((; the second needs a tag after struct, and this
struct is anonymous. So the scan returns NOTHING, Blk4_9B4 never enters carried, every decl naming
it reads as an unknown type, and the whole overlay is refused. Two overlays, one regex, and an error
message that pointed at the layer that was working.
The fix is to strip attributes before any type-name scan (_ATTR / _strip_attrs in
jr_isolate_all.py), applied at BOTH extraction sites — the carried pre-pass and _type_names.
Measured result. ov_SC07_000 and ov_SC03_029 both isolate, both build BYTE-IDENTICAL, and
jtbl_carve --probe moves from plan-refused to tail — standard §8a carve at gate time on both.
With the S71 static-placement fix (§411 companion) that is 5 of 6 CARVE-REFUSED overlays cleared:
ov_SC03_010, ov_SC03_013, ov_SC03_092, ov_SC07_000, ov_SC03_029. Only ov_SC06_029 remains,
and it fails differently — its isolate builds NOT byte-identical, so it is a real resegmentation
question rather than a scanner gap, and the lane's guard reverted it automatically.
The law. An attribute is grammar, not decoration. Any scan that locates a declarator by its
position relative to a brace or a keyword must strip __attribute__((…)) first — it can legally
appear between } and the name, after the name, and after the parameter list. This is the §134 class
(a scanner that cannot start where the C grammar actually puts things), and it is now the seventh tool
in this project to hit it.
§413 ★★★ — DIFFICULTY IS THE RESIDUAL CLASS, NOT nins — ROUTE THE MODEL TIER OFF HISTORY (P31 S71, Drew)
The observation. S71's wave ran one agent per function and logged wall-clock and tool-call counts. Duration tracks iteration count, and iteration count tracks the residual class — not size:
| function | ins | arm | wall | tool calls | what it was |
|---|---|---|---|---|---|
func_80181294 |
26 | opus | 18 min | 31 | REGALLOC-PERM, finished NEAR/2 |
func_80185D44 |
47 | opus | 21 min | 48 | LUID contradiction (read cc1's -dS trace) |
func_80185F4C |
60 | opus | 22 min | 33 | sched1 birthing-boost tie |
func_800D24D0 |
141 | opus | 33 min | 51 | memrefs_conflict_p PLUS-vs-LO_SUM |
func_8017DB98 |
122 | opus | 80 s | 10 | body recovered from a prior attempt |
func_80182CBC |
28 | opus | 115 s | 12 | §193-A twin remap |
A 26-instruction function took 18 minutes; a 122-instruction one took 80 seconds. Size predicted neither. What separates the two groups is whether the residual is a compiler-internal one (scheduling ties, birthing boost, register colouring, LUID order) where every hypothesis costs a compile-and-measure cycle — or an ordinary body question that a twin or a neighbour answers at once.
Why they never escalated. draw_waves.arm_for keys the model tier on nins alone, so a
47-instruction regalloc wall is STRUCTURALLY unable to be drawn at the higher tier, and nothing
escalates mid-run: each agent runs to its own budget and no watcher re-hands the target.
The fix, available only because the packs now carry history (§411). arm_from_history() reads the
function's own journal notes at draw time and returns fable when they name a compiler-internal
residual; it never downgrades what the size ladder chose. Negative-controlled over all 3,147 functions
with history × 3 size bands = 9,441 decisions: 4,020 upgrades (43%), 0 downgrades.
A defect the control caught, worth as much as the lever. The first negative control iterated only
journal keys carrying a binary and reported 0 decisions / 0 downgrades — a clean pass over an EMPTY
set. The agent verdict schema never had a binary field, so every historical note is name-keyed,
and the same name is a different function in another overlay (§238). Two consequences: the S71
past-attempt fuel can serve one overlay's history to another's target, and any future join on these
rows inherits it. claude_wave_draft.js's VERDICT schema now requires binary, so new rows are
exact; the historical corpus stays name-keyed and should be read with that caveat.
check-against-a-known-true-case, again: the instrument passed because it measured nothing.
§414 ★★★ — parallel_gate ON main IS A FALSE PASS, AND THE RULE WAS ALREADY WRITTEN DOWN (P31 S71)
What happened. S71 ran main through parallel_gate. It reported 11 banked, the merge
committed them, and the run looked like every other successful gate that session. The R22 clean-fleet
verify then came back 212 / 213: main did not compile from clean (conflicting types for func_8004355C, conflicting types for func_80038FFC), and once both declarations were reconciled it
built and was still not byte-identical. Every one of the 11 was then re-gated the honest way —
apply ONE function to a green src/800.c, make extract, make build, compare SHA1 — and
11 of 11 REJECTED.
The rule already existed, three files away. ox_campaign.gate_main_batch's docstring:
"main is gated by ONE CLEAN REBUILD of the whole EXE, never incrementally. gate_stage/ sweep_parallel build incrementally, and main's extract rewrites the linker script, so an incremental main gate returns a FALSE DIFF. Measured P31 S58: wave
abdrew 105 main cards and banked 0 of them while its non-main cards banked 82% — 105 competent drafts thrown away."
parallel_gate's worker is gate_stage, so it inherits that exactly — and S58 recorded the
false-DIFF direction while this is the false-PASS one, which is strictly worse: a false diff wastes
drafts, a false pass commits wrong bytes and reads green until the next clean fleet check.
Fixed as a refusal, not a note (R43): parallel_gate now returns REFUSED for
binary == 'main', naming tools/gate_main.py.
Two instrument lessons from the recovery, both the same shape.
- The per-function re-gate's FIRST form lifted each function body WITHOUT the
externblock above it, and the first three came back REJECT withcdReq_cdResult undeclared. That measured the extractor, not the body — while investigating a gate that had measured itself. Fixed by carrying the declaration preamble; the verdicts after that are real (they compile and the SHA differs). - The failure is R53's exact signature: a failed build leaves the previous object on disk, so a SHA1 check downstream of it reads green. R53 was written for a different tool and never applied here.
The law. A tool that wraps another tool inherits its refusals. Every constraint documented on
gate_stage binds parallel_gate, on harvest_verify, and on anything else that shells it — and
the place to put that knowledge is a refusal in the wrapper, not a paragraph in the callee.
§415 ★★ — A FILE-SCOPE DECL MAKES gcc-2.7.2 MERGE THE TU'S LATER BLOCK-SCOPE EXTERNS INTO IT (P31 S71; byte-proven ov_SC04_011/func_80180B24, 215 ins)
The trap. Adding a file-scope extern for a callee is the reflex fix when a draft's declaration
is missing. On a TU that declares that same callee at BLOCK scope further down, it is the thing that
breaks the build — and the error names the DEFINITION, not your decl.
ov_SC04_011_jr_8017D494.c declares func_80183BD0 at block scope twice (l.5019, l.5156, both
(u8 *)) and DEFINES it as void func_80183BD0(s32) around l.7238. Put ANY file-scope declaration
of it at l.4883 and pushdecl's different_binding_level path merges those later block-scope
externs into the global one; the merged global then conflicts with the definition. Declare it at
BLOCK scope, like its neighbours, and the TU compiles.
Companion, same function: a draft's file-scope extern s32 D_80126B58 collides with the TU's
block-scope u8 D_80126B58[] (l.7968) — resolved with the house __asm__ alias rather than by
changing either spelling.
The rule of thumb this makes explicit: match the TU's own binding LEVEL, not just its type.
§376/§378 are about the declaration's TYPE agreeing with the definition; this is the orthogonal axis,
and it is invisible to match_one for the usual reason — it compiles the function alone.
Method note worth copying. The agent proved both claims by splicing a scratch copy of the whole
TU and running the Makefile's own cpp+cc1 recipe on it, comparing diagnostics against an
unmodified base — a negative control on the TU, not on the function. That is the right instrument for
any "does my declaration break this TU" question.
§416 ★★ — FOUR LEVERS FROM THE S71 OVERNIGHT LANE, none of which the cookbook held (P31 S71)
All four are byte-proven, each on the function named.
1. RE-READ THE STORE, DO NOT PASS THE VALUE — CSE store-forwarding.
ov_SC06_000/func_80183398 (119 ins). The target does sh and then an in-place sll/sra on the
same location, which is gcc forwarding its own store. Writing f((s16)val) keeps the pseudo live in
$v1 and emits sll $a1,$v1,16 BEFORE the sh. Passing a RE-READ of the slot instead —
f(*(s16 *)(a0 + 0xA)) after the store — is what reproduces it. When the target reads back
something it just wrote, write that read in the source.
2. (&SYM)[3] vs p = &SYM; *p = 0; are different ADDRESSING, not style.
ov_SC01_084/func_80181310 (118 ins). addiu $s2,$a0,0xC proves D_801270D4 is written as
(&D_801270C8)[3]: loop.c hoists the constant address and -frerun-cse-after-loop re-relates it to
the base register. Spelling it r = &D_801270C8; *r = 0; puts the base in a register instead, and a
separate pointer local u = &D_801270D0; gives la + 0($v1) rather than two lo_sum forms. Three
spellings of one store, three different addressing modes.
3. ONE biv with +0/+2/+4, and a guard on the SYMBOL — not on the pointer.
Same function. combine_givs merges the +2/+4 accesses into $s0 at -0x2/0 only while there is a
SINGLE s16 * induction variable; introducing an explicit second pointer variable breaks both that
addressing and the preheader order (li $s4,1 / addiu $s2 / addiu $s0 — movables, then giv
inits). Guard on the separate symbol D_8018A9F0, never on *q.
4. A LOCAL'S WIDTH CHOOSES THE LOAD. Same function: an s32 local for a 16-bit global forces
lh; an s16 local emits lhu plus sll/sra. (This is §409 law 6 seen from the other side — there
the width cost two instructions per COMPARE, here it changes the LOAD itself.)
And one pack-quality note that recurs all night: the same-address "twin" hint was FALSE on
ov_SC06_000 (an unrelated 11-ins function), on ov_SC01_080 (two different functions), and on
ov_SC03_030. In every case the real fuel was a same-TU neighbour (§194-E). The address-twin
lead is worth checking and never worth trusting — law 1c exists for this.
§417 ★★★ — A REGISTER PIN CAN BLOCK jump.c's SELECT COLLAPSE, AND UNPINNING THEN EXPOSES A cse SKIP-BLOCKS MERGE (P31 S71; byte-proven ov_SC03_013/func_8017E6F4, 182 ins)
Two compiler passes in a chain, and the first agent misattributed the second to the first. The
prior attempt sat at a 3-instruction residual with a $3 pin on a select's result and concluded that
"unpinning costs +2". Both halves were wrong in an instructive way.
1. The pin blocked jump.c. A PINNED select arm expands as ior + a copy — TWO insns — so
jump.c:728's x = b; if (c) x = a; collapse cannot fire, and reorg emits bne/ori/move.
Removing the pin lets the collapse happen. A hard-register pin is not neutral to the RTL
optimisers: it changes the INSN COUNT of the pattern they pattern-match on.
2. Unpinning then exposed a different pass. With every select collapsed, cse's
cse_end_of_basic_block skip_blocks path now walks from the first D_80184D2C[idx] read all the
way to the second and CSEs the symbol_ref into $s3 — the +2 the earlier agent blamed on the
unpinning. The residual moved; it did not grow.
The fix is to end the cse block, not to restore the pin: put a REAL diamond between the two uses
(a vol clamp written as if/else, whose IOR arm is uncollapsible), keep register vol __asm__("$5") so both arms land in $a1, and spell the absolute value as
if ((s16)w < 0) w = -w; e = w;. Four compiles from there.
The general law. When removing a lever moves the residual instead of shrinking it, the new
residual is a DIFFERENT PASS — attribute it before re-adding the lever. "Removing X costs +2" is
almost always "removing X unblocked pass A, and pass B now fires"; re-adding X re-blocks A and hides
B again, which is how a function plateaus for three attempts. The instrument is the pass dump
(-dL, -dj, -dS), not another guess.
§418 ★★★ — TWO LOOP-STRUCTURE LEVERS: MAKE THE SECOND INDEX A GIV, AND KEEP A TABLE ADDRESS UNFOLDED (P31 S71; byte-proven ov_SC04_016/func_8017DF8C, 184 ins, 32 → 0 in seven compiles)
1. A second biv and a giv of the first are different CODE, not different style.
Writing the stride variable as its own induction variable gives one shape; writing it as
off = j * 0x50 at the top of the body — a giv of biv j — gives another. loop.c chains givs
LIFO (loop.c:4421), so with j = 0 spelled BEFORE idx/tp the move $s0,$zero emits LAST
and the arg-3 giv lands in its natural record slot. That single respelling fixed 14 preheader rows
and 8 latch rows at once. When the preheader ORDER is wrong, look at which variables are bivs and
which are givs before touching anything else.
2. To keep la + addu + lh 0(reg) unfolded, hoist the table pointer OUT of the loop — once.
A loop body that must emit la $t6 / addu $v0,$a1,$t6 / lh 0($v0) (rather than a folded
%hi/%lo(sym+K)) gets it from a function-scope pointer local set once outside both loops:
s16 *tbl = &D_80184D0A;. Two mechanisms combine — there is no LOG_LINK across basic blocks, so
combine cannot fold it; and with every $s register taken, reload rematerialises it from
reg_equiv_constant at the use and deletes the initialisation, which is why the hoist costs
nothing. Spell the sum idx + (s32)tbl to get (plus a1 t6); (s32)tbl + idx costs exactly one
instruction (operand swap).
MEASURED INERT here: §328's volatile cast, for a reg+sym address — it splits the extend into
lhu/sll/sra and still folds.
Relation to §20/§42d-2. Those cover keeping reads on one la via an opaque launder inside a
block; this is the cross-block form, and the lever is placement (outside the loop) rather than a
launder. The reload-rematerialisation is what makes the two look identical in the output.
§419 ★★★ — WHEN A PIN IS IMPOSSIBLE, WIN THE local-alloc DENSITY CONTEST INSTEAD (P31 S71; byte-proven ov_SC01_000/func_8017DD04, 297 ins)
Why the pin could not work. The obvious lever — pin the 0x80 constant to $7 — LOSES its
sched1 birthing boost, because $a3 (and $a2) are ALSO set by this function's own call-argument
copies, so reg_n_sets == 2. A hard-register pin on an argument register is unavailable to any
function that passes arguments in it. $6/$7 therefore have to come out of local-alloc
density, unpinned.
The density arithmetic, which is computable before you try anything. local-alloc ranks by
refs / live_length:
| pseudo | refs | live length | density |
|---|---|---|---|
0x80 |
13 | 319 | 4890 |
0xFFFFFF |
9 | 345 | 3130 |
0x80 out-densities its rival and takes $6 — a pure $a2↔$a3 swap, 24 instructions wrong.
The lever: buy references, not a register. One SIX-INPUT zero-byte asm at the blk3/blk4 boundary —
__asm__("" :: "r"(mlo), "r"(mlo), "r"(mlo), "r"(mlo), "r"(mlo), "r"(mlo)) — adds 6 references to
mlo and nothing to the output. Density becomes 5217 > 4890, so mlo takes $6, 0x80 falls to
$7, and its boost survives because it was never pinned. The boundary matters: pick the ONE cut no
hoisted constant crosses, or you change the live lengths you are trying to exploit.
MEASURED INERT on this function: pinning mlo = $6 (cse never substitutes a hard register for a
bitfield constant) or it explodes (+3, $s0); and a dead hard-register copy as a "register
suggestion" is simply deleted by flow.
The general law. A register assignment you cannot pin, you can still win — by changing the
density ranking rather than the register. Reference count is a dial (zero-byte asm inputs) and live
length is a dial (where you place the barrier); local-alloc's ordering is arithmetic on the two, so
compute it from the -df/-dl dumps and aim, instead of trying pins that a call-argument register
structurally forbids.
§420 ★★★ — A MULTI-CLUSTER SYMBOL REBASE, AND THE BARE-NAME DEDUP THAT HID THREE QUARTERS OF IT (P31 S71; 4 banked in 57 s)
The shape. One shared body (func_8016AB6C) lives in four overlays — ov_SC03_107,
ov_SC07_007, ov_SC07_010, ov_SC07_011. Every copy scored match_one closeness 0 and every copy
failed the gate with undefined reference to D_8018D538: the classic §171 stale-seed-symbol class, the
draft carrying the symbols of the overlay it was written for.
Defect 1 — aprop_symfix deduped the slate by BARE FUNCTION NAME (R48/§238). A four-row slate
reported "1 drafts audited". Three rows were silently dropped, and they were not redundant: each
overlay needs a DIFFERENT rebase, because each has its own target symbols. Keyed by (binary, fn),
all four audit. This is the same defect class as reloc_filter's binof (fixed earlier the same
session) and gate_lane's homonym staging — three tools, one root.
Defect 2 — the delta rule allowed exactly ONE uniform cluster. STALE-DELTA requires the sorted
zip to have a single target − draft delta, so it refused all four as AMBIGUOUS. The real structure
is two clusters of two, and it is identical in shape across all four overlays:
| overlay | run 1 | run 2 |
|---|---|---|
| ov_SC03_107 | −0x7F64 ×2 | −0x5A650 ×2 |
| ov_SC07_007 | −0x74DC ×2 | −0x2E898 ×2 |
| ov_SC07_010 | −0x844C ×2 | −0x4E3A0 ×2 |
| ov_SC07_011 | −0xBD8C ×2 | −0x65D08 ×2 |
Two data clusters, each moving as a block — exactly what one seed body copied into four overlays produces. The single-delta rule is the special case of one cluster.
The safe generalisation. Sort both symbol lists, zip positionally, split into runs of constant
delta, and require every run to have ≥ 2 members — a single-member run is a free guess, not a
cluster, and that is where a wrong symbol would come from. Filter to D_8xxxxxxx first: the draft's
Ent_/Obj_/SVec_ typedef names and the target's rand/jtbl_ carry the same vram-looking suffix
and are not data symbols.
Result: 4 banked in 57 seconds of gate time, zero drafting — and all four had been sitting at
closeness 0 behind a symbol rename. Verify after rewriting (match_one again, all four still
MATCH) before gating; a rebase that changes the body is a rebase you got wrong.