Skip to content

docs: the // @symbol marker convention for promoted TUs - #2065

Merged
andrewboudreau merged 2 commits into
mainfrom
docs/symbol-marker-convention
Aug 31, 2026
Merged

docs: the // @symbol marker convention for promoted TUs#2065
andrewboudreau merged 2 commits into
mainfrom
docs/symbol-marker-convention

Conversation

@andrewboudreau

Copy link
Copy Markdown
Collaborator

Documents the // @symbol marker convention for genuine-TU promotions. Adds section 6 to notes/tu-promotion-conventions.md plus 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.py scores a promoted TU per member, and // @symbol is 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:

Hand-written members use exact @symbol boundaries. Compiler-generated ctor/dtor variants use the inline lifecycle definition in a directly included class header. Anything without either form of evidence is scored against the entire file, preserving the old conservative behavior.

So one volatile object, one unk_<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.py on main @ 9bfcd5d86:

what where
SYMBOL = re.compile(r"//\s*@symbol\s+(\S+)") line 74
_marked_member_fragment — slices marker-to-next-marker, return None if len(matches) != 1 line 309
_balanced_lifecycle_fragment line 328
_lifecycle_member_fragment — the ctor/dtor second path line 378
score_file — the five criteria line 295
score_member — marker, then lifecycle, then whole file line 406
_code_only — comments blanked before scoring line 198

Marker counts on the landed promoted TUs, scored with tools/tiers.py against this tree:

promoted TU members @symbol markers 5/5 banked in converted-baseline.json
src/actors/daObjKinokoTag_c.cpp 9 7 6 6
src/actors/daObjFm_Battan_c.cpp 9 7 5 5
src/actors/daObjKm3_Kurumajiku_c.cpp 5 3 4 4
src/actors/daEyBm_c.cpp 13 0 2 2

In the first three, the unmarked members are exactly the two destructor variants, covered by the lifecycle path. daEyBm_c marks nothing: eleven of thirteen members fall to the whole-file fallback and all eleven fail the same two criteria — no_unk_field and no_mangled_refs — which are properties of the factory and of Behavior, 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 inline virtual ~daEyBm_c() {} at include/daEyBm_c.h:54.

The cost, on an open PR

Two things the code settled

  • The whole-file fallback poisons four criteria, not all of them. score_member overwrites real_name (from the symbol) and shared_header (from the whole file) after scoring the fragment, so only no_raw_offset, no_unk_field, no_codegen_trick and no_mangled_refs are at risk. shared_header is also not in CRITERIA (line 180) at all — it is reported alongside the tier, not part of it.
  • A member's slice runs to the next marker, so an extern "C" declaration written just above a function is charged to the preceding member. Text above the first marker belongs to no member. daObjKinokoTag_c gets this right (its ABI-seam block sits above the first marker on line 40); daEyBm_c does 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 on main. 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.pyno new dead references, no broken markdown links, exit 0.
  • Committed blob has zero CR bytes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ

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
@tangos-validator

tangos-validator Bot commented Aug 31, 2026

Copy link
Copy Markdown

✅ PR validation — Passed

noverify: no source/build-data changes in this PR

Each changed src/*.c|*.cpp is compiled and its relocated bytes compared to the binary data on a private build box. Passing requires every changed file to reproduce the ROM byte-for-byte with correct relocation targets — this catches WRONG-DEST relocations and non-reproducing near-misses that ledger-scoped linkcheck skips.

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
@andrewboudreau
andrewboudreau merged commit ce4b614 into main Aug 31, 2026
4 checks passed
@andrewboudreau
andrewboudreau deleted the docs/symbol-marker-convention branch August 31, 2026 04:14
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.

2 participants