docs: the // @symbol marker convention for promoted TUs - #2065
Merged
Conversation
tools/tiers.py scores a promoted TU per member, and `// @symbol` is the only
thing that tells it where one member ends. Without a marker score_member falls
back to scoring that member against the ENTIRE FILE, so one volatile object,
one unk_ field or one mangled call anywhere in the TU strips the tier from
every clean member in it. The convention is load-bearing and was documented
nowhere.
Adds section 6 to notes/tu-promotion-conventions.md and a reviewer-checklist
item. Measured against this tree:
src/actors/daObjKinokoTag_c.cpp 9 members 7 markers 6 banked
src/actors/daObjFm_Battan_c.cpp 9 members 7 markers 5 banked
src/actors/daObjKm3_Kurumajiku_c.cpp 5 members 3 markers 4 banked
src/actors/daEyBm_c.cpp 13 members 0 markers 2 banked
The two banked daEyBm_c members are D1/D0, rescued by the second fallback --
the inline `virtual ~daEyBm_c() {}` at include/daEyBm_c.h:54.
Also corrects a stale line: #2004 closed and Kurumajiku landed via #2057, so
that class is precedent now, not a draft data point.
Documentation-only. No tool, gate, config, ledger or src/ file is touched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ
✅ PR validation — Passednoverify: no source/build-data changes in this PR Each changed |
Two corrections to the section, both measured rather than reasoned. 1. The section told authors to mark ctor/dtor variants "anyway". No landed promoted TU does: daObjKinokoTag_c, daObjFm_Battan_c, daObjKm3_Kurumajiku_c and daEyBm_c mark zero structors between them. In an inline-destructor TU there is nothing to mark -- D1 and D0 have no definition text in the .cpp at all -- and a bare marker placed elsewhere is actively harmful, since the slice runs from it to the next marker and would charge the following member's text to the destructor. Inlining is also usually deliberate: out of line, mwcc emits the synthesized D0 ahead of the written D1, reversing cartridge order, and linkcheck refuses a TU whose licensed .text is not in ROM address order. The rule is now: do not mark them, keep the inline definition in a directly included header, and check the fragment survives. 2. The #2064 bullet said its unmarked func_ov006_0210a534 "cannot be rescued by a boundary anyway". Measured, the omission does cost -- just not there. With no marker after it, the preceding func_ov006_0210a600 fragment runs to end of file and swallows a534's volatile body, so an eight-byte 'return 1;' can never score readable. Rewritten to make the general point: the member that pays is rarely the member you left unmarked. Reviewer checklist item 7 updated to match. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ
This was referenced Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Documents the
// @symbolmarker convention for genuine-TU promotions. Adds section 6 tonotes/tu-promotion-conventions.mdplus one reviewer-checklist item.This is documentation-only. One file changed, 146 lines added. No tool, gate, workflow, header,
src/file, config or ledger is touched.Why
tools/tiers.pyscores a promoted TU per member, and// @symbolis the only thing that tells it where one member's text ends and the next begins.score_member(tools/tiers.py:406) says so in its own docstring:So one
volatileobject, oneunk_<off>field or one mangled call anywhere in a promoted TU strips the tier from every clean member in it. The convention was load-bearing and documented nowhere.Measured evidence
Line references, all in
tools/tiers.pyonmain@9bfcd5d86:SYMBOL = re.compile(r"//\s*@symbol\s+(\S+)")_marked_member_fragment— slices marker-to-next-marker,return Noneiflen(matches) != 1_balanced_lifecycle_fragment_lifecycle_member_fragment— the ctor/dtor second pathscore_file— the five criteriascore_member— marker, then lifecycle, then whole file_code_only— comments blanked before scoringMarker counts on the landed promoted TUs, scored with
tools/tiers.pyagainst this tree:@symbolmarkersconverted-baseline.jsonsrc/actors/daObjKinokoTag_c.cppsrc/actors/daObjFm_Battan_c.cppsrc/actors/daObjKm3_Kurumajiku_c.cppsrc/actors/daEyBm_c.cppIn the first three, the unmarked members are exactly the two destructor variants, covered by the lifecycle path.
daEyBm_cmarks nothing: eleven of thirteen members fall to the whole-file fallback and all eleven fail the same two criteria —no_unk_fieldandno_mangled_refs— which are properties of the factory and ofBehavior, not of the members being scored. Inserting the eleven marker lines locally and changing nothing else lifts it from 2 to 4 members at 5/5. Its two surviving members are D1/D0, rescued by the inlinevirtual ~daEyBm_c() {}atinclude/daEyBm_c.h:54.The cost, on an open PR
daObjHatenaSwitch_c, thirteen functions) adds zero markers. Its diff movesconfig/converted-baseline.jsonfrom"count": 2568to"count": 2560— 8 credits — and adds 8 rows toconfig/converted-backslide-exceptions.jsonl, one per lost member, all with the same reason: the TU "needs the volatile stack value to avoid a 17-word codegen mismatch". Thevolatileis in one member; the other eight paid for it.dScMgSingle3DBase_c, nine functions) adds 6 markers and 0 backslide-exception rows. Both are cited in the note as the worked pair, and flagged there as open rather than as landed precedent.Two things the code settled
score_memberoverwritesreal_name(from the symbol) andshared_header(from the whole file) after scoring the fragment, so onlyno_raw_offset,no_unk_field,no_codegen_trickandno_mangled_refsare at risk.shared_headeris also not inCRITERIA(line 180) at all — it is reported alongside the tier, not part of it.extern "C"declaration written just above a function is charged to the preceding member. Text above the first marker belongs to no member.daObjKinokoTag_cgets this right (its ABI-seam block sits above the first marker on line 40);daEyBm_cdoes not. That placement rule is now written down.Also in this PR
One stale line corrected: the note said #2004 (
daObjKm3_Kurumajiku_c) was an open draft and "not precedent". #2004 is closed and that class landed via #2057, so the file is onmain. Added a dated*Update.*under the original paragraph rather than rewriting it, so the section-2 measurements taken while it was a draft stay readable.Gates
python tools/check_dead_references.py—no new dead references,no broken markdown links, exit 0.🤖 Generated with Claude Code
https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ