Finding 122 declared GTE rows a blocked class needing a harness token. The gap was smaller than
it looked: the header already covered $0-$7 and $13-$31 but NOT $8-$12, which is where
IR1/IR2/IR3 ($9/$10/$11) live, and it had no macro for the GTE command instruction itself.
Added gte_ldIR1/2/3, gte_stIR1/2/3, gte_ldMAC1/2/3, gte_stMAC1/2/3, and a GENERIC gte_cmd(field)
so any command whose 25-bit field has been read off the original can be emitted without first
earning a semantic name.
VERIFIED, NOT ASSUMED: the field values are confirmed by counting them over the whole binary
(0x486012 x51, 0x49E012 x11, 0x41E012 x4) and a test compilation emits gte_cmd(0x486012) ->
cop2 0x486012 -> 0x4A486012, exactly the original's word.
Five rows were blocked on this across three workers, all with complete or partial derivations.
The disposition matters as much as the token: three workers independently reached 'no amount of
spelling will find this' and declared it BLOCKED rather than grinding -- which is what made the
gap visible as a TOOLING problem instead of three separate source puzzles.
141: worker D's own reads_unsaved_saved scanner flagged 0x80103434, D read the row, and the
verdict is NOT a fragment -- the detector treats op 0x12 (COP2) as a GPR use, which for
ctc2/mtc2/mfc2 is wrong. Every COP2 row in that flag list is suspect. That is the THIRD
independent instance of the same lesson: every shape heuristic in this project has needed a
worker to read a flagged row and say 'this is legal, and here is why'.
142: worker C's 2^k-1 add-back rule CLOSES worker D's open question on 0x82082083. D recorded it
as a bounded negative with a named direction; C recognised the answer from an unrelated row.
The third time a worker's finding closed another worker's negative -- the strongest argument for
recording a named open question rather than a vague failure.
Finding 84 named the transform; it is now implemented and 0x800FFBEC matches (80 B, 0 differing)
where it was 6 differing bytes without it.
IT IS A SWAP, NOT A MOVE, and getting that wrong cost one implementation: the candidate is
lw $31,16(sp) / addiu sp,sp,24 / jr $31 / nop and the original is lw $31 / nop / jr $31 /
addiu sp,sp,24 -- SAME instruction count, two words swapped. My first version moved the release
after the jump and dropped the nop, producing 3 instructions instead of 4 and turning an 80-byte
row into a 76-byte LENGTH-MISMATCH. A 'small mechanical transform' still has to be checked
against the bytes.
SCALE: 120 unclaimed rows have the filled epilogue in the ORIGINAL (scanned every worklist row's
tail for jr $31 followed by a positive addiu sp,sp,N). They are mostly SMALL -- 76, 76, 80, 92,
92, 96, 104 B -- so this is a large class of cheap rows that were blocked on a HARNESS GAP rather
than on source shape. 770 other rows have the unfilled shape and need nothing.
The tracked patch is regenerated and verified to reproduce both modified maspsx files from the
pristine checkout.
Worker D's 0x80025A2C (176 B, 2 spellings) -- the EXACT MIRROR of its own 0x80025ADC.
131 is now THREE instances with an identical mechanism (88->96, 48->56, 56->64, always a clean
8-byte frame deficit with everything else identical, always fixed by an unreferenced array
local). It is a rule with a one-line fix, not a trick.
138: a MIRROR PAIR -- the two rows are inverse conversions, adjacent in the image, both callees
of 0x80031BBC. Finding one made the other a two-spelling row. sf3_family cannot see this relation
because inverses are not similar: their opcode histograms DIFFER (one stores where the other
loads). A pair of inverse functions has a STRUCTURAL signature, not a similarity one.
Worker A's 0x80036F70 (460 B, first attempt, family score 1.000 AND adjacent to its own
0x80036DA4). Its family run finished 7 for 7 with five first-spelling matches.
136: worker A CALIBRATED the family tool. It checked the two 0.97-scoring entries and NEITHER
shares its sibling's body at all -- one is a table-allocation routine, the other a summing
loop. '1.000 is the useful band; below ~0.99 the histogram is matching common idioms, not
bodies.' That is the same false-positive mode as the redundancy ranker (finding 110). The
default threshold is now 0.99.
137: a family's signature can be a CONSTANT TRIPLE -- worker A's 0x80036F70 differs from its
sibling only in six constants, whose signature is (A, A+12, A-58). Searchable in a way no
similarity metric can be, because the shapes are identical and only the immediates differ.
132: worker B found the SECOND independent instance of 'when the original's short path is the
fall-through, INVERT the condition' (30 bytes of layout on 0x800A8984; the same shape as its
own 0x800FCA90). That promotes it from a heuristic to a rule with a stated direction -- and it
is the opposite of the usual instinct to write the guard as an early-exit.
133: worker A's one-byte family residual was a DECLARATION -- 'int i' emits slt where 'unsigned
int i' emits the original's sltiu. The family transfers the SHAPE and the LEVERS; the
declarations must still be re-derived.
134: a family hit is also a hint about the CALLEE -- the relation crosses the call graph, and
sf3_family does not model it. Two of worker A's family rows call rows that are themselves
unclaimed with the same object layout.
135: worker A predicted a fourth family member by pattern; I scanned all 1046 unclaimed rows for
the predicted bases and ZERO reference them. The family has exactly three members. A predicted
member that does not exist is worth recording so nobody re-derives the search.
130: worker D found the ranker's top is SIZE-BIASED -- redundancy and size are correlated because
a bigger body has more chances to repeat a 3-gram. Its top was 3288 B at 0.91 while the rows it
was matching sat at 0.64-0.74 in the small tail. The metric is not wrong about rows of equal
size; the raw score just cannot be compared across bands. Since the milestone counts BODIES, a
3288 B row and a 248 B row are worth one body each and the large one costs many times the
context. Worker D's effective filter was redundancy PER UNIT SIZE, now approximated by
--max-size, which is documented in the tool.
131: an unreferenced ARRAY local homes but an unreferenced SCALAR does not -- second independent
instance, identical mechanism, so it is a rule: when the frame is a clean multiple of 8 bytes
short and everything else is identical, add an unreferenced array local of that size.
Worker C's 0x8009F4B4 (248 B) and 0x80068874 (156 B), both first attempt.
127 CLOSES WORKER D'S OPEN QUESTION. D left 0x800FEE3C's magic 0x82082083 unexplained; worker C
solved it and the answer is a general rule: the divisor 63 is of the form 2^k-1, which is why
cc1 uses that magic with an ADD-BACK (mfhi; addu; sra 5) instead of a plain shift. An add-back
magic is the tell for a 2^k-1 divisor, NOT for a large one. Finding 67's decision procedure is
now complete: no mflo -> constant division D = 2^(32+s)/M; mfhi+addu+sra -> a 2^k-1 divisor;
mfhi AND mflo -> a genuine 64-bit multiply.
128: worker C classified a division-by-constant row on decode WITHOUT attempting it, because
'every division expression has several equally-plausible spellings, so it is idiom-redundant by
construction'. That characterises the ranker's false-positive class from the SOURCE side for the
first time -- exactly the class finding 110 showed cannot be separated by operand comparison.
129: adjacency is now 9-for-9 across three workers (A 3/3, C 5/5, D 1/1).
Worker A found that last-row-wins makes an unconditional 'released' from a worker who never
held the row WRONGLY FREE IT from whoever does. A found two addresses held as wip by worker C
and correctly re-appended C's wip row rather than its own released -- had it not, C's rows
would have been silently released and both workers could have started them.
New rule: only append 'released' for an address you yourself appended 'wip' for. Order is
check (range-aware) -> if free append wip -> work -> append your own released.
Second finding from the same episode: the roving list I gave worker A deliberately overlapped
the other partitions and its first two picks were both already held. A shared queue is only
worth it when the work is NOT already partitioned. Once the partitions were re-ranked on
global redundancy, the roving list was strictly worse than a worker's own slice, so it is
retired. That is the fifth defect found in a coordinator-written rule this phase.
Worker D's 0x8009F890 (248 B, first attempt) -- the THIRD member of a family it had already
matched twice, and the three were found by three different means: the size ranker, adjacency,
and the rebuilt global-redundancy rank. 'The finder varies, the price does not.' The
operational conclusion: families should be SEARCHED for explicitly rather than waited for.
126: worker D withdrew a named direction after checking its own notes and finding it had
already been tested and made the row worse. It declined a fifth spelling on a falsified lever
and corrected the row's record to the honest state. A direction that has been tested and
failed must be struck, or the next worker inherits a false lead.
123: a 'clamp to zero' written as a BITWISE MASK (x & (x >> 31)) compiles branchlessly while
the same thing as a TERNARY branches -- the opposite of the intuition. Worker B's MIN half now
matches exactly with the bitwise form. The MAX half (branchless slt/negu/and, i.e. x & -(x>0))
remains UNREACHED by any ternary or bitwise spelling, with a plausible SDK min/max macro as the
source. Recorded as a named open question.
124: worker B found that the partition design CONFOUNDED the size/redundancy measurement. The
partitions were rank-interleaved by the original worklist order (tier, size, address), not by
redundancy, so 'my <=200 B hit rate was high because my slice happened to be redundancy-rich,
not because small rows are inherently easy'. The within-worker evidence for 'cost is redundancy'
stands (A: 548 B first-attempt vs 176 B nine failures), but any CROSS-worker band comparison was
measuring the partition, not the rows. Fixed by re-partitioning on global redundancy. General
lesson: when work is divided among workers, any per-worker statistic is contaminated by the
division.
Worker A's 0x800689DC and worker C's 0x8009F4B4.
120: worker B's justification for why the ranker works -- 'the allocator makes copies
non-identical, so OPCODE repetition survives while WORD repetition does not'. That is exactly
why finding 110's full-word metric failed and why the opcode metric works. A repeated source
block produces the same opcodes with different registers; requiring operands to match destroys
the signal rather than sharpening it.
121: the filled-delay-slot class has TWO sub-cases with DIFFERENT fixes -- reorg fills the slot
(source-shape hunt) versus maspsx mode changing WHICH instruction lands in the slot (a harness
token choice). Same diagnostic, different remedy. Check whether toggling maspsx changes the
fill before hunting a source shape.
122: NEW BLOCKED CLASS -- a GTE coprocessor body needs a harness token, not more spellings.
Worker B's 0x8001FAFC reads mfc2 $12/$13/$14 and branches on t7/s6 which are NOT the o32
argument registers, so the inputs arrive through a non-standard convention. Team rule: if a
body contains mfc2/mtc2, do not spend spellings on it -- these are tooling-blocked rows to be
worked as a batch once a token exists.
Worker D's 0x80106AA8 (136 B, first attempt) -- found by the REDUNDANCY filter, not
adjacency, which is the first row where the ranker did the finding alone. Eight stores
through four global pointers, each re-materialised per store.
Cookbook 105's dial now has THREE measured settings: per statement (0x8006BC74 46x and
0x80106AA8 8x, both matched), once per block (matched), once per function (does not match).
So per-statement re-reads are the NORMAL shape, not an extreme.
119: worker D ran the fragment check, called 0x80058BA0 a confirmed fragment, then
SELF-CORRECTED -- it is legal, because in o32 a frameless leaf may both read and write the
caller's outgoing argument area (sp+0..sp+31). All three of D's suspects are legal. Worker B
found the read side, worker D the write side, and both had to read the row to do it: a
heuristic keyed on shape must state its exclusions, and only the worker reading the row can
find them.
116: worker A's hypothesis that the ranker cannot see LIVE RANGES (its conversion rate went to
zero over three consecutive rows, all failing on register allocation with the structure fully
confirmed). I tried to make it computable -- callee-saved registers saved and s-registers per
call -- and the proxy FAILED: matches span 0.00-2.00 s/call and failures span 0.17-1.33, with
a matched row having the HIGHEST s-register count and another matched row having 16 calls and
0 saved registers. The hypothesis is not refuted; the proxy is just not a good
operationalisation of 'live range'. Recorded as an open axis.
117: worker C's new signature -- correct control flow, +3 instructions, all three being j/jr
delay slots. DISTINCT from a nesting error (both give a small residual, but nesting moves
branch displacements while this moves instructions ACROSS a jump). Belongs with the post-pass
family.
118: call COUNT does not separate a real block from a call chain -- worker C's matched
0x80058CE8 is four loops that each call, and its false positive has zero calls. Fails in both
directions; recorded so nobody re-derives it.
Worker B's 0x80050CA8 (120 B, first attempt).
Lever: the status word is masked by TWO separate statements (&= -3; &= -5;), and the original
emits one load, two ands against two different constants, one store. Combining the masks
folds to a single and and LOSES an instruction -- the same principle as finding 81 (a slot
stored twice is two statements) applied to read-modify-write. Companion: the status load is
hoisted above nine halfword clears, so the clears' source order is only observable through
the store order.
The first version of the fragment check fired on any nonzero sp offset and flagged 2 of the
555 registered regions. Worker B read its own hit (0x800B704C) before committing and showed
it is a LEGAL FRAMELESS LEAF WITH EIGHT ARGUMENTS: in o32 the callee's sp is unchanged at
entry, so sp+16..sp+28 IS the caller's outgoing area -- arguments 4-7 -- and reading it
before any addiu sp,sp,-N is exactly what a frameless >4-argument leaf looks like. Its
evidence: exactly one jr ra, zero jal, zero addiu sp,sp,-N, zero sw ra/lw ra across all 324
bytes, and no callee-saved register touched.
Excluding the incoming argument area (sp+0..sp+31) and flagging only a negative offset or an
offset beyond the 8-argument area makes the check DISJOINT:
registered regions flagged: 2 of 555 -> 0 of 555
suspects across 4 partitions: 5 -> 1
The one remaining suspect is worker A's 0x800C3490.
This is the cleanest example in the phase of a worker ADJUDICATING a tool's output rather
than obeying it -- B was explicitly told 'advisory, do not skip', read the row anyway, and
its adjudication turned a noisy heuristic into a precise one.
Worker A found 0x800C3490 is not a matchable body: it starts mid-expression with sw v0,32(sp)
before any frame setup, and its identical tail also appears at 0x800C3470, so it is a
shared/jump-target block Ghidra promoted to a function -- inside no region, with the code
before it in no worklist, so nobody can match it standalone.
Its generalised rule is narrower than 'first instruction is not prologue-like', because a
function may legally start with beq/sh/move: a row is a FRAGMENT if its first instruction
touches the stack before any addiu sp,sp,-N, reads a stack slot, or uses a callee-saved
register that is never saved.
Implemented as a --fragments scan. Measured: 5 suspects across all four partitions, but 2
false positives across the 555 REGISTERED regions, so it is ADVISORY not an exclusion --
sufficient-but-not-complete like the trapping check. A hit means read before spending a
spelling, never skip.
111: $sp cannot be moved from C, so a stack switch is INLINE ASM and the statement SPLIT is
byte-load-bearing -- seven separate __asm__ volatile statements with the call between them,
and the nop in each jal's delay slot falls out of the following statement being a compiler
barrier. One asm block, or a register int sp, does not reproduce it.
112: an intermediate that must live in MEMORY (the original reloads it before each of three
calls; a scalar local gets register-allocated and loses 8 bytes of frame); the chained
assignment's store order; and finding 100's then/else diagnostic confirmed on a new row.
113: a region's END address is EXCLUSIVE, so the registry free-check must be a containment
test, not a string match. Worker A was wrongly blocked on 0x8006B7C0 because it is the
exclusive end of 0x8006B778's range. This is the third defect found in a coordinator-written
rule this phase; the workflow doc now requires a range test.
Worker D's 0x800307FC (92 B) and 0x80031EBC (112 B), both first spelling by adjacency.
109 records worker D's calibration claim -- 'adjacency finds the ROW, redundancy predicts the
PRICE' -- and the harder discipline behind it: D read 0x8006C044, identified it as a
tie-break-dense 3D-maths routine, and RELEASED it in favour of two small adjacent rows that
together cost less context than a first draft and returned two bodies instead of zero-to-one.
110 records a NEGATIVE RESULT from the coordinator. Worker C found a real false positive (the
ranker's top row is tie-break-dense, its score inflated by a repeated multu/mflo/sra idiom)
and proposed comparing full instruction words instead of opcodes. I implemented that and
measured it: it scores two KNOWN matches at ZERO and the known false positive HIGHEST. The
reason is fatal -- a genuine repeated source block does not produce identical instruction
words across copies because the allocator assigns different registers, so 'same opcodes,
different operands' describes a repeated block and a repeated idiom equally well. They are
indistinguishable at the instruction level. The opcode metric stays.
Worker B's comparability caution: the redundancy score is the MAXIMUM of the 2/3/4-gram repeat
ratios, so a high score means 'some length scale is very repetitive', not 'repetitive at every
scale'. Any implementation using a mean or a different normalisation gives non-comparable
numbers, which matters because workers were sharing rankings across partitions. Pinned in the
tool's docstring with this tool named as the reference implementation.
107: finding 43's goto lever is NECESSARY BUT NOT SUFFICIENT -- worker B found it overshoots
(152 vs 140) on 0x800FCA90, and this is the second independent instance after worker A's
0x800256F0. Treat it as one attempt, not as the fix.
Worker A appended three rows optimistically, classified them as near-matches, and then had
no way to clear them -- because the ledger was append-only with no terminal state. Every
other worker would have skipped three free rows.
Fixed: a status column (wip / released) where the effective state of an address is its LAST
row, and a rule that a worker must append 'released' when it stops. Worker A's three rows
are released.
The general lesson is recorded in the workflow doc: a write-ahead log needs a TERMINAL
state, not just an opening one, or the log itself becomes the stale data it was meant to
prevent. Any append-only coordination file needs a way to say done.
105: the pointer re-read granularity is a DIAL, measured in both directions by worker D --
re-read per store (700 B row, 46 times, matched), once per block (276 B row, matched),
per statement (80 bytes too long), and never (does not match). Cause is aliasing. It is the
same property as finding 45's named-locals family but as a COUNT rather than a yes/no.
Plus: a genuinely uninitialised read in the original must be preserved, not corrected.
106: which failure class a row lands in depends on the FILTER, not the band. Worker D's
cheap rows fail on frame/combiner/allocation/batch-shape and never on the branch
diagnostics, so those belong on tie-break-dense rows. Route a diagnostic to the row shape
it matches rather than broadcasting it.
Worker A's 0x80033DC8 (360 B) and 0x8006A98C (132 B), plus two gp symbol rows
(D_80122724, D_80122728) that unblock 0x800A4CA8.
PROCESS DEFECT FOUND AND FIXED. Worker C and worker D both matched 0x800320D8
independently and D overwrote C's source file. Nothing corrupted -- both spellings match
and the region still reports 276/0/MATCH -- but one worker's effort was duplicated. The
partitions are genuinely disjoint (273/279/278/271, union 1101 = sum), so there was NO
assignment error: the gap was that no worker could know another had started a row, since
the registry only knows about MERGED claims and both started before either merged. The
root cause is the adjacency rule (cookbook 99, 4-for-4) crossing partition boundaries --
the best dispatch heuristic found so far invalidated the assumption the assignment rested on.
Fix: .run/p11/inflight.tsv (write-ahead log alongside the merge registry's commit log),
with the protocol written up in docs/ORCHESTRATOR_WORKFLOW.md so the next orchestrator
inherits it.
Worker A's 0x800556E8 and 0x80055654 (both first/second attempt).
Cookbook 99 is a DISPATCH rule, not a codegen one: take the row ADJACENT to one you just
matched. The binary is laid out by translation unit, so neighbours share the author's habits.
3 for 3, all first or second attempt, and it beat both the size ranker and the LRS ranker.
Cookbook 100 puts the three branch-shaped diagnostics side by side -- each maps a residual
shape to exactly one cause and each is a glance rather than a spelling:
branch displacement words only -> block NESTING (95)
first few instructions, right length -> then/else ORDER of a single-statement arm
whole prologue, same multiset -> declaration vs assignment order
Vector copies are now confirmed on FIVE independent rows.
Worker A's two adjacent claims (0x80036B14, 0x80036DA4).
Cookbook 95 is the cleanest diagnostic of the phase: a correct-length candidate whose residual
is a handful of BRANCH WORDS means the block NESTING is wrong, not the code inside the blocks.
Worker A got exactly 656 bytes (correct length) with exactly 2 differing bytes, both branch
displacements, by writing two guards as siblings instead of nested. Residual = 2 bytes at a
branch displacement => go look at your braces.
Also: the project's 4-int vector type is identifiable from the frame (multiple of 16 with
offsets stepping by 16); vector copies are struct assignments (third independent confirmation);
and an OPEN question is recorded -- 'the original spills everything, cc1 promotes' -- with a
request for a recipe from any worker who has solved it.
Worker B's four first-attempt claims (0x800B255C, 0x8004857C, 0x80030858, 0x800909D8).
Cookbook 94 is the strategic one: worker B's failures cluster into exactly TWO mechanical
classes -- reorg slot-fill choice and rare-epilogue fill -- and neither is a shape problem.
Both are the post-pass family, which two workers have now independently arrived at and
stopped on. That is the strongest argument yet for writing the post-pass rather than
grinding these rows with source spellings.
Worker D's most transferable finding yet: the ADDRESS SYMBOL is what selects the addiu form.
A literal (int)0x8013F9B8 gives lui+ori; &D_8013F9B8 gives lui+addiu. So 'ori where the
original has addiu' means the source used a literal where it should reference the address
symbol -- a 40-byte error that presents as a LENGTH mismatch, which is why it reads like a
codegen problem. Extends findings 4/46.
90: a (gp)-relative access to an unregistered address is a SYMBOL REQUEST, not a source
problem -- the harness derives gp-ness only from config/symbols.tsv and no CLI option adds
it. Worker D verified its row by pointing --symbols at a scratch copy of the registry.
91: a real source-order quirk (two successive call addresses swapped) is preserved in a
match and is not a transcription error.
Worker A's 0x8009C904: 548 bytes, matched on the FIRST spelling. Cumulative evidence for the
redundancy ranker is now 548B/1st, 248B/3 spellings, 1232B/2, 700B/3 -- against 176B/9
failures for a tie-break-dense row.
Cookbook 87 is the important negative: cc1 does NOT unroll a constant-trip-count loop (the
for-loop form is 212 B against the original's 780 B), so whenever an original is unrolled the
SOURCE is unrolled too. That explains why the >800 B band is full of cheap rows -- those
bodies are unrolled in the source, and an unrolled repetitive body is exactly what the
ranker scores highest.
Worker B's 0x80069580 (88 B) and 0x8007E7FC (96 B), plus two gp symbol rows
(D_80122168, D_801221D0).
Cookbook 84 is the harness row for the post-pass: worker B isolated the rare-epilogue
transform exactly (move the frame release into the jump slot AND insert the load-delay nop
after lw ra), and established the load-bearing detail that as will NOT perform this fill
because doing so would put jr ra in the lw ra load-delay slot. So a post-pass that merely
moves the release into the slot produces wrong code. Also measured: maspsx=off is WORSE on
this row (72 bytes) because it strips nops from the beqz/jalr slots the original keeps, so
the two mechanisms are not substitutes.
85: cc1 folds SYM+N into a single la and SIX spellings do not defeat it.
86: cc1 cross-jumps identical guards; goto to a shared return label is the named lever.
Worker C's 0x800320D8 (276 B), matched on the FIRST spelling where its sibling 0x80031FC4
took 5 -- the family lever measured, on one family, both ways. Finding 55's limit confirmed
on the same family: a third row calling the same callee is NOT the same body and sits at
+16 instructions. The family transfers the derivation method and the stable positions,
never the body.
Also recorded: an OR nested inside an && chain is observable from the branch DIRECTIONS --
bne to the call block on one test and bnez to the manual-copy block on the other is
if (x == 0 && (a != 6 || b == 0)) call; else manual;
Worker D's 0x800910BC (280 B), its 8th match.
TWO CORRECTIONS TO THE COORDINATOR'S OWN COOKBOOK ENTRY, both from measurement:
- 67 was INCOMPLETE and cost worker D a spelling. The magic alone is AMBIGUOUS: D = 2^(32+s)/M
where s is the shift of the sra after the mfhi. 0x2AAAAAAB is /6 at s=0, /12 at s=1, /24 at
s=2, and worker D read it as /6 when the shift was 1. The corollary is worth having too: the
same magic twice in one function is not a contradiction (0x66666667 serves both /10 at s=2
and /5 at s=1, materialised once into a callee-saved register).
- The named-local rule is PER-SITE within one function. Naming a result the original consumes
immediately costs 2 words; naming one the original reuses is free. Apply the decision once
per VALUE, not once per function.
That is now the fourth correction to coordinator work this phase, and every one came from a
worker measuring something the coordinator had asserted.
Worker A built a repetitiveness score (repeated 2/3/4-instruction opcode subsequences,
normalised by body length) and produced the cleanest controlled comparison in the phase:
3 spellings on a 248 B repetitive row vs 9 failures on a 176 B tie-break-dense one. That
converts 'prefer a repetitive body' from a hunch into a sortable number, so size is
deprioritised as the ranking signal.
Also recorded: a transposed temp array is byte-required (int m[3][4] used as m[c][r]) with an
exact diagnostic -- right length + right instruction multiset + residual only on sp-relative
offsets means the frame LAYOUT is wrong, not the code; and when the original stores the same
slot twice, suspect two source statements rather than a scheduler quirk (GCC 2.7.2 has no DSE).
Worker C's 0x80031FC4 (276 B). Cookbook gains six entries, the most important of which is
worker C's correction of the COORDINATOR: a DEPENDENT row is one you cannot VERIFY, not one
you have MATCHED. C's 0x800A613C and 0x800FD120 had symbol rows outstanding, but
re-verifying against the tracked registry gave byte-identical results to the overlay runs --
both are still near-matches blocked on an ALLOCATION lever. Adding a symbol row unblocks the
verification, not the match; conflating the two would have had a worker stop working a row it
had not solved.
Also recorded: the address-taken value may be a PARAMETER not a local (frame 8 too big with
all offsets shifted by 8 is the tell); address-taken form forces a register; the struct
assignment is what BATCHES the loads where element stores serialise behind maspsx nops; and
the cop2 operand is the 25-bit field (0x486012 -> 0x4A486012).
Worker C found the defect while characterising an above-ceiling row: the Phase 10 fix
that made maspsx honour cc1's explicit `#nop` marker unconditionally is wrong for a
bare-symbol store consumer, because the store's own `lui $at` expansion fills the
delay slot and the marker is SPURIOUS. On 0x800AFDBC the original is
`lhu` / `lui at` / `sh` with NO nop, and honouring the marker costs 2 instructions --
turning a row that was otherwise byte-identical into a LENGTH-MISMATCH.
But the fix is genuinely needed for 0x80107C5C (112 vs 108). So the two rows want
opposite behaviour from the same instruction shape, and the honest resolution is to
make it a per-region mode rather than a global default.
`maspsx=nopmarker` -> --honour-nop-marker (default OFF)
VERIFIED BOTH WAYS:
make check with the mode OFF -> regions=510 AGREE, differing_bytes=0 MATCH, 253 tests OK
0x80107C5C WITH the mode -> 112 B, differing_bytes=0 MATCH
0x80107C5C WITHOUT it -> 108 B LENGTH-MISMATCH
So the default is the long-standing behaviour, and rows that need the marker opt in.
The tracked patch is regenerated and verified to reproduce both modified files exactly
from the pristine pinned checkout (tools/maspsx is git-ignored, so the patch is the only
reproducible carrier). docs/SETUP.md records the correction.
This is the third time this phase a worker found a defect in work the coordinator had
already shipped as verified -- the pattern is worth noting: a fix proven regression-free
against the CORPUS can still be wrong for an UNMATCHED row, because the corpus only
exercises the paths that already work.
Worker D's 0x8005A33C (312 B) and 0x8009F5AC (244 B) MATCH, both verified independently
and gated on the whole binary before promotion. Corpus max is now 312 B (244 -> 248 ->
264 -> 312 across two workers).
COOKBOOK 63 — NEW LEVER, the combiner constant-fold class. A residual of exactly one
addiu plus a matching shift in every displacement of one base register means cc1's
combiner folded a constant offset the original kept in a register. Writing the base
inline triggers it (308 B, one instruction short, 6 differing words all the same
thing); materialising the offset as a NAMED LOCAL blocks the fold and matches. It is a
4-byte class that is neither source structure nor scheduling, and it is INVISIBLE to a
mnemonic-level diff because every mnemonic is right.
COOKBOOK 64 — finding 59's family now has three load-bearing properties: row stride
(0x8009F6A0), total size (0x800308C4), and MEMORY RESIDENCE (0x8005A33C, where a union
lets cc1 scalarise the packed scratch into and/sra/sll).
COOKBOOK 65 — THE AMENDMENT TO FINDING 41 IS NOW MEASURED. Worker D ran the paired
control deliberately: the body immediately PRECEDING its first ceiling match, the
inverse transform with an identical frame shape. <=244 B: 1 attempted, 1 matched (100%),
first spelling. >244 B: 4 attempted, 3 matched (75%), 2-7 spellings. THE BAND WAS NOT
THE VARIABLE -- THE LEVER SET WAS. And the two LARGEST bodies were the EASIEST (312 B
and 264 B at 7 and 2 spellings), because a larger body has more redundant structure to
key off.
COOKBOOK 66 — a bounded negative with a named direction beats a blocked class: the
allocation failure on 0x800FF5A8 is the named-locals family in the OPPOSITE direction
(the original keeps a0 in TWO callee-saved registers, so what is missing is a named
local the allocator cannot coalesce).
Worker D's 0x800308C4 (264 B) MATCHES, verified independently and gated on the whole
binary before promotion. Three bodies now sit above the old 244 B ceiling
(248/248/264 B) from two independent workers, so the dispatch-artefact verdict is
settled by result.
COOKBOOK 59 EXTENDED WITH A NEW MECHANISM. D's first attempt was 8 words from a match
and all eight were the FRAME; 56 of 64 words were already byte-identical including
every call site, delay slot and register. The missing 8 bytes are an UNREFERENCED
ARRAY LOCAL, and the mechanism is measured across five spellings: cc1 allocates stack
space for an unreferenced ARRAY local but NOT for an unreferenced SCALAR local
(`int pad0, pad1;` gives no home; `int pad[2]` and `short pad[4]` both match). The 8
bytes are size-load-bearing and content-free -- a limit of the reconstruction, not a
recovered fact, recorded as such in the file header.
AMENDED RULE: when the residual IS the frame, vary the local aggregate's declaration
in THREE directions -- element type, row stride, and total size. Instance 1
(0x8009F6A0) needed the row stride; instance 2 (0x800308C4) needed the total size.
Finding 58 AMENDS finding 41, which was Phase 10's headline result and drove every
dispatch decision. The "1-in-12 for 200-800 B" comparison was taken on a queue that
had never been attempted (5 of 427 rows above 244 B ever tried, 1.2%; three in an
excluded class; both non-excluded attempts near-matched). The first row attempted
above the ceiling matched. The band's measured yield is a function of the lever set at
the time of measurement, and the lever set grows -- RE-MEASURE a band before
concluding it is exhausted, and never treat a band as closed at ~1% attempt coverage.
Findings 59-62: the local-aggregate row-stride/element-size lever that broke the
ceiling (int t[3][4] not int t[9]; frame 48 vs 40; residual concentrated on the frame
adjustment); the division_check trapped class; the localisation of the maspsx/GNU-as
mutual exclusion with the developer's ASPSX-as-oracle-only rule; and the fail-fast
validation of region override keys in sf3_merge.
Written at the developer's request after the control model changed from "the developer
spawns sessions" to "the orchestrator spawns, monitors and retires its own workers",
with pi-context-tools installed globally.
Covers: authority and what the orchestrator decides versus escalates; the herdr control
model with exact commands, the verified 2x2 topology, and the known failure modes
(including the transient agent_pane_not_found on a just-split pane, and the
mutual-ask intercom deadlock); the context model and the compaction-first policy with
the requirement it creates (state must live in files); roster sizing and the Goal A /
Goal B assignment split; the session lifecycle including the updated capability probe
with context_info; the hardened merge flow with the md5 drift guard; the verification
ladder; the full charter template; all nineteen process rules WITH THEIR PROVENANCE
(each earned by a real incident); and the phase open and close checklists.
Supersedes the session-management sections of PHASE8_PROTOCOL.md and PHASE9_PROTOCOL.md;
the merge/verification contract in those documents still stands.
MILESTONE MET AND EXCEEDED: 484 distinct matched bodies / 493 registered regions
(target 475, from the 400 baseline) — +84 bodies. Developer confirmation of the
milestone was requested and given before any close record was written.
Closing checklist all green from clean:
make clean && make all exit 0
cmp exit 0
SHA-1 both files e173426c157384ebf1b6caf8c6fea18a85a14af9
make test 237 tests, OK
make extents-verify regions=493 disagreements=0 AGREE
make gate c_regions=493 differing_bytes=0 MATCH
registry audit 493 rows, 0 overlaps, 0 unsorted, 0 bad extents,
0 missing sources, 484 distinct sources
worklist listed=1193, excluded_already_registered=493
negatives index 194 rows, address-ordered, 0 registered
git status --short src/ empty (0 untracked files)
firewall 0 prohibited-root paths (591 tracked files)
New records:
phase-ends/PhaseEnd_Phase10.md the phase record
docs/PHASE10_VERIFICATION.md the verification record
docs/MATCHING_COOKBOOK.md findings 41-57 (57 total)
phase-ends/CURRENT_PHASE.md CLOSED, with the checklist itemised
phase-ends/DIGEST.md the Phase 10 digest entry
The headline finding is methodological (finding 41, THE SIZE-BAND LAW): the matched
corpus median is 48 bytes with 454/459 at <=200 B while the remaining levered rows
had a median of 456 B, and two independent measurements — one controlled — put the
small band at 1-2 attempts per row against 1-in-12 for 200-800 B.
The phase's character: five of the coordinator's own generalisations were bounded by
workers (rare-epilogue class, register-field diagnostic, polarity lever, goto
trigger, load-delay consumer form). The rules that survived are the ones that were
bounded.
Five incidents recorded rather than smoothed over; the candidate gate rejected three
batches and the tracked registry was never corrupted. Scope held: the blocked classes
stay excluded, no scheduler-changing flag was granted, and inline asm was extended
only to shapes C provably cannot express.
STOPPING HERE. Phase 11 does not begin in this session.
cc1 emits an explicit `#nop` marker when it wants a load-delay nop. maspsx used to
RE-DERIVE the need and could overrule cc1 for a BARE-SYMBOL STORE consumer:
uses_at('sw\t$2,D_801221C4') -> True (macro store, expands via $at)
uses_at('sw\t$2,0($4)') -> False (register+offset, no macro)
nop_at_expansion is False for ASPSX >= 2.30
so neither test in _handle_nop_before_next_instruction fired, nop_required stayed
False, and an instruction cc1 had explicitly asked for was dropped (0x80107C5C at
108 vs 112; worker B's 0x8003A9C8). The fix honours the marker instead of
overruling it; only that path changes.
Worker B2 found the gap but mis-diagnosed it: its proposed fix was to extend the
`line_loads_from_reg` predicate, which ALREADY returns True for a store source, so
that patch would have been a no-op. The coordinator traced the actual call and found
the real mechanism in the uses_at / nop_at_expansion interaction. B2 then appended a
CORRECTION row to its own staged report superseding its paragraph — the right
response, and it records the general lesson: a named mechanism is a hypothesis until
it is traced, even when the observation is solid and reproducible.
REGRESSION VERIFICATION (the whole point of gating this):
make check exit 0
regions=489 disagreements=0 AGREE
c_regions=489 differing_bytes=0 MATCH
237 tests OK
All 489 previously-matched regions are byte-identical with the fix in place.
Carried as a tracked patch (tools/maspsx/ is git-ignored, so an in-place edit would
not survive a fresh clone); patch verified to reproduce both modified files exactly
from the pristine pinned checkout. docs/SETUP.md records the fix and its provenance.
0x80107C5C NOW MATCHES (112 B, 0 differing, verified against worker B2's variant
X3.c) — but that is a BARE variant with no header, and the project convention requires
a documented source stating the observed instructions and limits. So the row is
UNBLOCKED and one documented source away, not claimed. Recorded as a carry-forward.
Developer decision (2026-09-24), extending Phase 8's coprocessor/kernel allowance.
Trigger conditions recorded: plain C attempted and mechanism identified; the
inexpressibility is structural rather than a scheduling/allocation tie-break;
integer logic stays in C and scheduling still comes from cc1/maspsx; per-file
header documents the sequence, the reason and the limit.
First accepted instance: the PSX scratchpad stack switch (lui at,0x1f80 /
sw sp,1020(at) / lui sp,0x1f80 / ori sp,sp,0x3dc ... / lw sp,1020(sp)), found
independently by workers A (0x800BC658) and B (0x800BC6EC). A stack-pointer READ
was already covered by the register-name binding and needs no exemption; only the
switch does.
Explicitly does NOT reopen trapping-arithmetic, rare-epilogue or primitive-init.
32 new bodies from 400, all verified on the candidate whole-binary gate before
promotion. SHA-1 e173426c157384ebf1b6caf8c6fea18a85a14af9 stable.
Registry requests granted (each byte-verified with a failing control):
cc1=-G8 on 0x800A6BEC; gp=-D_80121B88 on 0x80015D50
symbols D_80122700, D_80122704, D_80121AD4 (gp)
Harness: per-region maspsx modes wired through sf3_match (maspsx=noreordernop,
maspsx=regread) plus --no-jump-slot-nop/--nop-on-reg-read for range. Both are
opt-in and default-off; make check green at 441 with them off, suite 229 -> 232
tests. Carried as a TRACKED patch (tools/patches/maspsx-phase10-r1r2.patch)
because tools/maspsx/ is git-ignored, so an in-place edit would not survive a
fresh clone; patch verified to reproduce the working tree byte-identically.
R1/R2 are recorded as a MEASURED NEGATIVE: neither closes a region (cookbook
finding 40 has the mechanism and the remaining developer-owned route).
Docs: cookbook finding 40 (rare-epilogue mechanism + why the obvious maspsx fix
fails); SETUP.md maspsx patch provenance and apply step.
Negatives: 0x8010AA28 imported; index sorted by address (140 rows, 0 registered).
Full clean audit green: make clean && make all exit 0, cmp exit 0, both SHA-1
match, registry 441/0 overlaps/0 bad extents/0 missing sources, 0 firewall.
Phase 9 closed at 400 distinct bodies / 409 regions (from 149/158), all
gates green from clean: CMP_OK, SHA-1
e173426c157384ebf1b6caf8c6fea18a85a14af9, 229 tests, make gate
c_regions=409 MATCH, extents-verify AGREE, worklist listed=1343 with
excluded_already_registered=409, firewall 0, src/ 0 untracked.
Milestone partially met (400 of 500): the blocker is recorded with per-class
evidence (tier-1 tie-break domination, class disjointness, measured rate
decay) per the plan's Or branch. PhaseEnd_Phase9.md, digest entry, and the
ledger archive (logs/Phase9.md) written. docs/PHASE9_PROTOCOL.md (scale-run
retrospective: queue fix, tier-2 pivot, family-set lever, rotation
discipline, leading-indicator rule, measured budget), docs/PHASE9_VERIFICATION.md
(all gates, incidents, class exclusions), cookbook findings 29-39,
conventions additions, README updated to 400/409.
Phase10_PLAN.md drafted (Goals A/B/C: tail squeeze vs library-boundary
investigation, developer decisions enumerated) — requires explicit approval
before any Phase 10 task; no Phase 10 work begins in this session.
Developer-confirmed milestone: 149 distinct byte-identical bodies across 158
registered regions, up from 34 / 39 at Phase 7 close - 115 new bodies against a
target of 70, produced by three sessions working one worktree under a written
protocol.
Adds PhaseEnd_Phase8.md and the digest entry, archives CURRENT_PHASE.md as
logs/Phase8.md, and completes docs/PHASE8_PROTOCOL.md with the retrospective the
developer asked for: which rules were load-bearing (worker/coordinator file
ownership, candidate-first merging, worklist regeneration, self-contained
charters), where the protocol needed a decision, the four failure modes seen with
their mitigations, and reusable CHARTER and worker-report templates so the next
coordinated phase does not pay the cost of writing them again.
Final gates: 203 tests; make clean, make all, cmp and SHA-1 e173426c...; make
check green at c_regions=158 with 0 differing bytes and regions=158
disagreements=0; registry audit 0 violations; no untracked file in src/; 244
tracked files, none under a prohibited root.
All gates exit 0 from a clean state: make clean, make all, cmp, both files SHA-1
e173426c..., and make check (203 tests, regions=158 disagreements=0, c_regions=158,
0 differing bytes). The registry audit reports 0 violations across 158 ordered,
non-overlapping regions whose every extent is graded exact and whose every source
exists. The worklist regenerates with excluded_already_registered=158, equal to
the registry. 244 tracked files, none under a prohibited root, and no unmatched
draft survives in src/.
Phase 8 went from 34 distinct bodies / 39 regions to 149 / 158 against a target of
70, with three pi sessions working one worktree under a written protocol. Adds the
verification record, the open-negatives index (addresses, sizes, statuses and class
labels only - no bytes, no instruction text), and the README status.
Worker B's last act is recorded as a result rather than a footnote: it tested
worker A's mirrored-layout lever against its own two bounded negatives and
produced a decision rule instead of a match - the lever applies when the
instruction count is already correct and the block order is wrong, so where the
count is wrong and the order right it only trades one defect for another.
5 new matches from worker A (func_800F75D0, func_80026180, func_8003768C,
func_80089D14, func_800681A4) -> 158 regions / 149 distinct bodies, candidate gate
0 differing bytes, make check green.
MATCHING_CONVENTIONS.md now records the four per-region override keys with the
byte-level measurement behind each (cc1=, as=, gp=-NAME, maspsx=off), the
inline-assembly convention as the developer decided it (accepted for coprocessor
and kernel instructions, documented per file, integer logic in C, no .word lumps,
and a register-name binding is not inline assembly), the rule that an unmatched
draft does not stay in src/, and the worklist classes that are excluded outright
with counted reasons.
Worker C found the strongest structural signal of the phase and asked me to act
on it rather than merely record it. I re-derived the census independently before
acting: 50 of the 1,937 exact extents contain trapping add/sub (funct 0x20/0x22,
410 instructions) and 0 of the 144 then-registered regions does. All four
unmatched duplicate groups are inside the class. Ten cc1 builds plus the real
CC1PSX 4.0-4.6 over ~30 C shapes and 15 flags never emit the trapping forms, and
-ftrapv is rejected by all of them.
tools/sf3_triage now detects the class and excludes it by default
(excluded_trapping_arith=50, --allow-trapping to list it), with three synthetic
tests, so no worker can spend budget on it by accident. Recorded as cookbook
finding 26 with the neg-macro clue that points at a macro-print-style difference
rather than a source difference. The class is now the best candidate for part of
the unresolved library-versus-game-code boundary - a hypothesis, not a claim.
Also recorded: worker A's maspsx load-delay defect (its predicate tests whether
the next instruction loads FROM the register, not whether it reads it at all, so
a load followed by a store of that register gets no delay nop), four mechanical
layout tells, and the struct-assignment lever for the base-in-register class.
Finding 22 is refined with worker C's third spelling: the written order of the +
operands decides the emitted addu order.
Cycle 5 merge: 65 claims, 8 accepted, 57 skipped, 0 rejected. Candidate gate
c_regions=152, 0 differing bytes; promoted; make check green at 203 tests.
Distinct matched bodies: 143.
P8-T2's protocol record plus worker B's cycle-1 merge.
Worker B verified 8 claims (7 bodies: g0029 is a two-address duplicate group).
It also classified its whole 946-row partition off one objdump and found that 226
rows (24%) access gp-relative globals, staging 335 distinct addresses. Those were
checked before use: every row is arithmetically exact under the list's decimal d
column (335 rows, 0 errors), and four sampled globals were confirmed against my
own Ghidra disassembly (0x564(gp) -> 0x80121E9C and three others). A risk check
for whether a new gp marker could change an already-matched region found only
worker A's unclaimed in-progress files, so no registered region is affected.
323 rows were new (12 already present), taking config/symbols.tsv to 354 rows and
unblocking a quarter of worker B's partition.
Candidate gate: c_regions=57, 0 differing bytes, SHA-1 e173426c. Promoted, then
make check green: 189 tests, regions=57 disagreements=0, c_regions=57 MATCH.
Distinct matched bodies: 34 -> 51.
Worker B also deferred 0x8001D98C (g0053, 436B, GTE 3x3 transform) into the
bounded GTE class, and recorded two byte-proven cookbook findings: BCD nibble
pairs must be their own parenthesised subexpression (a pure association tell, 9
shifted bytes), and a constant absolute address in a load folds the
carry-adjusted low half into the load displacement rather than using lui+ori.
Phase 7 closes the milestone: 34 distinct byte-identical bodies across 39
registered regions, up from 11 bodies / 12 regions at Phase 6 close, with every
region end derived from control flow instead of by hand.
New cookbook findings, each byte-proven:
- 13: a store-only function leaves its constant in v0 as scratch. Writing
`return 1` costs an instruction in the delay slot (0x8003636C: 12 vs 16 bytes).
- 14: the gp-relative offsets proved so far, with the global each one names.
- 15: shapes plain C cannot reach without SDK headers (GTE/COP2 code, BIOS
syscall wrappers, stack-pointer accessors).
Conventions updated: a region end comes from config/function_extents.tsv and is
enforced by make check; the duplicate check is the tracked census rather than a
manual search; the worklist is the ranked queue; and address-named symbols
resolve implicitly while anything else fails loudly.
Clean-state gates all exit 0: make clean, make all, cmp, both files SHA-1
e173426c..., and make check (168 tests, regions=39 disagreements=0,
c_regions=39, 0 differing bytes). 121 tracked files, none under a prohibited
root. Milestone confirmation requested before any PhaseEnd.
Every cross-reference used to need a hand-written config/symbols.tsv row, which
does not scale to a batch. A symbol whose name is an address now resolves to that
address with no row: func_XXXXXXXX, D_XXXXXXXX, g_XXXXXXXX, lbl_XXXXXXXX. This is
the convention the registry already used, applied without the row.
The names come from the object's own undefined-symbol list (nm -u), not from a
guess about the source, so a name the source defines but never references is
never mistaken for one needing resolution. A registry row still wins, which is
how a gp marker or a real name is attached. Anything neither registered nor
address-shaped now fails before the link with a message naming the symbol and the
fix, instead of a bare ld diagnostic.
First match from the worklist: func_800F8F9C (36 bytes, worklist rank 3), a
duplicate-group representative with a frame and a call. It matched on the first
attempt and is registered twice (0x800F8F9C, 0x80109314) against one source, so
two functions were matched for one body. Its callee needed no registry row.
Ghidra's independent body agrees with the derived extent.
make gate: c_regions=14, 0 differing bytes, SHA-1 e173426c. 168 synthetic tests.