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.
Phase 6 chose match targets by eye from the boundary inventory. tools/sf3_triage
now ranks every eligible candidate by (tier, size, address) from tracked inputs
alone and records why everything else was excluded.
Eligibility: an exact or fallthrough extent, a non-degenerate body, not already
registered, not the header entry, not named by --exclude. indirect, escape,
outside, runaway, contained and standalone are excluded and counted.
Tiers: 0 duplicate-group representative (one match, several addresses), 1 exact
leaf (no cross-references, so no symbol rows), 2 exact non-leaf, 3 fallthrough.
Result: 1916 listed (tier 0: 9, tier 1: 509, tier 2: 1394, tier 3: 4), with 252
degenerate bodies, 88 low-confidence grades, 12 registered, 1 header entry and
2 named near-misses excluded. The nine tier-0 entries are the real duplicate
groups: matching those nine bodies registers 22 function addresses.
The two deferred near-misses are excluded by name in the Makefile so the
exclusion stays visible rather than buried in the tool. 160 synthetic tests pass.
Matching conventions require a duplicate check before registering, because a
shared body is matched once and registered once per address. Phase 6 did that
check by hand and found one 12-byte pair. tools/sf3_dupes now hashes every
derived extent body and groups exact duplicates.
Results: 2284 extents, 65 multi-address groups, 2104 singletons. Only 10 groups
contain code (24 addresses, all exact-graded); 55 are all-zero bodies. The
hand-found pair 0x800262E0/0x800262EC is reproduced as g0002, which is the check
that the census measures what it claims. The largest real groups are 712 bytes
(0x8001084C/0x800189E8) and 436 bytes.
The zero groups are a real finding: 252 extents have all-zero bodies, 245 inside
the zero band 0x80147000..0x80170000. The cause is the inventory's jal grade,
which decodes every word as an instruction -- in a data region a word with
opcode 3 is graded as a call whose target lands in the zero band. The census
flags those groups rather than hiding them, and the worklist must exclude
degenerate bodies.
The census is tracked rather than ignored as the plan said, because it holds
addresses, sizes and grades only (the same class as the tracked inventory and
extents tables) and the worklist must be reproducible from tracked inputs. The
content hash is computed and never written.
Phase 6 graded function starts and left every end to be derived by hand. This
adds tools/sf3_extents, which explores all reachable control flow from each hard
start (jal/entry) and reports an extent plus how far it can be trusted.
Measured decisions, not stylistic ones:
- Soft starts are not walk boundaries. A body's second instruction can satisfy
the prologue grade exactly (0x800152AC is lw v1,8(gp) / addiu sp,sp,-176, so
0x800152B0 looks like a start). Enforcing soft boundaries stopped 155 of 416
walks inside a real body.
- The walk is a full reachability computation, not a first-terminal search: a
function whose paths return at different addresses must report the whole body.
Grades: exact 1940 (1666 packed, gap=0), fallthrough 256, indirect 73,
escape 15, contained 153, standalone 438; 63.8% of the payload covered.
Verification: all 12 registered regions reproduce exactly (make extents-verify,
now part of make check), 29 new synthetic tests (115 total), byte-identical
across two runs, and Ghidra's independent body for FUN_80017ad4 agrees. Two
defects were caught by writing the tests first and are recorded: reach had to be
an exclusive end, and a terminal j's delay slot must not continue linearly.
The table holds addresses, sizes, grades and site addresses only -- no bytes.
P5-T6. Adds docs/MATCHING_COOKBOOK.md (nine byte-proven compiler/assembler
findings, each with basis and limit), docs/MATCHING_CONVENTIONS.md (what counts
as a match, registry format, source naming, duplicate sharing, symbols, the
verification procedure, the firewall) and docs/PHASE5_VERIFICATION.md (outcome,
clean gates, bounded negatives, milestone request).
Tooling: make test and make check targets; region objects are now
symbol-localized so the documented duplicate-sharing mechanism (N registry rows
-> 1 source) links, covered by a synthetic test (53 tests).
Clean gates: 53/53 tests; make clean/all/cmp/SHA-1 green; make gate with one C
region -> c_regions=1, 0 differing bytes, SHA-1 e173426c...; 57 tracked files, 0
under any prohibited root. Phase 5 is not closed until the milestone is
confirmed.
P5-T5. Part A: the entry [0x800FB368,0x800FB410) is not compiler output. The
return address is round-tripped through an absolute global around the first
call, the range ends in break, the stack pointer is built from linker globals,
and the clear loop falls through with no jr ra. Two bounded compile experiments
failed and were stopped; the entry stays fallback and no C is claimed.
Part B: first byte-identical C match -- func_80017AD4 at 0x80017AD4..0x80017AE8
(20 bytes), src/func_80017AD4.c, registered in config/regions.tsv. make gate
reports c_regions=1, 0 differing bytes, SHA-1 e173426c157384ebf1b6caf8c6fea18a85a14af9.
Body is unique with one caller at 0x80014C14; a shared-tail near-miss was
checked and rejected as a duplicate. The Phase 3 baseline is unaffected.
Records six codegen findings, notably that GNU as expands the la macro with ori
while the original assembler (ASPSX 2.81) uses addiu, so la-using functions will
need maspsx. No ROM-derived material is tracked.
P5-T4: tools/sf3_match provides range/plan/build/gate. It compiles a C candidate
with the identified toolchain (egcs-2.91.66 psx, -O2 -G0 -mno-split-addresses),
compares the exact instruction range byte-for-byte against the original, and
builds the address-ordered executable from the tracked registry config/regions.tsv
(header 0x800 + data gaps + C regions, LMA 0x800, metadata discarded).
Promotes the Phase 4 ordered workflow from an ignored experiment to tracked
tooling. Verified: make gate with an empty registry and with one real C region
both reproduce SHA-1 e173426c...; the Phase 3 baseline is unchanged and green;
the synthetic suite is now 50 tests including deliberate-corruption failures and
safe refusal of an existing destination. No ROM-derived material is tracked.