278 novel-idiom candidates extracted by tools/idiom_harvest.py from 1,004 drafting notes, restricted to functions the WHOLE-BINARY BYTE-GATE banked (banked-ness derived from corpus.stubs, not from match_one, whose relocation masking would admit lessons drawn from functions we never actually reproduced). 26 new sections (§207 is the harvest header + discard ledger, §208-§232 the laws), 6 folded as addenda to existing sections (§30, §194-B, §176-B, §165-40, §164-63, §193-A/§194-E). ~200 candidates discarded or merged — 103 of them self-reported 'nothing the cookbook did not already cover', which is the knowledge base doing its job. §209 CORRECTS existing guidance: §194-B's 'needs >=2 sh stores' bound is byte-wrong (func_801A8738 has one sb and still needs the s16 declaration). Insertions only; the existing file is byte-identical by prefix md5.
2.3 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)
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)
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)
§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)
§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)
§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)
§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)
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.
§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.
§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).
§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.
§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.
§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.