commit edc69e06f3adb3e28162278281e22517db860d06
parent 01aaff143e7d8d45b0bcb7a4bae87a05c5917211
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 16 Jun 2026 07:38:59 -0700
doc/plan/O1-PATTERNS.md: kit-vs-clang -O1 disasm audit — residual linear-budget opportunities (L1-L10)
Diffstat:
2 files changed, 539 insertions(+), 1 deletion(-)
diff --git a/doc/plan/O1-PATTERNS.md b/doc/plan/O1-PATTERNS.md
@@ -0,0 +1,535 @@
+# O1 code-quality patterns: the post-W1 disassembly catalog
+
+A curated catalog of the *residual* `-O1` vs clang `-O1` code-quality gaps, from a
+per-file disassembly audit of the ecosystem corpus **after the entire O1.md §3
+worklist (W1–W10) landed** (2026-06-16, aggregate `__TEXT` ≈ 1.09× clang). This
+is the successor to that worklist: it identifies what remains, ranks the
+linear/no-SSA wins still on the table, and cleanly separates them from the gaps
+that genuinely require the parked O2 SSA mid-end.
+
+Read [O1.md](O1.md) first — especially §3 (the landed worklist), §4 (the SSA/O2
+out-of-scope list), and §5 (already-landed). Read [OPT.md](OPT.md) §3 for the O1
+pipeline and pass inventory. Every "linear" claim here is checked against those.
+
+---
+
+## 1. Methodology
+
+**Harness.** `scripts/o1_quality.sh` builds the per-file kit (`.k.o`) and clang
+(`.c.o`) objects into `build/o1_quality/`; disassemble with
+`./build/release/kit objdump -d NAME.k.o`. Per-file ratio = kit `__TEXT` ÷ clang
+`__TEXT` (`size -m`). Instruction counts are real instruction lines (objdump
+format is `addr:\t bytes \t mnemonic operands` — the mnemonic is after the
+*second* tab; a naive `grep ':\t\w+'` matches the byte column and is wrong).
+
+**Corpus and current standings** (kit / clang `__TEXT`, all aarch64/Darwin `-O1`,
+release kit):
+
+| file | kit/clang | note |
+|---------|----------:|------|
+| lz4 | **0.38×** | clang unrolls/inlines → clang bigger (NOT a kit gap) |
+| yyjson | 0.86× | kit smaller (clang inlines far more) |
+| lapi | 0.83× | kit smaller |
+| tinyexpr| 0.96× | ~par |
+| lparser | 1.07× | ~par; the SIZE_MAX/udiv guard pushes it over |
+| cjson | 1.31× | aggregate/array zero-init dominates |
+| miniz | 1.67× | csel + frame traffic in the deflate loops |
+| sqlite | 1.11× | frame traffic + bit-tests + addr folds |
+| lvm | **2.86×** | the lua interpreter loop — still the worst |
+
+W1.1 is confirmed landed in these objects: `sub xN,x29,#k` spill-address
+recomputes are **0** in lvm (was 8,774) and sqlite (was 35,360); every spill slot
+is a one-instruction positive `ldr/str [x29,#k]`. So the residual gap is no longer
+*spill addressing* — it is the **spilling itself** (frame round-trips that a real
+RA would not emit) plus a family of local peepholes the no-SSA O1 still misses.
+
+**Rubric.** Each pattern is graded LINEAR (a same-block / per-instruction forward
+peephole, no SSA, no superlinear axis) · NEEDS-SSA-O2 (wants GVN/DSE/LICM/IV or a
+cross-block register allocator) · NOT-A-DEFICIENCY (clang bigger via inlining/
+unrolling, or kit already smaller). Linearity verdicts and host-pass claims were
+spot-checked against the live passes (`pass_combine.c:1914` compact rule,
+`aa_set_bytes` at `aa64/native.c:2577`, `aa_add_lsl`/`aa_ldst_regoff` emitters).
+
+**Spot-checks performed** (all reproduced against the current `build/o1_quality/`
+objects): lvm store→reload-different-register (1495 adjacent + 1962 within-4),
+`mov wN,wM` 330 vs clang 5; cjson `strb` 338 with a 64-long consecutive run;
+yyjson double-cset 573 triples and cset→cbnz 438. No high-impact pattern was
+dropped on spot-check (one was *down*graded — see §4).
+
+---
+
+## 2. Linear / n-log-n O1 opportunities
+
+The actionable shortlist, ranked by (impact × confidence × cross-file breadth).
+This is the section that matters.
+
+### L1 — Store-to-load forwarding across a register mismatch ★ highest leverage
+
+**The gap.** After a spilled def is homed (`str rX,[slot]`) the O1 reload at the
+next use targets a *fresh rotating scratch* (`ldr rY,[slot]`, `rY != rX`), so the
+value round-trips through memory even though `rX` still holds it. The existing
+adjacent compaction (`opt_combine_compact_block`, `pass_combine.c:1934`) collapses
+the `str;ldr` pair **only when `same_reg_operand(store.src, load.dst)`** (rX==rY);
+the dominant different-register case falls straight through to `bl->insts[w++]`
+and survives.
+
+```
+kit (lvm _luaV_execute): clang keeps the bytecode word live:
+ str w10, [x29, #512] lsr x8, x28, #7
+ ldr w9, [x29, #512] ; w9 != w10 add x8, x21, x8, lsl #4
+ and w10, w9, #0xff ubfx w9, w28, #7, #8
+ str w10, [x29, #512] ldr x10, [x9]
+ ldr w9, [x29, #512] ; again (no stack round-trip at all)
+```
+
+**Where / frequency.** lvm: **1495** adjacent `str;ldr` same-slot different-reg
+(vs 79 same-reg already handled), 1962 within a 4-insn window; concentrated in
+`luaV_execute`. lz4: 191 intra-block store-then-adjacent-reload pairs (the two
+giant funcs). sqlite: ~8,500 store-then-reload-within-4. miniz: 1103 near same-slot
+store→reload. **The single biggest cross-file contributor to the ldr/str excess**
+(kit lvm ldr 4709 vs clang 731, str 3813 vs 214).
+
+**Impact.** Each forwarded reload becomes a `mov rY,rX`, which the existing copy
+substitution then folds into rY's uses (often deleting the `mov`); when rX is dead
+and the slot has no other reader, the store is dead too (→ W8 stack-DSE + mir_dce).
+Conservatively ~1400 insns in lvm alone (~9% of the object), with real wins in lz4
+(~380) / sqlite (intra-block subset) / miniz.
+
+**Host pass + sketch.** `pass_combine.c`, `opt_combine_compact_block`. In the
+`prev==IR_STORE && in==IR_LOAD && same_spill_slot_and_size` branch, drop the
+`same_reg_operand` requirement: when the registers differ, rewrite the load
+in-place to an `IR_COPY rY <- rX` instead of keeping it as a memory load. The
+existing copy substitution / `mir_dce` retire the copy and (if rX dead + slot
+unread) the store. The store-DSE half is W8, already present.
+
+**Stays linear:** single forward pass per block with the bounded last-store
+tracking the compact pass already does; no new analysis, no cross-block reasoning.
+Correctness boundary: only when no intervening clobber of rX, no aliasing memory
+write, and no redefinition of the slot between str and ldr — exactly the adjacency
+the compact pass already guarantees for the same-reg case.
+
+> Note vs O1.md: this is the natural extension of the **W8** machinery, not a new
+> pass. W8 today finds 0 sites because W2/DCE clear the store-store shape upstream;
+> the store→load-forward-on-mismatch shape is a *different* and very live pattern
+> that no landed item touches.
+
+### L2 — Fuse `cmp; cset rD,cc; cbnz/cbz rD` → `cmp; b.cc` (boolean-into-branch) ★ broadest
+
+**The gap.** For `if (relational) goto L`, kit materializes the condition into a
+GPR (`cset rD,cc`) and then re-tests it (`cbnz/cbz rD`), where clang branches
+directly off the flags. The original `cmp`'s NZCV already encodes the branch; the
+`cset` is the sole user and dies at the branch.
+
+```
+kit (yyjson _yyjson_arr_iter_init): clang:
+ cmp w8, #0 cmp w8, #0
+ cset w12, eq b.eq 0x604 (or cbz w8,0x604)
+ cbnz w12, 0x604
+```
+
+**Where / frequency — pervasive.** yyjson **438**, lvm 19 (→54 once L1 removes the
+intervening homing), lapi 27, lparser 5, lz4 10, sqlite (part of the cbnz +2,440
+delta). clang emits ~0 of these. This is the most cross-file pattern in the
+catalog.
+
+**Impact.** ~2 insns per site where a redundant cmp-against-0 also precedes the
+cset (yyjson's "double-cset", see L4), else 1 insn (the cset). yyjson ~880 insns
+(~2.1% of `__TEXT`); lapi ~27; broad small wins elsewhere.
+
+**Host pass + sketch.** `pass_combine.c` (or `pass_jump.c`, which already owns
+`IR_CMP_BRANCH`/`IR_CONDBR` terminators). When a block terminator is `IR_CONDBR`
+whose condition register's most-recent same-block def is a single-use `IR_CMP`,
+fuse into `IR_CMP_BRANCH` carrying the cmp's `CmpOp`+operands (invert for the
+cbz/`==0` case) and NOP the `IR_CMP`. This is the inverse of the fusion
+`cg_ir_lower.c:941` already performs in the other direction; reuse the `CombineCtx`
+`last_def` map + the existing hard-live use counts.
+
+**Stays linear:** same-block last-def lookup + single-use check, both already
+computed; no SSA.
+
+### L3 — Widen aggregate/`memset` zero-init (one `strb` per byte → wide stores) ★ cjson/miniz
+
+**The gap.** `aa_set_bytes` (the AGG_SET / memset expander, `aa64/native.c:2577`)
+emits exactly one `strb` per byte in a flat `0..size` loop, even for an aligned
+whole-struct/array zero. A 64-byte zero is 64 `strb`; clang does it in 3–4
+(`movi v0.16b,#0; stp q0,q0,...`) or, scalarized, `str xzr` runs.
+
+```
+kit (cjson _cJSON_PrintPreallocated): clang:
+ add x8, x29, #16 movi v0.16b, #0
+ movz x9, 0x0 stp q0, q0, [sp, #32]
+ strb w9, [x8] stp q0, q0, [sp]
+ strb w9, [x8, #1] ; 64 bytes in 3 insns
+ ... (64 consecutive strb)
+```
+
+**Where / frequency.** cjson: **338 `strb`, a 64-long consecutive run** confirmed
+(296 of 338 are zero-byte stores from this path; clang has 30 strb total).
+Hottest in `_cJSON_PrintPreallocated`/`_PrintBuffered` (64 each) and `_print_number`
+(26). miniz: large `strb` surplus from PNG/struct buffers.
+
+**Impact.** Largest single win in cjson — ~250–300 insns / ~25% of cjson's 1236-
+insn gap. Even a scalar widening (`str xzr` for 8-aligned runs, `str wzr`/`strh
+wzr`/`strb wzr` tail) without NEON captures most of it.
+
+**Host pass + sketch.** `aa64/native.c` `aa_set_bytes`: rewrite the per-byte loop
+to emit the widest aligned store covering the remaining run — `stp q`/`str q` if
+NEON is permitted, else `str xzr`, `str wzr`, `strh wzr`, `strb wzr` tail; or `bl
+_memset` above a size threshold. Pure per-call expansion, no analysis. The hardware
+zero register is already used for stored zeros (`pass_native_emit.c`).
+
+**Stays linear:** it *removes* emitted instructions from a single expander; no new
+pass. (rv64/x64 set_bytes should get the same widening for parity, but aa64 is the
+measured win.)
+
+### L4 — Drop the redundant boolean re-normalize (`cset; cmp rD,#0; cset`) — yyjson
+
+**The gap.** kit lowers `(a==b)` to `cmp; cset rD,eq` (a 0/1 bool), then a
+surrounding `bool != 0` / `if(bool)` context lowers to a **second** `cmp rD,#0;
+cset rD,ne` that re-normalizes an already-0/1 value. The outer cmp+cset are a
+no-op.
+
+```
+kit (yyjson _unsafe_yyjson_is_raw): clang (at each inline site):
+ cmp w8, #1 cmp w8, #1
+ cset w8, eq cset w0, eq
+ cmp w8, #0 ; redundant
+ cset w8, ne ; w8 == w8
+```
+
+**Where / frequency.** yyjson: **573** exact `cset; cmp #0; cset` triples (0 in
+clang); dense in the `is_*`/`get_*` predicate family. (lapi/sqlite have the related
+single-cset-of-a-bool but the triple is yyjson's signature.)
+
+**Impact.** ~2 insns each → ~1100 insns / ~2.6% of yyjson `__TEXT`. Combines with
+L2: many of these feed a branch, so the L2 fusion subsumes the outer half.
+
+**Host pass + sketch.** `pass_combine.c` same-block forward peephole: when an
+`IR_CMP`'s source operand's most-recent same-block def is itself an `IR_CMP`
+(0/1 bool) and the comparison is against imm 0/1 with eq/ne, drop the outer
+`IR_CMP` and rewrite its uses to the inner result (inverting for the `#0`/ne case).
+Uses the existing `last_def` map + hard-live use counts.
+
+**Stays linear:** single-block, single-use, checkable from existing state.
+
+### L5 — Single-use register copy not coalesced into its consumer
+
+**The gap.** kit produces a value into one register then `mov`s it into the
+register its sole consumer (a `sxtw`/store/call-arg/return) reads, instead of
+producing directly into the final register. The cross-block coalescer W3 cannot
+retire these because the destinations are multiply-defined physical pregs (W3's
+`opt_copy_cleanup` only fires when `ndef[dst]==1`); the per-block `mir_combine`
+substitution doesn't reach all the consumer paths.
+
+```
+kit (lvm): kit (lapi return):
+ ldrb w13, [x8, #11] and w12, w12, #0x7
+ mov w12, w13 ; redundant mov w0, w12 ; redundant
+ sxtw x19, w12 ; sxtw x19,w13 ok ret ; and w0,w8,#0x7 ok
+```
+
+**Where / frequency.** Reg-to-reg `mov wN,wM`: lvm 330 vs clang 5; lapi 512 vs 194;
+lparser 948 vs 541; miniz 1018 of 3203 are mov-of-prev-insn-dest; yyjson 209 feed a
+uxtb + 59 are return copies (`mov w0,wN` before ret). The single largest `mov`
+contributor across the value-heavy files.
+
+**Impact.** Several hundred insns across lapi+lparser+miniz+yyjson+lvm (conservative
+after excluding genuinely class-crossing or still-live moves).
+
+**Host pass + sketch.** `pass_combine.c` post-RA peephole using the existing
+`CombineCtx` liveness (`ctx_def_changed_since`, `count_uses_in_live_range`): for
+`mov rD,rS` where rD has a single use in its live range before its next redef and
+rS is not redefined between the mov and that use, forward rS to the use and drop
+the mov. Extend the consumer set beyond what `try_substitute`/`try_sink` reach
+today: convert/extend, store-value, call-arg, and `IR_RET` value operands. The
+`IR_RET`→return-reg back-propagation mirrors the existing redundant-second-copy
+handling at `pass_combine.c:~890`.
+
+**Stays linear:** the `CombineCtx` machinery already answers both guards; same-block.
+
+### L6 — Drop the redundant `sxtb`/`sxtw` after a same-width extending load
+
+**The gap.** Two shapes. (a) yyjson loads a signed byte as `ldrsb xD,[addr]` (full
+64-bit sign-extend) then emits `sxtb wE,xD` for a W-domain consumer — but xD's low
+32 bits are *already* the sign-extended byte, so the `sxtb` is a no-op (and clang
+just does `ldrsb wD`). (b) lvm/sqlite `sxtw` of a value sourced from a zero-extending
+`ldrb`/`ldrh`: kit loads always zero-extend, so a 0..255 byte widens identically
+signed or unsigned → the `sxtw` is a same-width copy.
+
+```
+kit (yyjson _yyjson_mut_ptr_getx): clang:
+ ldrsb x12, [x21] ldrsb w9, [x13]
+ sxtb w8, x12 ; x12<31:0> == byte cmp w9, #34
+ cmp w8, #47
+```
+
+**Where / frequency.** yyjson: 112 `ldrsb x..; sxtb w..` + 74 `ldrb; sxtb`. lvm:
+the `ldrb;mov;sxtw` triple subset (~6 tight + more across the body). sqlite: part of
+the sxtw +2,582 (the ldrb/ldrh-rooted subset). This is the signed mirror of the
+already-landed ZEXT-of-load fold (OPT.md §3 / commit 8826982d).
+
+**Impact.** yyjson ~186 insns; lvm ~30–80 from the byte-rooted cases; sqlite a slice
+of the sxtw delta. Medium confidence on the broad count, high on the exact-shape
+subset.
+
+**Host pass + sketch.** `pass_combine.c` `combine_exts` (already folds ext-of-ext):
+a `SXTB`/`SXTH`/`SXTW` whose source's most-recent same-block def is a sign-extending
+load of equal-or-smaller width, *or* a zero-extending narrow load whose value can't
+have the sign bit set in the widened position, is redundant → drop it (rewrite uses
+to the load dst). Folding `ldrb;sxtb`→`ldrsb` needs a load-with-extend MemAccess
+rider (noted as structurally needed in prior O0 work); the pure
+`sxtb`-after-`ldrsb`/`sxtw`-after-`ldrb` *drop* needs no rider and is the easy win.
+
+**Stays linear:** per-instruction, same-block last-def; no SSA.
+
+### L7 — Fold a single-use shift into the consuming ALU op (`lsl;add` → `add ...,lsl #k`)
+
+**The gap.** kit folds shifts into *memory* operands (`try_addr_synth` handles ISHL
+into an indirect index scale) but not into general ALU consumers. So pointer
+scaling whose sum is stored/compared (not a load address) emits `lsl xT,xS,#k; add
+xD,xB,xT` where clang uses the one-instruction shifted-register form.
+
+```
+kit (lapi _index2value): clang:
+ sxtw x12, w1 sxtw x21, w1 ; once
+ lsl x12, x12, #4 add x8, x8, x21, lsl #4
+ add x8, x13, x12
+```
+
+**Where / frequency.** lvm 14 `lsl;add` (+`lsl;sub`); miniz 105 `lsl`-feeds-
+add/orr/eor/sub (clang folds 74); lapi ~15–25; sqlite part of the lsl +1,335 delta.
+kit emits ~0 shifted-register ALU ops; clang uses them heavily. Strongest in miniz's
+LZ hash (`eor ...,lsl #5`) and byte-pack code.
+
+**Impact.** miniz ~105 insns; lvm/lapi ~20–30 each; plus a freed scratch that
+relieves the spill pressure feeding L1.
+
+**Host pass + sketch.** `pass_combine.c` recognition + `aa64/native.c` emission.
+Mark a single-use `IR_BINOP SHL reg,imm` (sh 1..4) feeding an `IADD`/`ISUB`/AND/ORR/
+EOR and emit the fused shifted-register form. The emitter exists (`aa_add_lsl`,
+`native.c:750`; the ISA exposes `AA64_FMT_LOG_SR`/`ADDSUB_SR`) — it just isn't
+reachable for non-memory consumers. Needs a shift rider on the register operand (the
+current `Operand` only carries `log2_scale` inside the `ind` memory variant). x64 can
+use this only for IADD (LEA scale); rv64 has no shifted-add → skip there.
+
+**Stays linear:** local single-use peephole, same live-range guards as the existing
+ISHL-into-EA path. Medium confidence (needs the operand-model shift-rider).
+
+### L8 — Fold a `sxtw`/`uxtw` index into the load/store addressing mode
+
+**The gap.** For signed-int array indexing kit emits `sxtw` to widen the index, a
+separate `add ...,lsl #scale` to form the address, then a plain `[reg]` access —
+three instructions. aa64 has `[Xbase, Wm, sxtw #scale]` as a single addressing mode
+(`aa_ldst_regoff`, `native.c:600`); clang uses it everywhere, kit zero times.
+
+```
+kit (miniz): clang:
+ sxtw x14, w12 ldr w8, [x9, w8, sxtw #2]
+ add x9, x0, x14, lsl #2 strh w9, [x26, w13, sxtw #1]
+ ldrh w20, [x9]
+```
+
+**Where / frequency.** miniz 15 `sxtw;add...,lsl` feeding a load/store (clang 98
+extended-reg memory operands, kit 0). sqlite **546** `sxtw` immediately feeding a
+load/store offset. lapi/lparser index math. Big total across sqlite+miniz.
+
+**Impact.** sqlite ~546 insns (the standalone sxtw folds into the access); miniz
+~100–200 (removes sxtw + the address-forming add). LINEAR slice of the larger
+sxtw story (the cross-block redundant *re*-extend is NEEDS-SSA-O2, see §3).
+
+**Host pass + sketch.** `pass_addr_fold.c` already builds `OPK_INDIRECT` with
+index + `log2_scale` but does not absorb a `sxtw`/`uxtw` of the index. Add an
+extend-into-index rule: fold a single-use `sxtw`/`uxtw` producer of an index operand
+into the indirect operand, recording the extend kind, and emit `aa_ldst_regoff_v`
+with the extend in `aa64/native.c`. The scaled-index infra + REGOFF encoding exist.
+
+**Stays linear:** per-instruction forward fold, existing addr-fold machinery.
+
+### L9 — Constant-operand integer divide/multiply not folded
+
+**The gap.** kit materializes two constants and issues a runtime `udiv`/`sdiv`/`mul`
+for fully compile-time-constant expressions (classically `ArraySize = sizeof/elem`,
+and the Lua `MAX_SIZET/sizeof(T) >= LIMIT` growvector guard). The guard's SIZE_MAX
+takes a 4-insn `movz/movk` chain into a register, then `udiv` + `cmp` + `b.hi`;
+clang decides the always-true compare and materializes only the `LIMIT` immediate.
+
+```
+kit (sqlite _sqlite3_status64): clang:
+ movz x9, 0x50 cmp w0, #0xa
+ movz x10, 0x8
+ udiv x8, x9, x10 ; = 10 at compile time
+ cmp w20, w8
+```
+
+**Where / frequency.** sqlite 108 div/mul sites with both operands freshly `movz`'d.
+lparser **22 udiv** (12 udiv-by-1, 10 the SIZE_MAX guard); lapi 9 sdiv; tinyexpr 5.
+The SIZE_MAX guard is the dominant reason lparser is *larger* than clang.
+
+**Impact.** sqlite ~300–400 insns (each site collapses movz+movz+udiv+mov). lparser
+~80–90 (the guard is ~7–8 insns → 1 movz at ~10 sites + the udiv-by-1). Each udiv is
+also the slowest integer op, so a latency win too.
+
+**Host pass + sketch.** Two pieces. (a) `pass_simplify.c`: add a same-block forward
+`LOAD_IMM`→known-value tracker (per-block reg→value map, invalidated on redef/block
+boundary — no SSA, consecutive within-block) and feed it into `operand_const` so the
+existing `BO_UDIV`/`BO_SDIV` folds and a new const-op-const fold fire when the
+constant lives in a register rather than an inline `OPK_IMM`. The resulting `LOAD_IMM`
+then lets `simplify_cmp` decide the always-true guard. (b) The divide-by-1 →
+copy/sub falls out of (a).
+
+**Stays linear:** a bounded per-block reg map + the existing folds. Strength-reduction
+of *non-constant* divide-by-constant (magic multiply, miniz adler32) is a separate,
+also-linear `pass_lower.c` item but lower priority (~25 sites, medium confidence).
+
+### L10 — Frame elision on no-spill leaf functions
+
+**The gap.** kit emits a full `stp x29,x30,[sp,#-16]!; add x29,sp,#0 ... ldp` frame
+in functions that are leaves (no call), spill nothing, and never reference `[x29]` —
+i.e. need no frame. clang omits it entirely; for a one-op accessor the frame is the
+majority of the function.
+
+```
+kit (tinyexpr _add): clang:
+ stp x29, x30, [sp, #-16]! fadd d0, d0, d1
+ add x29, sp, #0 ret
+ fadd d0, d0, d1
+ ldp x29, x30, [sp], #16
+ ret
+```
+
+**Where / frequency.** yyjson 90 frameless-able leaves; lapi ~21; lparser ~10;
+tinyexpr 8. The `is_*`/`get_*`/cast accessor family. (Caveat: many of these are
+exactly the predicates clang *inlines away*, so the realizable win depends on
+whether kit also inlines them — the frame-elision is independently correct for the
+leaves that remain.)
+
+**Impact.** ~3 insns/function (stp+add+ldp). yyjson ~360, lapi ~63, lparser ~30,
+tinyexpr ~24. Medium confidence on realizable total (the inlining caveat).
+
+**Host pass + sketch.** `aa64/native.c` prologue/epilogue (NativeFrame): a
+per-function `needs_frame` flag = false when no `IR_CALL`, no spill slots, no
+escaping `IR_ADDR_OF(local)`, no varargs. All known at frame-finalize time in one
+pass; emit the stp/add/ldp only when set. x29 already sits at frame bottom (W1.1), so
+omission is straightforward.
+
+**Stays linear:** single per-function check, no analysis.
+
+---
+
+## 3. Needs SSA / the O2 mid-end
+
+Real gaps, but none is a linear no-SSA peephole — each maps to a parked O2 item
+(OPT.md §3 schedule, `pass_o2.c`/`pass_ssa.c`; O1.md §4 out-of-scope list).
+
+- **Register-resident locals across blocks/calls (the over-spilling itself).** The
+ *largest* remaining gap on the big functions: kit's frame traffic is ~6× clang's
+ on sqlite (62,708 vs 10,412), ~4.3× on lz4, dominant in lvm/miniz. clang keeps
+ loop-carried accumulators/pointers in callee-saved regs across the whole live
+ range; kit's no-SSA linear-scan allocator (no splitting, falls to memory residency
+ in big bodies) cannot. **L1 (store→load forwarding) is the linear slice that
+ reaches the intra-block subset; the cross-block residency needs the SSA RA with
+ live-range splitting + global copy propagation.** → O2 register allocator + GVN.
+- **csel / csinc / ccmp selection (if-conversion).** kit emits **0** csel/csinc/ccmp;
+ clang uses them heavily (lvm 72+19, miniz 218+34, sqlite 1,264+436, cjson 32, lz4
+ 499). Every `x = cond ? a : b`, MZ_MIN/MAX clamp, and double→int saturate becomes a
+ compare+branch diamond with the selected value threaded through the frame (feeding
+ the spill gap). Doing it well needs a select op in the IR + a profitability/legality
+ gate that proves both arms side-effect-free and reasons about the merge of two
+ definitions — the SSA/O2 domain. (A narrow peephole for the exact
+ `cmp;b.cc;mov-imm;b;mov-imm` diamond could fit `pass_jump.c` but is fragile and
+ lower-value than the §2 items — *flag, do not prioritize*.) → O2 if-conversion.
+- **`tbz`/`tbnz` from `and #(1<<k); cbz/cbnz`.** sqlite has 715 `and wN,#bit; cbz/cbnz`
+ that fold to a single `tbz/tbnz`; clang uses tbz/tbnz 3,102 times, kit 0. The
+ *narrow* single-bit-AND-feeding-an-adjacent-branch case is arguably LINEAR (a
+ `pass_native_emit`/`pass_jump` peephole dropping the `and` and emitting tbz/tbnz);
+ it is listed here rather than §2 because the broader csel/ccmp family it sits inside
+ is SSA-shaped, and the narrow slice should be validated before committing. → mostly
+ O2, with a possible linear `tbz` slice worth a spike.
+- **Cross-block redundant re-extension / re-materialization.** Repeated `sxtw rX,wY`
+ of an unchanged source across blocks/calls (sqlite ~173 conservatively measured;
+ more in reality). L6/L8 handle the same-block and addressing-mode-foldable cases;
+ the cross-block redundant *re*-extend needs GVN / cross-block copy propagation. → O2 GVN.
+- **FP constant folded at the wrong (narrower) width.** tinyexpr's `NAN = 0.0/0.0` /
+ `INFINITY = 1.0/0.0` fold to a *float* NaN/Inf, materialize into a GPR, then `fcvt
+ d,s` to widen (12 sites, +24 insns). Folding at the *result* width avoids the
+ widen, but correctness needs care that float→double of the folded constant is the
+ same value (canonical NaN payload), so it is a width-selection fix in the
+ constant-fold path, not a trivial peephole. Marked NLOGN/needs-care. → folder fix
+ (cg_ir_lower.c), schedule with care, low priority.
+
+---
+
+## 4. Not a deficiency
+
+Where clang is bigger via inlining/unrolling, or kit is already smaller — so future
+readers don't chase these.
+
+- **clang inlines/unrolls far more at `-O1`.** lz4 (kit **0.38×** clang) and yyjson
+ (0.86×), lapi (0.83×) are *smaller* in kit because clang inlines aggressively
+ (yyjson kit bl=942 vs clang 294; lz4 `LZ4_compress_generic`/`LZ4_count`/`LZ4_hash5`
+ fully inlined into every caller). Every clang-heavy mnemonic on those files
+ (movz/cmp/add/sub/movk/csel/madd/strh/ldrh) is an inline-site materialization/
+ selection artifact, **not a kit gap**. Tuning kit's inline-pressure cap (W4, landed)
+ *upward* would increase total `__TEXT`, not decrease it — net size impact
+ neutral-to-negative.
+- **`ret` / function-count deltas are inlining shape.** lparser kit 96 funcs / 96 ret
+ vs clang 24 / 20; kit emits exactly one ret per function (optimal per-function, no
+ duplicated epilogues). tinyexpr `blr` 16 vs clang `br` (tail-call dispatch after
+ inlining). These belong to the parked O2 inliner/tail-call work.
+- **NEON / `.inst` columns.** clang's hundreds of `.inst` are `umulh`/`smulh`
+ magic-multiply divides, `movi` vector zeroing, and `stp q` — vectorization/strength-
+ reduction kit doesn't do. The zero-init half is recoverable as **L3** (scalar
+ widening); the magic-multiply half is the optional L9 strength-reduction tail.
+- **`cbz` deltas favoring clang.** cjson `cbz` is −107 (kit 208 vs clang 315) — kit is
+ *not* over-emitting branches here; do not read the raw cbz delta as a kit gap.
+
+**Downgraded on spot-check:** the lz4/miniz "address bases spilled and reloaded" and
+"large struct-field address rebuilt per use" items the per-file agents rated high
+were largely the *cross-block* residency problem (NEEDS-SSA-O2, §3), not a clean
+linear peephole — only their strictly-intra-block, same-base-unchanged subset is
+linear, and that subset is already covered by **L1** (store→load forwarding) and the
+landed **W1a** (frame-address sub/add-CSE) + **W5** (local CSE). They are not given
+separate §2 entries to avoid double-counting.
+
+---
+
+## 5. Suggested next O1 work
+
+In the style of O1.md's worklist — ranked by impact × confidence × breadth, each a
+linear no-SSA change with a clear host pass and a `test/opt/` structural guard.
+
+1. **P1 — store→load forwarding on register mismatch** (`pass_combine.c`,
+ `opt_combine_compact_block`). Highest single lever (lvm ~1400 insns, plus lz4/
+ sqlite/miniz intra-block). Drop the `same_reg_operand` requirement in the
+ store;load branch; emit a copy on mismatch, let substitution + W8 + mir_dce clean
+ up. Guard: assert the str;ldr-different-reg pattern is gone on an lvm fixture.
+2. **P2 — `cset;cbnz/cbz` → `b.cc` fusion** (`pass_combine.c`/`pass_jump.c`). Broadest
+ cross-file win (yyjson 438, lapi 27, lvm/lz4/sqlite). Fuse `IR_CONDBR` of a
+ single-use `IR_CMP` into `IR_CMP_BRANCH`; inverse of the existing cg_ir_lower fusion.
+3. **P3 — widen `aa_set_bytes` zero-init** (`aa64/native.c`). ~25% of cjson's gap;
+ helps miniz. Emit `str xzr`/`stp q0` aligned runs (or `bl _memset`) instead of
+ per-byte `strb`. Self-contained expander change.
+4. **P4 — drop yyjson double-cset + redundant `sxtb`-after-`ldrsb`** (`pass_combine.c`
+ / `combine_exts`). ~1100 + ~186 insns on yyjson. Same-block last-def peepholes;
+ the sxtb-drop is the signed mirror of the landed ZEXT-of-load fold.
+5. **P5 — constant-operand div/mul fold + same-block `LOAD_IMM` const tracker**
+ (`pass_simplify.c`). Fixes lparser being larger than clang (the SIZE_MAX guard) +
+ sqlite's 108 const-divide sites. A bounded per-block reg→value map feeding the
+ existing BO_UDIV/SDIV folds.
+6. **P6 — single-use copy coalescing into convert/store/call-arg/return consumers**
+ (`pass_combine.c`) and **`sxtw`-index-into-addressing-mode** (`pass_addr_fold.c`).
+ The `mov` and standalone-`sxtw` surplus across the value-heavy files; both reuse
+ existing CombineCtx/addr-fold machinery.
+
+**Definition of done per item:** measurable `__TEXT`/instruction reduction on the
+affected corpus files (via `scripts/o1_quality.sh`), sqlite `-O1` compile time
+unchanged (linearity held — the §8 synthetic sweep), all correctness gates green
+(`make test-opt test-toy`, ecosystem `-O0`/`-O1` vs clang, smoke-x64/rv64), and a
+`test/opt/` structural disasm guard added.
+
+> Sequencing note: P1 + P3 are the two biggest size levers and touch disjoint files
+> (`pass_combine.c` vs `aa64/native.c`) → parallelizable. P2/P4/P5/P6 share
+> `pass_combine.c`/`pass_simplify.c` → one owner, land + re-measure one at a time
+> (gate = correctness, not byte-identity). The true ceiling on lvm/sqlite/miniz/lz4
+> remains the SSA register allocator (§3) — these linear items close the *local*
+> waste, not the over-spilling.
diff --git a/doc/plan/O1.md b/doc/plan/O1.md
@@ -71,7 +71,10 @@ diff text/mix across the corpus) — it was ad-hoc for this analysis.
> sqlite 35,360 → 0; lvm `__TEXT` 96,788 → 59,276, **4.66× → 2.86×**; sqlite
> 1.27× → 1.11×; miniz 1.94× → 1.67×; the clang-favored lz4/yyjson held at
> 0.38×/0.86×). sqlite `-O1` compile stayed ≈1.6→1.67s (linear; W3 the main
-> contributor). The pre-campaign analysis below is retained for context.
+> contributor). The pre-campaign analysis below is retained for context. **The
+> residual gap is now catalogued in [O1-PATTERNS.md](O1-PATTERNS.md)** — a
+> kit-vs-clang disassembly audit of what remains, ranking the further linear/
+> no-SSA wins (L1–L10) and separating them from the parked-O2 SSA gaps.
**[Pre-campaign] Aggregate `__TEXT` ≈ 1.25× clang `-O1`** across the corpus — but
the ratio is bimodal and the aggregate understates the truth: