commit c80d7eb9de3126085252647b5738adf8977cc63b
parent cb0a4c2eaa613a365372d11eeaf259f237a92435
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 13 Jun 2026 01:28:22 -0700
doc(perf): redesign plan to slim the vtable stack + measured first-pass results (PERF-TCC-SLIM.md)
The structural map for closing the tcc gap by slimming (not removing) the
layered backend: CgTarget + NativeTarget stay real vtables, MCEmitter is
monomorphic and devirtualized. Includes the measured outcomes of the first
implementation pass (D6/F1/F2 = -8.9M, byte-identical) and the finding that the
per-operation micro-levers land 1-2 orders below estimate -- the gap needs the
codesize/coalescing redesign, not per-op slimming.
Diffstat:
1 file changed, 391 insertions(+), 0 deletions(-)
diff --git a/doc/plan/PERF-TCC-SLIM.md b/doc/plan/PERF-TCC-SLIM.md
@@ -0,0 +1,391 @@
+# Slimming the vtable stack to close the tcc gap — a redesign plan
+
+Companion to `PERF.md` (running standings) and `PERF-TCC-GAP.md` (structural
+diagnosis). Those establish *where* the 2.22 B vs 0.66 B (3.35×) instruction gap
+lives and *why*. This doc is the **redesign**: a concrete, ranked, dependency-ordered
+campaign to slim every layer of the pipeline so each does tcc-spare work per
+operation — **keeping the layered architecture, not deleting it.**
+
+The brief that scoped this plan: *keep the vtable stack, but slim it down
+significantly to match tcc's more spare construction; structural wins, 3× fewer
+instructions.* Written after a six-strand code-path audit (lex+pp+intern, parse+
+sema+cg_adapter, the CG value-stack/type machinery, NativeDirectTarget, the
+CgTarget seam, MCEmitter+objwrite).
+
+---
+
+## 1. The shape we drive toward — keep the layers, remove the redundancy
+
+The architecture is the asset; the per-operation **work** is the liability. tcc is
+fast because each source byte is touched ~once, each token is an `int`, each
+operator is a `vtop` adjustment plus ≤1 four-byte store, and there is **no second
+copy of the program anywhere**. kit pays for pluggability (multiple backends share
+one frontend through the `CgTarget` seam) and for a typed value model — and those
+are worth keeping. What is *not* worth keeping is the redundant work piled on top:
+re-interning every token, re-decoding every type 8–12× per statement, copying a
+56 B value node and a 48 B location descriptor per operand, and crossing **three**
+indirect-call layers per emitted primitive where one would do.
+
+The single most important structural finding — and where an early framing had to
+be corrected: **not all three "layers" are real vtables.** Two are genuinely
+polymorphic and earn their indirection; one is monomorphic and the indirection is
+pure overhead.
+
+- **CgTarget** is polymorphic — `NativeDirectTarget`, the opt IR recorder,
+ `c_target`, `wasm`, and `check` each install one (`NativeDirectTarget` *is-a*
+ `CgTarget`, `native_direct_target.c:112`). One indirect call at the seam. **Keep.**
+- **NativeTarget** is polymorphic **per arch** — separate impls in
+ `src/arch/{aa64,x64,riscv}/native.c` (+ `check_target.c`). NDT is the
+ arch-*neutral* -O0 driver and holds `NativeTarget* native`
+ (`native_direct_target.h:114`); `nd_binop → d->native->binop` is a **real arch
+ dispatch**, not redundant. It is not removable without compile-time
+ monomorphizing NDT per arch (a much larger change, out of scope). **Keep the
+ vtable.** What *is* removable here is the **48 B `NativeLoc` marshalling
+ round-trip** (NDT already resolved the operand to a `Reg`; it wraps it in a
+ NativeLoc, the arch immediately unwraps it back to a register number) and the
+ **number of crossings** per statement — copies and counts, **not** the indirect
+ call.
+- **MCEmitter** is **monomorphic** — `mc.h:10` states it outright (*"One generic
+ implementation in `src/arch/mc.c` serves every machine-code arch"*), and only
+ `mc.c` ever assigns the slots; `mc_new` is the sole constructor. All 16 function
+ pointers point at the single `m_*` function. **386 call sites** pay an indirect
+ call for zero polymorphism. **This vtable should not exist** — devirtualize it.
+
+So "keep the vtable stack, slim it" resolves to:
+
+| Layer | Polymorphic? | Decision |
+|---|---|---|
+| **CgTarget** | yes (NDT / opt-recorder / c_target / wasm / check) | **KEEP** — one vtable, untouched signatures |
+| **NativeTarget** | yes — **per arch** (aa64 / x64 / rv64 / check) | **KEEP** — NDT correctly holds it; slim the `NativeLoc` marshalling + crossing count, not the dispatch |
+| **MCEmitter** | **no — one impl (`mc.c`)** | **DEVIRTUALIZE** — `static inline` the hot base-only ops, direct-call the rest, delete the 16 pointers |
+
+At -O0 the descent becomes **one indirect call (CgTarget seam) + one indirect arch
+dispatch (NativeTarget, real) + one inline byte store (MCEmitter, devirtualized)**,
+instead of three indirect calls + a `NativeLoc` marshalling round-trip. Two real
+vtables survive; the fake one and the copies do not.
+
+---
+
+## 2. The honest arithmetic — what this gets us, and what it does not
+
+Summing the strands' own (deliberately conservative) per-track estimates, with
+overlaps de-duplicated:
+
+| phase | now | est. after campaign | residual ratio vs tcc |
+|---|--:|--:|--:|
+| lex + pp | 0.97 B | ~0.74 B | ~2.1× |
+| parse + sema + CG-drive | 0.87 B | ~0.54 B | ~2.2× |
+| native emit + objwrite | 0.37 B | ~0.24 B | ~4.8× |
+| **total** | **2.22 B** | **~1.52 B** | **~2.3×** |
+
+The emit-phase number is deliberately conservative: keeping the `NativeTarget`
+per-arch dispatch (it is real, §1) means the indirection savings there come only
+from devirtualizing the monomorphic MCEmitter (F2) and slimming marshalling (F3) —
+not from collapsing the whole descent. The bulk of the remaining emit headroom is
+the **o0-codesize track** (fewer emitted bytes), which is out of this plan's scope
+but compounds with it (below).
+
+**This campaign, fully landed, credibly takes kit from 3.35× to ~2.3× tcc — it
+does not reach parity by itself.** That is the truth the plan must own. Closing
+the last ~2× requires two things this plan *sets up* but does not *finish*:
+
+1. **The o0-codesize track (Track B) compounding.** kit emits ~2× tcc's machine
+ bytes; that inflates emit, objwrite, and assemble *multiplicatively*. Track B
+ has already taken the object 3.45×→1.71× tcc; every further byte removed
+ shrinks the 0.37 B emit phase below the per-op wins counted above. The two
+ tracks multiply, they don't add.
+2. **Re-profiling after each landing.** The phase shares shift as work is removed;
+ the highest lever moves. Treat the estimates as a starting order, not a budget
+ — re-measure on RELEASE best-of-7 after every shippable step and re-rank.
+
+The destination beyond this plan is `PERF-TCC-GAP.md` Track 4 (tcc's single-pass
+data-flow shape: a shared mutable token slot, symbols resolved through pointers on
+the interned token, a thin value stack the parser drives by calling emit directly
+into the final section image). The moves below are independently shippable,
+byte-identical-gated steps **toward** that shape — several of them (the contiguous
+section buffer, the binding-on-Sym cache, the intern-once cache) are literally the
+first bricks of Track 4.
+
+---
+
+## 3. Foundational shared changes — do these first, they unblock multiple tracks
+
+Three changes are depended on by more than one strand. Land them first so the
+per-phase tracks build on them and nobody double-implements.
+
+### F1. One packed `{size, align, regclass}` memo on the type id
+
+**The highest shared-leverage change in the whole plan.** Today `abi_cg_type_info`
+(`src/abi/abi.c:27`) is an *uncached* recursive switch with alias/enum chasing;
+`abi_cg_sizeof`/`abi_cg_alignof` (`abi.c:90-95`) each call it independently, and it
+is hit several times per statement from **all three** downstream layers:
+
+- parser: `pcg_mem`/`pcg_sizeof`/`pcg_alignof` on every load/store/convert
+ (`cg_adapter.c:9-39`, 17 sites) — and `pcg_mem` calls size+align *twice*;
+- cg: `api_alloc_temp_local` calls sizeof **and** alignof per temp
+ (`value.c:225-226`), `api_mem_for_lvalue` per memop (`value.c:246-247`);
+- NDT: `nd_type_mem` calls size+align per native frame load/store
+ (`native_direct_target.c:477-479`), plus the arch's own `loc_is_64`/`type_size32`
+ re-query yet again (`aa64/native.c:349,374`).
+
+Only the 1-byte `api_type_class` is memoized today (`type.c:289-307`, with a CACHED
+bit + builtin fast path). **Extend exactly that proven mechanism** to a packed
+`{u32 size, u16 align, u8 regclass}` word on `CgApiType`, primed for builtins in
+`cg_api_init_builtins` and filled lazily for user types. Route `abi_cg_sizeof`/
+`abi_cg_alignof`/`abi_cg_type_info` and the parser's `pcg_*` through it; collapse
+the double-call sites to one read.
+
+- **Payoff:** ~0.08–0.12 B across the parse+sema phase **and** part of the emit
+ phase (it serves NDT too). Single biggest middle-phase lever.
+- **Gate:** byte-identical (pure memoization of a deterministic function).
+- **Owner:** cg strand (lives in `type.c`/`abi.c`). Consumed by parser + NDT —
+ they must *not* keep their own parallel size/align paths after this lands.
+
+### F2. Devirtualize MCEmitter (the fake vtable) — direct calls + `static inline` hot path
+
+**MCEmitter is monomorphic** (`mc.h:10`; only `mc.c` assigns slots; `mc_new` is the
+sole constructor) — its 16 function pointers are pure indirection across **386 call
+sites** with zero polymorphism. Convert:
+
+- **`static inline` in mc.h** the 3 hot, base-only ops — they touch only the
+ *public* `MCEmitter` base struct (`cur_bytes`, `loc`, `obj`; `mc.h:49-65`):
+ `emit_bytes` (**155 sites** — `mc.c:298`, just `buf_write`/`obj_write`, this is
+ tcc's `o()`), `pos` (70 sites), `set_loc` (3 sites). These three cover 228/386
+ sites. While here, cache a `{u8* cur; u8* end;}` cursor on the base so
+ `emit_bytes`/a new `mc_emit32`/`mc_emit_words` do `if (cur<end){ store; cur+=n; }
+ else slow()` — bounds-test + store + bump, no staging, no memcpy.
+- **Direct extern call** the other 13 — they need `MCImpl` private state
+ (labels/fdes) or are cold: un-`static` the `m_*` bodies, declare `mc_*` in mc.h,
+ rewrite `mc->X(mc,…)` → `mc_X(mc,…)`. Sites: `emit_reloc_at` 55, `label_new`/
+ `label_place` 27/27, `emit_label_ref` 20, `cfi_*` 32, `set_section` 6,
+ `emit_align`/`fill`/`label_data_reloc`/`reloc`/`destroy` ~8.
+- **Delete the 16 pointers** from `struct MCEmitter` (−128 B).
+
+This removes an indirect call **plus its caller-save churn** from every emitted
+instruction. It is the one genuinely-redundant indirection of the three layers.
+
+- **Payoff:** ~5–10 M from the call-shape change + the inline cursor; larger in
+ practice because `emit_bytes` is the single highest-frequency call in the backend
+ (one+ per emitted word). It is also the **sink F3 lands in.**
+- **Gate:** byte-identical (same functions, same order; only call shape changes).
+- **Owner:** MCEmitter strand. The cursor is shared with D5's section-buffer
+ decision — settle the storage type once.
+
+### F3. Slim the NDT ↔ NativeTarget seam — kill the `NativeLoc` round-trip, batch crossings (NOT the dispatch)
+
+`NativeTarget` is a **real per-arch vtable** — keep it. The waste is the
+marshalling and the crossing *count*, not the indirect call. NDT already resolves
+operands to a `Reg` (`nd_dst_reg`/`nd_materialize_operand`), then wraps each in a
+48 B `NativeLoc`, passes it by value, and the arch immediately unwraps it to a
+register number (`aa64/native.c:2288`). Two moves, both keeping `d->native->X`:
+
+1. **Narrow register-only entry points** — `binop_rr(NativeTarget*, BinOp, RegLoc
+ dst, RegLoc a, RegLoc b)` with a 12 B `RegLoc`, for the post-materialization
+ ops (the contract already guarantees they receive only `NATIVE_LOC_REG`,
+ `native_target.h:484-488`). Keep the fat `binop` for the opt replay path
+ (`opt.c:990-1018`, unchanged).
+2. **Batch** the cold frame-operand case into one fused NT call (`binop_mem`)
+ instead of `load`+`load`+`binop`, so a cold `a+b` is one crossing not three.
+
+- **Payoff:** ~3–5 % of the emit phase from the marshalling (48 B→12 B × 3/op,
+ retired stores at IPC 3.7), more on the batched cold path.
+- **Gate:** byte-identical for the narrow-hook refactor; run-correctness +
+ determinism for the x64 mem-operand fused form (different instruction selection).
+- **Owner:** NDT strand + per-arch backends (one additive `NativeTarget` change).
+- **Multi-backend safety:** additive hooks only; opt/-O1 keeps the fat vtable, gate
+ the -O0 fast paths on `opt_level == 0`. C-source/Wasm never touch NDT.
+
+---
+
+## 4. Track A — lex + pp + intern (0.97 B)
+
+The binding/resolution model is **already tcc-shaped** — macros (`MacroTab`) and
+keywords (`KwTab`) are O(1) `Sym`-indexed loads via `KIT_SYMTAB` (the Round-8
+sym-centric work). Do **not** rebuild resolution. The gap is *production-side*
+intern-once and copy density.
+
+| # | Change | Payoff | Gate | Notes |
+|---|---|--:|---|---|
+| A1 | **Per-lexer last-spelling intern cache** + promote the punct cache to the PP layer so includes don't re-intern. Real C is enormously repetitive at the token level; a 16–64 entry direct-mapped cache keyed on `(first_byte,len)` catches the dominant fraction before `pool_intern_slice` (`lex.c:594,573,784,679,1029`; `pool.c:120`). | −0.06…0.10 B | byte-identical (full byte-compare on hit; never cache digraphs) | biggest lex+pp lever; `pool_intern_slice` is 14% of self-time |
+| A2 | **Self-sufficient interner probe slot** — put `{hash, sym}` (optionally `+len+SSO prefix`) in the table slot so a probe rejects without the random `entries[sym]` load (`pool.c:139-142`). `{hash,sym}` is the safe floor; SSO width needs an A/B (PERF.md's "inline-prefix" dead-end was *entry-side*, not slot-side — the distinction matters). | −0.02…0.05 B | byte-identical | complements A1 (A1 = hits, A2 = misses/probes) |
+| A3 | **`pp_next_into(Pp*, Tok*)` out-pointer** mirroring `pp_next_raw_into`; collapse the 3 sret copies in `pp_next`→`fetch_tok`→`advance`. | −0.02…0.04 B | byte-identical | bounded (pull-fusion was ~5%); co-owned with the parser strand (it owns `fetch_tok`/`advance`/`p->cur`) |
+| A4 | **`Tok` 24→16 B** via an inline-packed 32-bit position (NOT a side table — the side-table LocId attempt was the documented regression), lazy `(file,line,col)` decode only at diagnostics + `pcg_set_loc`. | −0.03…0.06 B **or neutral** | **byte-identical, gate HARD** (DWARF, `-E` line markers, `__LINE__`/`#line`, diag-across-splice) | **highest-risk item.** Needs a genuinely O(1) decode helper at the warm `pcg_set_loc` site (34 expr sites) or it repeats history. Do **after** A1–A3 + jointly with the parser strand |
+| A5 | **Kill the manufactured `TOK_NEWLINE`** on the cc path (fold BOL/space into the next token's flags + a line counter; `-E` keeps a newline-emitting variant) and **skip the macro-body pre-copy** for paste-free bodies (`has_paste` flag set at definition; replay immutable body directly — `pp_expand.c:374,633`). | −0.03…0.07 B | byte-identical (`-E` newline placement; macro corpus) | lower priority; BOL/directive/invocation semantics are subtle |
+
+**Track A subtotal: ~0.16–0.32 B.**
+
+---
+
+## 5. Track B — parse + sema + cg_adapter (0.87 B)
+
+No re-introduced superlinear axis was found (scope_lookup, declare_function,
+typedef/global intern, make_local_aligned are all O(1)-amortized). The gap is pure
+constant factor: redundant type queries, struct zeroing, and stack-copy bandwidth.
+
+| # | Change | Payoff | Gate | Notes |
+|---|---|--:|---|---|
+| B1 | **Route `pcg_mem`/`pcg_sizeof`/`pcg_alignof` through F1's memo** instead of raw `kit_cg_type_size`/`align` cross-API calls; build `KitCgMemAccess` from one `c_abi_type_info` call (`cg_adapter.c:9-39`). Halves the per-memop type work. | ~3–6% of phase | byte-identical | depends on **F1**; keep volatile read inline |
+| B2 | **Lazy/right-sized `PcgLvAux` clear** — most pushes are rvalues that never read aux; clear the 32 B aux only when a slot is tagged lvalue, drop the 5 explicit `pad[]` writes, use designated-init (`cg_adapter.c:41-51,497,678,993`). Attacks the 8% `memset`. | ~4–7% of phase | byte-identical (audit aux readers behind `was_lvalue`/lvalue tag) | parser-side, independent |
+| B3 | **Shrink `PcgSlot` 48→~24 B** (bit-field/subobject riders off-node) **in lockstep with cg's `ApiSValue` shrink (C3)**; memoize `{rank,signed,bits}` in a 24-entry `TypeKind` table so integer usual-arithmetic-conversions read array slots, not 6 hashmap probes (`cg_adapter.h:65`, `cint_common_type` 613-627). | ~3–5% of phase | low (table) / medium (slot split — bitfield+member corpus) | **co-own with cg strand** — the two stacks are kept symmetric (`cg_adapter.h:8`) |
+| B4 | **Classify the leading keyword once** into a `CKw` then `switch`, instead of a ~13-deep `is_kw` chain in `parse_stmt`/`parse_unary` (`parse_stmt.c:885-959`). tcc's `switch(tok)`. | ~1–3% of phase | very low (pure control-flow) | independent |
+| B5 | **Cache the resolved binding on the interned `Sym`** (parser-side `KIT_SYMTAB(BindingTab, SymEntry*)` with push/restore on scope enter/exit) so `scope_lookup` becomes one pointer load instead of an N-scope hash walk (`parse.c:369-380`). tcc's `sym_identifier`. | ~5–10% of phase (depth-dependent) | medium-high (save/restore + shadow/redef discipline; full corpus) | parser-side, **no pool change needed** — coordinate with intern strand only if you prefer the on-Sym layout |
+| B6 | **Hoist `pcg_convert`'s `src==dst` early-out** above its size/tid queries (`cg_adapter.c:882-892`). | ~1–2% of phase | very low | stacks with B1 |
+
+**Track B subtotal: ~0.17–0.33 B** (some also visible in `-fsyntax-only`).
+
+---
+
+## 6. Track C — the CG value-stack + type machinery (part of 0.87 B)
+
+The CgTarget seam sits **above** all this, so even `-fsyntax-only` pays it. Note
+the **load-bearing caveat** resolved in §8: the fold ladder is a deliberate -O0
+peephole, not dead weight — slim the *decision*, keep the *effect*.
+
+| # | Change | Payoff | Gate | Notes |
+|---|---|--:|---|---|
+| C1 | **F1 applied inside CG** — collapse `api_alloc_temp_local`'s double size+align decode and `api_mem_for_lvalue` to single memo reads. | ~0.05–0.10 B (counted in F1) | byte-identical | this *is* F1's CG-side consumption |
+| C2 | **Single type-classify per binop** — stamp `{foldable-width, is_int, is_float}` on the value node at `api_push` (alongside the existing cached `wide_kind`), then read once in `api_cg_binop`; pass the decoded width into `api_can_delay_int_arith`/`api_try_strength_reduce`/`api_try_collapse_binop_identity` so they skip re-deriving it. Removes 6–9 alias-chases per integer binop (`arith.c:29-107`). Also drop the 4 i128/wide64 stack probes per op in `kit_cg_int_binop` (`arith.c:1103-1121`) for the i32/i64-native common case. | ~0.04–0.08 B | low (cached width must match `api_int_like_width`'s int-vs-int-like distinction exactly) | **the correct form of "slim the fold ladder"** — see §8 |
+| C3 | **Shrink `ApiSValue` 56→~24 B** — move `bitfield` (12 B) + `source_local` off-node behind a sparse rider keyed by slot (PLACEs are the minority); make `pinned` a transient local in `dup` (its only reader). Lockstep with B3. | ~0.02–0.06 B | medium (riders must survive dup/swap/rot3/pop; bitfield + source-local corpus) | PERF Round 7 found the *delayed*-payload shrink neutral — do this for **copy bandwidth**, measure before committing; most invasive, land after C1/C2 |
+| C4 | **Designated-init the per-op `memset`s** in `api_alloc_temp_local` / `api_mem_for_lvalue` (the idiom `api_op_imm` already uses, `value.c:22-25`). | ~0.01–0.025 B | very low | stacks with C1 |
+
+**Track C subtotal: ~0.07–0.18 B net new** (excl. the F1 portion already counted).
+
+---
+
+## 7. Track D — NDT register discipline + descriptor + emit/objwrite
+
+| # | Change | Payoff | Gate | Notes |
+|---|---|--:|---|---|
+| D1 | **F2** (MCEmitter devirtualization — direct/inline the monomorphic byte sink). | counted in F2 | byte-identical | the genuine indirection win; highest-frequency call in the backend |
+| D2 | **Replace the LRU register-cache with a fixed round-robin write-back cache** — drop `use_tick`/`last_use`/`reg_last_use`/`nd_touch_local` and the linear `nd_pick_cache_victim` scan; keep the consumer-resident write-back mechanism the recent commits (f75874cb/3516b3d3) built — swap only the *victim policy* (`native_direct_target.c:516-930`). | ~5–10% of emit phase | **run-correctness + determinism** (changes reg selection → different bytes; compile sqlite twice + `cmp`, run `84|2`, toy/parse/smoke) | coarser eviction may cost a few emitted bytes — measure both ways |
+| D3 | **F3** (slim the NDT↔NativeTarget marshalling: `NativeLoc` 48 B → 12 B `RegLoc` register-only hooks + batch the cold frame-operand crossings; `native_target.h:234,258,484-488`). Keep the per-arch dispatch — it is real. | counted in F3 | byte-identical (narrow hooks) / run-correctness (x64 fused mem-operand) | coordinate the `NativeTarget` struct change once across archs + the opt path |
+| D4 | **NDT reads F1's memo** for `nd_type_mem`, and carries `is64`/`size_log2` on the `NativeLoc` pad so the arch's `loc_is_64`/`type_size32` stop re-querying (`aa64/native.c:349,374`). | ~0.02 B | byte-identical | depends on **F1** |
+| D5 | **Zero-copy section write** — `buf_write_to_writer(Buf*, Writer*)` walks chunks straight to the Writer, eliminating the `buf_flatten` temp-buffer second copy of all machine code (`macho/emit.c:814-829`, `elf/emit.c:730-742`). Stretch: a single contiguous geometric section buffer so `mc_emit32` stores into the final image (full tcc shape) — larger lift, shared cursor with F3. | ~4–8 M (chunk-walk) up to ~8 M (contiguous) | byte-identical (`test-elf test-macho test-link`) | decide the buffer type once with F3 |
+| D6 | **Index relocations by section** at `obj_finalize` (stable bucketing) to kill the O(n_sections × n_relocs) rescan in object write (`obj.c:1121-1128`, `macho/emit.c:538-550`, `elf/emit.c:468-479`). **A latent superlinear axis** — grows with `-ffunction-sections`/literal fan-out. | tens of M on section-heavy objects | byte-identical (stable buckets preserve order) | independent; a "bug" by PERF.md's rule |
+| D7 | **Single-pass symbol-table emission** + chunk-walk the string table (drop the recount pass + the strtab flatten, `macho/emit.c:392-534,854-857`); add the rv64 `if (mc->debug)` guard `aa_emit32` already has (`riscv/native.c:81`); `mc_emit_words` for multi-word idioms (F3). | ~0.01–0.02 B | byte-identical | mop-up, all in the 7× phase |
+
+**Track D subtotal: ~0.16–0.27 B** (incl. the F2 portion).
+
+---
+
+## 8. The one resolved conflict — do NOT gate the fold ladder "off"
+
+The CgTarget strand proposed gating the -O0 fold/strength-reduce/identity ladder
+off; the CG strand flagged this as a **trap**, and it is right:
+
+> The ladder at `arith.c:48-99` is **not a no-op at -O0.** It const-folds
+> immediates, strength-reduces, and collapses identities — which *reduces emitted
+> machine bytes*. The o0-codesize track (Track B in the standings, 3.45×→1.71×
+> object) **depends on it.** Gating it off would change emitted bytes, fail the
+> byte-identity gate, and **regress the worst-ratio emit phase** — moving backward
+> on the exact phase with the most headroom.
+
+**Resolution:** keep the ladder's *byte-producing effects*; slim only the
+*decision cost*. That is **C2** (classify once, drop the delay-node bookkeeping
+whose product is never read at -O0), not "turn it off." Any future "fuse the seam"
+work must preserve the ladder's output and the `untyped_values` bitcast-elision
+flag (`arith.c:276`). This is the one place a naive implementation shoots itself in
+the foot — flagged here so it doesn't.
+
+---
+
+## 9. Sequencing & dependency graph
+
+```
+WAVE 0 (foundations, byte-identical, unblock everything):
+ F1 (type memo) ──> B1, C1, C4, D4
+ F2 (MCEmitter devirt + inline cursor) [the fake-vtable removal; F3's sink]
+ D6 (reloc index, independent superlinear fix)
+
+WAVE 1 (high-leverage, mostly byte-identical):
+ F3/D3 (NDT↔NativeTarget marshalling slim) [lands in F2's inline emit]
+ C2 (classify-once binop) [independent of F1, but co-measure]
+ A1 (intern last-spelling cache)
+ B2 (PcgLvAux right-size), B4 (kw switch), B6 (convert early-out)
+ D5 (zero-copy section write), D7 (symtab/emit mop-up)
+
+WAVE 2 (medium risk, lockstep pairs):
+ B3 + C3 (PcgSlot + ApiSValue shrink — MUST land together, symmetric stacks)
+ A2 (probe slot), A3 (pp_next_into — co-own fetch_tok with parser)
+ D2 (round-robin cache — switches to run-correctness gate)
+
+WAVE 3 (highest risk, gate hard, do last & jointly):
+ A4 (Tok 16B — needs O(1) decode at pcg_set_loc, joint lex+parser)
+ B5 (binding-on-Sym cache)
+ A5 (TOK_NEWLINE / macro pre-copy)
+ D5-stretch (contiguous section buffer → emit into final image)
+```
+
+Re-profile (RELEASE best-of-7, `tmp/projects/sqlite-amalg/sqlite3.c`) after **each
+wave** and re-rank — the phase shares shift as work is removed.
+
+---
+
+## 10. Gate strategy
+
+- **Byte-identical (the default):** every change that restructures *how* work is
+ done without changing emitted bytes — F1, F2, F3, A1–A5, B1–B6, C1–C4, D1, D3,
+ D4, D5, D6, D7. Use `scripts/perf_identity_gate.sh` (60-category golden-vs-candidate;
+ `-E` text, `-g` DWARF, objects, diagnostics). Lexer changes (A1–A5) additionally
+ need the line-splice battery + a diagnostic line across a splice.
+- **Run-correctness + determinism (only where bytes legitimately change):** D2
+ (round-robin register selection) and the x64 fused mem-operand sub-case of F3/D3
+ (the register-only narrow hooks stay byte-identical). Compile sqlite twice and `cmp` the objects
+ (self-identical across runs), plus `make test-toy test-parse test-smoke-x64
+ test-smoke-rv64` and the sqlite end-to-end (`84|2`).
+- **A/B before committing the uncertain ones:** A2 (SSO width), A4 (decode cost),
+ C3 (Round 7 found the analogous shrink neutral). Build them, measure on RELEASE,
+ keep only if positive — the same discipline that caught the de-intern and
+ 16 B-Tok dead ends.
+- **Measure on RELEASE** (`make bin RELEASE=1`, `PROFILE=1` for sampling), never
+ the ASan default. Instructions via `/usr/bin/time -l` best-of-7; trust
+ instructions over wall/cycles.
+
+---
+
+## 10b. Measured results (first implementation pass)
+
+Wave 0 + the reloc fix were implemented and measured on `sqlite3.c -c`
+(best-of-7, instruction metric is deterministic to ±0.2M on a fixed binary;
+each change byte-identical via the 60-category gate). **The actuals came in 1–2
+orders of magnitude below §2's estimates** — the estimates assumed the hot paths
+were fat, but they were already tight:
+
+| change | estimate | **measured** | note |
+|---|--:|--:|---|
+| D6 (reloc index) | tens of M | **−5.6M** | also a real superlinear fix; grows with section count |
+| F1 (type memo) | ~80–120M | **−2.1M** | `cg_type_get` already memoized size/align on the `CgType`; only the alias/enum recursion + double-calls remained |
+| F2 (MCEmitter devirt) | ~5–10M | **−1.2M** | the indirect calls were single-target → branch-predicted to near-free; the win is removing the fn-ptr load |
+| **cumulative D6+F1+F2** | | **−8.9M (−0.41%)** | 2160.7M → 2151.8M |
+
+Two findings worth keeping:
+
+- **F2 must be a direct call, not `static inline`.** Inlining the hot
+ `emit_bytes`/`pos` into the per-arch leaf emitters bloats them enough to break
+ the inliner's own cascade (e.g. `aa_emit32` stops being inlined), a measured
+ **net +21M** regression. Direct extern call removes the indirection without
+ that effect.
+- **The metric is deterministic, not noisy** — so these small deltas are real,
+ and a per-operand tracking pass added elsewhere shows up immediately (a
+ concurrent in-tree `-O0` transient-liveness/coalescing WIP was measured adding
+ ~+26M to `-fsyntax-only` while disabled, which is the actual high-value lever
+ in this subsystem).
+
+**Implication for the remaining micro-levers (F3, the rest of Track A/B/C):**
+they will each land in the single-digit-M range for substantial churn. The
+instruction gap to tcc (~1500M) is **not** closable by per-operation slimming;
+it needs the **codesize track** (fewer emitted bytes → less emit/objwrite/
+assemble, the multiplicative lever) and the value-stack/coalescing redesign.
+Treat the micro-levers as cleanups that compound, not as the path to parity.
+
+## 11. What this is not
+
+- **Not Track 4.** This keeps the typed value model and the seam; it does not adopt
+ tcc's shared-mutable-token-slot single pass. It builds toward it (A1/A4/B5/D5 are
+ the first bricks) but stops short.
+- **Not the codesize track.** Track B (o0-codesize) shrinks emitted *bytes* and
+ must run in parallel — it compounds with §3–§7 multiplicatively and is most of
+ the remaining gap to parity after this plan (§2).
+- **Not parity by itself.** ~3.35× → ~2.3× is the credible reach of this campaign;
+ parity needs codesize compounding + iteration. Stated plainly so the goal stays
+ honest.