Skip to content

config: repoint 12 stale legacy_source rows left behind by symbol renames - #2021

Merged
andrewboudreau merged 1 commit into
mainfrom
config/legacy-source-renames
Aug 30, 2026
Merged

config: repoint 12 stale legacy_source rows left behind by symbol renames#2021
andrewboudreau merged 1 commit into
mainfrom
config/legacy-source-renames

Conversation

@andrewboudreau

Copy link
Copy Markdown
Collaborator

What this is

legacy_source on a TU-manifest function row names the per-function src/ file that
function is delinked from. When a func_ovNNN_AAAAAAAA symbol is later named, the
delinks entry and the file get renamed together to the mangled spelling — but the
manifest row does not, so its legacy_source keeps pointing at a path that no longer
exists. #2016 fixed the rows whose .c had become a .cpp. This is the residue: rows
where neither extension exists, because the stem itself changed.

Derivation

Across the 89 entries in config/tu_manifest.d/**/*.json there are 1108
legacy_source rows (only functions rows carry the field — data and bss never
do). Classified against git ls-tree -r origin/main:

class before after
stale — .c row, .cpp on disk 0 0
inverse — .cpp row, .c on disk 0 0
dangling — neither present 80 68 (all deliberate, see below)

Address-resolution rule

For each dangling row, parse the module's config/arm9/overlays/<mod>/delinks.txt
(config/arm9/delinks.txt for arm9) and take the entry whose section range starts
at the row's address
. Never guessed from the symbol name.

All 12 rows fixed here resolved to exactly one entry, and every match was exact on
both ends — start == address and end == address + size — with the entry still
marked complete. No range moved; none was handed back to the cartridge. This is stale
bookkeeping, not lost coverage.

entry before after fixed
ov020/HauntedChair 7 0 7
ov029/WaterDiamond 3 0 3
ov010/PeachPainting 1 0 1
ov002/Enemy 1 0 1
ov070/daKrpa_c (promoted) 25 25
ov070/daKpFr_c (promoted) 21 21
ov100/daObjPathLift_c (promoted) 8 8
ov002/daObjAbuku_c (promoted) 7 7
arm9/ActorDerived (promoted) 5 5
arm9/ActorBase_SceneNode (promoted) 2 2
total 80 68 12

Representative resolution:

ov020/HauntedChair  func_ov020_021129dc  0x021129dc + 0x124
  delinks ov020: src/_ZN12HauntedChair6State3Ev.cpp  complete  .text 0x021129dc-0x02112b00
  legacy_source: src/func_ov020_021129dc.c  ->  src/_ZN12HauntedChair6State3Ev.cpp

The promoted entries are deliberately left alone (68 of the 80)

All six promoted entries in the manifest are 100% dangling — every row, not some.
None is mixed, so leaving them untouched is fully self-consistent per entry, and there
is no other promoted entry establishing a different convention to match.

Dangling there is the designed terminal state, not a defect:

  • notes/translation-unit-reconstruction-plan.md §6 lists the field as
    "legacy source paths to remove on promotion".
  • tools/tu_promote.py git rms every legacy_source as part of promoting, and
    replaces the N per-function delinks entries with one spanning complete entry at
    promoted_source. It rewrites status and source and deliberately leaves
    legacy_source as the historical record.

Repointing them would also be actively harmful: tu_promote.rewrite_attribution
writes one attribution override per absorbed symbol, keyed off the legacy path's
stem (lineage.get("src/" + stem)). Collapsing 25 distinct stems onto one TU path
would destroy that 1:1 symbol→author mapping.

Resolving them by address confirms the reading: only the first function of each
promoted TU resolves at all
— its address is the promoted span's start and it maps to
promoted_source (e.g. _ZN8daKrpa_cD1Ev @ 0x02121118src/actors/daKrpa_c.cpp
.text 0x02121118-0x02121b48). The other 62 interior addresses are no longer entry
starts, exactly as promotion intends.

Post-change verification

Re-derived on the resulting tree via python tools/tu_manifest.py export:

  • dangling 68, all on promoted entries, named individually above — the set
    deliberately left;
  • stale 0, inverse 0 — unchanged;
  • every legacy_source on a non-promoted entry resolves to a path that exists;
  • no duplicate legacy_source introduced within any entry;
  • each of the 12 new paths has exactly one entry in its delinks.txt — the
    precondition tu_promote.py checks, which a dangling row would have failed. Fixing
    these unblocks promotion for these four entries.

No delinks / symbols / relocs / src / src_tu / include / tools changes

 config/tu_manifest.d/ov002/Enemy.json         |  2 +-
 config/tu_manifest.d/ov010/PeachPainting.json |  2 +-
 config/tu_manifest.d/ov020/HauntedChair.json  | 14 +++++++-------
 config/tu_manifest.d/ov029/WaterDiamond.json  |  6 +++---
 4 files changed, 12 insertions(+), 12 deletions(-)

12 changed lines, one per row, patched in place — the 2-space indent, LF endings
and trailing newline are byte-preserved and no unrelated line reflows. Nothing here is
a gate input, so no rebuild is implied. Pre-push passed unaided (port_refcheck 405
references, duplicate-sources 11250 stems, check_src_tu_compiles 89/89).

Unrelated pre-existing finding (not touched here)

While asserting the tu_promote precondition tree-wide, one row outside the 80 fails
it: ov009/Birdfunc_ov009_0211145c. Its legacy_source
src/func_ov009_0211145c.c does exist on disk, but has zero entries in
config/arm9/overlays/ov009/delinks.txt — there is a gap at 0x0211145c-0x021115d8
(0x17c bytes, exactly the manifest's declared size) between the entries ending and
starting on either side. So the manifest licenses a range that is still handed back to
the cartridge. It predates this branch (also absent on origin/main) and fixing it
would mean touching delinks.txt, so it is out of scope here — flagging it for a
follow-up. Tree-wide, 105 src/ files are unenrolled in any delinks.txt, but the
other 104 are SDK/BIOS stubs (CpuFastSet.c, BitUnPack.c, …) that no manifest row
claims; this is the only one a manifest entry points at.

…ames

Mechanism. `legacy_source` on a TU manifest function row names the per-function
`src/` file that function is delinked from. When a `func_ovNNN_AAAAAAAA` symbol is
later named, the delinks entry and the file are renamed together to the mangled
spelling -- but the manifest row is not, so its `legacy_source` keeps pointing at a
path that no longer exists. #2016 fixed the rows whose `.c` had become a `.cpp`;
these are the residue where NEITHER extension exists because the stem itself changed.

Derivation. Over the 89 entries in config/tu_manifest.d there are 1108
`legacy_source` rows (only `functions` rows carry the field; `data`/`bss` never do).
On d773264: 0 stale (.c row / .cpp on disk), 0 inverse (.cpp row / .c on disk),
80 dangling (neither extension present).

Resolution rule -- by address, never by name. For each dangling row, parse the
module's config/arm9/overlays/<mod>/delinks.txt (config/arm9/delinks.txt for arm9)
and find the entry whose section range STARTS at the row's `address`. All 12 rows
fixed here resolved to exactly one entry, and in every case the match was exact on
both ends -- start == address and end == address + size -- and the entry is still
marked `complete`. No range moved and none was handed back to the cartridge; this is
stale bookkeeping only. Example: ov020/HauntedChair's `func_ov020_021129dc`
(0x021129dc + 0x124) is owned by src/_ZN12HauntedChair6State3Ev.cpp, complete,
0x021129dc-0x02112b00.

Promoted entries are deliberately left alone -- 68 of the 80. All six `promoted`
entries in the manifest are 100% dangling; none is mixed, so leaving them is
self-consistent per entry. Dangling there is the designed terminal state, not a
defect: notes/translation-unit-reconstruction-plan.md section 6 lists the field as
"legacy source paths to remove on promotion", and tools/tu_promote.py `git rm`s
every `legacy_source` as part of promoting, collapsing the N per-function delinks
entries into one spanning `complete` entry at `promoted_source`. Repointing them
would also be actively harmful: tu_promote's rewrite_attribution keys one
attribution override per absorbed symbol off the legacy path's STEM
(`lineage.get("src/" + stem)`), so collapsing 25 distinct stems onto one TU path
would destroy that 1:1 symbol->author mapping. Resolving them by address confirms
the reading -- only the first function of each promoted TU resolves at all (its
address is the promoted span's start, and it maps to `promoted_source`); the other
62 interior addresses are no longer entry starts, exactly as promotion intends.

Counts: dangling 80 -> 68 (all 68 promoted, deliberate). stale 0 -> 0,
inverse 0 -> 0. Every row on a non-promoted entry now resolves to a path that
exists, with no duplicate `legacy_source` introduced inside any entry.

Config only: 12 changed lines across 4 manifest files, patched in place so the
2-space indent, LF endings and trailing newline are byte-preserved. No delinks.txt,
symbols.txt, relocs.txt, src/, src_tu/, include/ or tools/ change, so no gate input
moves and no rebuild is implied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ
@tangos-validator

tangos-validator Bot commented Aug 30, 2026

Copy link
Copy Markdown

✅ PR validation — Passed

Committed merge introduces no reconstruction or attribution regression.

Full merge validation

Check Result
Committed test merge yes
Byte-verified functions 10,994 / 11,347 (96.89%, +0)
Byte-verified code bytes 2,045,116 / 2,211,124 (92.49%, +0)
Claimed, not byte-verified 222 functions, 60,188 bytes (+0)
Perfect source moves 0 R100
Enrolled ranges (delinks complete) 11,058 functions, 2,052,772 bytes (92.84%, +0) -- differs from byte-verified by +64
Contributor credit 0 added, 0 changed, 0 lost
Relocation check 0 checked; no affected slots
Module fidelity 106/106 exact; 100.000000% compared bytes
Code linked from verified source 11,087 functions, 2,066,772 bytes (93.47%)
Module bytes from source 2,066,772 / 3,049,600 (67.8%); 811,492 (26.6%) are data no delink entry reaches
ROM data reproduced from source 456 symbol(s) exact, 241 partial, 9 differ

Byte-verified means the range carries complete in a delinks.txt, so the ROM build compiled it and compared it to the cartridge. The 222 claimed functions have a src/ file named after the symbol with no NONMATCHING banner, and nothing compiles them -- dsd fills their addresses with the ROM's own bytes. Both together are the 11,216 this project calls matched.

The private worker commits a test merge, builds the stock ROM profile, compares every executable module, measures matched and source-built code, checks contributor lineage, and verifies affected relocations. The mod profile is opt-in and is not part of this merge gate.

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Gatekeeper check on the 12 repointed rows, independent of the derivation: every new path exists on disk (12/12), and the scope is config/tu_manifest.d only — no delinks.txt, symbols.txt, relocs, src/, src_tu/, include/, or tools/. Nothing here can move a byte.

Agreed on leaving the 68 promoted-entry rows alone: tools/tu_promote.py git rms the legacy paths while deliberately preserving the legacy_source field, and rewrite_attribution keys one override per absorbed symbol off that path's stem — collapsing 25 stems onto one TU path would destroy the symbol->author mapping. Dangling is the designed terminal state there, not rot.

Filed the ov009/Bird finding separately rather than folding it in here; it needs a delinks.txt edit and does not belong in a config-only PR.

Cleared; merging when PR validation completes.

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Correction to my note above, before anyone chases it: the ov009/Bird finding is not coverage loss. I said I would file it separately; on inspection there is nothing to file.

src/func_ov009_0211145c.c opens with:

// NONMATCHING: hand-written asm, not a C decompilation. Byte-exact via an asm hatch on a
// proven mwccarm 1.2 register-allocation/scheduling wall; does NOT count as matched.

The "status":"matched" row in config/match_attempts.jsonl is that asm hatch, not a C match. Every genuine C attempt on this function sits at divergences=3 against a documented register-allocation floor -- four independent sessions (grok-4.5/grok-4.6, high and xhigh) each reconfirming the same three words, with notes like "FLOOR(ordering) div=3; 0x6a outgoing-arg always steals r0 over rematerializable pool s".

So the delinks gap at 0x0211145c-0x021115d8 is deliberate: the range is filled from the cartridge precisely because the body is asm. Enrolling it would be the mistake, not the fix -- that is the standing NONMATCHING byte-match is an asm body rule.

The legacy_source row is still correct; it records draft provenance, which is what the field is for. The one durable consequence is that ov009/Bird (status: text-verified, source: src_tu/actors/Bird.cpp) can never be promoted while that body is asm -- a property of the function, not a defect in this branch or a candidate for the promotion queue.

Nothing here changes my clearance of the 12 rows.

@andrewboudreau
andrewboudreau merged commit 3c5a2fe into main Aug 30, 2026
4 checks passed
@andrewboudreau
andrewboudreau deleted the config/legacy-source-renames branch August 30, 2026 17:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant