Skip to content

CONVERTED tier: stop scoring MMIO as a codegen trick, and TU promotions as deletions - #2007

Merged
andrewboudreau merged 1 commit into
mainfrom
tools/tiers-mmio-and-tu-moves
Aug 30, 2026
Merged

CONVERTED tier: stop scoring MMIO as a codegen trick, and TU promotions as deletions#2007
andrewboudreau merged 1 commit into
mainfrom
tools/tiers-mmio-and-tu-moves

Conversation

@andrewboudreau

Copy link
Copy Markdown
Collaborator

Two defects in the CONVERTED-tier gate. Both are the same species — the gate counting something other than what it says it counts — and both are tools/ + baseline only, with no source touched.

Defect 1 — tools/tiers.py scored hardware registers as a codegen trick

no_codegen_trick ORs LAUNDER, VOLATILE and ASM, and VOLATILE was a bare \bvolatile\b. On a Nintendo DS the only way to reach VRAM, the geometry engine or the IPC/DMA/divider registers is a volatile-qualified pointer, so the criterion failed the code that had no alternative:

file volatile hits what they are
src/_ZN8dScene_c22ResetHardwareRegistersEv.cpp 74 stores to 0x0400xxxx and VRAM bank registers
src/_ZN2GX13SetBankForTexEt.cpp 25 VRAM bank control
src/_ZN3G2x12SetBGyAffineEPVtP9Matrix2x2iiii.cpp 5 the register block is a parameter

That compounds with the TU work rather than sitting still: the criteria are file-wide, so a reconstructed TU that absorbs any of those inherits the failure whole.

The two shapes are separable by what is volatile-qualified:

  • MMIO qualifies the pointed-to type, so a * follows it — *(volatile u32 *)0x4000400, volatile u16 *ime, volatile DMAChannelRegs *reg, f(volatile void *dst).
  • Match hack qualifies an objectvolatile int li;, volatile Vector3 v;, volatile s32 zero = 0;, the (s32)(volatile s32)rsc round-trip that demotes a local out of a register, and Node *volatile arr[4] where the pointer rather than the pointee is volatile.
-VOLATILE = re.compile(r"\bvolatile\b")
+VOLATILE = re.compile(r"\bvolatile\b(?![\s\w:]*\*)")

A negative lookahead rather than the equivalent greedy form on purpose: [\s\w:]*\* would backtrack into the type name, stop mid-identifier and call volatile u32 *p a scalar.

Both directions were measured, not assumed — a volatile regex that stops catching match hacks is a worse defect than the one being fixed.

  • Files scoring a codegen trick: 655 → 254. All 401 released were confirmed MMIO-only.
  • The 254 kept still include every volatile int li; spill pad, every volatile Vector3 v; stack reserver, every volatile int dummy[4]; frame filler, the (volatile s32) round-trip and the Node *volatile form.
  • no_codegen_trick 10,623 → 11,020; CONVERTED 2,511 → 2,568 functions, 22.20% → 22.71%. Additions only.

Known conservative reading, documented at the regex rather than hidden: typedef volatile u32 vu32; used only as vu32 * (4 files) still scores. Excluding typedefs opens a real evasion — typedef volatile int vi; vi dummy; would carry no volatile at the use site at all.

Defect 2 — tools/tiers_ratchet.py reported a TU promotion as a vanished file

The ratchet banks the SET of paths passing all five criteria and fails when a path leaves. A TU promotion consolidates N per-symbol src/_ZN....cpp files into the one src/actors/X.cpp they always were; git records that as N deletions plus one addition, so every one read as:

GONE -- not a tracked source file any more (deleted, renamed or moved)

Measured on PR #1882 (tu/inline-dtor-order, 9c6396c5f): 90 of 90 backslid paths were TU legacy_source entries whose TU is "status": "promoted" and whose promoted_source exists on the branch. Zero were real deletions. A gate whose entire output is false alarms trains people to re-bank without reading it.

A GONE path is now resolved through the manifest — via tools/tu_manifest.py, never the files — and reported as a MOVE naming the absorbing file and what that file does with the five criteria:

  src/_ZN15daObjPathLift_cD1Ev.cpp
      MOVED -- absorbed into src/actors/daObjPathLift_c.cpp by TU promotion
      (ov100/daObjPathLift_c), which fails: No raw offset arithmetic
      (*(u32*)(c + 0x74)); Calls things by real names, not mangled _Z

versus a genuine deletion, which still reads GONE.

A promotion is not free. The criteria are file-wide, so a clean function merged into a file with one bad line really does lose its status, and that case still exits 1. Only a move into a file that itself passes all five is silent (it is then an ordinary addition on the next --update). Both halves are exercised on live data by the current manifest: the two _ZN7fBase_c9SceneNode* legacy paths absorbed into src/actors/ActorBase_SceneNode.cpp are clean moves; the eight absorbed into src/actors/daObjPathLift_c.cpp still fail.

In practice a promotion lands in the failing case by construction: a reconstructed TU must spell _ZN7fBase_cnwEj, _ZN8dActor_cC2Ev and _ZN8dActor_cD2Ev directly or its range will not link, so no_mangled_refs can never pass for one. That is structural, not sloppiness, and it is not fixed by exempting mangled refs — byte-match outranks readability, and --update --reason is where that trade gets a name against it. --check now says so in its footer when a MOVE is among the failures.

Re-banked in the same PR

Changing a criterion requires re-baking the baseline or CI goes red.

python tools/tiers_ratchet.py --update
wrote config/converted-baseline.json: 1957 -> 2567 (+610 / -0)

Zero removals — set-diffed against HEAD, not trusted from the counter. The diff looks larger than +610 only because the previous file was not sorted and write_baseline sorts; the old "count": 1958 was also stale against its own 1957-entry list and is now consistent.

Verification

python tools/tiers_ratchet.py --check     # CONVERTED ratchet PASS  baseline 2567  current 2567   (exit 0)
python tools/tiers.py                     # clean, exit 0
python -m unittest tools.test_tiers        # 29 tests, OK
python -m unittest tools.test_tu_manifest  # 25 tests, OK
python tools/check_python_names.py         # PASS
python -m pyflakes tools/tiers*.py         # clean

tools/rombuild.py and tools/eligible.py were deliberately not run: no source file is touched and the build directory is shared with other agents.

Also in here

  • tools/test_tiers.py (new, 29 tests) pins both readings in both directions — MMIO must not score, a match hack still must; a promotion must read as a MOVE, and a move into a failing file must still fail the gate.
  • .github/workflows/converted-ratchet.yml runs those tests, and now also triggers on tools/tu_manifest.py and config/tu_manifest.d/** — a PR that only re-labels an entry "status": "promoted" changes what this gate says about an existing baseline.
  • notes/converted-tier.md gains a dated section for both, in the style of the existing writeup.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ

…ns as deletions

Two defects in the CONVERTED-tier gate, both of the same species: the gate
counted something other than what it says it counts.

1. tools/tiers.py scored hardware registers as match hacks

`no_codegen_trick` ORed LAUNDER, VOLATILE and ASM, and VOLATILE was a bare
`\bvolatile\b`. On a Nintendo DS the only way to reach VRAM, the geometry
engine or the IPC/DMA/divider registers is a volatile-qualified pointer, so
the criterion failed the code that had no alternative:

  src/_ZN8dScene_c22ResetHardwareRegistersEv.cpp   74 hits, all 0x0400xxxx
  src/_ZN2GX13SetBankForTexEt.cpp                  25 hits, all VRAM banks
  src/_ZN3G2x12SetBGyAffineEPVtP9Matrix2x2iiii.cpp the block is a PARAMETER

A reconstructed TU absorbing any of those inherits the failure whole, so it
compounds with the TU work rather than sitting still.

The shapes are separable by WHAT is volatile-qualified. MMIO qualifies the
pointed-to type, so a `*` follows it. A match hack qualifies an OBJECT --
`volatile int li;`, `volatile Vector3 v;`, the `(s32)(volatile s32)rsc`
round-trip, `Node *volatile arr[4]` where the pointer not the pointee is
volatile.

  VOLATILE = re.compile(r"\bvolatile\b(?![\s\w:]*\*)")

Files scoring a codegen trick: 655 -> 254. All 401 released were checked to be
MMIO-only, and the 254 kept still contain every match-hack form -- the negative
direction was measured, not assumed. CONVERTED 2,511 -> 2,568 functions
(22.20% -> 22.71%); no_codegen_trick 10,623 -> 11,020. Additions only.

2. tools/tiers_ratchet.py reported a TU promotion as a vanished file

A promotion consolidates N per-symbol src/_ZN....cpp files into the one
src/actors/X.cpp they always were; git records N deletions plus one addition,
and every one read as `GONE -- not a tracked source file any more`. Measured on
PR #1882 (tu/inline-dtor-order, 9c6396c): 90 of 90 backslid paths were TU
`legacy_source` entries whose TU is "status": "promoted" and whose
`promoted_source` exists on the branch. Zero were real deletions.

A GONE path is now resolved through the manifest (via tools/tu_manifest.py,
never the files) and reported as a MOVE naming the absorbing file and what that
file does with the five criteria.

A promotion is NOT free. The criteria are file-wide, so a clean function merged
into a file with one bad line loses its status, and that still exits 1. Only a
move into a file that itself passes all five is silent. In practice a promotion
lands in the failing case by construction -- a reconstructed TU must spell
_ZN7fBase_cnwEj, _ZN8dActor_cC2Ev and _ZN8dActor_cD2Ev directly or its range
will not link -- so no_mangled_refs can never pass for one. That is structural;
the answer is --update --reason, not exempting mangled refs.

Re-banked config/converted-baseline.json in the same commit: 1,957 -> 2,567,
+610 / -0. Set-diffed against HEAD to confirm zero removals.

tools/test_tiers.py pins both readings in both directions (29 tests) and
converted-ratchet.yml now runs it, plus watches tu_manifest.py and
config/tu_manifest.d/**.

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 11,048 / 11,347 (97.36%, +0)
Byte-verified code bytes 2,050,236 / 2,211,124 (92.72%, +0)
Claimed, not byte-verified 168 functions, 55,068 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 +10
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 446 symbol(s) exact, 242 partial, 15 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 168 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
andrewboudreau merged commit b6da506 into main Aug 30, 2026
5 of 6 checks passed
@andrewboudreau
andrewboudreau deleted the tools/tiers-mmio-and-tu-moves branch August 30, 2026 14:27
andrewboudreau added a commit that referenced this pull request Aug 30, 2026
Resolves three generated files that main regenerated wholesale:

- config/converted-baseline.json: REGENERATED with the post-#2007 tiers.py
  (tools/tiers_ratchet.py --update), not resolved by side or key union.
  Keeping the branch's copy would have banked 1951 and silently lowered the
  ratchet floor by ~600 entries with every gate green. New count 2559 =
  main's real current CONVERTED score (2565) minus the six per-symbol files
  this PR's ov070/daKpFr_c promotion absorbs.

- config/converted-backslide-exceptions.jsonl: the six daKpFr_c rows this
  branch already logged are kept as-is; two rows added for
  src/_ZN12daObjAbuku_cD0Ev.cpp and D1, which main's own ov002/daObjAbuku_c
  promotion (#1996) absorbed without re-banking. Both are classified
  "MOVED -- absorbed into src/actors/daObjAbuku_c.cpp" by the tool's own
  classify_missing(), and that file exists in this tree.

- notes/cpp-tu-current-state.md: regenerated with tools/cpp_tu_state.py
  --write-note; --check-note is clean.

No source file, delinks entry, or byte changed in this merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ
andrewboudreau added a commit that referenced this pull request Aug 30, 2026
Two conflicts, resolved without --ours/--theirs on either:

* attribution.json -- key UNION of the two override maps. Verified first
  that neither side removed a key from the merge base and that the 5
  keys both sides touched agree on their value, so the union is the
  whole 3-way merge, not a pick. 845 base -> 1011 ours + 852 theirs
  -> 1018.

* config/converted-baseline.json -- taken from main VERBATIM at 2567.
  NOT regenerated and NOT lowered. This branch's tree scores 2483 under
  the post-#2007 tools/tiers.py, so re-banking here would write 2483
  over main's 2567 and silently drop the floor. `--check` is therefore
  red on this branch, deliberately and loudly: all 78 backslides are
  `MOVED -- absorbed into ... by TU promotion`, zero deletions. See the
  PR discussion before re-banking.
andrewboudreau added a commit that referenced this pull request Aug 30, 2026
Carries origin/main through the restacked #2000. Same two generated files
conflicted and are resolved the same way:

- config/converted-baseline.json: REGENERATED with the post-#2007 tiers.py
  (tools/tiers_ratchet.py --update), never by side or key union. Count
  2552 = the parent branch's 2559 minus the seven per-symbol files this
  PR's ov070/daKrpa_c promotion absorbs. All seven are classified
  "MOVED -- absorbed into src/actors/daKrpa_c.cpp" by classify_missing(),
  that file exists in this tree, and all seven already had rows in
  config/converted-backslide-exceptions.jsonl from this branch's own
  earlier run, so no duplicate rows were added.

- notes/cpp-tu-current-state.md: regenerated with tools/cpp_tu_state.py
  --write-note; --check-note is clean.

No source file, delinks entry, or byte changed in this merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ
andrewboudreau added a commit that referenced this pull request Aug 30, 2026
…het on main (#2011)

Main has been out of sync since #1996 merged. `config/converted-baseline.json`
banked `src/_ZN12daObjAbuku_cD0Ev.cpp` and `src/_ZN12daObjAbuku_cD1Ev.cpp`,
which that PR deleted when it promoted the ov002/daObjAbuku_c translation unit.

Nobody's checks were wrong; they just never overlapped. #1996's green ran
against a base predating #2007's re-bank, which is what first banked those two
files. By the time #1996 merged, the paths it deletes were in the baseline it
never re-read. The next unrelated PR to touch src/** -- #1978, which has
nothing to do with ov002 -- is the one that went red.

The removal is legitimate and is banked with a reason, not reverted:

  MOVED -- absorbed into src/actors/daObjAbuku_c.cpp by TU promotion
  (ov002/daObjAbuku_c), which fails: No raw offset arithmetic; No unk_<off>
  fields; Calls things by real names, not mangled _Z

A reconstructed TU must spell vague-linkage symbols directly
(_ZN7fBase_cnwEj, _ZN8dActor_cC2Ev, _ZN8dActor_cD2Ev) or its range will not
link, so `no_mangled_refs` structurally cannot pass for an absorbing file.
Byte-match outranks readability. No source changed and no byte moved.

Also adds a `push: [main]` trigger. The workflow's design note claimed
staleness "only ever runs one way" -- permissive, never falsely red. TU
promotion is the exception, because it REMOVES banked paths, and the note now
says so. The trigger gates nothing (no branch protection; a red main is
advisory) but it attributes the breakage to the merge that caused it instead
of to whoever opens the next PR.

  tiers_ratchet --check        PASS  baseline 2565  current 2565
  pytest tools/test_tiers.py   29 passed
  check_dead_references        no new dead references


Claude-Session: https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Alberto12345678999 pushed a commit to Alberto12345678999/sm64ds-decomp that referenced this pull request Aug 30, 2026
…erences gate

tangosdev#2007 (b6da506) landed prose in tools/tiers_ratchet.py and
notes/converted-tier.md that names `src/actors/X.cpp` as a stand-in for
"whichever file the promotion absorbed the symbols into".

check_dead_references reads every repo-rooted path in prose as a real
reference, so it read the stand-in as a rename that missed the prose and
turned main red:

    FAIL: 2 prose reference(s) name a path that does not exist
      notes/converted-tier.md      names `src/actors/X.cpp`
      tools/tiers_ratchet.py       names `src/actors/X.cpp`

Spelling it `src/actors/<Class>.cpp` fixes it at the source rather than
suppressing it: GLOBBY already skips any reference containing `<>`, and a
metavariable is what the sentence meant. The sibling stand-in in the same
paragraphs, `src/_ZN....cpp`, has always been skipped for the same reason
(GLOBBY also matches `...`) -- this makes the two consistent.

The `src/actors/X.cpp` literals in tools/test_tiers.py are untouched: they
are fixture values in code, not prose, and the gate never read them.

  check_dead_references  no new dead references (133 unresolved, was 135)
  pytest tools/test_tiers.py   29 passed
  tiers_ratchet --check  PASS  baseline 2567  current 2567

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ
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