commit f203f822edbed3bc9c9ee38095985e542bdd43db
parent 830bc7b70276572fcda41678e495c9606ef69fe4
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 16 Jun 2026 19:31:21 -0700
doc/plan: clean up roadmaps — retire shipped/landed docs, promote durable design
Triaged all 29 doc/plan/ roadmaps. Plan docs whose work has fully landed are
deleted after their durable "as-built" design moves up to the design set;
genuinely-open roadmaps stay; speculative not-yet-started sketches move to
doc/ideas/.
Deleted (design moved up first):
- O1.md, O1-PATTERNS.md -> doc/OPT.md + doc/plan/OPTIMIZER.md (§3/§4)
- PERF.md -> doc/BENCHMARKING.md (new; methodology only)
- CG-TYPES.md, CG-TYPE-DEBUG-SPLIT.md -> doc/CODEGEN.md ("Type system")
- CG-STACK-API.md, LEX-PP-API.md -> doc/FRONTENDS.md
- FRONTEND-SHAPE.md (abandoned outright)
- BUILD_COMMANDS.md (already in doc/DRIVER.md)
- KERNEL.md -> doc/KERNEL.md (new design doc)
- BOOTSTRAP.md -> doc/BUILD.md (fixed point + triage playbook)
- windows.md -> doc/WINDOWS.md (aarch64 self-host)
- ARCH.md, WASM.md -> open items folded into doc/plan/TODO.md
- LINKER-COMPAT.md -> folded into doc/plan/LINKER.md
Promoted: PORT.md -> doc/PORT.md (added to DESIGN.md index).
Archived: RQL.md, RSN.md -> doc/ideas/ (+ ideas/README.md).
Carved out: kit prof (host-native sampling profiler) -> doc/plan/PROF.md.
Updated in place: LINKER.md (consolidated + corrected an overstated
"file-incremental done for ELF" claim), RELEASE.md (removed 18 verified-landed
items; corrected stale ecosystem-bug prose), DEBUG.md, OPTIMIZER.md, README.md,
TODO.md, DESIGN.md, and doc/LINK.md (linker-script subset has grown — MEMORY/
PHDRS/PROVIDE/etc. are now parsed, no longer "rejected").
doc/plan/ 29 -> 13 docs, all open roadmaps. All cross-links verified resolving.
Diffstat:
38 files changed, 1769 insertions(+), 7497 deletions(-)
diff --git a/doc/ARCH.md b/doc/ARCH.md
@@ -390,5 +390,7 @@ is only true under the optimizer's `func_begin_known_frame`.
---
-Remaining and planned per-arch work (deferred niche encodings, audit
-follow-ups) is tracked in [plan/ARCH.md](plan/ARCH.md).
+Remaining and planned per-arch work — x64/rv64 tail-call realization,
+prologue/epilogue cost-model alignment, x64 debugger step-out unwind, and the
+deferred niche `as`/inline-`asm()` encodings — is tracked in the "Arch-backend
+parity" entry of [plan/TODO.md](plan/TODO.md).
diff --git a/doc/BENCHMARKING.md b/doc/BENCHMARKING.md
@@ -0,0 +1,253 @@
+# Benchmarking compile speed and code size
+
+This is the reference for *how to measure* kit's two performance axes —
+**compile+link throughput** and **emitted code size** — reproducibly. It is the
+methodology a contributor reuses to attribute a regression, validate a change, or
+re-rank where the cost is. It is deliberately free of standings and rankings
+(those move every landing); it describes the tools and the traps.
+
+The two axes here are distinct from the two other doc surfaces nearby. Generated-
+code *quality* (runtime speed / density of optimized output) is the optimizer's
+concern — see [OPT.md](OPT.md) and [plan/OPTIMIZER.md](plan/OPTIMIZER.md). The
+*correctness* gates a perf change must also pass (the byte-identity gate, the toy/
+opt/smoke suites, the ecosystem run-and-diff) are in [TESTING.md](TESTING.md). A
+static line-count and binary-byte breakdown of kit *itself* by component is in
+[CODE_SIZE.md](CODE_SIZE.md); this doc is about measuring the C *workload* kit
+compiles.
+
+## Two rules
+
+- **A superlinear axis is a bug.** A compile-speed regression that scales worse
+ than linearly in some input dimension (function count, declaration count, …)
+ means an inner step is O(n²)-ish — a scan run per item, a table that rebuilds,
+ a list re-walked. The synthetic scaling guard below exists to catch these.
+- **Measure on a RELEASE build.** Use `make bin RELEASE=1` (binary at
+ `build/release/kit`), or `PROFILE=1` for sampling — never the default
+ `make bin`, which is `RELEASE=0` and links ASan/UBSan. ASan amplifies
+ *memory*-op cost and dilutes *compute* cost, which inverts the ranking of where
+ the time goes. Correctness gates are build-mode-independent; timers are not.
+
+A kit binary copied outside the build tree fails with `support dir not found` —
+it resolves its `rt`/support dir relative to its own path. To A/B two builds keep
+both binaries inside `build/release/` (e.g. `kit_baseline`, `kit_fixed`), each
+beside its own `support/rt` sibling.
+
+## The reference workload and the bar
+
+The standing real workload is the **sqlite amalgamation** — one ~9 MB / 263 K-line
+C file (`tmp/projects/sqlite-amalg/sqlite3.c`): real, huge, declaration/macro/
+type-heavy, and doubling as an end-to-end correctness test (compile + link the
+shell, run a query). Commands below assume `SDK=$(xcrun --sdk macosx --show-sdk-path)`
+and a release kit at `build/release/kit`.
+
+The code-size and compile-speed bar is **tcc** (the fastest mainstream C
+compiler); clang is recorded for context. Build tcc from the mob mirror as an
+optimized binary:
+
+```sh
+git clone --depth 1 https://github.com/tinycc/tinycc tmp/tinycc && cd tmp/tinycc
+export SDKROOT=$(xcrun --sdk macosx --show-sdk-path) # else "stdio.h not found"
+sed -i 's|^CFLAGS=.*|CFLAGS=-Wall -O3 -DNDEBUG -Wdeclaration-after-statement|' config.mak
+./configure ; make clean ; make CC=cc # CC=cc = Apple clang (xcrun-aware)
+# binary at tmp/tinycc/tcc ; compile with: tcc -c sqlite3.c -o x.o
+```
+
+## Compile speed: instruction count (macOS)
+
+**`instructions retired` is the metric to trust** — it is load-independent, so it
+reproduces under noise. `/usr/bin/time -l` is the macOS `perf stat`; cycles/wall
+are low-load readings worth recording only for context. Take best-of-N to filter
+scheduling noise:
+
+```sh
+m(){ local bi=9e18 bc=9e18 br=9e18; for i in $(seq 1 7); do
+ /usr/bin/time -l "$@" >/dev/null 2>/tmp/m.txt
+ local I=$(awk '/instructions retired/{print $1}' /tmp/m.txt)
+ local C=$(awk '/cycles elapsed/{print $1}' /tmp/m.txt)
+ local R=$(awk '/real/{print $1}' /tmp/m.txt)
+ awk "BEGIN{exit !($I<$bi)}"&&bi=$I; awk "BEGIN{exit !($C<$bc)}"&&bc=$C
+ awk "BEGIN{exit !($R<$br)}"&&br=$R; done; echo "instr=$bi cycles=$bc real=${br}s"; }
+cd tmp/projects/sqlite-amalg
+m build/release/kit cc -c sqlite3.c -o /tmp/k.o --sysroot "$SDK"
+m tmp/tinycc/tcc -c sqlite3.c -o /tmp/t.o
+```
+
+**Caveat:** Apple clang spawns a `cc1` that `/usr/bin/time` does **not** count —
+measure clang with `cc -fintegrated-cc1 -c …`. kit and tcc are single-process, so
+their figures are whole-compile.
+
+### Phase decomposition
+
+To split lex+pp from the rest, **do not use `-E`**. `-E` adds a token→text
+serialization that neither compiler does at `-c`, and kit and tcc serialize at
+wildly different cost, so `-E` is a misleading lex+pp proxy. Instead **drain the
+token stream to EOF** (lex+pp only, no parse, no output) in each compiler:
+
+```sh
+# kit: KIT_PP_DRAIN runs pp_next_parse to EOF then returns (lang/c/c.c)
+m env KIT_PP_DRAIN=1 build/release/kit cc -c sqlite3.c --sysroot "$SDK" -o /dev/null # lex+pp
+m build/release/kit cc -fsyntax-only sqlite3.c --sysroot "$SDK" # +parse/sema/types/CG
+m build/release/kit cc -c -o /tmp/k.o sqlite3.c --sysroot "$SDK" # +emit+objwrite
+# tcc: the -bench hook drains next() to EOF; patched (tccpp.c tcc_preprocess)
+# to use -c parse_flags. TCC_DRAIN_FLAGS=c also decodes literals in the lexer.
+m env TCC_DRAIN_FLAGS=c tmp/tinycc/tcc -E -bench sqlite3.c -o /dev/null # tcc lex+pp
+m tmp/tinycc/tcc -c sqlite3.c -o /tmp/t.o # tcc full -c
+```
+
+The three `kit` invocations are nested (each adds a phase), so successive
+differences attribute instructions to lex+pp, parse/sema/types/CG, and
+emit+objwrite respectively.
+
+### macOS hotspot sample (self-time leaves)
+
+A single `-c` is too fast for `sample`; build a profiling kit (`-g` + frame
+pointers, no strip) and merge ~80 short runs. This gives self-time *leaves* only —
+for inclusive attribution use Linux callgrind (below).
+
+```sh
+make RELEASE=1 PROFILE=1 CC=clang BUILD_DIR=build/bench bin # -g + frame ptrs, no strip
+cd tmp/projects/sqlite-amalg ; KIT=build/bench/kit ; rm -f /tmp/p_*.txt
+for i in $(seq 1 80); do
+ $KIT cc -c sqlite3.c -o /tmp/kk.o --sysroot "$SDK" 2>/dev/null & pid=$!
+ sample $pid 5 1 -file /tmp/p_$i.txt -mayDie >/dev/null 2>&1 ; wait $pid; done
+for f in /tmp/p_*.txt; do awk '/Sort by top of stack/{f=1;next}/Binary Images/{f=0}f' "$f"; done \
+ | sed -E 's/\(in [^)]*\)//' | grep -oE '[A-Za-z_][A-Za-z0-9_]*[[:space:]]+[0-9]+' \
+ | awk '{c[$1]+=$2;t+=$2} END{for(k in c) printf "%.1f%% %s\n",100*c[k]/t,k}' | sort -rn | head -25
+```
+
+## Compile speed: Linux callgrind (inclusive attribution)
+
+There is no valgrind on macOS, and `sample` sees only self-time leaves. The
+**inclusive, instruction-grounded** profiler — the one that attributes a parent's
+cost across the functions it calls — is Linux `valgrind --tool=callgrind`.
+`scripts/perf_callgrind.sh` codifies the recipe; one-time
+`scripts/perf_callgrind.sh image` bakes the container, then:
+
+```sh
+scripts/perf_callgrind.sh run <tag> # builds kit in-container, runs callgrind, prints total Ir
+ # -> build/linux-prof/cg.<tag>.annot.txt (self Ir + callers)
+```
+
+For an `-O1` *optimizer-finalize* profile of sqlite, the full collection
+overflows Valgrind's brk segment on the default VM, so give the VM more memory,
+toggle collection at `opt_on_finalize`, and force glibc malloc onto `mmap`:
+
+```sh
+podman machine stop || true
+podman machine set --memory 3072
+podman machine start
+podman run --rm --platform linux/arm64 -v "$PWD":/work:Z kit-prof sh -c '
+ set -eu
+ cd /work/tmp/projects/sqlite-amalg
+ env MALLOC_MMAP_THRESHOLD_=1 MALLOC_ARENA_MAX=1 \
+ valgrind --tool=callgrind --cache-sim=no --branch-sim=no --dump-instr=no \
+ --collect-jumps=no --collect-atstart=no --toggle-collect=opt_on_finalize \
+ --callgrind-out-file=/work/build/linux-prof/cg.sqlite_o1.opt_finalize.mmap.out \
+ /work/build/linux-prof/kit cc -O1 -c sqlite3.c \
+ -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION -lc -o /tmp/sqlite_o1.o \
+ 2>/work/build/linux-prof/cg.sqlite_o1.opt_finalize.mmap.vg.log
+ callgrind_annotate --threshold=99.5 \
+ /work/build/linux-prof/cg.sqlite_o1.opt_finalize.mmap.out \
+ >/work/build/linux-prof/cg.sqlite_o1.opt_finalize.mmap.annot.txt
+'
+podman machine stop ; podman machine set --memory 2048 ; podman machine start # restore
+```
+
+Gotchas baked into the script (do not relearn the hard way):
+
+- **Pass `-lc`** so kit finds the glibc sysroot even for `-c`. Use a **bookworm**
+ base (glibc 2.36), *not* ubuntu 24.04 (glibc 2.39's `bits/math-vector.h` uses a
+ vector-typedef attribute the C frontend rejects).
+- **`strip --strip-debug`** the kit binary *in place* before profiling — valgrind
+ 3.19 chokes on clang DWARF5, and callgrind needs only `.symtab`. Strip in place
+ so kit still resolves `support/rt` relative to its own path.
+- **Trust self/exclusive `Ir`, not inclusive %.** Recursion in the recursive-
+ descent parser makes `--inclusive=yes` double-count to absurd numbers. (Self-`Ir`
+ may be summed across callgrind's `'2` symbol splits for a recursive function.)
+- The **Linux/ELF total is not comparable to the macOS/Mach-O instruction count**
+ — different libc, sysroot, and format, and the Linux figure includes glibc + the
+ loader + the `-lc` probe. Use callgrind for *where* (distribution); use macOS
+ `instructions` for *how much*. An `-O1` optimizer-finalize profile excludes
+ frontend recording, so it is a distribution, not a total comparable to a full
+ `-O0` compile.
+
+## Heap allocation counts (KIT_METRICS)
+
+`KIT_METRICS=1` prints the hosted-heap counters (allocs / large allocs ≥32 KiB /
+reallocs / frees) at exit. The counts are host-independent — kit issues the same
+`h->alloc` calls everywhere — so they isolate allocation *behavior* from the
+host's malloc cost:
+
+```sh
+KIT_METRICS=1 build/release/kit cc -c sqlite3.c --sysroot "$SDK" -o /tmp/k.o
+# -> kit heap metrics: heap.allocs=... heap.large_allocs=... heap.reallocs=... heap.frees=...
+```
+
+`KIT_METRICS=1` is also the way to see where an `-O1` compile spends its time:
+each `metrics_scope_*` bracket in the optimizer becomes a `<scope> <ticks> ticks`
+line, with per-pass counters (`opt.funcs`, `opt.blocks`, `opt.pregs`, the inliner
+refusal histogram, …). Ticks are the raw host cycle counter; within one run the
+scopes are directly comparable. See [OPT.md](OPT.md) §8 for the scope tree and
+interpretation; the heap counters reuse the same `KitProfiler` machinery
+(embedder counter range).
+
+## Code size
+
+The honest size metric is **`.text` machine code**, not the object-file size: the
+object is format-skewed (kit emits Mach-O, tcc ELF) and not comparable. `kit size`
+reports `.text`; the per-mnemonic histogram diff localizes where the byte excess
+(or surplus) is:
+
+```sh
+build/release/kit cc -c sqlite3.c --sysroot "$SDK" -o /tmp/k.o ; build/release/kit size /tmp/k.o
+tmp/tinycc/tcc -c sqlite3.c -o /tmp/t.o ; build/release/kit size /tmp/t.o
+# per-mnemonic excess (heavier in kit = positive count delta):
+hist(){ build/release/kit objdump -d "$1" | grep -E '^[[:space:]]+[0-9a-f]+:' \
+ | sed -E 's/.*\t([a-z][a-z0-9._]*).*/\1/' | grep -E '^[a-z]' | sort | uniq -c | sort -rn; }
+diff <(hist /tmp/k.o) <(hist /tmp/t.o)
+```
+
+For `-O1` generated-code-quality size comparison against **clang** per ecosystem
+file, `scripts/o1_quality.sh` builds the per-file kit (`.k.o`) and clang (`.c.o`)
+objects into `build/o1_quality/`; the per-file ratio is kit `__TEXT` ÷ clang
+`__TEXT` via `size -m`. When counting instruction lines from `kit objdump -d`,
+note the format is `addr:\t bytes \t mnemonic operands` — the mnemonic is after
+the *second* tab, so a naive `grep ':\t\w+'` matches the byte column and is wrong.
+clang on macOS needs `-isysroot "$SDK"`; kit needs `--sysroot "$SDK"`.
+
+## The synthetic scaling guard (`make bench-cc`)
+
+`make bench-cc` is the regression guard for the *superlinear-axis-is-a-bug* rule.
+It sweeps each input dimension — `fn-count`, `body-size`, `global-decl`,
+`type-decl`, `locals-per-fn`, `pp-macro`, `pp-include`, `ref-density`,
+`obj-count`, `symbol-count` — over a geometric series, fits a scaling exponent
+(`p ≈ 1.0` is the goal; `p ≥ 1.4` is flagged `SUPERLINEAR` and treated as a bug),
+samples hotspots, and records a clang reference. Output lands in `build/bench/cc/`
+(`scaling.md`, `hotspots.md`). Sources of truth: `scripts/cc_bench_gen.py` (the
+axis catalog), `scripts/cc_bench.sh` (harness), `scripts/cc_bench_report.py`
+(fit + verdicts).
+
+Use the synthetic sweep to catch a *re-introduced* O(n²); use the sqlite profile
+to decide *where* to optimize — the synthetic axes over-weight codegen relative
+to real declaration/macro-heavy code.
+
+For an `-O1` quality change specifically (which deliberately changes emitted
+bytes), the relevant linearity check is that sqlite `-O1` compile time does not
+move materially, validated with this sweep plus a before/after timing of the
+sqlite `-O1` compile.
+
+## End-to-end correctness as a perf gate
+
+The sqlite workload is also a correctness oracle, run at both opt levels:
+
+```sh
+build/release/kit cc sqlite3.c shell.c -o /tmp/sq --sysroot "$SDK" -lc
+/tmp/sq :memory: "create table t(a,b); insert into t values(2,40),(10,32);
+ select sum(a)+sum(b), count(*) from t;" # -> 84|2
+```
+
+The full perf-change gate (byte-identity for compile-speed changes,
+run-correctness + determinism for code-size changes, and the suite list) is in
+[TESTING.md](TESTING.md); `scripts/perf_identity_gate.sh` and
+`scripts/perf_axis_time.py` are the gate/timer tooling, driven by
+`make perf-golden` / `make perf-gate`.
diff --git a/doc/BUILD.md b/doc/BUILD.md
@@ -277,3 +277,71 @@ through the bootstrapped compiler.
The host-clang seed is the current root of trust. A full diverse-double-compilation
/ hex0-style seed chain (a tiny seed binary that needs no pre-existing C compiler)
is a separate concern and is deliberately outside the boundary of this build.
+
+### Reach and host coverage
+
+The fixed point is reached on aarch64 across macOS (Mach-O), Linux (ELF, both
+musl and glibc), and FreeBSD (ELF), at both `-O0` and `-O1`. In every
+configuration `cmp stage2/kit stage3/kit` is byte-identical, the per-object diff
+across all stage2/stage3 `*.o`/`*.a` reports zero differences, and the
+bootstrapped stage3 runs the Toy corpus. Because the byte-identity check leans on
+the deterministic output above, any nondeterminism in codegen or object layout
+surfaces here as a stage2/stage3 mismatch — so the bootstrap doubles as the
+project's strongest end-to-end correctness oracle, exercising the C frontend,
+every optimizer pass, the native backends, the object writers, the linker, and
+the archive tools on a real, substantial program.
+
+`make bootstrap` keys off the build host's own `uname`, selecting the native
+toolchain and object format with no cross-compilation. A Linux or FreeBSD target
+is bootstrapped from the macOS dev host by running the ordinary three-stage build
+*inside* a matching arm64 container or VM (`scripts/linux_bootstrap.sh
+[musl|glibc]`, `scripts/freebsd_bootstrap.sh aarch64`), where it is a plain
+native build. Three host-environment differences from the macOS reference are
+handled by those scripts / `mk/bootstrap.mk`: LeakSanitizer must be disabled
+(`detect_leaks=0`) because kit's arena allocator deliberately never frees;
+`HOST_SYSROOT_{C,LD}FLAGS=-lc` is passed to the stage2/3 sub-makes so kit's
+hosted profile wires up the libc include/library dirs (on macOS the `-isysroot`
+already supplies them); and glibc + Linux-UAPI headers need the C-frontend/pp
+GCC-ism compatibility handled by the frontend.
+
+### Triage playbook for fixed-point regressions
+
+A stage2/stage3 mismatch (or a stage3 link failure) is almost always a
+miscompile or a nondeterministic codegen change, not a build-system bug. The
+following approach is the default starting point.
+
+- **Use object reproduction, not "does it link", as the oracle.** A stage3 link
+ failure is usually a *symptom* of a malformed object emitted earlier. Compile
+ one suspect TU with both the host `kit` and the stage2 `kit` under identical
+ flags and `cmp` the two objects; this separates malformed-object bugs from
+ link-driver symptoms and points straight at the diverging codegen.
+- **Narrow with hybrid relinks.** Relink stage2 after replacing one suspect TU
+ with a clang-built object, then use that stage2 to compile the known-differing
+ target object — isolating a linker bug from codegen for a specific source file.
+- **Inspect MIR around the suspect symbol.** A filtered MIR dump taken after
+ lowering and the combine pass is usually enough to see the divergence (e.g. a
+ call argument referencing a backend scratch register where it should reference
+ an allocable one).
+- **Avoid `-g` while triaging `-O1` codegen.** Debug info changes object layout
+ and can create or mask layout-sensitive bugs; triage on the non-`-g` object
+ first.
+
+The bug classes that have historically broken the `-O1` fixed point all live in
+the interaction between the optimizer's register-level reasoning and the
+backend's scratch-register discipline, with the object/link layer as where the
+symptom surfaces. Keep them in mind when bringing up a new arch or platform:
+
+- **Operand clobber in native emit** — materializing the left operand of a
+ binop/compare into a scratch register that already holds the right operand
+ (compute the RHS location first and exclude its register when materializing the
+ LHS).
+- **Copy propagation across backend scratch registers** — scratch registers may
+ appear in lowered MIR but must not be extended across later instructions, since
+ native lowering reuses them as transient temporaries.
+- **Coalesce overlap checks** must use raw range points, not compressed points.
+- **Lower-pass hint fallback** must not place values live across a call into
+ caller-saved hint registers.
+- **Native scratch budget** — a backend needs enough integer scratch registers
+ for all-spilled three-operand operations (aa64 needs three).
+- **Aggregate copy/set with pointer operands** must not force-home the pointer
+ local; genuinely frame-backed pointer locals need prematerialized indirect bases.
diff --git a/doc/CODEGEN.md b/doc/CODEGEN.md
@@ -111,6 +111,76 @@ table. Source-like targets override `switch_` to emit a native construct (the C
target a real `switch`, a wasm target `br_table`). Same semantic input, different
realization.
+## Type system
+
+Types flow through CG as `KitCgTypeId`, a `uint32_t` handle into one
+compiler-owned type table (`src/cg/type.c`). The handle is the *only* thing
+stored in IR, ABI records, frontends, and backends, so it stays cheap to copy.
+Ids are stable only within one `KitCompiler` and are never serialized as semantic
+identities. Decode is a direct table index — builtins occupy the first nonzero
+ids, user entries append after them — with no segment/bias scheme. `0`
+(`KIT_CG_TYPE_NONE`) means **invalid/absent only**.
+
+`KitCgTypeId` is a pure **storage/ABI/operational** identity. Its kind lattice
+(`KitCgTypeKind`) is exactly `VOID, BOOL, INT, FLOAT, PTR, ARRAY, FUNC, RECORD,
+ENUM, VARARG_STATE`. Two load-bearing consequences:
+
+- **Void is a real builtin type**, never `KIT_CG_TYPE_NONE`. A function that
+ returns no value uses `kit_cg_type_builtin(c, KIT_CG_BUILTIN_VOID)` as its
+ result type; `KitCgFuncResult.type` is always a valid id. No caller tests
+ `result.type == KIT_CG_TYPE_NONE` to mean "void"; `kit_cg_type_is_void` and
+ `kit_cg_func_result_has_value` answer that. This separates "no result type
+ yet" (invalid) from "the void type" (a type), which were conflated before.
+- **Integers are width-only.** `int` and `unsigned` both lower to the `i32`
+ builtin; signedness is not on the type. It is carried by operations
+ (`sdiv`/`udiv`, `sext`/`zext`), by ABI extension attrs
+ (`KIT_CG_ABI_SIGNEXT`/`ZEROEXT`), and by the per-load `KIT_CG_MEM_*` signedness
+ flag. There are **no transparent wrapper kinds** in the lattice — no `ALIAS`,
+ no source-spelling base — so the operational type is its own storage type. The
+ old pervasive unalias step is gone: `kit_cg_type_resolve_alias` is identity and
+ `KitCgTypeInfo.storage_id` always equals `id`.
+
+Construction is interned for structural shapes and nominal for the rest. Pointer,
+array, and function constructors return a stable id for the same exact shape
+(pointer key is `{pointee, address_space}`, array key `{elem, count}`, function
+key the full result/params/attrs/conv/variadic shape). Records and enums get
+fresh nominal identities.
+
+**Records are nominal and two-phase.** `kit_cg_type_record_decl(tag, is_union)`
+mints an incomplete record id; `kit_cg_type_record_complete` fills its fields
+exactly once. A pointer to an incomplete record is legal, so the recursive
+`struct S; struct T { struct S *p; }; struct S { … };` pattern declares `S`, uses
+`S*` while building `T`, then completes the same `S` id. Sizing, by-value fields,
+arrays, by-value params/results, locals, and memory accesses require a
+complete/sized type. The C frontend therefore no longer erases record-context
+pointees to `void*` to make layout proceed — it caches the declared id and
+completes it in place.
+
+**Layout authority is the ABI, not the type table.** Size, alignment, scalar
+width, and record field offsets come from `TargetABI`; the type entry only
+*caches* the computed `KitCgTypeLayout`, filled on demand after the facts it
+depends on are final (record layout at completion). `kit_cg_type_record_layout`
+returns a CG-owned, lifetime-stable `KitCgRecordLayout` that frontends cache
+rather than copying offsets into a parallel table. This keeps 32-bit, Windows,
+and bit-field ABI rules in one place.
+
+### Debug spelling is a separate channel
+
+Source-facing spelling — primitive sign+name (`signed char`, `unsigned int`),
+typedef names, the enum underlying spelling — is not on `KitCgTypeId`. It rides a
+separate, optional `KitCgDebugType` handle that exists only when debug info is
+requested on the session; the builders return `KIT_CG_DEBUG_TYPE_NONE` when it is
+not. `kit_cg_debug_base`/`_typedef`/`_ptr`/`_array`/`_func`/`_enum` build the
+debug graph, and `kit_cg_debug_of_type` derives a default from an operational
+type (reusing the existing record/enum/func emission). The frontend overrides
+only the leaves it cares about (named scalars, typedefs) and attaches the result
+to a decl via the optional `debug_type` field on the function/object/local attrs;
+`0` there means "derive the default from the operational type." This keeps the
+hot operational path free of any debug concern — debug types never enter interned
+operational descriptors — while preserving faithful signed/unsigned/char DWARF
+and enum enumerators. Non-debug frontends (toy, wasm) pass `0` and rely on the
+default derivation.
+
## CgTarget realizations
`session.c`'s `kit_cg_begin` picks the realization. It asks the arch
diff --git a/doc/DESIGN.md b/doc/DESIGN.md
@@ -211,6 +211,7 @@ unless an API states otherwise.
| [INTERPRETER.md](INTERPRETER.md) | The bytecode interpreter over the optimizer IR used by `run --no-jit`. |
| [OBJ.md](OBJ.md) | The format-neutral object model and ELF/Mach-O/COFF/Wasm read/write behind `ObjFormatImpl`. |
| [LINK.md](LINK.md) | Linking: symbol resolution, layout, relocation, linker scripts, and incremental linking. |
+| [KERNEL.md](KERNEL.md) | The freestanding kernel build + image pipeline: `build-obj`/`build-exe`, the linker-script subset, `kit image` / `objcopy -O binary`, flat-kernel `Image` headers, and `kit cpio` initramfs archives. |
| [JIT.md](JIT.md) | The JIT image model, executable-memory and TLS host hooks, and publish/append/replace. |
| [EMU.md](EMU.md) | The user-mode guest-ELF emulator and its per-block JIT translation. |
| [DWARF.md](DWARF.md) | DWARF debug-info production and the consumer used by the debugger and dumpers. |
@@ -222,6 +223,9 @@ unless an API states otherwise.
| [RUNTIME.md](RUNTIME.md) | The freestanding headers and compiler-rt/libc-style support in `rt/`. |
| [BUILD.md](BUILD.md) | The build system and `KIT_*_ENABLED` component gating. |
| [TESTING.md](TESTING.md) | The test suites and harnesses under `test/`. |
+| [PORT.md](PORT.md) | The portability test surface: the harmonized `cross` + `selfhost` make targets and the substrates beneath them, parametrized over one support-set matrix. |
+| [BENCHMARKING.md](BENCHMARKING.md) | How to measure compile speed and code size reproducibly: instruction counts, Linux callgrind, the `make bench-cc` scaling guard, and the vs-tcc/clang bars. |
| [CODE_SIZE.md](CODE_SIZE.md) | Line counts per component (per-format/per-target split from core). |
-Planned work and roadmaps live under `doc/plan/`.
+Planned work and roadmaps live under `doc/plan/`; speculative not-yet-committed
+designs live under `doc/ideas/`.
diff --git a/doc/FRONTENDS.md b/doc/FRONTENDS.md
@@ -142,6 +142,28 @@ the preprocessor traffics in identifiers, not keywords. `parse_c` interns the
C11 keyword spellings into symbols once at startup (`kw_names[]` in
`parse.c`) and recognizes keywords by symbol identity as it consumes tokens.
+The token that flows across the whole lexer -> preprocessor -> parser lane is a
+**lean 32-byte `Tok`** (`lang/cpp/lex/lex.h`), and the boundary deliberately
+keeps the layers separate without paying per-token costs to do so:
+
+- **Identity is eager, spelling is lazy.** `Tok.aux` carries the interned `Sym`
+ for an identifier or the integer `Punct` code for a punctuator, so macro
+ lookup and parser keyword/binding checks are one indexed lookup and
+ punctuators — roughly half a large C stream — never hit the symbol table.
+ Exact spelling rides a `TextRef`, materialized only when `-E`, stringize,
+ paste, literal decode, or a redefinition check actually needs the bytes; a
+ `TEXT_SRC` ref names a span in the source's retained post-splice buffer, a
+ `TEXT_SYM` ref carries an interned `Sym` for synthetic tokens, and a canonical
+ punctuator carries no text at all (it reconstructs from `aux`).
+- **Location is a compact `LocRef`** — `(file_id, byte_off)` — not an eager
+ line/column. The scanner tracks no running line number; `pp_materialize_loc`
+ resolves a `LocRef` to a `SrcLoc` on demand from the owning source's line map,
+ applying any `#line` overlay. Diagnostics and dynamic `__LINE__` go through
+ that helper, so the hot token never pays column arithmetic.
+- **Slot-based handoff.** `pp_next_parse` / `pp_next_raw` and `lex_next` write
+ into a caller-owned `Tok` slot rather than returning a 32-byte struct by
+ value, avoiding the hidden sret copy at every step of the stream.
+
### Preprocessor — lang/cpp/pp
`pp.c` (+ `pp_directive.c`, `pp_expand.c`) implements translation phase 4 and
@@ -155,16 +177,21 @@ Internally the preprocessor runs a **token-source stack**. Each source is
either a `Lexer` (the main file or an `#include`d file) or a pre-built token
buffer (a macro expansion in progress). Includes push a new lexer; macro
invocations push an expansion buffer; EOF pops. This is what makes the
-include stack and macro rescanning fall out of one mechanism.
-
-Macro expansion uses the **Prosser hideset** algorithm (the standard's
-"nested replacement" / blue-paint rule). Every token in an expansion buffer
-carries a hideset: the set of macro names it must not be re-expanded by during
-rescan. Function-like expansions compute the result hideset as the invocation
-hideset unioned with the just-expanded macro name, which is exactly what stops
-infinite recursion through self-referential and mutually-referential macros.
-Hidesets are interned into a small table (`pp_expand.c`) and kept sorted for
-canonical identity, so identical hidesets share one id.
+include stack and macro rescanning fall out of one mechanism. Because lean
+tokens hold only a `LocRef`/`TextRef`, the PP keeps a **`SrcInfo` registry**
+(`pp_priv.h`): each pushed source registers its retained byte buffer, line map,
+and `#line` overlay, kept alive until `pp_free` so a token's location and
+spelling still materialize after its lexer has been popped.
+
+Macro expansion uses the **disabled-frame** availability model (cpplib-style),
+not a per-token hideset. A macro's replacement-list frame on the source stack
+carries a `disabled_owner`: pushing it increments that macro's `disabled_depth`
+and popping it decrements it, so the macro is unavailable for re-expansion across
+the entire rescan of its replacement (and any nested replacement above it). A
+macro expands only when its `disabled_depth` is zero. This is what stops infinite
+recursion through self- and mutually-referential macros — the same guarantee the
+hideset gave — but as a stack property rather than a set carried on every token,
+which keeps the lean token free of any per-token expansion state.
`pp_directive.c` owns the rest of phase 4: the `#if` nesting stack and the
preprocessor constant-expression evaluator, `#include` search and file open
@@ -185,11 +212,16 @@ types/declarators in `parse_type.c`, initializers in `parse_init.c`, and
statements in `parse_stmt.c`.
Because lowering happens inline, the parser uses the **CG operand stack as its
-typed expression stack**. Each CG stack entry carries frontend-owned
-`lang_type`/`lang_flags` facts: the C type plus value flags for lvalue,
-modifiable-lvalue, bit-field, register, and null-pointer-constant state. The
-parser reads those facts with `kit_cg_slot_info()` and stamps fresh results with
-`kit_cg_retag_*()`; structural CG stack ops move the facts with the value.
+typed expression stack** — there is no parser-side shadow stack. The CG stack is
+the single liveness authority: a value or place is live exactly while a stack
+slot references it, and popping that slot is the cheap single-pass death signal
+the native direct target and temporary reclaim already rely on. Each CG stack
+entry carries frontend-owned `lang_type`/`lang_flags` facts (stored inline on the
+slot, opaque to CG): the C type plus value flags for lvalue, modifiable-lvalue,
+bit-field, register, and null-pointer-constant state. The parser reads those
+facts with `kit_cg_slot_info()` and stamps fresh results with `kit_cg_retag_*()`
+/ `kit_cg_set_top_flags()`; a producing op clears the new slot's lang facts and
+structural CG stack ops (`dup`/`swap`/`rot3`/…) move them with the value.
### Parser codegen helpers
@@ -207,10 +239,29 @@ does not keep a separate lvalue auxiliary record. Loads, stores, address-taking,
and `store_keep` consume the same CG places, with the C facts kept only in the
inline lang fields.
-Codegen suppression is CG unevaluated mode. The parser still has semantic forks
-for C control-shape rules and constant-expression legality, but type-only and
-constant-evaluation paths preserve CG stack shape and C slot facts without
-target emission.
+Codegen suppression is CG unevaluated mode. Bracketing a parse with
+`kit_cg_unevaluated_push`/`_pop` keeps the value-stack shape, the inline C slot
+facts, and CG's always-on constant payload maintained while skipping all target
+emission, temp/local allocation, and `-O0` analyses. The parser still has
+semantic forks for C control-shape rules and constant-expression legality, but
+type-only paths (`sizeof` operands, `_Generic` arms, dead expression arms) run on
+the normal grammar with nothing emitted.
+
+Integer-constant-expression evaluation rides this same machinery. The parser
+evaluates an ICE by parsing the real expression grammar once under unevaluated
+mode and reading the folded value from CG's constant-value model
+(`kit_cg_top_const_int_ex` over a width-complete `KitCgConstInt`, which carries
+known integer bits plus width/signedness independent of whether the value would
+have been an immediate, a local, or a runtime call). There is no second constant
+grammar: the old `cexpr_*` / `cint_*` parser subsystems are gone. What stays
+parser-owned is a syntactic/semantic legality guard, `CConstGuard`
+(`parse_expr.c`), because a *foldable* result is not the same as a *legal* C
+integer constant expression — `(0, 1)` folds but the comma operator is illegal in
+an ICE, enum constants are ICE operands while ordinary objects are not, and
+`sizeof(x++)` is legal because the increment is unevaluated. The guard records
+those events as the normal parse routines pass over them, and `eval_const_int`
+accepts a value only when both the guard says the syntax is permitted and CG
+reports a known constant of the required type.
### Semantic layers — type / decl / sem / abi
diff --git a/doc/KERNEL.md b/doc/KERNEL.md
@@ -0,0 +1,288 @@
+# Kernel build and image pipeline
+
+kit can build freestanding kernels from C and assembly and turn the linked
+result into the flat load images and initramfs archives a bare-metal or
+QEMU-driven boot needs. This is a thin policy layer on top of the existing
+toolchain: the same compile, link, and object machinery used for hosted
+programs, plus three things a kernel author specifically needs — a richer
+linker-script subset, an image emitter (`kit image` / `objcopy -O binary`), and
+a cpio packager (`kit cpio`).
+
+kit deliberately stops at the artifact boundary. It does not generate startup
+code, set up page tables, stacks, TLS, or privilege-mode entry, and it does not
+implement any boot protocol. Kernel startup and boot-protocol compliance are the
+author's responsibility; kit produces a deterministic ELF, a byte-exact flat
+image, and a well-formed archive, and nothing about how they are loaded or run.
+
+Related: [DRIVER.md](DRIVER.md) (the multi-call binary and tool registry),
+[LINK.md](LINK.md) (symbol resolution, layout, relocation, linker scripts),
+[OBJ.md](OBJ.md) (the object/image model the emitter reads), and
+[RUNTIME.md](RUNTIME.md) (the freestanding headers, compiler-rt helpers, and the
+TLS contract — note there is no `crt0`).
+
+## Freestanding targets and the build shape
+
+The supported freestanding ELF targets are:
+
+- `x86_64-none-elf`
+- `aarch64-none-elf`
+- `riscv64-none-elf`
+- `riscv32-none-elf`
+
+A `*-none-elf` triple (EI_OSABI STANDALONE) resolves to a non-PIE ELF target by
+default and puts the driver and linker into *freestanding* mode, which turns on
+the strict validation described below.
+
+There is no separate `kit kernel` command. The compile/link front doors are the
+ordinary build verbs (see [DRIVER.md](DRIVER.md)):
+
+- **`build-obj`** compiles a polyglot source set (C / asm / toy / wasm) to one
+ object; multiple sources combine into a single relocatable object via `ld -r`.
+- **`build-exe`** compiles a source set in memory and links it — together with
+ any `.o` / `.a` / `.so` inputs — into an executable, with no intermediate
+ files.
+
+Both accept the freestanding and link flags a kernel needs, and `build-exe`
+accepts the common direct linker flags (not only the `-Wl,` escape hatch). A
+representative kernel link:
+
+```
+kit build-exe -target x86_64-none-elf \
+ -ffreestanding -nostdlib -nostartfiles -static -no-pie \
+ -mcmodel=kernel -mno-red-zone \
+ -ffunction-sections -fdata-sections \
+ -T kernel.ld -e _start \
+ -Wl,--gc-sections \
+ --map kernel.map --symbols kernel.sym \
+ -o kernel.elf \
+ boot.S kernel.c mm.c
+```
+
+The flags that matter for kernel code, and that kit honors rather than merely
+accepts:
+
+- `-ffreestanding` / `-fhosted` select freestanding vs hosted assumptions
+ (whether sysroot-hosted profiles may engage).
+- `-nostdinc`, `-nostdlib`, `-nodefaultlibs`, `-nostartfiles` precisely control
+ include paths and runtime/CRT/libc insertion. kit never invents a startup
+ object for a freestanding kernel.
+- `-static`, `-no-pie`, `-fno-pic`, `-fno-pie` produce static non-PIE,
+ ET_EXEC-style code unless PIC/PIE is requested explicitly.
+- `-mcmodel=...` selects the code model.
+- `-mno-red-zone` disables the x86-64 SysV red zone — it changes backend frame
+ selection, it is not just parsed.
+- `-mgeneral-regs-only` prevents accidental SIMD/FP codegen where the
+ privilege-mode context does not save those registers.
+- `-fno-builtin` and the stack-protector flags constrain what the frontend may
+ synthesize.
+- `-ffunction-sections` / `-fdata-sections` compose with `--gc-sections` and
+ script `KEEP(...)`.
+
+`-Ttext` / `-Tdata` / `-Tbss` and `--section-start=.name=addr` place a
+freestanding image at a fixed load address from the command line, and work
+through both `build-exe` and `ld`.
+
+### Strict freestanding validation
+
+A freestanding executable link (triggered by a `*-none-elf` input, or forced by
+the same `freestanding_strict` trigger from either `build-exe` or `ld`) rejects,
+by default, anything that implies a dynamic loader: DSO inputs, dynamic
+interpreter paths, dynamic sections and PLT/GOT imports, unresolved symbols, and
+cross-input target / object-format mismatches. `--no-undefined` makes unresolved
+references an error explicitly; `--allow-undefined` is the escape hatch.
+
+## Linker-script subset
+
+Kernel layouts need a structured GNU-ld-compatible script subset larger than the
+default `SECTIONS`-only form. The script is parsed by a hand-written
+recursive-descent parser (`src/link/link_script.c`) into a structured
+`KitLinkScript`; the linker accepts only that structured form, and `ld` parses
+`-T` text into it. Unsupported directives are rejected with a diagnostic rather
+than silently ignored. See [LINK.md](LINK.md) for how scripted layout replaces
+the default permission-bucket placement.
+
+The supported constructs:
+
+- `MEMORY` with `ORIGIN` / `LENGTH` and attributes, with region-overflow
+ diagnostics, and output-section placement into regions.
+- Load-memory placement via `AT(expr)` / `AT> REGION`, so VMA/LMA-split kernels
+ are representable.
+- `PHDRS`, the `:phdr` section attribute, and segment flags, so a kernel
+ controls its program headers and segment permissions. Multiple sections naming
+ one `:phdr` coalesce into a single PT_LOAD (perms unioned); a section listing
+ several phdrs appears under each.
+- `PROVIDE`, `PROVIDE_HIDDEN`, and `HIDDEN` symbol definitions; top-level and
+ in-section assignments; `. = expr` dot moves, which apply at their textual
+ position interleaved with the section walk.
+- `ASSERT(expr, "message")`.
+- `EXTERN(symbol)` as a GC root / undefined-symbol declaration.
+- Input-section patterns including `EXCLUDE_FILE` and the alignment helpers
+ `ALIGN` / `BLOCK`.
+- Section fills (`=0x...` and `FILL(...)`), which lay a repeating big-endian
+ pattern across holes.
+- `NOLOAD` output sections: PROGBITS content is forced to occupy no file bytes,
+ and relocations into a NOLOAD section are skipped rather than written through a
+ null buffer.
+- `OUTPUT_ARCH` / `OUTPUT_FORMAT` as validation directives (mismatches are
+ reported; they do not drive target selection).
+
+`KEEP(...)` roots continue to interact correctly with `--gc-sections`.
+
+## Link-side side outputs
+
+The linker produces deterministic side files, available from both `ld` and
+`build-exe`, so a kernel build can audit its own layout:
+
+- `--map FILE` writes a link map: target triple and output kind, entry symbol
+ and address, memory regions and usage, program headers, output sections with
+ VMA / LMA / file offset / size / alignment, input contributions, linker-defined
+ symbols, discarded sections, and unresolved symbols. Input paths are
+ normalized to basenames so no absolute host path leaks in.
+- `--symbols FILE` writes post-link absolute symbols; `--symbols-format=nm|json`
+ selects the format.
+- `--cref FILE` writes a cross-reference table (including imported/undefined
+ symbols).
+- `--print-memory-usage` summarizes per-`MEMORY`-region usage (overflow-safe for
+ high-half regions).
+
+These are linker-owned, not image-owned: the image metadata sidecar (below) is a
+report about the image transform, not a substitute for `--map`. Supporting
+policy flags include `--defsym name=expr` (can satisfy an otherwise-undefined
+reference), `--orphan-handling=place|warn|error|discard`, and `--fatal-warnings`.
+
+## Image emission
+
+Two front doors turn a linked ELF into a flat load image, both backed by one
+lower-level emitter (`include/kit/image.h`, implemented in `src/obj/image.c`):
+
+- **`objcopy -O binary`** stays a minimal object-transformer path:
+
+ ```
+ kit objcopy -O binary kernel.elf kernel.bin
+ ```
+
+- **`kit image`** owns image-building policy:
+
+ ```
+ kit image --format bin kernel.elf -o kernel.bin
+ ```
+
+The emitter consumes already-opened object/image state plus the original input
+bytes (see [OBJ.md](OBJ.md) for the linked-image view it reads). It never reads
+the filesystem itself, chooses a boot protocol, or invokes an emulator.
+
+### Formats
+
+- `bin` — flat binary derived from loadable segments (the default) or selected
+ sections.
+- `rom` — fixed-size flat binary: requires `--pad-to`, fills unused bytes
+ deterministically, and fails when the payload exceeds the requested size.
+- `sections` — concatenate `--section NAME` (repeatable) in declared order;
+ missing sections are rejected and the address metadata is made explicit so a
+ concatenation is not mistaken for a loadable memory image.
+- `elf` — normalize / strip / split-debug. This is **deliberately delegated** to
+ `objcopy` + `strip`; the emitter returns a diagnostic pointing at those tools
+ rather than duplicating the object-rewrite machinery. The `--strip-debug` /
+ `--split-debug` / `--keep-symbols` knobs are only meaningful with `--format
+ elf`.
+
+Segment-based emission reads loadable ranges from the program headers, sorts by
+the selected address kind, detects overlaps, fills or rejects holes per policy,
+and preserves bytes exactly as they would be loaded. The main flag groups:
+
+- Selection: `--from segments|sections`, `--segment PT_LOAD` (repeatable),
+ `--only-section` / `--remove-section` / `--section` (all repeatable).
+- Addressing: `--addr vaddr|paddr|lma`, `--base ADDR`, `--bias N`.
+- Layout: `--fill BYTE`, `--fail-on-holes`, `--max-hole SIZE`, `--align`,
+ `--pad-to`, `--max-size` (sizes accept `K`/`M`/`G` suffixes).
+- Validation: `--require-entry`, `--require-symbol NAME`, `--require-section
+ NAME`, `--no-dynamic` — checked against the opened object before any bytes are
+ written.
+- Reporting: `--metadata FILE` writes a deterministic JSON sidecar (stable key
+ order, no host paths, no timestamps) describing target, object format, entry,
+ build id, the selection that actually contributed bytes, base/bias/fill policy,
+ payload/output sizes, and warnings.
+
+### Flat-kernel `Image` header (arm64 / riscv64)
+
+QEMU's `-kernel` path on arm64 and riscv consumes the flat Linux `Image` format:
+a raw loadable binary prefixed with a fixed 64-byte header the loader reads to
+place and size the image. The first 32 bytes are common to both arches: `code0`
+/ `code1` (the entry branch; `"MZ"` low half when EFI), a u64 `text_offset`
+(load offset from a 2 MiB-aligned base), a u64 `image_size` (the in-memory
+footprint **including BSS**), and a u64 `flags` (bit 0 endianness; on arm64 bits
+1-2 page size, bit 3 placement). The tails differ: arm64 carries `magic =
+"ARM\x64"` at offset 56; riscv64 carries a `version` u32 (currently `0x2`) at 32
+and `magic2 = "RSC\x05"` at 56.
+
+kit supports this two ways:
+
+- **Author-owned, pass-through.** A kernel's own `head.S` can emit the 64-byte
+ header (placing `code0`/`code1` and declaring `image_size` from linker
+ symbols). `kit image --format bin` / `objcopy -O binary` copy the PT_LOAD
+ bytes verbatim, so the leading header survives byte-exact and the result is a
+ loadable `Image`. kit needs nothing extra for this path.
+
+- **kit-synthesized.** The awkward field for authors is `image_size`, the
+ in-memory span including BSS, which kit already knows from each loadable
+ segment's `vsize`. So `--image-header[=arm64|riscv|auto]` (on `--format bin` or
+ `rom`; `auto` infers the arch from the object's machine) synthesizes the
+ header. It *overlays* the first segment rather than prepending: the author's
+ first 8 bytes (the `code0`/`code1` entry branch) are preserved and kit fills
+ the 56-byte metadata tail, so the output stays the same size as a plain `bin`.
+ The author's first segment must reserve those 56 bytes. `image_size` is the
+ in-memory span including BSS, computed from `vsize` over all selected loadable
+ segments (BSS-only `file_size == 0` segments count toward the span but
+ contribute no bytes).
+
+Because `flags` encodes boot semantics, header synthesis is an explicit opt-in,
+never a default, and the boot-semantic fields are surfaced as explicit options
+(`--image-text-offset`, `--image-endian`, `--image-page-size` [arm64 only,
+4k/16k/64k]) rather than invented — they error if given without
+`--image-header`. kit fills the magic/version and `image_size` deterministically
+and chooses no boot policy.
+
+## `kit cpio` — initramfs archives
+
+The Linux kernel unpacks its initramfs from a `cpio -H newc` archive (magic
+`070701`, or `070702` for the CRC variant), optionally compressed; the
+early-microcode convention is just an uncompressed cpio concatenated ahead of
+the compressed main archive. This is an archive format, not a boot protocol — a
+sibling of `ar` — so `kit cpio` lives in the byte-utility tool family rather than
+in the image emitter. The newc codec is driver-local (`driver/cmd/cpio.c`,
+mirroring `tar.c`'s stateless shape) since only this one tool consumes it; the
+driver has no `-Isrc`. kit packages and inspects the archive; it does not build,
+mount, or boot it.
+
+```
+kit cpio -o -F initramfs.cpio -z init etc/ # create, gzip-compressed
+kit cpio -t -F initramfs.cpio.gz # list (auto-detects gzip)
+kit cpio -i -F initramfs.cpio # extract
+```
+
+What it supports:
+
+- **SVR4 `newc` only** (`-H newc` default, `-H crc` for the `070702` checksum
+ variant). The legacy `bin`/`odc` formats are out of scope.
+- Create (`-o`), list (`-t`), and extract (`-i`) over regular files,
+ directories, and symlinks. Special/device nodes are out of scope.
+- **Deterministic output**: members sorted by archived path (a stable sort that
+ is a valid DFS, each directory before its children), normalized metadata
+ (uid/gid 0, mtime 0, sequential inode, mode = type | perms keyed on the source
+ executable bit), a closing `TRAILER!!!` record, and a 512-byte tail pad.
+ Identical inputs yield byte-identical output.
+- **Concatenation**: the reader continues past a `TRAILER!!!`, skips
+ inter-segment zero padding, and resumes on the next magic, so early-init
+ segments (built by `cat` of 512-padded archives) list and extract cleanly;
+ trailing non-cpio data is noted, not fatal.
+- **Compression** as a create-time flag (`--compress=gzip|lz4`, with short `-z`
+ for gzip and `--lz4`), via the public `kit/compress.h` codecs. An initramfs is
+ just a compressed newc archive, so there is no separate `initramfs` tool. gzip
+ and lz4 only; a zstd/xz magic or `--compress=zstd|xz` gets a specific
+ diagnostic rather than pretend support. On read, `-d` and always-on
+ auto-detection round-trip a compressed archive with no separate step.
+- **Safety**: absolute names and `..` components are refused on both create and
+ extract, so a crafted archive cannot escape the destination directory.
+
+The tool is gated in `driver/main.c` behind `KIT_TOOL_CPIO_ENABLED`, alongside
+the other archive utilities.
diff --git a/doc/LINK.md b/doc/LINK.md
@@ -237,10 +237,15 @@ The script itself is parsed by `kit_link_script_parse` (link_script.c),
a hand-written recursive-descent parser for a deliberately small GNU-ld
subset: `ENTRY(sym)`, top-level and in-section symbol assignments with a
small arithmetic-expression grammar, `. = expr` dot moves and alignment,
-`SECTIONS { output : { input-matchers } }`, and `/DISCARD/`. Unsupported
-directives (`MEMORY`, `PHDRS`, `PROVIDE`, `OVERLAY`, `OUTPUT_FORMAT`,
-`GROUP`, ...) are rejected with a diagnostic rather than silently
-ignored. The linker accepts only the structured `KitLinkScript` form —
+`SECTIONS { output : { input-matchers } }`, and `/DISCARD/`. The subset has
+since grown to cover the directives a freestanding kernel link needs:
+`MEMORY` regions (with `ORIGIN`/`LENGTH` and `> REGION` / `AT> REGION`
+placement), `PHDRS` segment assignment, `PROVIDE` / `PROVIDE_HIDDEN` /
+`HIDDEN`, `ASSERT`, `EXTERN`, `FILL`, `NOLOAD`, `EXCLUDE_FILE`,
+`OUTPUT_ARCH`, and `OUTPUT_FORMAT` — see [KERNEL.md](KERNEL.md) for the
+kernel-facing usage. Genuinely unrecognized directives are rejected with a
+diagnostic rather than silently ignored. The linker accepts only the
+structured `KitLinkScript` form —
there is no text setter on the `Linker`; hosts that have GNU-ld text run
the parser first. Input matchers use a `*`-only glob.
diff --git a/doc/OPT.md b/doc/OPT.md
@@ -259,7 +259,7 @@ improve the code are exactly these, none of which needs SSA:
loop-invariant immediates once in the entry block instead of per iteration.
- **`dead_def_elim_with_live`** — liveness-driven pre-RA dead-definition removal.
- **`regalloc_locations`** — point-bitmap linear-scan allocation, **without**
- live-range splitting. A *linear* move-coalescer (O1.md W3) now populates the
+ live-range splitting. A *linear* move-coalescer now populates the
union-find (`opt_coalesce_parent`) before allocation so copy-related values
share a location; the O2-only quality knobs that stay off are live-range
splitting and the O(n²) conflict-**matrix** coalescer (`opt_coalesce_ranges`).
@@ -277,9 +277,9 @@ improve the code are exactly these, none of which needs SSA:
Everything else under §4 (`build_ssa`, `gvn`, `dse`, `licm`, `copy_prop`,
`simplify`, live-range splitting, the O(n²) matrix coalescer) is O2-only and
never runs. (Linear move coalescing *does* run at O1 — see `regalloc_locations`
-above and O1.md W3; only the splitting/matrix variants are O2-only.) The O1.md
-worklist (frame layout, remat, switch/cmp immediates, branch cleanup, inline cap)
-is landed; its passes are noted inline above and recorded in O1.md §5.
+above; only the splitting/matrix variants are O2-only.) The -O1 code-quality
+work (frame layout, rematerialization, switch/cmp immediates, branch cleanup,
+inline cap) is landed; its passes are noted inline above.
The reachability decision lives *outside* this pipeline, in the finalize sweep
(Section 1), identical on every architecture. At module finalization
@@ -461,7 +461,7 @@ transform or analysis; the file paths orient the reader.
`opt_regalloc_locations` is a point-bitmap linear-scan allocator producing the
canonical `Func.preg_locs` location table (hard reg or spill slot per PReg)
without mutating HIR operands. Two coalescers feed its union-find: the **O1
- linear coalescer** `opt_coalesce_linear` (O1.md W3) merges copy-related values
+ linear coalescer** `opt_coalesce_linear` merges copy-related values
using bounded per-root member lists + on-demand range-overlap tests (no
matrix), and `opt_verify_alloc` treats same-root PRegs as one value; the **O2
matrix coalescer** `opt_coalesce_ranges`, gated on live-range splitting, builds
@@ -668,17 +668,80 @@ object links and runs correctly: the ecosystem gate compiles and runs sqlite at
`-O0` and `-O1` against clang.
The three copy/extension folds above (`addr_of [base+0]`, same-width convert,
-ZEXT-of-load) were added after a disassembly audit of the ecosystem `-O1`
-output found the same redundancies recurring: a register move per `&*p`, a move
-per pointer cast, and a `uxtb`/`uxth` after every narrow unsigned load (C's
-integer promotions). Each is a local, target-agnostic canonicalization, so they
-keep `-O1` linear (sqlite `-O1` compile time is unchanged); correctness is held
-by the ecosystem golden + vs-clang run at `-O0`/`-O1` and the toy/opt suites.
-Measured `__TEXT` reduction across the ecosystem corpus (aarch64/Darwin, `-O1`): sqlite −1.9%, lua VM −3.2%,
-`lua/lapi.c` −9.1%, yyjson −4.3%, miniz/lz4 ≈ −3.4%, ~−2.4% summed. The folds
-are not the whole story — `-O1` text is still several times clang's on
-inline-heavy files, because the wins clang gets from GVN / DSE / redundant-load
-elimination and post-inline cleanup are SSA-only and stay parked in the O2
-mid-end (Section 3). Within the no-SSA budget the remaining linear headroom is
-cross-block copy elimination (the per-block `mir_combine` cannot retire a copy
-whose result is live-out) and local rematerialized-constant CSE.
+ZEXT-of-load) are examples of the local, target-agnostic canonicalizations that
+make up the `-O1` density work. Each was added after a disassembly audit of the
+ecosystem `-O1` output found the same redundancy recurring: a register move per
+`&*p`, a move per pointer cast, and a `uxtb`/`uxth` after every narrow unsigned
+load (C's integer promotions). The folds are not the whole story — `-O1` text is
+still several times clang's on inline-heavy files, because the wins clang gets
+from GVN / DSE / redundant-load elimination and post-inline cleanup are SSA-only
+and stay parked in the O2 mid-end (Section 3).
+
+### The shape of `-O1` quality work
+
+Two design constraints bound everything done to improve `-O1` generated code,
+and together they define the method:
+
+- **No SSA — the compile must stay linear.** `-O1` is the no-SSA pipeline of
+ Section 3: local + linear-scan machinery, no dominance-frontier phi insertion,
+ no value numbering, no interference graph. A `-O1` density transform must keep
+ that property — a per-function `O(n·log n)` slot sort or a bounded per-block
+ side table is fine, but anything that needs SSA, a full interference graph, or
+ an O(n²) analysis belongs in the parked O2 mid-end, not here. The synthetic
+ scaling sweep (Section 8) and sqlite `-O1` timing are the guard that a quality
+ change did not add a superlinear axis; the correctness gate is the ecosystem
+ run-and-diff vs clang at `-O0`/`-O1` plus the toy/opt suites (these changes
+ deliberately alter emitted bytes, so byte-identity is *not* the gate).
+
+- **The transforms are linear same-block peepholes.** Within that budget the
+ realized wins are a family of forward, single-block peepholes over the post-RA
+ MIR (most live in `pass_combine.c`, branch-shaped ones in `pass_jump.c`):
+ store-to-load forwarding across a register mismatch, boolean-into-branch fusion
+ (`cset;cbnz` → `b.cc`), redundant-extension drops, single-use copy/shift
+ folding into the consuming op, same-block redundant-load/CSE, and same-block
+ stack dead-store elimination. Each reuses the combiner's existing per-block
+ last-def map and hard-register liveness, so it stays O(1) per instruction. The
+ structural ceiling these *cannot* reach — the cross-block over-spilling that
+ dominates the residual gap on large, high-pressure functions — needs the SSA
+ register allocator and is out of scope for `-O1` (Section 3 / the optimizer
+ roadmap).
+
+### Frame shape drives density
+
+The largest `-O1` density lever is not a peephole but the **frame layout**, and
+it exploits a standing `-O1` asset the `-O0` single-pass path lacks: at `-O1`
+the frame is *fully known before the body is emitted* (`*_func_begin_known_frame`
+fixes the final frame size and slot list up front). Two layout choices follow:
+
+- **Hot-slot-low ordering (all arches).** Body frame slots are ordered so the
+ most-frequently-accessed spills get the smallest final displacement from the
+ layout's addressing base — small offsets are cheapest to encode everywhere
+ (`disp8` vs `disp32` on x64, inside the scaled reach on aa64, inside the ±2 KB
+ `imm12` window on rv64). The per-slot priority is the allocator's spill-cost
+ metric, aggregated over every PReg that shares a reused slot; ordering is a
+ layout choice with no added analysis (one slot sort per function).
+
+- **Positive-offset spill addressing (aa64).** aa64's only wide
+ single-instruction memory form is the unsigned-scaled `ldr/str [base,#pos]`
+ (reach 0..32760); its signed unscaled `ldur` reaches only ±256. So
+ one-instruction slot access requires the addressing base to sit *below* the
+ slots. The aa64 known-frame layout therefore anchors the frame pointer x29 at
+ the *bottom* of the static slots (just above the outgoing-arg area), uniformly,
+ so every slot is a one-instruction positive `ldr/str [x29,#k]` (the
+ `add x16,x29,#hi; ldr [x16,#lo]` build only past the 32 KB scaled reach). This
+ replaces the old top-anchored layout's `sub x17,x29,#k; ldur` fallback — two-to-
+ four instructions per access, recomputed for each access — which on large
+ high-pressure functions was the single biggest `-O1` density cost. x29 is the
+ (always reserved, alloca-stable) frame pointer, so the change is contained in
+ the aa64 backend with no regalloc change, and alloca falls out for free: the
+ saved fp/lr pair is co-located *at* x29 (`[x29]`/`[x29+8]`) to preserve the
+ frame-pointer chain kit's unwinder walks, and outgoing args stay sp-relative so
+ calls after an alloca still address their arg area from the current sp. x64
+ needs none of this — `mov [rbp-disp32]` already reaches any slot in one
+ instruction; rv64's ±2 KB window covers any realistic hot working set, so it
+ takes the shared ordering only.
+
+The wins compound: hot-slot ordering and positive addressing shrink the
+*cost* of each spill, the linear move coalescer (Section 4) lowers the spill
+*count*, and rematerialization (recompute an input-less spilled value at its use
+instead of reloading) cuts spill *traffic*. None of the three needs SSA.
diff --git a/doc/PORT.md b/doc/PORT.md
@@ -0,0 +1,279 @@
+# Portability test surface: `cross` + `selfhost` over one support set
+
+This doc is the spec for kit's portability test surface — the harmonized
+top-level make targets and the scripts beneath them that answer two questions:
+
+1. **cross** — can kit, running on the dev host, cross-compile a *correct*
+ executable for every target in its support set?
+2. **selfhost** — can kit be built to *run on* each target, and then compile +
+ run a program there?
+
+It supersedes the old scattered targets (`test-hosted*`, `test-libc*`,
+`test-freebsd*`, `test-coff-windows-*`, `test-toy-*-vm`, `test-link-x64`,
+`test-parse-rv64-wide`, `bootstrap-linux*`, `bootstrap-freebsd`, the standalone
+`windows_cross.sh`, and the freestanding smokes). See "Migration" at the end.
+
+Related: [BUILD.md](BUILD.md) (the 3-stage self-build mechanism),
+[plan/SYSROOTS.md](plan/SYSROOTS.md) (sysroot provisioning),
+[WINDOWS.md](WINDOWS.md).
+
+## Model: two modes over one matrix
+
+```
+ support set (one canonical list in scripts/hosted.sh)
+ ┌──────────────────────────────────────────────────────┐
+ mode cross │ host kit builds for T → artifact runs correctly on T │
+ mode selfhost│ build a kit that runs on T → it builds + runs a program │
+ │ on T │
+ └──────────────────────────────────────────────────────┘
+ substrate (native / qemu-user / podman / VM / qemu-system) = IMPLICIT,
+ resolved per target by the exec seam; never named in a target.
+```
+
+The support set, one token grammar everywhere: **`<os>[-<libc>]-<arch>`**.
+
+```
+cross matrix (16):
+ linux-{glibc,musl}-{aa64,x64,rv64} (6)
+ freebsd-{aarch64,amd64,riscv64} (3)
+ windows-{aarch64,x64} (2)
+ macos-aarch64 (1)
+ freestanding-{aa64,x64,rv64,rv32} (4) ← rv32 lives here only
+
+selfhost matrix (12): the same, minus freestanding
+```
+
+Freestanding has **no OS**, so there is nothing to run a compiler *on*: it
+participates in `cross` only. `selfhost` of a `freestanding-*` target is a hard
+error.
+
+Arch tokens are the short forms **`aa64` / `x64` / `rv64`** (plus `rv32`,
+freestanding-only). The long forms (`aarch64`/`amd64`/`x86_64`/`riscv64`) are
+accepted as input aliases by `scripts/hosted.sh`.
+
+## Top-level make targets
+
+```
+make test-port # test-cross + test-selfhost
+make test-cross [TARGET=…] [DEPTH=…] [KIT_VM=…] [RUN=0|1]
+make test-selfhost [TARGET=…] [DEPTH=…] [KIT_VM=…]
+make kit-cross [TARGET=…] [CROSS_CC=kit|clang] [VERIFY=0|1]
+make provision [TARGET=…] [KIT_VM=…]
+```
+
+Defaults: `TARGET ?= all`, `DEPTH ?= smoke`, `KIT_VM ?= 1`, `RUN ?= 1`,
+`CROSS_CC ?= kit`, `VERIFY ?= 0`.
+
+`test-port` is **not** part of the default `make test` (it is heavy and needs
+provisioning). The native 3-stage `bootstrap` + `test-bootstrap-toy` stay in the
+default suite as the fast self-reproduction check.
+
+### `TARGET=` selector grammar
+
+Resolved centrally by `scripts/hosted.sh expand <selector> [--mode=cross|selfhost]`:
+
+| Selector | Expands to |
+|---|---|
+| `all` | every support-set token (mode- and `KIT_VM`-filtered) |
+| `linux` / `freebsd` / `windows` / `macos` / `freestanding` | all configs for that OS |
+| `linux-musl` / `linux-glibc` | that libc, all 3 arches |
+| `linux-glibc-x64`, `freebsd-aarch64`, `freestanding-rv32`, … | one exact config |
+| `a,b,c` | comma list of any of the above |
+
+`--mode=selfhost` drops `freestanding-*` from the expansion. `KIT_VM=0` drops
+`freebsd-*` and `windows-*`.
+
+### `DEPTH`
+
+`test-cross` accepts:
+
+- `coarse` — cross-compile and link the smoke artifacts across the expanded
+ support set, but do not execute them. This is the named form of
+ `make test-cross RUN=0`.
+- `smoke` (default) — two cases, building up platform coverage:
+ 1. `exit` — an `#include`-free exit-code program (`test/cross/cases/exit.c`,
+ returns 42): toolchain + link + crt + the exit-code path; on freestanding
+ the bare-metal exit oracle. Runs on **every** config (the only case
+ freestanding can run — no libc there).
+ 2. `hello` — the libc hello-world (`hello.c`, prints + returns 0): the
+ **sysroot headers + libc + stdout**. Hosted configs only.
+
+ Each case runs once per **link mode** (`link_modes`): **musl exercises both
+ `static` (libc.a) and `dynamic` (libc.so)** — the two ways the sysroot is
+ consumed; lanes are suffixed `:static`/`:dynamic`. FreeBSD is static; glibc is
+ dynamic-only (static glibc is discouraged / NSS-fragile); macOS/Windows use
+ their single default shape. So a musl target yields four smoke lanes
+ (exit/hello × static/dynamic), a glibc target two, freestanding one.
+- `full` — the two smoke cases + the toy and parse corpora (cross-compiled and
+ run on the target) + the libc cases (linux only). Orchestrates the existing
+ mature `test/{toy,parse,libc}` runners with the right arch/tag; it does not
+ reimplement them.
+
+`smoke` is the default to honor "prefer targeted runs over mass runs"
+(CLAUDE.md). `DEPTH=full` is the deliberate heavy run, usually with a scoped
+`TARGET`.
+
+`test-selfhost` accepts `smoke` and `full`; it has no build-only `coarse` lane
+because its purpose is to run the newly built compiler on the target.
+
+### `kit-cross` — cross-build the compiler itself
+
+`test-cross` asks "can kit cross-compile a *program* for a target?";
+`kit-cross` asks "can kit (or clang) cross-compile *kit itself* to run on a
+target?". It is the general form of the old `windows_cross.sh`: pick any hosted
+token and a backend, and it produces a runnable `kit` binary.
+
+```
+make kit-cross TARGET=windows-x64 # kit.exe for windows-x64, via kit
+make kit-cross TARGET=linux CROSS_CC=clang VERIFY=1
+```
+
+- `CROSS_CC=kit` (default) dogfoods kit as the cross-compiler; `CROSS_CC=clang`
+ uses an independent clang + lld (and llvm-mingw's compiler-rt for Windows)
+ toolchain — a useful differential check, since clang's strictness surfaces
+ source issues kit's frontend tolerates.
+- `VERIFY=1` runs the freshly built `kit` *on the target* through the same exec
+ seam the tests use (`kit_cross_verify.sh`: bare `kit` prints its banner and
+ exits 0 — exercises load + dynamic-linker + libc init + main).
+- `freestanding-*` is excluded (kit needs an OS to run on): the selector uses
+ `expand --mode=selfhost`.
+- Output: `build/kit-cross/<backend>/<target>/kit[.exe]`. The per-target engine
+ is `scripts/kit_cross.sh`; the makefile loop is in `mk/port.mk`.
+
+The two backends differ only in toolchain wiring, not source: the Windows
+sources are plain-clang-clean (no `__try`, no `_environ` extern, `__thread`
+rather than `__declspec(thread)`), and `kit cc` auto-appends `.exe` to an
+extension-less `-o` for a Windows target exactly as gcc/clang-mingw do, so both
+backends write `kit.exe` directly. The GC flag is the one linker-specific knob:
+GNU/lld and kit ld take `--gc-sections`, Apple ld (clang on darwin) takes
+`-dead_strip`.
+
+## Architecture
+
+### Source of truth — `scripts/hosted.sh`
+
+Already owns the support set and the `triple`/`path`/`tag` resolvers. Extended
+with:
+
+- `hosted.sh list [selector]` — emit tokens (one per line).
+- `hosted.sh expand <selector> [--mode=…]` — the selector grammar above.
+
+The Makefile reads it via `$(shell …)`; nothing else re-encodes the matrix.
+
+### Two orchestrators
+
+- `scripts/cross_test.sh <selector> [DEPTH]` — expand (mode=cross) → **fail-fast
+ provisioning pre-flight** → compile/link all smoke artifacts → optional
+ execution phase (`DEPTH=coarse`, `RUN=0`, or `KIT_CROSS_RUN=0` disables it)
+ → aggregate report.
+- `scripts/selfhost.sh <selector> [DEPTH]` — expand (mode=selfhost) → pre-flight
+ → per-token self-host (dispatch by OS) → on-target corpus → report.
+
+Both share `test/lib/kit_sh_report.sh` for the verdict/summary layer.
+
+### Provision-or-error
+
+Provisioning (network + VM prepare) is **separate** from running, and a requested
+target whose sysroot / image / VM is missing is a **hard error**, never a silent
+skip. The orchestrators do an aggregated pre-flight: they collect *all* missing
+provisioning for the expanded TARGET set, print each with the exact
+`make provision TARGET=…` (or host-tool install) to fix, and exit non-zero
+before running anything.
+
+`make provision [TARGET=…]` wraps `hosted.sh prepare`, `make test-images`, the
+glibc run images, the Windows UCRT sysroots, and VM prepare — scoped by TARGET
+and KIT_VM. rt archives are local build artifacts (not network): the runners
+build them on demand via idempotent `make rt-…`.
+
+### One exec front door, three backends
+
+`test/lib/exec_target.sh` is the single tag-dispatched front door. A tag is
+`<arch>-<os>[-<libc>]`. It routes to one of three backends:
+
+```
+exec_target.sh
+├── stateless os ∈ {linux, macos} → native / qemu-user / podman
+├── exec_vm.sh os ∈ {freebsd, windows} → boot VM, run
+└── exec_bare.sh os == freestanding → qemu-system bare-metal (NEW)
+```
+
+`exec_bare.sh` is the consolidated owner of all per-arch bare-metal scaffolding
+(reset stub + linker script + exit-code oracle + `qemu-system` invocation),
+generalizing the old `exec_rv32_bare.sh` to four arches and absorbing the stubs
+that were inlined in `freestanding_system.sh`. It exposes two contracts:
+
+- `exec_bare_run_image <arch> <kernel.elf> <out> <err> <rc>` — run a ready
+ bootable image (what `test/link`'s `kernel_image` cases need; `exec_kernel.sh`
+ is now a thin shim over this).
+- `exec_bare_setup <arch> <work>` + `exec_bare_run <arch> <obj> <work> <rc>` —
+ link a corpus `.o` (entry `main`, returns the exit code) with the per-arch stub
+ + rt into a bootable image, then run it (what toy/parse need).
+
+Exit-code oracle per arch (so callers compare `rc == expected` uniformly):
+
+| arch | mechanism | decode |
+|---|---|---|
+| aa64 | ARM semihosting `hlt #0xf000` + `ADP_Stopped_ApplicationExit` | qemu rc = guest code |
+| rv64/rv32 | SiFive test finisher MMIO at `0x100000` | stub writes `0x3333\|(code<<16)` (or `0x5555` for 0); qemu rc = code |
+| x64 | `isa-debug-exit` (iobase `0x501`) | qemu rc = `(code<<1)\|1` → code = `(rc-1)>>1` |
+
+## Self-host shapes (the substrate asymmetry)
+
+`selfhost.sh` dispatches by OS, because "build a kit that runs on T" differs:
+
+- **macos** → native 3-stage `bootstrap` here, then the corpus through stage3.
+- **linux** → native 3-stage in a podman container (emulated for non-host arch),
+ then the corpus. Generalizes `scripts/linux_bootstrap.sh` to (arch, libc).
+ **rv64 uses a hybrid seed**: an in-container clang stage1 would itself run
+ emulated (slow), so the stage1 seed is cross-built on the host with clang
+ (`kit_cross.sh linux-musl-rv64 --cc=clang`, native speed) and fed to
+ `mk/bootstrap.mk` via `BOOTSTRAP_SEED`; only stages 2/3 run emulated. They are
+ still byte-identical because the seed and stage2 are the same kit source, hence
+ functionally identical compilers. Emulated builds write objects to
+ container-local storage (the `:Z` virtiofs mount flakes new-file creates under
+ sustained emulated write load) and copy the stage kits back.
+- **freebsd** → native 3-stage in the VM, then the corpus
+ (`scripts/freebsd_bootstrap.sh`).
+- **windows** → **cross**-build `kit.exe` on the host (the VM has no seed
+ compiler), then run it on the VM to cc + run a program
+ (`scripts/windows_cross.sh`).
+
+Every selfhost leaf ends with the same contract: *a kit running on the target
+compiled and ran a program there.*
+
+## Status & phased backlog
+
+`cross`:
+
+| Config | coarse | smoke | full |
+|---|---|---|---|
+| linux-{glibc,musl}-{aa64,x64,rv64} | ready | ready | ready (toy X + parse E + libc) |
+| macos-aarch64 | ready | ready (native) | ready (native toy/parse) |
+| freebsd-{aarch64,amd64,riscv64} | ready | ready (VM) | ready (toy via VM) |
+| windows-{aarch64,x64} | ready | ready (VM) | ready (toy via VM) |
+| freestanding-rv32 | ready | ready (bare) | ready (toy X + parse E, bare) |
+| freestanding-{aa64,x64,rv64} | ready | ready (bare smoke) | **deferred** |
+
+`selfhost`: macos (native), linux musl/glibc aarch64 (container), linux x64/rv64
+(container under emulation), freebsd aarch64/amd64 (VM), windows aa64/x64 (cross
++ VM).
+
+### Backlog: freestanding `DEPTH=full` for aa64 / x64 / rv64
+
+Running the toy/parse corpora bare-metal on these three arches is new capability
+beyond the smoke payload. The smoke stubs in `exec_bare.sh` are minimal; the
+corpus exercises TLS, soft-float, large frames, i128, etc., which need hardened
+per-arch stubs (the rv32 path already has this — its reset stub seeds a static
+TLS image and enables the FPU). Work items:
+
+1. Harden the aa64/rv64 `exec_bare` stubs (TLS image seed + thread-pointer setup,
+ matching the rv32 stub and the toy X-lane `start.c`).
+2. Build the x64 long-mode corpus stub (the rv32/aa64 reset path + a full
+ exit-code oracle via `isa-debug-exit`).
+3. Wire a bare lane into `test/{toy,parse}/run.sh` for aa64/x64/rv64 (the rv32
+ "X / V" path generalized), gated on `qemu-system-<arch>`.
+4. Flip the table rows above from **deferred** to **ready** and drop this section.
+
+Until then, `test-cross TARGET=freestanding-{aa64,x64,rv64} DEPTH=full` runs the
+smoke payload and `log`s that corpus-depth is not yet wired for the arch (it does
+not silently claim full coverage).
diff --git a/doc/WINDOWS.md b/doc/WINDOWS.md
@@ -5,9 +5,47 @@ kit's Windows targets are PE/COFF, 64-bit only:
- `x86_64-windows`
- `aarch64-windows`
-The hosted profile is MinGW-w64 UCRT via llvm-mingw. kit uses the target
-headers, CRT objects, and import libraries from that sysroot; it does not use
-llvm-mingw's compiler, assembler, or linker tools for the cross-compile path.
+The hosted profile is MinGW-w64 UCRT via llvm-mingw (not MSVC): kit advertises
+`__MINGW32__`/`__MINGW64__`, never `_MSC_VER`. kit uses the target headers, CRT
+objects, and import libraries from that sysroot; it does not use llvm-mingw's
+compiler, assembler, or linker tools for the cross-compile path.
+
+## Native self-host
+
+kit cross-builds into a native Windows `kit.exe` that runs as a Windows
+toolchain. `scripts/windows_cross.sh aarch64` uses the host `build/kit` as the
+cross-compiler to produce a PE32+ console `kit.exe`, overriding `HOST_OS=windows
+HOST_ARCH=aarch64` so `mk/env.mk` selects `driver/env/windows.c`. (There is no
+seed C compiler in the VM, so the Windows binary is always cross-produced on the
+dev host — unlike the native Linux/FreeBSD bootstraps.) On the ARM64 Win11 VM
+that `kit.exe` runs `kit cc` (compile + link real C programs, building
+`libkit_rt` on demand) and `kit run` (the in-process JIT, including external
+calls and libc I/O via `dlsym`) natively; the Toy AOT corpus passes when
+compiled, linked, and executed by the native `kit.exe`.
+
+Windows-specific points the codegen and link paths have to honor:
+
+- **Large-frame stack probe.** Windows commits stack pages lazily via a guard
+ page, so a function whose frame exceeds one page must touch each page on entry
+ or it faults past the guard. The backends emit a probe (an inline page-walk on
+ aarch64, `__chkstk` on x64) at the ABI-defined interval
+ (`abi_stack_probe_interval`); without it, large-frame functions — including
+ many inside kit itself — crash on the VM.
+- **`setjmp`.** mingw's `setjmp.h` expands `setjmp` to `_setjmp` declared as a
+ COFF `WEAK_EXTERNAL` aliasing `__intrinsic_setjmp` from the CRT-private
+ `api-ms-win-crt-private-l1-1-0.dll` api-set, which fails to load on some
+ runtimes. `libkit_rt` exports strong `_setjmp`/`_setjmpex` (over `__mingw_setjmp`)
+ so the alias is never pulled; on the Windows link line `libkit_rt.a` is placed
+ before the hosted libc group so its strong `_setjmp` is defined before
+ libucrt.a's lazy scan runs (and a second time before `crtend.o`, to catch the
+ late `__chkstk` undefs that libucrt members introduce — lazy archive scanning
+ keeps either placement from defining a symbol twice).
+- **COFF imports.** The COFF reader/linker handles short-import library members
+ and `EXPORTAS` so DLL imports whose export name differs from the import name
+ resolve correctly.
+- **JIT executable memory.** `kit run`'s JIT maps code pages with
+ `FILE_MAP_WRITE` then flips them executable, the Windows analog of the POSIX
+ `mmap`/`mprotect` W^X dance.
## UCRT Sysroots
diff --git a/doc/ideas/README.md b/doc/ideas/README.md
@@ -0,0 +1,11 @@
+# Ideas
+
+Speculative, not-yet-committed designs: sketched in full, but with no code in the
+tree and no scheduled work. They are parked here — out of the active roadmaps in
+`doc/plan/` — until something is actually built against them. Treat each as a
+captured idea, not a plan of record.
+
+| Sketch | Scope |
+|--------|-------|
+| [RQL.md](RQL.md) | A typed relational query language (a clean-break SQL replacement): surface syntax, logical algebra/IR, physical plan, algebraic laws, option types instead of NULL. |
+| [RSN.md](RSN.md) | A statically-typed scripting language to replace make/shell/Python for kit's build and test orchestration: typed CG-API frontend, staged arena/GC runtime, cooperative fibers. |
diff --git a/doc/plan/RQL.md b/doc/ideas/RQL.md
diff --git a/doc/plan/RSN.md b/doc/ideas/RSN.md
diff --git a/doc/plan/ARCH.md b/doc/plan/ARCH.md
@@ -1,182 +0,0 @@
-# Arch-Backend Completeness (planned work)
-
-This roadmap consolidates the *remaining* native-backend work across the three
-machine-code targets (aa64, x64, rv64). The bulk of the NativeTarget port -- the
-single-pass (-O0) path and the known-frame (-O1) path for all three arches, plus
-the asm/disasm/link-reloc/dwarf matrix -- is already in tree and is treated here
-as the **baseline**, not as planned work. What follows is the genuinely-open
-follow-up: per-arch hooks where x64/rv64 still trail the aa64 reference,
-prologue/epilogue and tail-call cost-model parity, and a small set of niche
-asm/disasm and debugger gaps. The backend abstraction and ABI layer this work
-sits behind are documented in the design set:
-[../CODEGEN.md](../CODEGEN.md), [../OPT.md](../OPT.md), [../ASM.md](../ASM.md),
-[../DWARF.md](../DWARF.md).
-
-## Baseline (done -- context, not planned work)
-
-- All three backends implement the full NativeTarget vtable on both the
- single-pass (`NativeDirectTarget`) and known-frame (`func_begin_known_frame`)
- paths: prologue/epilogue, `bind_param`, frame slots, calls/returns, atomics,
- variadics, inline/file-scope asm, TLS (Local-Exec), and intrinsics.
-- x64 carries both ABIs (SysV + Win64), including shadow space, callee-saved
- XMMs, the `__chkstk` large-frame probe, and the SysV 176-byte variadic
- register-save area. rv64 covers LP64D with the s0-anchored frame and Zba/Zbb
- use where available.
-- asm/disasm/link-reloc/dwarf parity across the OS matrix (ELF/COFF/Mach-O as
- applicable) is in place: rv64 relocation emission, the x64 `.eh_frame` RBP
- DWARF-reg fix, the aa64 FP/SIMD and x64 SSE disasm rows, named params/locals in
- x64 and rv64 DWARF, and the shared LEB128 / `.comm` assembler directives.
-
-The items below are what is *not* yet at aa64 parity.
-
-## 1. Tail-call realization on x64 and rv64 (blocker-check removal)
-
-aa64 realizes sibling (tail) calls whenever the outgoing stack-argument area fits
-the caller's incoming parameter window -- `aa_no_tail` returns a blocker *only* on
-that size check. The same is true of the size check on x64 and rv64 (x64 even
-accounts for the shadow-space prefix), and the restore-before-jump *machinery*
-already exists on both: `x64_emit_tail_site` / `rv_emit_tail_site` emit the
-callee-save restore and frame teardown ahead of the tail jump exactly the way
-aa64 does. What remains is conservatism in the realizability gate -- `x64_no_tail`
-and `rv_no_tail` still bail out with `"callee-saved registers in use"` whenever
-the function has any callee-save live (`frame.ncallee_saves != 0`), so those
-functions fall back to a normal call + return even though the tail site could
-handle them.
-
-This is the single largest aa64-vs-rest divergence and matters most for the
-recursion-heavy / interpreter-dispatch workloads that the O(1)-tail-call work
-targets (see the interpreter and toy `musttail` tracks).
-
-- Remove the `ncallee_saves` guard from `x64_no_tail` / `rv_no_tail` so the
- size check alone gates realizability, letting the existing tail-site restore
- sequences run for callee-saves-live functions.
-- x64: confirm the existing restore ordering interacts correctly with the SysV
- vs Win64 callee-save sets (Win64 also saves XMM6-15) and with a forwarded sret
- pointer before lifting the guard.
-- rv64: confirm the s2-s11 / fs2-fs11 and s0/ra restore-then-`jr` sequence holds
- for the previously-blocked frames once the guard is gone.
-- Extend the tail-call test corpus to cover the callee-saves-live case on x64 and
- rv64, since those paths were unexercised while the guard masked them.
-- Win64 FP-arg tail-call interaction is noted as a deferred sub-case in the port
- notes; validate or document the restriction explicitly.
-
-## 2. Prologue / epilogue cost-model parity (per-call overhead)
-
-The fixed per-call overhead -- prologue + epilogue + arg setup, independent of the
-body -- is the dominant cost on call-heavy code. aa64 picks one of four frame
-shapes per function to minimize it. x64 and rv64 now select a cheaper known-frame
-shape too (see **Done** below); the design rationale lives in
-[../ARCH.md](../ARCH.md); the aa64 measurements and the remaining body-level warts
-are tracked alongside [../OPT.md](../OPT.md) and [OPTIMIZER.md](OPTIMIZER.md).
-
-aa64 tiers (baseline, for reference):
-
-| tier | when | fixed insns |
-| --- | --- | ---: |
-| `slim_prologue` (Tier A) | no callee-saves, no alloca, no body slots, no outgoing stack | 3 (optimal) |
-| `fp_at_bottom` | >=1 callee-save/body slot, **no outgoing stack args**, frame <= 504 | 5 (optimal) |
-| `slim_small_frame` | as above but with outgoing stack args | 7 |
-| fat | large frame / alloca / big saved-pair offset | 7+ |
-
-The known-frame asymmetry (bottom-record only on the -O1 path) is intentional:
-the frame-size-dependent offsets require the frame to be final before the body,
-which only the optimizer's frame planner guarantees.
-
-Leaf-ness is surfaced to the backends through `NativeKnownFrameDesc.is_leaf`
-(set in `plan_frame`, `pass_native_emit.c`, as "no `IR_CALL` of any kind --
-regular or sibling/tail"). A leaf never clobbers the return-address register or
-the stack below sp, which is what unlocks the no-frame / red-zone shapes below.
-
-Done:
-
-- **x64 slim + red-zone tiers** (`x64_func_begin_known_frame`). Two known-frame
- shapes, both keeping the `push rbp; mov rbp,rsp` record (so the `leave`
- epilogue, the `CFA = rbp+16` CFI, and every rbp-relative offset are unchanged)
- and only dropping the `sub rsp` reservation:
- - `slim_frame` -- empty frame (no callee-saves, no body slots, no outgoing
- args, no alloca). Safe for non-leaves too: `push rbp` keeps rsp 16-aligned
- for any register-only call, and nothing lives below rsp. SysV + Win64.
- - `redzone_leaf` -- SysV leaf with a small frame (`is_leaf`, no alloca, no
- outgoing args, `frame_size <= 128`). Locals/callee-saves stay at their
- rbp-relative offsets, which now land in the 128-byte red zone. Leaf-only,
- since any call would clobber the red zone; Win64 (no red zone) is excluded
- by the `shadow_space == 0` gate.
-
- No x64 *fold* tier: `push rbp` already folds the sp-move into the store, so
- there is no aa64-`fp_at_bottom`-style win to capture.
-- **rv64 leaf tier** (`rv_func_begin_known_frame`, `slim_prologue`). A leaf with
- no callee-saves, no body slots, no outgoing args, no sret/variadic and
- register-only params (`signature_stack_bytes == 0`) never reads s0 nor clobbers
- ra (both are reserved, never allocable), so it emits **no prologue** and a bare
- `ret` -- the whole frame setup/teardown is elided (~8 insns/leaf). CFI is
- `def_cfa(sp, 0)`, matching the CIE default (ra stays live in its register).
-- **rv64 frame fold: intentionally not ported.** Porting aa64's `fp_at_bottom`
- to rv64 was measured at a **zero** instruction win: RISC-V has no
- pre/post-indexed store, so moving the saved s0/ra pair to the bottom still
- needs a separate `addi sp,sp,-N` plus the `sd`/`addi s0,sp` -- the same four
- instructions as the top-record shape. The fold only relocates data, it removes
- no instruction, so it was skipped rather than add a fold-aware offset-helper
- layer for no benefit. (Per the "quantify the win before committing" guidance
- that previously stood here.) The rv64 leaf tier above is the real rv64 win.
-
-Still open:
-
-- **Cost-model alignment.** `signature_stack_bytes` / `call_stack_bytes` are the
- shared hooks the optimizer uses to size the outgoing area and gate tail-call
- realizability; they exist on all three. As the tail-call paths (section 1)
- land, verify the optimizer's per-call cost estimates reflect the cheaper
- shapes so frame/spill decisions stay consistent across arches.
-
-Body-level per-call warts from the aa64 study that are arch-shared and still
-open:
-
-- **Redundant branch chain.** An if/else merge can emit `b A; A: b B` -- a
- conditional branch to a label that just unconditionally branches onward.
- `cleanup_layout_fallthrough_branches` in the jump pass does not yet thread this
- shape; this is an optimizer pass fix, surfaced per-arch at the call site.
-
-## 3. x64 debugger step-out / unwind
-
-`kit_dwarf_unwind_step` has no memory provider, and x64 (unlike aa64/rv64, which
-have a link register) has no link-register fallback, so step-out can't recover the
-return address from the stack. Compounding it, the JIT debugger doesn't populate
-`.eh_frame` for in-process images.
-
-- Add a memory-reading unwind variant so the unwinder can read the saved RA /
- RBP from the stack on x64.
-- Populate `.eh_frame` (or an equivalent CFI source) for JIT in-process images so
- the debugger has unwind data to consume.
-
-This is a debugging-UX robustness item with test-infra dependencies; see
-[../DBG.md](../DBG.md) and [../DWARF.md](../DWARF.md). Sibling debugger roadmap:
-[DEBUG.md](DEBUG.md).
-
-## 4. Niche assembler / disassembler gaps
-
-These are in the standalone `as` / inline-`asm()` encode-decode paths only. The
-compiler's codegen emits machine code directly and never routes through the text
-assembler, and the shipped runtime `.s`/`.S` files don't use these forms, so
-none of this blocks any build. They are GNU-as / llvm-mc parity gaps for
-hand-written assembly. Design context: [../ASM.md](../ASM.md).
-
-- **aa64 atomics, remaining encode forms.** `CASP`, the LSE min/max family
- (`ldsmax`/`ldsmin`/`ldumax`/`ldumin`), and `LDAPR`/`STLLR` are not yet encoded.
-- **aa64 disasm rows for the new encode-only forms.** The recently-added
- exclusive/LSE atomics, register-offset, and writeback load/store forms encode
- correctly but have no decode rows, so a round-trip currently renders them as
- `.inst`. Add the matching disasm rows.
-- **TLS relocation modifiers in operands.** `:tprel_*:` (aa64) and `%tls_*`
- (rv64) operand syntax is not yet accepted; the non-TLS modifiers
- (`:lo12:`/`:got:`, `%hi`/`%lo`/`%pcrel_*`, x64 `@PLT`/`@GOTPCREL`) are done.
-- **`.L`-prefixed local-label spellings in operand references.** Plain labels
- work (including as the `%pcrel_lo` anchor); the `.L`-prefixed spelling in an
- operand position is a shared-lexer change.
-
-## 5. Cross-cutting hygiene
-
-- Keep the three backends converging on the shared `NativeFrame` / `native_argmove`
- (parallel-copy shuffle) scaffolding rather than re-implementing per-arch; new
- fold tiers and tail-call paths should reuse it.
-- As each gap above closes, prefer locking it in with a targeted corpus case
- (per-arch, per-form) over broad sweeps, per the testing guidance in
- [../TESTING.md](../TESTING.md).
diff --git a/doc/plan/ARM32.md b/doc/plan/ARM32.md
@@ -32,7 +32,7 @@ producing and consuming correct `arm-none-eabi` ELFCLASS32 objects and static
executables for **ARMv7-M (Cortex-M3)** and **ARMv7E-M (Cortex-M4/M7)**,
validated under `qemu-system-arm` exactly the way `riscv32-none-elf` is validated
under `qemu-system-riscv32` (the `cross` matrix, `freestanding-*` only — see
-[PORT.md](PORT.md)). The recently-closed rv32 work is the direct precedent: it
+[PORT.md](../PORT.md)). The recently-closed rv32 work is the direct precedent: it
solved 32-bit-on-a-32-bit-arch legalization and the freestanding bare-metal
test lane; arm32 reuses both.
@@ -277,7 +277,7 @@ branch targets; since everything is Thumb on Cortex-M, BL never relaxes to BLX.
arm32 is **freestanding, cross-only**: it joins the `cross` matrix as
`freestanding-arm32`, never `selfhost` (no OS to run a compiler on). The exec
seam already has the right backend — `test/lib/exec_bare.sh`, the `qemu-system`
-bare-metal front door ([PORT.md](PORT.md) "One exec front door, three
+bare-metal front door ([PORT.md](../PORT.md) "One exec front door, three
backends"). We add an `arm32` arm to it, plus a smoke oracle and the corpus
lanes.
@@ -487,4 +487,4 @@ kit-compiled reset stub (needs kit `as` for MSR/MRS/BKPT or a C+inline-asm stub)
template `rv32.sh`), `test/toy/run.sh` (`cross_one_arm32`), `test/parse/run.sh`
(E lane) + `test/lib/kit_test_target.h`, `test/arch/arm32_decode_test.c`,
`test/elf/unit/arm32_class32.c`, `test/link/arm32_jit_test.c`,
- `scripts/hosted.sh`, `mk/{test,test_unit}.mk`. Design of record: [PORT.md](PORT.md).
+ `scripts/hosted.sh`, `mk/{test,test_unit}.mk`. Design of record: [PORT.md](../PORT.md).
diff --git a/doc/plan/BOOTSTRAP.md b/doc/plan/BOOTSTRAP.md
@@ -1,281 +0,0 @@
-# Self-Build Bootstrap (current state and roadmap)
-
-This roadmap covers the staged self-build of kit: building the compiler with
-itself until it reproduces its own output byte-for-byte. The mechanics and
-products of the build are described in [../BUILD.md](../BUILD.md); this document
-tracks the reproducibility goal, the current baseline, the open problems that
-remain, and the next steps for widening coverage. The bootstrap is the strongest
-end-to-end correctness oracle in the project, because it exercises the C
-frontend, every optimizer pass, the native backends, the object writers, the
-linker, and the archive tools, all on the compiler's own source.
-
-## Goal: a self-reproducing fixed point
-
-The bootstrap builds kit three times and requires the last two stages to be
-identical:
-
-- **stage1** = the host-built kit, copied aside, exposing `cc`/`ld`/`ar`/`ranlib`/`as`.
-- **stage2** = the whole tree rebuilt with stage1 as the toolchain (`CC`/`AR`/`LD`).
-- **stage3** = the whole tree rebuilt again with stage2 as the toolchain.
-- The invariant is `cmp stage2/kit stage3/kit` — they must be byte-identical.
-
-Stage2 vs stage3 is the fixed point: once the compiler reproduces itself, a third
-pass cannot change anything. The bootstrap drives the *normal* Makefile with
-`CC`/`AR`/`LD` repointed at each stage's symlinks, so there is no separate build
-system to maintain — it is the same rules run with kit as the toolchain. This
-depends on the reproducible-build guarantees in [../BUILD.md](../BUILD.md)
-(deterministic ordering, no embedded timestamps/paths); any nondeterminism in
-codegen or object layout surfaces here as a stage2/stage3 mismatch.
-
-Driving targets (see [../BUILD.md](../BUILD.md)):
-
-- `make bootstrap` runs both the debug (`-O0`) and release (`-O1`) chains.
-- `make bootstrap-debug` / `make bootstrap-release` run one chain.
-- `make test-bootstrap-toy` additionally runs the Toy corpus through the
- bootstrapped compiler as a behavioral check on top of the byte-identity check.
-
-## Current baseline
-
-Done (baseline) on aarch64-macos:
-
-- Both the `-O0` (debug) and `-O1` (release) chains reach the fixed point:
- `cmp stage2/kit stage3/kit` is clean, and the per-object check across all
- `*.o` in stage2 vs stage3 reports zero differences in both modes.
-- Both bootstrapped compilers run the full Toy corpus clean (1034 pass, 0 fail,
- 8 skip) across the run, link/native, C-backend, and Wasm paths at Toy opt
- levels 0 and 1.
-
-Done on aarch64-linux (ELF), run natively inside an arm64 Linux container from
-the macOS host (see "Bootstrapping a Linux target from a non-Linux host" below):
-
-- **musl** (alpine): both the `-O0` and `-O1` chains reach the fixed point;
- `cmp stage2/kit stage3/kit` is clean and the per-object check across all 321
- `*.o`/`*.a` in stage2 vs stage3 reports zero differences in both modes. The
- bootstrapped stage3 runs the Toy corpus at 1365 pass / 15 fail / 39 skip; the
- 15 failures are not bootstrap/codegen issues (per-object is byte-identical) —
- they are Mach-O-tuned `.objdump` golden substrings that differ on ELF, emitted
- C the container's host clang rejects under `-Werror`, and one Linux JIT-TLS
- `.tdata`-init discrepancy (see below).
-- **glibc** (debian): reaching the fixed point required a series of kit
- C-frontend / preprocessor compatibility fixes for the glibc + Linux-UAPI
- header set (musl's ISO-C headers never exercised them): erase `__extension__`
- on all targets, map `__signed__`/`__volatile__`/`__const__` to the canonical
- keywords, and support GNU named variadic macro parameters (`args...`) in the
- preprocessor.
-
-Done on aarch64-freebsd (ELF), run natively inside the FreeBSD aarch64 VM from
-the macOS host (`scripts/freebsd_bootstrap.sh aarch64`; see "Bootstrapping a
-Linux target from a non-Linux host" — the FreeBSD VM path is the same shape):
-
-- **Both the `-O0` (debug) and `-O1` (release) chains reach the fixed point**:
- `cmp stage2/kit stage3/kit` is byte-identical in both modes. The bootstrapped
- stage3 runs the Toy corpus at 1378 pass / 2 fail / 39 skip in both chains; the
- 2 failures are the JIT-TLS `.tdata`-init `R`-lane discrepancy
- (`141_threadlocal_mutate`) — a non-bootstrap gap shared with aarch64-linux
- (the in-process JIT does not set up a per-thread TLS block / copy the `.tdata`
- initializer; the native-link path is already `.link.skip`-gated), not a
- codegen issue.
-
- Four fixes were needed, in the order they surfaced:
- - kit `cc` accepting `-rdynamic` (FreeBSD's `HOST_ENV_LDFLAGS` passes it; the
- other ELF hosts do not).
- - **ELF symbol-version (Verneed/Versym) emission.** FreeBSD's INO64 transition
- left `stat`/`fstat`/... as two incompatible `struct stat` ABIs behind a
- hidden `FBSD_1.0` (compat) and the default `FBSD_1.5`; kit emitted
- *unversioned* undefined references, so the runtime bound the compat version
- and read `st_size` at the wrong offset — stage2 then failed to read its own
- source files. The linker now reads each DSO's `.gnu.version_d`/`.gnu.version`
- and emits a matching `.gnu.version_r` + `.gnu.version`, gated on the DSO
- carrying versions (musl/static links unchanged; glibc links gain correct
- `GLIBC_*` requirements).
- - **(`-O1`) Deferred-symbol globalization in the assembler.** `-O1` *deferred*
- anonymous const-data / jump-table symbols (`.Lkit_ro.N` / `.Lkit_jt.N`) are
- LOCAL tombstones (`obj_symbol_defer`, `removed=1`) until
- `opt_whole_module_finalize` materializes them. `promote_undef_externs`
- (`src/asm/asm.c`) — which globalizes undefined LOCAL externs — walked every
- slot *including tombstones* (the only `obj_symiter` consumer not honoring the
- `removed` contract) and flipped them to defined GLOBALs. It bit FreeBSD
- because `<stdlib.h>` injects a file-scope `__asm__(".symver …")` whose replay
- runs that pass *before* the deferred data is materialized, so the four hosted
- `driver/env/*.o` each defined a global `.Lkit_ro.0` → `duplicate definition
- of global symbol '.Lkit_ro.0'`. Fix: skip `removed` tombstones. Not
- FreeBSD-specific — any TU with a file-scope `asm` + a deferred const-data
- symbol at `-O1` reproduces it on any target.
- - **(`-O1`) `--gc-sections` rooting of DSO back-references.** With the above
- fixed the stage2 link succeeded, but the `-O1` `kit` failed to *load*
- (`ld-elf.so.1: /lib/libc.so.7: Undefined symbol "__progname"`): the release
- chain links `-Wl,--gc-sections`, and kit's section-GC liveness
- (`src/link/link_resolve.c`) did not root executable definitions that a
- linked DSO references, so `__progname`/`environ` (crt-defined, needed by
- `libc.so.7`) were collected out of the dynsym. `read_elf_dso` now records
- each DSO's undefined-symbol names and the GC pass roots the executable's
- definitions of them — matching GNU ld's default "keep what shared libraries
- need" behaviour (`-O0` has no `--gc-sections` and is unaffected).
-
-This gives four fully self-hosting configurations — aarch64-macos,
-aarch64-linux (musl + glibc), and aarch64-freebsd — each at both `-O0` and
-`-O1`. The remaining work is breadth: the other native targets (x86-64, rv64),
-and guarding the property over time.
-
-## Open problems and next steps
-
-### Widen target and platform coverage
-
-The fixed point holds for aarch64-macos, aarch64-linux (musl + glibc), and
-aarch64-freebsd. The bootstrap should hold for every supported native target
-and object format. Until
-each is green it is an open question whether its backend + object writer are
-fully deterministic and self-consistent.
-
-- [ ] Reach the fixed point on x86-64 (ELF and Mach-O) for both `-O0` and `-O1`.
-- [ ] Reach the fixed point on rv64 (ELF) for both `-O0` and `-O1`.
-- [x] Reach the fixed point on aarch64-linux (ELF), distinct from the macOS
- Mach-O path already covered. Done for musl and glibc; the aarch64-linux
- backend + ELF writer are confirmed deterministic and self-consistent.
-- [x] For each new configuration, run the per-object diff and the Toy corpus
- through the bootstrapped compiler, not just the final `cmp`. Done for
- aarch64-linux (321/321 objects identical; Toy 1365/15/39).
-
-### Bootstrapping a Linux target from a non-Linux host
-
-`make bootstrap` keys off the build host's own `uname` (`HOST_OS` + machine), so
-it selects the native toolchain and object format with no cross-compilation. To
-bootstrap aarch64-linux from the macOS dev host, run the normal three-stage
-build *inside* an arm64 Linux container, where it is an ordinary native build:
-
-- `scripts/linux_bootstrap.sh [musl|glibc] [both|debug|release]` drives a podman
- container (alpine for musl, debian for glibc — the same image families the
- hosted test suite uses), provisions a seed clang + make + libc headers, and
- runs `make bootstrap` with the stage tree under `build/linux-boot/<libc>/`.
- `KIT_LINUX_BOOT_TOY=1` additionally runs the Toy corpus through stage3.
-- `make bootstrap-linux` (→ `-musl`) / `make bootstrap-linux-glibc` wrap it.
-
-Three host-environment differences from the macOS reference, all handled by the
-script / `mk/bootstrap.mk`:
-
-- **LeakSanitizer.** The `-O0` chain builds stage1 with ASan+UBSan as on macOS,
- but LSan (unsupported on Darwin, so never run there) flags kit's arena
- allocator — which deliberately never frees — and aborts every stage1 `cc`. The
- container sets `ASAN_OPTIONS=…:detect_leaks=0`, the honest equivalent of
- Darwin's behavior.
-- **`-lc` for the kit-compiled stages.** On macOS kit gets its system headers via
- the `-isysroot` in `HOST_SYSROOT_CFLAGS`; on Linux that is empty and kit's
- hosted profile only wires up the libc include + library dirs once libc is
- requested, so `mk/bootstrap.mk` passes `HOST_SYSROOT_{C,LD}FLAGS=-lc` to the
- stage2/3 sub-makes (Linux/FreeBSD only).
-- **glibc header compatibility.** glibc + Linux-UAPI headers use GCC-isms musl's
- cleaner headers don't; reaching the glibc fixed point needed the C-frontend /
- pp fixes listed in the baseline above.
-
-Non-bootstrap gaps surfaced by the aarch64-linux Toy run (tracked, not
-fixed-point blockers — the per-object diff is byte-identical):
-
-- **JIT-TLS `.tdata` initializer.** `141_threadlocal_mutate` returns 3 instead
- of 43 on the R (in-process JIT) lane: the JIT zero-initializes the
- thread-local block instead of copying its `.tdata` initializer (40). Native
- link of this case is already `.link.skip`-gated; the JIT path is the gap.
-- **`.objdump` golden substrings** for a few cases (`122_data_entsize`,
- `127_switch_forced_jump_table`, `62_decl_data_attrs`) encode Mach-O section /
- entsize spellings and need ELF-flavored sidecars.
-- **C-backend emitted source** for ~7 cases is rejected by the container's host
- clang under `-Wall -Wextra -Werror` (host-toolchain strictness, varies by
- clang version).
-
-The native-ELF Toy `L` lane links hosted (`kit cc -lc`, so the crt provides
-`_start`) rather than freestanding `kit ld`, because an ELF executable needs a
-crt entry where Mach-O drives `LC_MAIN` straight to `main`; `test/toy/run.sh`
-selects this automatically on non-Darwin hosts (`KIT_TOY_L_HOSTED`).
-
-These connect to the per-arch backend state tracked in [../CODEGEN.md](../CODEGEN.md)
-and [../ARCH.md](../ARCH.md), and to the object/format paths in
-[../OBJ.md](../OBJ.md) and [../LINK.md](../LINK.md) / [LINKER.md](LINKER.md). A
-new arch's first bootstrap is also the most thorough regression test those
-components get.
-
-### Guard the property over time
-
-The fixed point is easy to break with a single nondeterministic or
-miscompiling change, and a regression is expensive to bisect after the fact.
-
-- [ ] Run `make bootstrap` (or at least one chain) in CI on the reference host so
- breakage is caught at the offending change, not after multiple commits
- have been made.
-- [ ] Keep the per-object diff available as a first-line triage signal: it points
- directly at the diverging translation unit, which is far cheaper than
- diffing whole linked binaries.
-
-### Cross-bootstrap (stretch)
-
-The current chains are native (host arch building host arch). A cross-bootstrap —
-host kit building a stage2 for a *different* target, then validating that
-stage2 reproduces a stage3 when run under [../EMU.md](../EMU.md) or on hardware —
-would prove the backends independent of the host. This is a stretch goal that
-depends on the emulator being able to host the full compiler.
-
-## Triage playbook for fixed-point regressions
-
-When a stage2/stage3 mismatch (or a stage3 link failure) appears, the following
-approach has proven effective and should be the default starting point.
-
-**Use object reproduction, not just "does it link", as the oracle.** A stage3
-link failure is usually a *symptom* of a malformed object emitted earlier, not a
-linker bug. The decisive question is whether stage2, used as a compiler,
-reproduces the same `.o` that the host-built compiler produces. Compile one
-suspect TU with both the host `kit` and the stage2 `kit` using identical
-flags, then `cmp` the two objects. This separates malformed-object bugs from
-link-driver symptoms and points straight at the diverging codegen.
-
-**Narrow with hybrid relinks.** Relink stage2 after replacing one suspect TU (or
-one piece of a split TU) with a clang-built object, then use that stage2 to
-compile the known-differing target object. This isolates whether a failure is in
-the linker itself or in codegen for a specific source file.
-
-**Inspect MIR around the suspect symbol.** A temporary filtered MIR dump around
-the target function, taken after lowering and the combine pass, is usually enough
-to see the divergence (e.g. a call argument that should reference an allocable
-register but instead references a backend scratch register).
-
-**Avoid `-g` while triaging `-O1` codegen.** Debug info changes object layout and
-can create or hide layout-sensitive bugs; one historical "regalloc" diagnosis was
-actually a `-g` artifact. Triage on the non-`-g` object first.
-
-## Root-cause classes seen at the fixed point
-
-These bug classes were responsible for past `-O1` fixed-point and stage3-link
-failures. They are fixed in the baseline, but they map the parts of the pipeline
-most likely to break the property again, so they are worth keeping in mind when a
-new arch or platform is brought up. See [../OPT.md](../OPT.md) for the passes.
-
-- **Operand clobber in native emit.** Materializing the left operand of a binop,
- compare, or compare-branch into a scratch register that already holds the right
- operand. The general rule: compute the RHS location first and exclude its
- register when materializing the LHS. A real instance produced `1 << 1` for
- `1u << (n & 31)`, which corrupted Mach-O section alignments and only manifested
- as a downstream `ld -r` / stage3 link failure.
-
-- **Copy propagation across backend scratch registers.** Treating backend scratch
- registers as ordinary hard registers during the combine pass: scratch registers
- may appear in lowered MIR, but they must not be extended across later
- instructions, because native lowering reuses them as transient temporaries.
- A real instance rewrote a stack-argument call operand back to scratch `x9`,
- which was then clobbered before the store, sending an unrelated value into a
- stack slot and flipping an inline-always flag.
-
-- **Coalesce overlap checks** must use raw range points, not compressed points.
-
-- **Lower-pass hint fallback** must not place values that are live across a call
- into caller-saved hint registers.
-
-- **Native scratch budget.** A backend needs enough integer scratch registers for
- all-spilled three-operand operations (aa64 needs three).
-
-- **Aggregate copy/set with pointer operands.** Pointer-valued operands of
- aggregate copy/set must not force-home the pointer local; genuinely
- frame-backed pointer locals need prematerialized indirect bases.
-
-The throughline: the fragile interactions are between the optimizer's
-register-level reasoning ([../OPT.md](../OPT.md)) and the backend's scratch-register
-discipline ([../ARCH.md](../ARCH.md)), with the object/link layer
-([../OBJ.md](../OBJ.md), [../LINK.md](../LINK.md)) as where the symptom finally
-surfaces. New backends should expect to re-litigate these before reaching their
-own fixed point.
diff --git a/doc/plan/BUILD_COMMANDS.md b/doc/plan/BUILD_COMMANDS.md
@@ -1,292 +0,0 @@
-# kit build commands
-
-Forward-looking roadmap for the kit-native build verbs — `build-exe`,
-`build-lib`, `build-obj` — that **replace** the `compile` tool. They compile a
-mixed-language set of sources entirely in memory and produce a final artifact
-(executable / static or shared library / object) in one invocation, with full
-control over both the per-source compile and the whole-build link. Design doc
-when shipped: [../DRIVER.md](../DRIVER.md).
-
-Distinct from [BUILD.md](BUILD.md) (the CAS-backed incremental build
-*coordinator*) and from [../BUILD.md](../BUILD.md) (kit's own Makefile build).
-This is about the driver's single-shot build commands.
-
-## Motivation
-
-Today the kit-native compile path splits awkwardly:
-
-- **`compile`** (driver/cmd/compile.c) resolves exactly one frontend (by `-x`
- or suffix), forwards frontend-specific flags (e.g. wasm `-mfeature=`), and
- emits objects / `.s` / portable C / IR — but **never links** and rejects
- `.o`/`.a` inputs. Linking means writing intermediate objects to disk and
- invoking `ld`/`cc`/`run` separately.
-- **`cc`** (driver/cmd/cc.c) already compiles a *polyglot* source set
- (`.c .s .S .toy .wat .wasm`, language resolved per-file) to in-memory
- `KitObjBuilder*`, links them with byte-loaded `.o`/`.a`/`.so` via a single
- `KitLinkSession`, and emits an executable or shared library — **with no
- intermediate files**. But `cc` is deliberately a GCC-compatible C driver: its
- flag surface is a GCC subset and it does **not** expose frontend-specific
- flags.
-
-So the in-memory, no-temp-files, polyglot compile+link pipeline already exists
-and is proven inside `cc`'s link path (driver/cmd/cc.c `cc_run_link_exe`). What
-is missing is a **kit-native front door** to that pipeline: one that is polyglot,
-forwards per-language frontend flags, exposes the full link-flag surface, and
-lets the caller scope compile flags to individual sources or groups of sources —
-without pretending to be `gcc`.
-
-The public API already supports every output we need:
-
-| Artifact | API |
-|----------|-----|
-| executable | `KitLinkSession` + `KIT_LINK_OUTPUT_EXE` → `kit_link_session_emit` |
-| shared library | `KitLinkSession` + `KIT_LINK_OUTPUT_SHARED` |
-| combined relocatable object | `KitLinkSession` + `KIT_LINK_OUTPUT_RELOCATABLE` |
-| single object | `kit_obj_builder_emit` (include/kit/object.h) |
-| static archive | `kit_obj_builder_emit` each member, then `kit_ar_write` (include/kit/archive.h) |
-
-This work is therefore **almost entirely a driver-layer reorganization**, not new
-core machinery: lift `cc`'s link path into a shared engine, add a kit-native
-argument grammar on top, and retire `compile`.
-
-## The command set
-
-A Zig-inspired trio. Every command is polyglot, compiles in memory, and writes
-no intermediate files.
-
-| Command | Produces | Backend |
-|---------|----------|---------|
-| `kit build-exe` | executable | link session, `OUTPUT_EXE` |
-| `kit build-lib` | static `.a` (default) or shared library (`-dynamic`) | `kit_ar_write` / `OUTPUT_SHARED` |
-| `kit build-obj` | a single object; or `--emit=asm\|c\|ir`; or `-fsyntax-only` check | one `KitObjBuilder`, or `OUTPUT_RELOCATABLE` for multi-source |
-
-`build-obj` is the full replacement for `compile`: it keeps `--emit=obj|asm|c|ir`,
-`-fsyntax-only`, the single-frontend-or-polyglot source handling, and frontend
-flag forwarding — and it gains the ability to **combine several sources into one
-relocatable object** (`ld -r` style) via `KIT_LINK_OUTPUT_RELOCATABLE`. The
-standalone `kit check` (cc.c `driver_check`) and `cc`'s own `--emit=`/`-S` are
-unaffected and remain available.
-
-The three share ~90% of their code; like `cc`/`check` they live in one file
-(`driver/cmd/build.c`) with three thin entry points (`driver_build_exe`,
-`driver_build_lib`, `driver_build_obj`) over a shared parse+run parameterized by
-output kind.
-
-## Command-line grammar
-
-### Two flag tiers
-
-1. **Global / per-output flags** apply to the whole build and may appear
- anywhere outside a group. These are everything that must agree across the
- link, plus the optimization and debug knobs (per the decision below):
-
- - `-target TRIPLE` / `--target=`, and target-feature flags
- - `-O0|-O1|-O2`, `-g`
- - `-fPIC|-fPIE`, `-fvisibility=hidden|default`
- - `-ffunction-sections`, `-fdata-sections`
- - all **link** flags: `-l`, `-L`, `-e`, `-T`, `-static`/`-dynamic`,
- `-pie`/`-no-pie`, `--build-id=`, `-Wl,…`, soname/rpath, subsystem, …
- - all **output** flags: `-o`, `--emit=`, `-S`, `-fsyntax-only`
- - `-Werror`, `-fmax-errors=N`
-
-2. **Scopable flags** may appear globally (baseline for every source) *and*
- inside a `--group` (override for that group's sources only). The scopable set
- is intentionally small — only what is genuinely per-translation-unit:
-
- - preprocessor: `-I`, `-isystem`, `-D`, `-U`
- - language selection: `-x LANG`
- - frontend-specific: `-X<lang> FLAG` (see below)
-
-Placing a global flag inside a `--group` is a **usage error** with a pointed
-diagnostic (e.g. `-O is a per-output flag; place it before any --group`). This
-keeps the rule a one-liner: *outside a group = whole build; inside a group =
-those sources.*
-
-### Groups
-
-```
---group [scopable flags…] -- source [source…]
-```
-
-Each `--group` bundles scopable overrides with the sources listed up to the next
-`--group` or the end of arguments. The `--` separates the group's flags from its
-sources. Sources listed **outside** any group ("bare" sources) receive only the
-global flags.
-
-Inheritance and precedence within a group, relative to the global baseline:
-
-- **Include dirs** (`-I`/`-isystem`): group dirs are prepended to the global
- search path (searched first), global dirs still apply.
-- **Defines** (`-D`/`-U`): additive; a group `-D` of an already-defined name
- overrides it for that group.
-- **Language** (`-x`): a group `-x` overrides suffix resolution for that group.
-- **Frontend flags** (`-X<lang>`): a group's apply only to that group's sources
- of `<lang>`; global `-X<lang>` applies to all sources of `<lang>`.
-
-Link order is the left-to-right order of source/object/archive appearance; a
-group contributes its sources at the group's position. Bare inputs (`.o`/`.a`/
-`.so`) keep their command-line position for the linker.
-
-### Per-language frontend flags: `-X<lang>`
-
-`compile` could forward leftover flags unambiguously because it resolved exactly
-one frontend. A polyglot build cannot, so frontend flags are explicitly
-language-scoped:
-
-```
--X<lang> FLAG # e.g. -Xwasm -mfeature=simd128
-```
-
-`-X<lang>` consumes exactly one following token and routes it to that frontend's
-`kit_frontend_parse_options` (the same entry `compile` uses). Repeatable.
-`<lang>` is `c|asm|toy|wasm`. Works both globally and inside a group. (Current
-kit frontend flags are single-token; a multi-token form is a future extension if
-ever needed.)
-
-### Naming conventions (hybrid)
-
-Keep kit/`cc`'s established output vocabulary; adopt Zig's clearer link-kind
-selectors:
-
-- **Output form**: `--emit=obj|asm|c|ir` and `-o PATH` (unchanged from `compile`).
- `-S` is sugar for `--emit=asm`.
-- **Link kind**: `-static` / `-dynamic` instead of `-shared`. `build-lib`
- defaults to a static `.a`; `-dynamic` makes a shared library. `build-exe`
- defaults to the target's normal dynamic linking; `-static` produces a fully
- static executable. `-shared` is accepted on `build-lib` as a **hidden alias**
- for `-dynamic` (eases `cc`/`gcc` muscle memory) but is omitted from help, which
- steers to `-dynamic`.
-
-### Output defaults
-
-- `build-exe`: `-o` optional; default `a.out` (`a.exe` on Windows).
-- `build-lib`: `-o` **required** (no single obvious base name across N sources);
- shared output respects soname/`--version`.
-- `build-obj`: single source → default `<base>.o` (as `compile` does today, via
- the equivalent of `compile_default_out`); multiple sources → `-o` required and
- output is one relocatable object; `--emit=c` still requires `-o`; `--emit=ir`
- still requires `-O1+`.
-
-`-o -` writes the emit to stdout for **all** emit forms (obj/asm/c/ir),
-reusing the existing `driver_stdout_writer` that `cc` uses — natural for
-pipelines (e.g. `build-obj --emit=ir -o - kernel.wat | less`). Binary objects to
-a tty are unusual but harmless and not specially rejected.
-
-A single `-target` governs the whole build — mixing targets in one invocation is
-an error (one link, one machine).
-
-## Worked examples
-
-```sh
-# Polyglot executable: C + a hand-written asm TU + a Wasm module, in memory.
-kit build-exe -target aarch64-linux-gnu -O2 -o app \
- main.c util.c \
- --group -DFAST -Iinc/fast -- hot1.c hot2.c \
- --group -Xwasm -mfeature=simd128 -- kernel.wat \
- prebuilt.o -Llib -lfoo
-
-# Static library from mixed sources (default kind).
-kit build-lib -O2 -o libmix.a a.c b.toy c.s
-
-# Shared library with a soname.
-kit build-lib -dynamic -fPIC -Wl,-soname=libmix.so.1 -o libmix.so.1 a.c b.c
-
-# Combine three TUs into one relocatable object (ld -r).
-kit build-obj -O1 -o combined.o a.c b.c c.c
-
-# Inspect: emit IR for a Wasm module compiled with a frontend feature flag.
-kit build-obj -O1 --emit=ir -Xwasm -mfeature=simd128 -o k.ir kernel.wat
-
-# Check only, no output.
-kit build-obj -fsyntax-only main.c util.c
-```
-
-## Implementation plan
-
-The work is a factor-out + new-grammar exercise. Proposed file moves:
-
-1. **`driver/lib/link_engine.{h,c}`** — lift the body of cc.c `cc_run_link_exe`
- into a reusable step. Input: a populated link plan (in-memory `KitObjBuilder*`
- list, byte-loaded objects/archives/DSOs, an ordered `KitLinkInputOrder`
- list, and a filled `KitLinkSessionOptions`). It opens the writer, builds the
- `KitLinkSession`, adds inputs in order, and emits. `cc_run_link_exe` becomes a
- thin caller, so `cc` and `build-*` share one link path (no behavior change to
- `cc`). The runtime-archive insertion (`libkit_rt.a`), hosted-libc wiring
- (driver/lib/hosted), and `-l`/`-L` resolution (driver/lib/lib_resolve) are
- already factored and are reused as-is.
-
-2. **`driver/lib/archive_engine.{h,c}`** (small) — `driver_archive_emit(objs[],
- names[], n, writer)`: `kit_obj_builder_emit` each member to bytes, then
- `kit_ar_write`. Used by `build-lib` (static) and reusable by a future `ar`
- pipeline.
-
-3. **`driver/cmd/build.c`** — the new grammar and the three entry points. Reuses
- `driver_compile_run` (driver/lib/compile_engine.h) for the per-source compile,
- `DriverCflags` (driver/lib/cflags) for `-I/-D/-U`, and
- `driver_target_features_*`. New here: the `--group … --` parser, the
- global-vs-scoped validation, the `-X<lang>` router, and per-group cflag/
- frontend-option contexts (one `DriverCflags` baseline plus per-group deltas).
-
-4. **`driver/main.c`** — register `build-exe`/`build-lib`/`build-obj` in
- `driver_tools[]`, gated by new `KIT_TOOL_BUILD_*_ENABLED` flags
- (include/kit/config.h); add them to the default install group. Remove the
- `compile` entry and its `KIT_TOOL_COMPILE_ENABLED` gate.
-
-5. **Remove `driver/cmd/compile.c`** and its help. Its capabilities are fully
- covered by `build-obj`.
-
-### Per-group compile state
-
-The compile loop already builds one `KitObjBuilder*` per source through a shared
-`KitCompiler`. The only new state is per-group compile options: each source
-carries (a) a `KitPreprocessOptions` derived from global cflags + the group's
-cflag delta, (b) a resolved `KitLanguage` (group `-x` or suffix), and (c) the
-`lang_extra` from that group's `-X<lang>` flags. This mirrors how `compile`
-already calls `kit_frontend_parse_options` per frontend — now keyed per group.
-
-## Migration
-
-- **Tests**: `test/toy/run.sh` and any harness invoking `kit compile` move to
- `kit build-obj` (same flags: `--emit=`, `-x`, `-fsyntax-only`, frontend flags).
- The toy corpus exercises CG via the toy frontend → `build-obj`.
-- **Config/install**: drop `KIT_TOOL_COMPILE_ENABLED`; add
- `KIT_TOOL_BUILD_EXE_ENABLED` / `_LIB_` / `_OBJ_`. Update the `install` default
- tool set and the centralized tool table in main.c.
-- **Docs**: update [../DRIVER.md](../DRIVER.md) and the project `CLAUDE.md` code
- map (the `compile` bullet → the three `build-*` bullets) when this ships.
-- **`cc` unaffected**: it keeps its GCC-compatible surface; it just calls the
- shared `link_engine` instead of its inlined copy.
-
-## Future work (post-v1)
-
-- **`@file` response files** and **attach-by-name overrides**
- (`-Con GLOB : FLAGS`) are deferred. Both layer onto the `--group` grammar
- later without breaking it (`@file` is pure argv preprocessing; attach is
- additive). Add `@file` first if build-system drivers hit command-line length
- limits — it is net-new (no existing expander in the driver) but small and
- standard (gcc/ld/ar).
-- **JIT/`run` reuse** — `KIT_LINK_OUTPUT_JIT` already backs `kit run`; a future
- `build-exe --run` could share the same `link_engine` plan.
-
-## Verification notes
-
-- **Relocatable-object combine** — `build-obj` multi-source
- (`KIT_LINK_OUTPUT_RELOCATABLE`) must match `ld -r` for symbol visibility and
- common symbols. Cover with tests against the existing relocatable-link path
- before release; this is the one v1 feature whose semantics need confirming
- rather than just wiring.
-
-## Decisions (2026-06-04)
-
-| Decision | Choice |
-|----------|--------|
-| Replace `compile`? | Yes — trio `build-exe`/`build-lib`/`build-obj`; `build-obj` subsumes `compile`. |
-| Flag scoping syntax | Explicit `--group [flags] -- sources` blocks. Outside = global/per-output, inside = scoped; a group of one = per-source. |
-| Global (per-output) flags | `-O`, `-g`, `-fPIC/-fPIE`, `-fvisibility` are all global (plus `-target`, all link, all output flags). |
-| Scopable-in-group set | `-I/-isystem/-D/-U`, `-x`, `-X<lang>` frontend flags. |
-| Naming conventions | Hybrid: keep kit `--emit=`/`-o`; adopt Zig `-static`/`-dynamic` for link kind. |
-| Inspection / check home | `build-obj` keeps `--emit=asm\|c\|ir`, `-fsyntax-only`, and gains multi-source → relocatable `.o`. |
-| `-shared` on `build-lib` | Accepted as a hidden alias for `-dynamic` (not shown in help). |
-| `build-obj` multi-source | Relocatable combine ships in v1, gated by `ld -r` parity tests. |
-| `-o -` to stdout | Supported for all emit forms (obj/asm/c/ir) via `driver_stdout_writer`. |
-| v1 input ergonomics | `--group` grammar only; `@file` and attach-by-name deferred to post-v1. |
diff --git a/doc/plan/CG-STACK-API.md b/doc/plan/CG-STACK-API.md
@@ -1,721 +0,0 @@
-# CG stack API: parser-driven codegen without a shadow stack
-
-**Goal.** Make the C parser drive `KitCg` directly, in the same single-pass
-shape it has today, without maintaining a duplicate `pcg` value stack. The
-codegen stack stays the liveness authority: a value/place is live exactly while
-it is represented by a stack slot, and popping that slot is the cheap
-single-pass death signal the native direct target and temporary reclaim already
-use.
-
-This is not a plan to replace the stack API with free-floating value handles. It
-is a plan to (a) make stack slots rich enough for a language frontend to use them
-as its expression stack, (b) move all effective-address folding behind the CG
-API, (c) make codegen suppression a CG mode that preserves stack shape,
-frontend slot facts, and constant facts, and (d) **delete the `pcg` adapter
-layer** so the parser drives the CG API directly.
-
-This is a **clean-break rewrite, no backcompat**. The implementation is phased
-only where subsystems are genuinely separable; each phase is a one-time cutover
-with the old path removed for that subsystem. There is no dual-stack
-compatibility mode and no byte-identity gate.
-
----
-
-## 1. Decisions (resolved)
-
-| # | Decision | Choice |
-|---|---|---|
-| D1 | Keep the stack API, add stack-owned inline frontend slot facts | yes |
-| D2 | Free `KitCgValue`/`KitCgPlace` handles independent of stack lifetime | **rejected** — loses the "on stack == live" invariant |
-| D3 | Where the type/flags stack lives during suppression | **CG unevaluated mode** — KitCg maintains stack shape, inline lang facts, and constant facts while target emission is off |
-| D4 | Effective-address folding | **Automatic behind the CG API.** The parser builds places naively (`deref`, `field_at`, `elem`); CG decides whether to fold into the fused `[base + index*scale + offset]` operand or materialize. **No deferral or folding logic in the parser.** |
-| D5 | Success gate | **Correctness + determinism** (full suites pass, output deterministic). Emitted bytes MAY change vs today; byte-identity is *not* a gate. |
-| D6 | Fused `store_void` / `store_keep` as CG ops | **in scope** |
-| D7 | The `cg_adapter.c` / `pcg_*` layer | **deleted entirely.** The parser drives `kit_cg_*` directly and applies C facts via `kit_cg_retag_top`/`set_top_flags`. C-semantic operations survive as plain helper *functions* (op-enum maps, the conversion lattice, atomic/inline-asm/inc-dec shaping) — not a stack-mirroring layer, and not under a `pcg_` prefix. |
-| D8 | Call-arg stack-mark API (old §4.4) | **dropped** — C already passes args on the CG stack (`kit_cg_call`), so a mark API adds nothing |
-| D9 | `KitCgSlotInfo` + new ops are public API | **forced** — `lang/` builds only against `include/kit/cg.h` (no `src/cg` includes) |
-| D10 | Lang facts storage | **inline in `ApiSValue`; final node size is 64 bytes.** Pack `kind`/`res`/`pinned`/`lvalue` into a `uint16_t flags` word and make `lang_flags` `uint16_t`, giving room for `const void* lang_type` while preserving the `ApiSValue <= 64` invariant (`src/cg/value.c:14`). |
-| D11 | C integer-constant-expression evaluation | **moves onto a CG constant-value model, with parser-owned C legality tracking.** The parser evaluates an ICE by parsing it once through the normal path under unevaluated mode, checking the syntactic/semantic ICE guard, and reading the folded value from CG. The parser's duplicate `cexpr_*` grammar and `cint_*` arithmetic engine are **deleted in the final ICE phase**, not during the stack/place cutover. |
-| D12 | Constant payload shape | **stack-parallel constant payload, not `OPK_IMM` alone.** `kit_cg_top_const_int` today only answers for <=64-bit immediate operands; the new model carries known integer bits + width + type independently of whether the emitted operand would be an immediate/local/runtime call. |
-| D13 | `__int128` constant policy | **support useful 128-bit integer bit-pattern folding in CG, but do not require 128-bit div/rem.** CG already has i128/u128 types and emitted i128 div/rem lower through runtime helpers; those are dynamic code, not compile-time constants. Move the existing parser `CConstInt` add/sub/mul/bitwise/shift/compare/cast behavior into CG so current static i128 initializer coverage stays supported; i128 div/rem returns "unknown constant" until a true compile-time word-int division implementation is added. |
-
----
-
-## 2. Current state (what we are removing)
-
-The C frontend currently runs two stacks in lockstep:
-
-- **The real CG stack:** `KitCg.stack` of `ApiSValue` (`src/cg/internal.h:99`).
- `api_push`/`api_pop` (`src/cg/value.c:161/187`) maintain `-O0` transient
- reference counts that feed cheap single-pass liveness and temp reclaim. The
- node already carries `type` (CG type id), an `lvalue` byte (PLACE vs VALUE),
- and an `ApiBitField` (bit-field geometry of a PLACE).
-- **The parser shadow stack:** `Parser.cg_slot_stack` of `PcgSlot`
- (`lang/c/parse/parse_priv.h:250`, `cg_adapter.h:73`). Each slot carries
- `const Type*`, a cached `cg_id`, C value flags, and a `PcgLvAux`
- effective-address record (`cg_adapter.h:50`).
-
-`PcgSlot` duplicates four things; only one is genuinely C-only:
-
-| `PcgSlot` field | Status |
-|---|---|
-| `const Type*` (C type) | **C-only**; opaque to CG; must live somewhere |
-| `cg_id` (cached CG id) | **redundant** — the CG node already has `type` |
-| flags: LVALUE / MODIFIABLE / BITFIELD / NULL_PTR_CONST / REGISTER | LVALUE≈node `lvalue`, BITFIELD≈node bit-field; MODIFIABLE/NULL_PTR_CONST/REGISTER are **C-only** |
-| `PcgLvAux` offset/scale/base_kind/bit_* | **redundant** with the CG place operand (`OPK_INDIRECT` base+index*scale+ofs) + node bit-field — kept only to *defer* the deref and avoid re-crossing C-layout |
-
-CG already has a complete PLACE/VALUE addressing model (`include/kit/cg.h:668`):
-`push_local` / `deref(offset)` / `field(index)` / `elem` / `elem_scaled` /
-`addr` / `load` / `store`, with offset/index/scale folded into the `OPK_INDIRECT`
-operand (`kit_cg_field`, `src/cg/control.c:1157`, folds the field offset into the
-displacement and preserves any index/scale). **toy and wasm use this model
-directly.** The C frontend bypasses `kit_cg_field`/`kit_cg_elem` and reimplements
-the same folding in `PcgLvAux` (`pcg_materialize_lv_to_ptr`,
-`pcg_lv_to_memop_place`, `cg_adapter.c:585/647`) purely to (1) defer the deref so
-`*p`/`p->f` stay a pointer rvalue, and (2) avoid re-deriving an offset it already
-knows. D4 deletes this: the folding moves behind the CG API.
-
-The sqlite layer profile makes the cost visible: ~1.43 M `kit_cg_*` calls per
-`sqlite3.c`, CG API ~19% of compile ticks, of which the stack/memory family is
-~49%. The hot expression seam is paid twice — once per CG stack edit, once per
-shadow-stack edit.
-
----
-
-## 3. Invariants (load-bearing)
-
-1. **Stack lifetime stays authoritative.** A transient operand is live iff a live
- `ApiSValue` stack entry references it. `api_push`/`api_pop` remain the lifetime
- hooks. Inline lang facts and the constant payload never own lifetime.
-2. **No free expression handles.** Every API returns either a
- local/symbol/label handle with existing lifetime rules, or a short-lived
- stack depth — never a separately-live expression value.
-3. **The frontend can inspect and retag slots.** Read top/top2/depth-N lang type
- + flags; retag the result slot after an op.
-4. **CG does not depend on C frontend types.** `src/cg` must not include `lang/c`
- headers or dereference the lang payload. The lang payload is an opaque
- `const void*` copied/dropped with the slot.
-5. **Backend boundaries stay intact.** `CgTarget` / NDT / `NativeTarget` / MC
- receive no C frontend state. The new place ops lower through the *existing*
- CgTarget contract — **no backend changes**.
-6. **The hot `ApiSValue` node stays `<= 64` bytes.** Inline lang facts are allowed
- only because the packed layout keeps the node at exactly 64 bytes (D10).
-7. **No global state.** Inline lang facts live on each `ApiSValue`; the constant
- payload and unevaluated counter hang off `KitCg`.
-8. **No separate lang side allocation.** Non-C frontends leave `lang_type=NULL`
- and `lang_flags=0`; they pay the 64-byte node size but no extra side array or
- enable/disable machinery.
-9. **Constant facts are stack-owned CG facts, not backend facts.** The constant
- payload shadows the CG stack in lockstep, carries known/unknown integer bits
- plus width/type, and has no `CgTarget`, local, temp, or native-backend
- lifetime. Frontends read it only through public CG APIs; they never inspect
- `ApiSValue` to decide whether a value is constant.
-10. **Constant tracking is always-on.** The constant payload is a CG semantic
- fact, not a C frontend feature and not an optimization-level feature. It is
- maintained for every `KitCg` stack slot in normal emitting mode and in
- unevaluated mode; there is no `kit_cg_const_enable` and no "constants off"
- mode.
-
----
-
-## 4. The new architecture
-
-### 4.1 One stack + inline opaque lang facts
-
-`ApiSValue` carries the frontend facts directly. There is no `KitCg.lang_side`,
-no enable call, and no per-frontend side allocation. `lang_type` is an opaque
-pointer copied/dropped with the stack node; `lang_flags` is a frontend-defined
-16-bit field. A separate always-on constant payload still shadows the same stack
-depths; it is owned by `KitCg`, follows the stack in lockstep, and is independent
-of the frontend type/flags fields.
-
-Planned internal shape, preserving a 64-byte hot stack node:
-
-```c
-typedef struct ApiSValue {
- Operand op; /* 24 bytes: immediate/local/global/indirect operand */
- ApiDelayed* delayed; /* off-node SV_CMP/SV_ARITH payload, else NULL */
- const void* lang_type; /* opaque to CG; lang/c stores a const Type* */
- KitCgTypeId type; /* CG type id */
- KitCgLocal source_local; /* owned/fixed local tracking */
- ApiBitField bitfield; /* 12 bytes; bit_width != 0 => bit-field PLACE */
- uint16_t lang_flags; /* frontend-defined (C: LVALUE/MODIFIABLE/...) */
- uint16_t flags; /* kind:2, res:2, pinned:1, lvalue:1, spare:10 */
-} ApiSValue; /* 64 bytes, 8-byte aligned */
-```
-
-`kind` is one of `SV_OPERAND`, `SV_CMP`, or `SV_ARITH`; `res` is one of
-`RES_INHERENT`, `RES_LOCAL`, or `RES_FIXED_LOCAL`; `pinned` and `lvalue` are
-booleans. These fit in six bits, leaving ten spare bits in the packed word.
-`ApiDelayed` remains an off-node pooled extension for delayed compare/arithmetic
-fusion only; it is not a language-fact or constant-value store.
-
-```c
-typedef struct KitCgSlotInfo {
- KitCgTypeId cg_type; /* read-only echo of the node's CG type, for queries */
- const void* lang_type; /* opaque to CG; lang/c stores a const Type* */
- uint16_t lang_flags; /* frontend-defined (C: LVALUE/MODIFIABLE/...) */
-} KitCgSlotInfo;
-```
-
-`kit_cg_slot_info` reads `{lang_type, lang_flags}` from `ApiSValue` and fills
-`cg_type` from `stack[i].type` at query time. The C flags are frontend-private
-bits and must fit in `uint16_t`; suggested C layout:
-
-```
-C_LVALUE /* a C lvalue (distinct from CG place-ness; e.g. a const lvalue) */
-C_MODIFIABLE /* modifiable lvalue (not const/array/func/void) */
-C_BITFIELD /* C-semantic bit-field marker (geometry lives on the CG place) */
-C_NULL_PTR_CONST/* integer 0 / null-pointer-constant */
-C_REGISTER /* `register` storage class — forbids `&` */
-```
-
-`PcgLvAux.is_subobject` disappears: with eager places (4.3) the place operand
-*is* the exact sub-object, so the to_rvalue struct-materialize heuristic
-(`parse_expr.c:1040`) collapses to "leave the place as-is."
-
-### 4.2 Slot queries and retag
-
-```c
-KitCgSlotInfo kit_cg_slot_info(KitCg*, uint32_t depth_from_top); /* depth 0 == top */
-void kit_cg_retag_top(KitCg*, const void* lang_type, uint16_t lang_flags);
-void kit_cg_retag_at(KitCg*, uint32_t depth_from_top,
- const void* lang_type, uint16_t lang_flags);
-void kit_cg_set_top_flags(KitCg*, uint16_t set, uint16_t clear);
-```
-
-`kit_cg_stack_depth` already exists (`include/kit/cg.h:706`). Retag touches only
-the inline lang fields — never the node's CG type (the producing op sets that).
-C-type changes that carry no CG op (qualifier strip on a struct lvalue; the
-deref retype) become a retag. Today's C queries — `pcg_top_type`, `pcg_top2_type`,
-`pcg_top_is_lvalue`/`_modifiable_lvalue`/`_null_ptr_const`/`_bitfield`/
-`_register`, and the `pcg_retag_*` family — are replaced by direct
-`kit_cg_slot_info` reads and `kit_cg_retag_*` calls at the parser sites (small
-static inline accessors over the flag bits are fine; there is no `pcg` layer).
-
-**Producer rule:** an op that produces a *fresh* result slot (load, call result,
-binop/cmp result, convert, addr, push_*) clears that slot's inline lang facts
-(`lang_type=NULL, lang_flags=0`). The parser stamps the C facts immediately after
-via `kit_cg_retag_top`. Structural ops (`dup`/`dup2`/`swap`/`rot3`/`drop`)
-move/copy whole `ApiSValue` nodes, so lang facts move with the value. This is
-exactly today's "emit op, then `pcg_retag_top`" idiom, with structural movement
-done automatically inside CG.
-
-### 4.3 Eager places; folding behind the CG API (D4)
-
-The parser builds places naively and immediately; CG folds. One new op is
-needed; the rest already exist.
-
-```c
-/* PLACE -> PLACE: project to a sub-object at a known byte offset and field type.
- * Folds the offset into the place operand (no record-layout lookup — the
- * frontend already knows the offset from its own C layout). Errors if TOS is not
- * a PLACE. Bit-field geometry, when present, is attached separately via
- * kit_cg_field_bits. */
-void kit_cg_field_at(KitCg*, int64_t byte_offset, KitCgTypeId field_type);
-```
-
-Composition (all folding decided inside CG, as `kit_cg_field` already does):
-
-| C expression | CG op sequence | result |
-|---|---|---|
-| `x` (local) | `push_local x` | PLACE |
-| `*p` | `deref 0` | PLACE |
-| `s.f` | … `field_at(off_f, ty_f)` | PLACE |
-| `p->f` | `deref 0; field_at(off_f, ty_f)` | PLACE |
-| `a[i]` | decay `a`→`elem*` (`addr; bitcast`); push `i`; `elem 0` | PLACE |
-| `a[i].f` | …`elem 0; field_at(off_f, ty_f)` | PLACE |
-| `&e` | build place; `addr` | VALUE(ptr) |
-| read of any lvalue `e` | build place; `load` | VALUE |
-
-`deref(0); field_at(off)` collapses inside CG to one
-`OPK_INDIRECT(base=p, ofs=off)` memop; `elem; field_at` keeps the fused
-`[base + i*scale + off]` form (the `kit_cg_field` INDIRECT branch already does
-this). The "stride differs from access type" case (`arr[i].f`) is handled by the
-natural `elem` (scales by `sizeof(elem)`) then `field_at` (adds `off_f`)
-composition — so the C frontend no longer needs `kit_cg_elem_scaled` for it.
-
-Folding intelligence that currently lives in the parser moves into the CG place
-ops, where it belongs:
-
-- constant offset → `OPK_INDIRECT` displacement (no extra add);
-- dynamic index → fused `[base + i*scale + off]`;
-- out-of-int32 displacement → materialize an explicit `base+ofs` pointer;
-- the widening-signed-load hint (`KIT_CG_MEM_SEXT_LOAD`, today set in
- `pcg_load`, `cg_adapter.c:540`) is decided inside `kit_cg_load` from the
- access type;
-- bit-field geometry rides the place via `kit_cg_field_bits`
- (`src/cg/control.c:1253`), set by the frontend right after `field_at`.
-
-`pcg_materialize_lv_to_ptr`, `pcg_lv_to_memop_place`, `pcg_lv_member`,
-`pcg_lv_subscript`, `pcg_decay_array`, and the whole `PcgLvAux` struct are
-**deleted**. `kit_cg_field` (by index) and `kit_cg_elem`/`kit_cg_elem_scaled`
-stay for toy/wasm.
-
-### 4.4 Fused load/store (D6)
-
-```c
-void kit_cg_load(KitCg*, KitCgMemAccess); /* [place] -> [value] (exists) */
-void kit_cg_store(KitCg*, KitCgMemAccess); /* [place, value] -> [] (exists) */
-void kit_cg_store_keep(KitCg*, KitCgMemAccess); /* [place, value] -> [value] */
-```
-
-`kit_cg_store` already is the `[place,value] -> []` primitive; the parser names
-the discard intent through it (`store_void` == `store`, kept as the documented
-spelling). `kit_cg_store_keep` adds the C assignment-expression value
-preservation (`a = b`, `if ((x=f()))`) as one op, so the parser drops the
-`dup`/`rot3`/`swap` choreography it does today (`cg_adapter.c:769-773`). CG keeps
-the delayed-RHS-into-local fast path (`kit_cg_store`, `src/cg/memory.c:506-551`)
-in both forms.
-
-### 4.5 Unevaluated mode: fold + types, zero emission (D3, D11)
-
-Unevaluated mode is a CG execution mode, not the full definition of C
-"unevaluated operand" legality. It serves two implementation needs that share
-one stack machine:
-
-- **Type/diagnostic-only parsing:** `sizeof` non-VLA operands, `_Generic`
- controlling expressions and inactive associations, statically-dead expression
- arms, `extern inline` bodies when the TU only needs diagnostics, and other
- parse paths that must compute C result types/flags without emitting code.
-- **Constant-value parsing:** ICE and static arithmetic-constant contexts where
- the parser must parse the real expression grammar, suppress target emission,
- then read the folded value from CG (§4.7).
-
-These uses share the same CG mechanics, but not the same C legality rules. CG
-answers "what value/type would this expression stack produce if it can be folded";
-the parser's constant guard (§4.7) answers "is this syntax permitted in this C
-constant-expression category."
-
-```c
-void kit_cg_unevaluated_push(KitCg*); /* enter fold+type-only mode (nestable) */
-void kit_cg_unevaluated_pop(KitCg*);
-```
-
-Contract while `unevaluated > 0`:
-
-- **Maintained:** stack depth, inline lang facts, and the constant payload.
- Every producing operation leaves a stack slot of the requested CG type when a
- type is available; structural ops move/copy `ApiSValue` nodes and update the
- constant payload in lockstep with the value stack.
-- **Folded:** integer `push`/`cast`/`unop`/`binop`/`cmp` update the constant
- payload using the CG fold core. A producing op that cannot fold pushes an
- unknown constant payload, not an emitted value. Unknown is the value-level
- "not a constant" signal; it is distinct from "illegal C ICE syntax."
-- **Skipped:** every `CgTarget`/emission call, temp allocation, local allocation,
- const-local store/load tracking, `-O0` ref accounting, and aggregate-place
- validation that exists only to protect emitted operands. Handle-returning APIs
- that the parser reaches while suppressed return benign dummy handles; those
- handles exist only to keep one parse path shaped correctly and cannot be used
- to recover emitted values later.
-- **Early op rule:** every stack-mutating op must test unevaluated mode before
- it forces operands into locals, allocates temps/locals, materializes delayed
- values, calls target hooks, or updates emitted-code-only analyses. In this
- path the op performs only its declared stack effect, inline-lang preservation
- or clearing, constant-payload movement, and constant-payload folding/unknown
- marking.
- This matters for ops that are "structural" in API shape but not in the current
- implementation: `dup` can emit a copy today, `deref`/`addr` can allocate a
- pointer temp, and place/field/elem ops can materialize address arithmetic.
-- **File-scope safe:** the mode touches no function state, so it works before a
- function is open. This is required for file-scope array bounds, enum values,
- bit-field widths, and static initializers.
-- **Type-only tolerant:** pure type/diagnostic paths may use
- `KIT_CG_TYPE_NONE` when no CG type is needed. Constant folding still requires
- real CG integer types so CG knows width and signedness.
-
-`&&`, `||`, and `?:` need explicit handling; they are not solved by making
-labels and branches no-op. In emitting mode they keep their normal lowering
-through labels, branches, and temporaries. In unevaluated mode the parser's
-control-shape helpers parse each syntactic operand once, compute the C result
-type/flags from `KitCgSlotInfo`, and push a known or unknown result constant:
-
-- `&&` and `||` can fold from the known left operand without requiring the
- short-circuited operand to be value-known, but the skipped operand is still
- parsed for types and diagnostics under the correct C "not evaluated" guard.
-- `?:` folds when the condition is known and the selected arm is known; the
- unselected arm is still parsed for type merging and diagnostics under the
- correct guard. When the condition is unknown, the result constant is unknown
- even if both arms happen to have the same value, unless CG later grows a
- deliberate meet rule.
-
-The important boundary is that the parser may keep semantic branches for C
-control-shape constructs. What disappears is parser-owned emission bookkeeping:
-`Parser.suppress_codegen`, `pcg_emit_enabled`, duplicate shadow-stack updates,
-and forks whose only purpose was "emit this CG op or do not emit it." Parser-side
-semantic side-effects in suppressed regions (VLA-size bookkeeping, `_Generic`
-association collection, diagnostics) still run on the normal parse path.
-
-### 4.6 Calls (D8)
-
-C call arguments already flow through the CG stack: the C call path drives
-`kit_cg_call(nargs, fn_type, attrs)`, which consumes the callee+args segment.
-There is no second frontend arg list to remove. **No mark API is added.** The
-parser stamps the result slot's C type via retag.
-
-### 4.7 Constant-expression evaluation on CG (D11)
-
-C integer-constant-expression evaluation stops being a parser subsystem. Today
-`eval_const_int` → `cexpr_cond` (`parse_expr.c`) is a *complete second expression
-grammar* (`cexpr_mul`/`add`/`shift`/`rel`/`eq`/`band`/`bxor`/`bor`/`land`/…) plus
-a *second constant-arithmetic engine* (`cint_*`, with 128-bit lo/hi, casts,
-conversions) — and it re-implements `offsetof` / `__builtin_constant_p` /
-enum-constant handling separately from the real parser, a standing drift hazard.
-All of it is **deleted in the ICE cutover phase**.
-
-"A real constant-value model" means CG no longer treats "constant" as a property
-of `ApiSValue.operand == OPK_IMM`. The current `kit_cg_top_const_int` can only
-answer for <=64-bit immediate operands. The new model is a stack-parallel
-constant payload owned by `KitCg`: every CG stack slot has known/unknown state
-plus integer bits, width, signedness, and result CG type. It can represent a
-folded value even when normal emission would have produced a local, a wide value
-lowered through a runtime helper, or no emitted value at all because unevaluated
-mode is active.
-
-Proposed public shape:
-
-```c
-typedef struct KitCgConstInt {
- uint64_t lo; /* low bits, always truncated to width */
- uint64_t hi; /* high bits for width > 64 */
- uint16_t width; /* 1..128 for integer constants */
- uint8_t is_signed;
- uint8_t known;
-} KitCgConstInt;
-
-int kit_cg_top_const_int_ex(KitCg*, KitCgConstInt* out);
-int kit_cg_top_const_i64(KitCg*, int64_t* out); /* convenience */
-void kit_cg_push_const_int(KitCg*, KitCgTypeId type, const KitCgConstInt* v);
-```
-
-The exact names can change, but the surface must be width-complete and must not
-require frontends to inspect `ApiSValue` or know which operands are immediates.
-
-Constant payload policy:
-
-- **Always-on:** every `KitCg` maintains the payload for every value-stack slot,
- regardless of frontend, optimization level, or unevaluated depth. This matches
- CG's existing design pressure: the semantic layer already folds immediate
- arithmetic, delayed arithmetic/compare, and local-constant loads for `-O0`
- code quality, and C already queries constant-ness during normal expression
- parsing.
-- **Stack fact, not operand fact:** `kit_cg_top_const_int_ex` reads the payload,
- not `ApiSValue.op.kind`. `OPK_IMM` becomes only one way to seed a known
- payload. Values materialized as locals, delayed arith/cmp nodes, or wide
- constants may still carry known payload bits.
-- **Unknown is explicit:** fresh runtime values (`load` from unknown memory,
- call results, atomics, volatile access, inline asm, address values, and
- unsupported arithmetic such as i128 div/rem) produce an unknown payload.
-- **Local-constant forwarding is separate:** `ApiSourceLocal.const_*` tracking
- may seed the payload for an actual emitted load from a tracked local, but it is
- still an emitting-mode optimization. The parser's `CConstGuard` decides whether
- a local identifier is a legal C constant-expression operand; ordinary locals
- do not become ICE operands just because CG can forward their current value.
-
-Fold coverage required before the ICE cutover:
-
-- **<=64-bit integers:** add/sub/mul, div/rem, bitwise ops, shifts, comparisons,
- integer casts, and booleanization. Div/rem by zero must fail cleanly with a
- diagnostic path the parser can report; it must not silently produce an
- arbitrary constant.
-- **128-bit integers:** move the existing parser `CConstInt` two-limb behavior
- for add/sub/mul, bitwise ops, shifts, comparisons, and casts into CG so
- existing static i128 initializer coverage remains supported. Compile-time
- i128 div/rem is not required in this plan: emitted i128 div/rem lowers through
- runtime helpers, and helper calls are dynamic code. CG marks i128 div/rem
- payloads unknown until a true compile-time word-int division implementation is
- added.
-
-The replacement parser helper is one pass over the normal grammar plus a guard:
-
-```c
-CConstEval eval_const_int(Parser* p, SrcLoc loc, CConstKind kind) {
- CConstGuardMark mark = c_const_guard_push(p, kind);
-
- kit_cg_unevaluated_push(p->cg);
- parse_conditional(p); /* the real grammar */
- int ok_const = kit_cg_top_const_int_ex(p->cg, &value);
- KitCgSlotInfo info = kit_cg_slot_info(p->cg, 0);
- kit_cg_drop(p->cg);
- kit_cg_unevaluated_pop(p->cg);
-
- int ok_guard = c_const_guard_pop(p, mark);
- if (!ok_guard || !ok_const || !c_type_is_integer(info.lang_type))
- perr(loc, "integer constant expression required");
- return (CConstEval){ value, info.lang_type };
-}
-```
-
-The syntactic/semantic guard is mandatory. A foldable result is not the same
-thing as a legal C integer constant expression:
-
-- `(0, 1)` folds to `1`, but the comma operator is not permitted in an ICE when
- it is evaluated.
-- `(x = 1, 1)` and `f(), 1` can have foldable tails, but assignment and function
- call syntax still make the evaluated expression illegal.
-- `sizeof(x++)` can be valid when the operand is non-VLA because the increment is
- not evaluated; the guard must understand these not-evaluated exceptions rather
- than reject tokens blindly.
-- `enum` constants are ICE operands; ordinary objects are not, even if some
- optimization or local-const tracking could know their value.
-- Floating constants are only allowed in the narrow C cases such as the immediate
- operand of a cast to integer type; the fold payload alone cannot encode that
- syntactic permission.
-
-Implement the guard as parser mode/counter state, not a second expression
-grammar. Normal parse routines call small note functions when active, for
-example "comma operator in integer constant expression" or "assignment in static
-initializer constant." The guard also has explicit "not evaluated" submodes for
-`sizeof` non-VLA operands and short-circuited `&&`/`||`/`?:` arms. At the end,
-both checks must pass: the guard says the syntax is legal for the requested C
-constant category, and CG says the stack top has a known value of the required
-type.
-
-Minimum guard event list for the ICE cutover:
-
-- evaluated comma operator;
-- evaluated assignment and compound assignment;
-- evaluated pre/post increment and decrement;
-- evaluated function call, including builtin calls that are not specifically
- permitted by the active constant category;
-- identifier/reference classification: enum constants are legal ICE operands,
- ordinary objects/functions are not, regardless of any CG local-const fact;
-- floating constant use, including the narrow case where a floating constant is
- immediately cast to an integer type and the broader cases where it is illegal
- for ICE;
-- pointer/address expressions, string-literal addresses, label addresses, and
- relocation-like constants, which belong to the static-address category rather
- than C ICE;
-- `sizeof` / `_Alignof`: type-name forms are immediate constants; expression
- forms enter a not-evaluated submode unless the operand is a VLA, where the VLA
- size expression is evaluated and must be guarded normally;
-- `_Generic`: the controlling expression is not evaluated; selected and
- unselected associations follow the standard's type/diagnostic rules, and only
- the selected expression contributes the resulting value category;
-- `offsetof`: accepted through the parser's existing builtin path, but member
- designator syntax must stay constrained to the builtin's rules;
-- short-circuit `&&`/`||` and `?:`: skipped operands are parsed under the
- correct not-evaluated submode; selected/evaluated operands are guarded
- normally;
-- unsupported arithmetic that CG marks unknown, including i128 div/rem in this
- plan, reports "constant expression required" only after the guard has also
- accepted the syntax.
-
-Keep the categories separate:
-
-- **C ICE:** integer result plus the C ICE guard; used by case labels, enum
- values, bit-field widths, array bounds that require ICE, `alignas`, and
- `_Static_assert`.
-- **Static arithmetic constants:** can reuse the CG payload and normal parser
- path, but may have different C legality from ICE.
-- **Static address/relocation constants:** `&x + 4`, string-literal addresses,
- and designated-init relocations remain in `parse_init.c` for this plan. A
- later project can move them onto a CG constant-data model, but this plan does
- not.
-
----
-
-## 5. What moves where
-
-| Today in `pcg` / `Parser` | Target owner |
-|---|---|
-| `PcgSlot.type` (`const Type*`) | inline `ApiSValue.lang_type` (opaque) |
-| `PcgSlot.cg_id` | gone — read the node `type` via `kit_cg_slot_info` |
-| `PcgSlot.flags` (C value flags) | inline `ApiSValue.lang_flags` |
-| `Parser.cg_slot_stack` / `cg_type_sp` / `cg_type_cap` | gone — the CG stack is the typed stack |
-| `PcgLvAux` (offset/scale/base_kind/bit_*) + `pcg_materialize_lv_to_ptr` / `pcg_lv_to_memop_place` / `pcg_lv_member` / `pcg_lv_subscript` / `pcg_decay_array` | gone — CG place ops (`deref`/`field_at`/`elem`/`addr`/`field_bits`) fold automatically |
-| `Parser.suppress_codegen` + `pcg_emit_enabled` forks | CG unevaluated mode; parser keeps only semantic forks required by C control-shape constructs |
-| `pcg_dup`/`swap`/`drop` (mirror onto shadow stack) | direct `kit_cg_dup`/`swap`/`drop` (inline lang facts move with `ApiSValue`; CG updates the constant payload) |
-| assignment `dup`/`rot3`/`swap` sequences | `kit_cg_store` / `kit_cg_store_keep` |
-| `cg_adapter.c` / `cg_adapter.h` / the `pcg_*` layer | **deleted** — parser drives `kit_cg_*` directly |
-| `cexpr_*` grammar + `cint_*` constant-arith engine (`parse_expr.c`) | **deleted in the ICE cutover** — parser owns the legality guard; CG owns constant-value folding (§4.7) |
-| C conversions, usual-arithmetic-conversions, binop/cmp/atomic op maps, lvalue-legality, null-ptr/register/bitfield C rules | plain helper *functions* the parser calls (no stack mechanics, no `pcg_` prefix) |
-
-After the cutover there is **no adapter layer**. The parser calls `kit_cg_*`
-directly and stamps C facts with `kit_cg_retag_top`/`set_top_flags`. The
-genuinely C-semantic operations survive as ordinary functions in the parse module
-— the op-enum maps, the conversion lattice (today's `pcg_convert`), inc/dec,
-call/atomic/intrinsic/inline-asm shaping — each driving `kit_cg_*` + retag
-directly, with no parallel stack and no EA state.
-
----
-
-## 6. Op-by-op: inline lang + constant lockstep
-
-Every stack-mutating CG op maintains inline lang facts and the constant payload
-so depths stay in sync. Grouped by effect:
-
-- **Producers (push a fresh slot, clear inline lang facts, set known/unknown
- const):**
- `push_int`, `push_float`,
- `push_null`, `push_local`, `push_local_addr`, `push_symbol_addr`,
- `push_label_addr`, `load`, `addr`, `deref`, `field`/`field_at`/`elem`/
- `elem_scaled` (consume then push a place — clear), `alloca`, `vararg_next`,
- call result, `atomic_load`/`atomic_rmw`/`atomic_cmpxchg` result,
- `intrinsic` result, overflow-builtin result, `inline_asm` outputs.
-- **Retypers (1→1, keep depth):** `trunc`/`sext`/`zext`/`bitcast`/`fpext`/
- `fptrunc`/`int<->float`/`ptr<->int`, `int_unop`/`fp_unop`. Result inline lang
- facts are cleared; parser retags. The constant payload is converted/folded
- when CG can do so, otherwise marked unknown.
-- **Combiners (N→1):** `int_binop`/`fp_binop`/`int_cmp`/`fp_cmp` (2→1),
- `field`/`elem` (consume base/index), `store_keep` (2→1), `call`/`call_symbol`
- (N→0/1), `va_copy` (2→0), `atomic_store` (2→0), `atomic_cmpxchg` (3→…).
- Integer combiners fold the constant payload when operands are known and the op
- is supported; otherwise the result payload is unknown.
-- **Pure structural:** `dup` (copy top `ApiSValue` + constant payload), `dup2`,
- `swap`, `rot3`, `drop`, `store`/`store_void` (2→0). These move inline lang
- facts and constant payloads with no semantic change.
-- **Consumers (→0):** `branch_true`/`branch_false`/`switch`/`computed_goto`,
- `ret`.
-- **Scope edges:** `api_scope_store_results`/`api_scope_push_results` move
- carried `ApiSValue` results and their constant payloads when active. (C uses
- only void scopes, so this is inert for C, but must be correct for any future
- result-carrying frontend.)
-
-Implementation choke points: `api_push`/`api_pop` (`src/cg/value.c:161/187`) and
-the structural ops `kit_cg_dup`/`dup2`/`swap`/`drop`/`rot3`
-(`src/cg/memory.c:598-735`). `ApiSValue` copies carry inline lang facts
-automatically, while producers must clear them explicitly. Routing
-constant-payload movement through `api_push`/`api_pop` (which every
-producer/consumer already calls) covers most ops automatically; the structural
-ops and scope-edge movers need explicit handling.
-
----
-
-## 7. Cutover plan (clean break, gate = correctness + determinism)
-
-No dual-stack interim, no byte-identity gate. Each phase is either CG
-infrastructure with no C frontend cutover, or a one-time frontend cutover that
-removes the old path for that subsystem. Do not land a phase where the parser's
-shadow stack and inline CG lang facts both authoritatively model live
-expressions.
-
-1. **CG inline lang fields + slot queries.** Add the packed 64-byte `ApiSValue`
- layout, `KitCgSlotInfo`, `kit_cg_slot_info`, `kit_cg_retag_top`/`_at`, and
- `kit_cg_set_top_flags`; wire producer clearing, structural copies, and
- scope-edge movers. Add focused CG API tests for producer clearing,
- structural/scope movement, and query/retag behavior. No C frontend change
- yet.
-2. **CG place + store ops.** Add `kit_cg_field_at` and `kit_cg_store_keep`; move
- the SEXT-load hint decision into `kit_cg_load`; confirm `deref`/`elem`/
- `field_at`/`field_bits` compose to fused operands without parser help. Add
- targeted place tests for local/global load+store, `s.f`, `p->f`, `a[i]`,
- `a[i].f`, bit-fields, displacement overflow, volatile/atomic accesses,
- `&` of each lvalue form, assignment value preservation, `++`/`--`, and
- compound assignment.
-3. **CG unevaluated + constant payload substrate.** Add
- `kit_cg_unevaluated_push`/`pop`, the stack-side constant payload, the
- width-complete constant query/push API, <=64 div/rem folding, and the required
- 128-bit bit-pattern folds excluding div/rem. No C frontend cutover yet. Add CG
- API tests for file-scope/no-function use, unknown constants including i128
- div/rem, dummy handles, no target/local calls, nested unevaluated mode, and
- inline-lang/constant lockstep under suppression.
-4. **C frontend one-stack cutover.** Use inline lang fields for C; replace
- parser stack reads/writes with `kit_cg_slot_info` + retag; replace EA folding
- with eager places; replace assignment choreography with `store`/`store_keep`;
- move `Parser.suppress_codegen` uses to CG unevaluated mode for type-only
- paths; delete `cg_slot_stack`, `cg_type_sp`, `cg_type_cap`, `PcgSlot`,
- `PcgLvAux`, and parser-owned effective-address folding. The existing
- `cexpr_*`/`cint_*` subsystem may remain only because ICE is a later subsystem;
- it must not keep or resurrect a live expression shadow stack.
-5. **Adapter deletion and helper relocation.** Remove `cg_adapter.c`/`.h` and
- the `pcg_` prefix. Move the C-semantic helpers (op maps, conversion lattice,
- inc/dec, atomic/intrinsic/inline-asm shaping) into ordinary parse-module
- functions that call `kit_cg_*` directly. Update `doc/FRONTENDS.md` to describe
- the durable parser → CG contract.
-6. **ICE cutover.** Add the parser `CConstGuard`; reimplement `eval_const_int`
- over the normal parse path under unevaluated mode (§4.7); delete the
- `cexpr_*` grammar and `cint_*` engine. Re-test every ICE context: case labels,
- array sizes (block + file scope), enum values, bit-field widths, `alignas`,
- `_Static_assert`, designated-init indices, invalid ICE syntax that still
- folds, `sizeof` not-evaluated exceptions, short-circuit exceptions, i128
- constant expressions, i128 div/rem rejection/unknown behavior, and
- divide-by-zero diagnostics for supported div/rem widths.
-
-Use targeted gates and redirect output to files per `AGENTS.md`:
-
-- Inline-lang/constant substrate: `make test-cg-api test-toy`.
-- C frontend cutover: `make test-cg-api test-parse test-pp test-toy`.
-- Codegen-sensitive place/store changes: `make test-cg-api test-isa
- test-aa64-inline` plus targeted native smoke cases for x64/aa64/rv64 as
- appropriate.
-- Cross/portability: run `make test-cross TARGET=<selector> DEPTH=smoke` after
- the one-stack C cutover is green on native targets, and a broader portability
- pass before final handoff. `make test-port` is not a per-phase gate.
-- Determinism: same input → identical output across repeated runs; byte identity
- against the old implementation is not required.
-
----
-
-## 8. Requirements checklist (the agent verifies against this)
-
-- R1 Single liveness-authoritative value stack; `api_push`/`api_pop` unchanged as
- lifetime hooks. (§3.1)
-- R2 No free expression handles; only handles + stack depth escape. (§3.2)
-- R3 Read/retag lang type+flags at top/top2/depth-N. (§4.2)
-- R4 CG never dereferences the lang payload; it is `const void*`. (§3.4)
-- R5 No backend (`CgTarget`/NDT/NT/MC) change; place ops lower through existing
- contracts. (§3.5)
-- R6 `ApiSValue` is 64 bytes with inline `const void* lang_type`,
- `uint16_t lang_flags`, and a packed `uint16_t flags` word. (§3.6, D10)
-- R7 No global state; inline lang facts live in `ApiSValue`, while the constant
- payload and unevaluated counter hang off `KitCg`. (§3.7)
-- R8 No language side allocation or enable call; constant payload is always-on
- for every `KitCg`. (§3.8, §3.10)
-- R9 Suppression: type+flags+constant facts maintained, zero emission, zero
- temp/local alloc, no forced CG-type lowering for type-only paths; parser loses
- emission-bookkeeping forks, not semantic forks required by C control-shape
- constructs. Every op checks unevaluated mode before local/temp allocation,
- delayed materialization, target calls, or emitted-code-only analysis updates.
- (§4.5)
-- R10 Eager places; all EA folding (const offset, dynamic index, displacement
- overflow, SEXT-load hint) decided inside CG; `PcgLvAux` deleted. (§4.3, D4)
-- R11 Bit-field geometry rides the CG place (`field_bits`); C keeps a semantic
- `C_BITFIELD` flag for `sizeof`/`&` rejection. (§4.1, §4.3)
-- R12 C value flags (MODIFIABLE / NULL_PTR_CONST / REGISTER / LVALUE) fit in
- `uint16_t lang_flags`. (§4.1)
-- R13 `store` / `store_keep` replace assignment dup/rot3/swap. (§4.4)
-- R14 No call-mark API; C args stay on the CG stack. (§4.6, D8)
-- R15 ICE evaluation moves to CG's constant-value model plus a parser-owned
- syntactic/semantic C constant guard. (§4.7, D11)
-- R16 Gate = correctness + determinism, not byte-identity. (D5)
-- R17 `cg_adapter.c`/`.h` and the `pcg_` prefix deleted; parser drives `kit_cg_*`
- directly; C-semantic helpers survive as plain functions. (§5, D7)
-- R18 inc/dec, compound assignment, va_arg, atomics, intrinsics, inline-asm,
- calls, returns all re-expressed on the new place ops + slot queries. (§6)
-- R19 `&&`/`||`/`?:` have explicit unevaluated fold paths; they do not rely on
- no-op labels/branches to leave the right stack result. (§4.5)
-- R20 `eval_const_int` is reimplemented over the normal parse path with
- `CConstGuard`; the `cexpr_*` grammar and `cint_*` engine are deleted; all ICE
- contexts and invalid foldable-but-not-ICE cases are re-tested. (§4.7, §7.6)
-- R21 CG exposes an always-on, width-complete integer constant payload API;
- frontends do not inspect `ApiSValue` or `OPK_IMM` to determine constant-ness,
- and local-const forwarding is not C ICE legality. (§4.7, D12)
-- R22 <=64-bit div/rem folding moves into CG with clean divide-by-zero failure;
- 128-bit add/sub/mul/bitwise/shift/compare/cast folding moves into CG, while
- i128 div/rem returns unknown until a true compile-time word-int division
- implementation exists. (§4.7, D13)
-- R23 Cutovers are one-time subsystem moves with old paths removed; no dual-stack
- or backcompat phase is introduced. (§7)
-
----
-
-## 9. Expected payoff
-
-CG API self time is ~19% of compile, half of it the stack/memory family, so the
-directly visible ceiling of this cleanup is ~9-10% of compile. The real win is
-larger only insofar as deleting the parser's duplicate stack + EA bookkeeping
-also removes frontend work (one push/pop instead of two; no `PcgLvAux`
-maintenance; no `if (emit)` branching). A realistic target is **5-10% total
-compile improvement**, plus the qualitative payoff: one expression stack, no
-fragile choreography, "parse expression, drive CG," and a clean home for future
-frontend instrumentation — with single-pass liveness and temp reclaim preserved.
-
----
-
-## 10. Non-goals
-
-- No AST pass; no handle-based expression IR.
-- No collapse of parser / CG / NDT / NT / MC into one module.
-- No C frontend type dependency from `src/cg`.
-- No attempt to close the whole ~2.8× tcc compile-speed gap with this refactor.
diff --git a/doc/plan/CG-TYPE-DEBUG-SPLIT.md b/doc/plan/CG-TYPE-DEBUG-SPLIT.md
@@ -1,373 +0,0 @@
-# CG type/debug split: move source spelling off the operational type id
-
-**Goal.** Make `KitCgTypeId` a storage/ABI/operational identity *only*, and carry
-source-facing spelling (primitive sign+name, typedef names, enum underlying
-spelling) on a separate, optional debug channel consumed at the points where
-debug info is actually produced. The result removes pervasive alias-resolution
-from the CG hot path: with no transparent wrapper kinds left in the operational
-lattice, `api_unalias_type`, the `storage_id`-vs-`id` split, and the
-per-backend `if (ALIAS) recurse; if (SOURCE_BASE) recurse` boilerplate all
-disappear.
-
-This resolves [CG-TYPES.md](CG-TYPES.md) §10 Open Question 1
-("Should `KIT_CG_TYPE_ALIAS` remain a true public kind, or should source aliases
-move entirely into debug/C-backend metadata?") — extended to cover
-`KIT_CG_TYPE_SOURCE_BASE`, which was added after that question was written.
-
-This is a **clean-break redesign, no backcompat**, in the spirit of CG-TYPES.md.
-The capability that `KIT_CG_TYPE_SOURCE_BASE` delivers (faithful signed/unsigned/
-char debug, plus enum enumerators) is **preserved** — only its placement moves.
-
----
-
-## 1. Current State
-
-The public type lattice (`include/kit/cg.h`, `src/cg/type.c`) contains two
-*transparent wrapper* kinds whose only purpose is debug-info spelling:
-
-- `KIT_CG_TYPE_ALIAS` — a named typedef over a base type (`kit_cg_type_alias`).
- **Dead for every real frontend**: the only callers are `test/api/cg_type_test.c`.
- The C frontend never creates aliases (so C typedef names are already absent
- from DWARF today).
-- `KIT_CG_TYPE_SOURCE_BASE` — a source-facing scalar spelling (name +
- `KitCgDebugEncoding`) over a width-only storage builtin (`kit_cg_type_source_base`,
- added by commit `244f1039`). The C frontend lowers **every** integer/char
- scalar to one of these (`lang/c/type/type.c`, `type_cg_scalar`), so in practice
- almost every integer id flowing through CG is a wrapper.
-
-Both kinds are *transparent to storage*: every ABI/codegen/predicate site strips
-them to the terminal builtin via `api_unalias_type` before use.
-
-### What the wrappers buy us (measured)
-
-- The **only** consumer of the exact wrapper kind is debug info: `api_debug_type`
- (`src/cg/debug.c`) maps `ALIAS → DW_TAG_typedef` and
- `SOURCE_BASE → DW_TAG_base_type + DW_ATE_*`. Reached at three live sites — the
- subprogram type at `kit_cg_func_begin_attrs` and locals/params in
- `api_debug_emit_source_locals` (`src/cg/session.c`).
-- `src/opt/` has **zero** references to either kind. The optimizer only ever
- sees unaliased storage types.
-- Nothing in ABI/calling-convention/operation lowering reads the wrapper:
- signedness of operations comes from op selection (`SDIV`/`UDIV`, `sext`/`zext`)
- and from `KIT_CG_ABI_SIGNEXT`/`ZEROEXT` attrs; load signedness comes from the
- `KIT_CG_MEM_SOURCE_SIGNED` flag. The encoding on a source-base is a debug
- vehicle, never consulted for codegen.
-- **Struct/union member types do not reach DWARF at all today**: `api_debug_type`'s
- record case calls `debug_type_record_begin` then immediately
- `debug_type_record_end`, never `debug_type_record_field`. So source-base
- spelling on field types — and on interned func-sig param types — is consumed by
- nothing.
-- Enums *do* now thread enumerators (commit `244f1039`): `api_debug_type` emits
- `DW_TAG_enumeration_type` with `DW_TAG_enumerator` children over the enum's
- underlying type. The underlying type may itself be a source-base.
-
-### What it costs
-
-- `api_unalias_type` is called pervasively: ~33 per-op hot-path sites
- (`src/cg/value.c`, `arith.c`, `call.c`, `memory.c`), ~9 more inside `type.c`
- itself (the float/i128 predicates, predicate-bits fill, complete/sized/valid,
- enum-base validation, `same_storage`), plus the recursive alias-stripping
- prologue duplicated across **every ABI backend** (`src/abi/abi.c`,
- `abi_rv64.c`, `abi_sysv_x64.c`, `abi_aapcs64.c`, `abi_win64_x64.c`,
- `src/arch/wasm/abi.c`) and ~10 functions in `type.c`/`fold.c`
- (`cg_type_pointee`, `cg_type_func_ret_id`, `kit_cg_type_int_width/float_width`,
- `api_int_like_width`, `api_type_is_bool`, `cg_type_complete_id`,
- `cg_type_sized_id`, `cg_type_same_storage_rec`).
-- A per-type-entry caching subsystem that exists largely to amortize the above:
- `storage_id` (the unalias terminal, filled once by `api_type_info_fill`),
- `pred_bits`/`pred_valid` (predicate bitset computed on the *unaliased* type).
-- A two-headed type-info surface (`kit_cg_type_view` identity prefix vs
- `kit_cg_type_info` full snapshot) plus `storage_id`,
- `kit_cg_type_resolve_alias`, and `kit_cg_type_same_storage`, all there to
- manage the alias↔storage split. `kit_cg_type_is_void` is a two-hop through
- `storage_id`.
-
-### Recent movement (reconciled)
-
-Three same-day changes touched this area and the plan is written against the
-*current* working tree:
-
-- `64afd07c` (clean cutover) — direct-index ids; void is always the void
- builtin; `KIT_CG_TYPE_NONE` is invalid/absent only.
-- `244f1039` — *added* `SOURCE_BASE` and threaded enum enumerators into debug
- info. This plan reverses the **placement** of that spelling (onto a debug
- channel) without dropping the **capability**.
-- Uncommitted working tree — consolidated the unalias caching: deleted the
- redundant `CgApiTypeKind`/`CgApiType.kind` (entry kind is now `e->cg.kind`),
- folded the old `unaliased`/`unalias_filled` fields into a single
- `info.storage_id` filled by `api_type_info_fill`, and made `kit_cg_type_info`
- delegate to `kit_cg_type_view`. This *cleaned up* the cost but did not remove
- the unaliasing — the pervasiveness above is fully intact, and `storage_id` is
- now load-bearing.
-
-This consolidation is evidence for the split, and makes it cleaner to land: the
-unalias walk is now concentrated in one place (`api_type_info_fill`) instead of
-two.
-
----
-
-## 2. Problems
-
-### 2.1 One id, two roles
-
-`KitCgTypeId` is overloaded to be both the storage/ABI/operational identity (what
-every op needs; width-only for integers) and the source spelling for debug (name
-+ signedness). To make one id serve both, transparent wrappers carry role-2 data
-but must be seen through for every role-1 use. That see-through is the pervasive
-`api_unalias_type`.
-
-### 2.2 Distinct spellings force distinct operational ids
-
-Two C types (`int`, `unsigned int`) share storage `i32`, so a spelling cannot
-hang off the shared storage id — each spelling needs its own id. As long as those
-ids are *operational* and handed to ops, ops must map them back to storage. The
-only way to remove unaliasing is for ops to receive storage ids and for spelling
-to travel separately.
-
-### 2.3 The payoff is tiny and shrinking
-
-The exact wrapper kind is consumed at three debug sites; `ALIAS` is dead; struct
-members (the bulk of would-be spelled types) are not emitted at all. The cost —
-pervasive unalias + a caching subsystem + per-backend boilerplate + a split type
-API — is paid everywhere to feed a narrow, cold consumer.
-
----
-
-## 3. Design Decisions
-
-| # | Decision | Choice |
-|---|---|---|
-| E1 | Operational lattice | `KitCgTypeId` kinds shrink to `VOID, BOOL, INT(width), FLOAT(width), PTR, ARRAY, FUNC, RECORD, ENUM, VARARG_STATE`. No `ALIAS`, no `SOURCE_BASE`. |
-| E2 | Storage identity | `storage_id == id` for every operational type. `api_unalias_type` is deleted; `kit_cg_type_resolve_alias` becomes identity; `kit_cg_type_is_void` is a single kind check. |
-| E3 | Debug spelling home | A separate, optional `KitCgDebugType` handle, built only when debug info is requested, consumed only at debug emission. |
-| E4 | Debug graph reuse | `KitCgDebugType` references operational ids for structural/nominal pieces (records, enums, funcs) via `kit_cg_debug_of_type`; the frontend overrides only leaves (primitive spellings) and inserts typedefs. No duplication of record/enum/func debug emission. |
-| E5 | Attachment | Decl-bearing APIs gain an optional `KitCgDebugType` field; `0` (`NONE`) means "derive the default from the operational type" — preserving today's behavior for frontends that do not set it. |
-| E6 | Interning purity | Debug types never enter interned operational descriptors (`KitCgFuncSig`/`KitCgFuncParam`), so spelling never splits structural interning. |
-| E7 | Capability preservation | Faithful signed/unsigned/char debug and enum enumerators (from `244f1039`) are preserved via the debug channel; DWARF output is byte-identical. |
-| E8 | Non-C frontends | Toy and Wasm pass `0` and rely on the existing default derivation (which they already use today — `void`/`bool`/`float` are never wrapped). No change required. |
-
----
-
-## 4. New Public Shape
-
-### 4.1 Debug-type handle and builders
-
-Thin public wrappers over the existing `src/debug/` DebugType producer (which
-already has base/typedef/ptr/array/record/enum/func builders). Available only on
-a `KitCg` with debug enabled; no-ops returning `NONE` otherwise.
-
-```c
-typedef uint32_t KitCgDebugType; /* 0 = derive from the operational type */
-#define KIT_CG_DEBUG_TYPE_NONE 0u
-
-/* A named primitive leaf (DW_TAG_base_type): "int"/signed/4, etc. */
-KIT_API KitCgDebugType kit_cg_debug_base(KitCg*, KitSym name,
- KitCgDebugEncoding, uint32_t bytes);
-/* A named typedef (DW_TAG_typedef) over another debug type. */
-KIT_API KitCgDebugType kit_cg_debug_typedef(KitCg*, KitSym name, KitCgDebugType);
-/* Structural debug types mirroring operational composition. */
-KIT_API KitCgDebugType kit_cg_debug_ptr(KitCg*, KitCgDebugType pointee);
-KIT_API KitCgDebugType kit_cg_debug_array(KitCg*, KitCgDebugType elem,
- uint64_t count);
-/* Derive a debug type from an operational type: records/enums/funcs (with their
- * existing emission) and default-named scalars. The leaf-override + typedef
- * builders above are layered on top of this. */
-KIT_API KitCgDebugType kit_cg_debug_of_type(KitCg*, KitCgTypeId);
-```
-
-Composition examples (frontend side):
-
-- `int*` → `debug_ptr(debug_base("int", SIGNED, 4))`
-- `unsigned` → `debug_base("unsigned int", UNSIGNED, 4)`
-- `struct S*` → `debug_ptr(debug_of_type(S))`
-- `double` → `debug_of_type(f64)` (default name is correct)
-- `typedef T int; T x;` → `debug_typedef("T", debug_base("int", SIGNED, 4))`
- (this *gains* typedef DIEs, which the current design silently drops)
-
-`KitCgDebugEncoding` is unchanged — it is already the clean debug-only enum; it
-moves from `kit_cg_type_source_base` to `kit_cg_debug_base`.
-
-### 4.2 Attachment points
-
-Debug-only fields; `0` ⇒ derive default from the operational type.
-
-```c
-typedef struct KitCgLocalAttrs {
- KitSym name;
- uint32_t align;
- uint32_t flags;
- KitCgDebugType debug_type; /* NEW: 0 = derive from the local's type */
-} KitCgLocalAttrs; /* covers both kit_cg_local and kit_cg_param */
-
-typedef struct KitCgFuncAttrs {
- /* ... existing ... */
- KitCgDebugType debug_type; /* NEW: the subprogram's type DIE; 0 = derive */
-} KitCgFuncAttrs; /* NOT in the interned KitCgFuncSig (E6) */
-
-typedef struct KitCgObjectAttrs {
- /* ... existing ... */
- KitCgDebugType debug_type; /* NEW: global object's type DIE; 0 = derive */
-} KitCgObjectAttrs;
-```
-
-`api_debug_type(KitCgTypeId)` becomes a resolver over `KitCgDebugType`: when a
-decl site supplies a debug type, emit that; when it supplies `NONE`, fall back to
-the operational-type walk that exists today. Record member types (when DWARF
-members are eventually emitted) will carry `KitCgDebugType` on `KitCgField` at
-that time; nothing depends on it now.
-
-### 4.3 Operational API simplifications
-
-- Delete `kit_cg_type_alias`, `kit_cg_type_source_base`, and their accessors
- (`kit_cg_type_alias_name/base`, `kit_cg_type_source_base_name/storage/encoding`).
-- Delete `KIT_CG_TYPE_ALIAS`, `KIT_CG_TYPE_SOURCE_BASE` from `KitCgTypeKind`.
-- `kit_cg_type_resolve_alias` returns its argument (identity). Keep as a trivial
- documented accessor or remove; `kit_cg_type_same_storage` reduces to structural
- storage comparison with no alias step.
-- `KitCgTypeInfo.storage_id` always equals `id`. Decision point (Open Q1):
- keep the field as a documented `== id` identity (zero churn to the struct/ABI)
- or remove it.
-
----
-
-## 5. Internal Architecture
-
-### 5.1 Type table
-
-`CgApiType` loses the wrapper-specific state once the kinds are gone:
-
-- `api_type_info_fill` (`src/cg/type.c`) drops its `while (ALIAS||SOURCE_BASE)`
- walk; `info.storage_id = id`.
-- `api_unalias_type` is deleted. Its ~33 hot-path callers and ~9 in-`type.c`
- callers use the id directly.
-- `pred_bits`/`pred_valid` no longer need an unalias step — the kind is now
- exact, so `cg_type_is_int`/`_float`/`_ptr`/etc. read the kind directly. The
- bitset cache can be kept (one indexed load) or dropped (kind lookup is already
- one load); prefer dropping it to shed state unless a measurement says keep.
-- The `ALIAS`/`SOURCE_BASE` recursion deletes from `cg_type_pointee`,
- `cg_type_func_ret_id`/`_result_id`/`_param_id`, `kit_cg_type_int_width`,
- `kit_cg_type_float_width`, `api_int_like_width`, `api_type_is_bool`,
- `cg_type_complete_id`, `cg_type_sized_id`, `cg_type_same_storage_rec`.
-
-### 5.2 ABI layer
-
-The `if (t->kind == KIT_CG_TYPE_ALIAS) recurse; if (... SOURCE_BASE) recurse`
-prologue deletes from `abi_cg_type_info` (`src/abi/abi.c`) and from every
-per-arch classifier (`abi_rv64.c`, `abi_sysv_x64.c`, `abi_aapcs64.c`,
-`abi_win64_x64.c`, `src/arch/wasm/abi.c`). ABI queries see only real storage
-kinds.
-
-### 5.3 Debug producer
-
-`src/debug/` is untouched. `src/cg/debug.c` changes from "walk the operational
-`CgType` and special-case `ALIAS`/`SOURCE_BASE`" to "walk a `KitCgDebugType`
-graph; for an operational-type reference, dispatch into the existing
-record/enum/func/ptr/array emission." Enum enumerators stay on the operational
-enum entry (nothing unaliases an enum, so they cost no pervasiveness); only the
-enum's *underlying spelling* comes from the debug channel.
-
----
-
-## 6. Frontend Impact
-
-### 6.1 C frontend (the only affected frontend)
-
-- `type_cg_scalar` (`lang/c/type/type.c`) stops wrapping in source-base; returns
- the bare builtin. `t->cg_id` becomes a pure storage id, used unchanged at all
- operational sites (memaccess, convert, params, locals, fields, calls).
-- Add a parallel lazy `type_cg_debug(Type*) → KitCgDebugType`, cached as
- `t->dbg_id`, structurally identical to `type_cg_lower`. The existing 13-entry
- `kScalarDbg` table now drives `kit_cg_debug_base` instead of
- `kit_cg_type_source_base`. The two lowerings can share one traversal (they
- recurse in lockstep) if that is cleaner.
-- At decl sites — `kit_cg_local`, `kit_cg_param`, `kit_cg_func_begin_attrs`,
- global object definition — set `attrs.debug_type = type_cg_debug(t)`.
-- Enum lowering keeps passing enumerators to `kit_cg_type_enum` (operational);
- the enum's debug underlying spelling rides on `type_cg_debug`.
-
-### 6.2 Toy / Wasm
-
-No change. They pass `0` for `debug_type` and get today's default derivation.
-
----
-
-## 7. Implementation Plan
-
-### Phase 1: Add the debug channel, migrate C behind it
-
-- Add `KitCgDebugType`, the builders, and the decl-attr fields.
-- Make `api_debug_type` a resolver: supplied `KitCgDebugType` wins; `NONE`
- derives the default from the operational type (today's behavior).
-- Add `type_cg_debug` to the C frontend; set `debug_type` at decl sites.
-- `SOURCE_BASE`/`ALIAS` still exist at this point; the goal is for debug to stop
- *reading* them. Verify DWARF byte-identity (`make test-debug test-dwarf`,
- `kit cc -g` spot checks for `DW_ATE_unsigned`/`signed_char`/`unsigned_char` and
- enumerator DIEs).
-
-### Phase 2: Delete the wrappers and all unaliasing
-
-Once debug no longer reads the wrappers, cut over in one clean break:
-
-- Stop the C frontend creating source-base ids (`type_cg_scalar` returns the
- builtin).
-- Delete `kit_cg_type_alias`/`source_base` + accessors, the two `KitCgTypeKind`
- values, `api_unalias_type`, and the recursion/prologues listed in §5.
-- Collapse `storage_id` to `id` (or remove the field — Open Q1).
-- Simplify `kit_cg_type_resolve_alias`/`is_void`/`same_storage`.
-
-### Phase 3: Tests
-
-- Rewrite `test/api/cg_type_test.c` alias/source-base cases as `KitCgDebugType`
- builder + emission tests.
-- Add a debug-channel test: a local of `unsigned`, a `signed char`, an enum, and
- a typedef each produce the expected DWARF DIE.
-
----
-
-## 8. Validation
-
-- `make test-debug test-dwarf` — primary gate; DWARF must be byte-identical for
- the signed/unsigned/char/enum cases `244f1039` introduced.
-- `make test-cg-api` — type API contract.
-- `make test-parse test-toy test-opt` — frontend + mid-end unaffected.
-- `make test-smoke-x64 test-smoke-rv64` — end-to-end with `-g`.
-- Ecosystem / byte-identity gate (see [PERF.md](PERF.md) and the ecosystem
- harness): object output must be unchanged; DWARF must match.
-
-Scenario coverage: `int`/`unsigned`/`char`/`signed char`/`unsigned char` locals
-and params; enums with enumerators; typedef DIEs (new, previously dropped);
-function pointers; records (members still opaque — confirm no regression);
-`va_list`; non-C frontends emit unchanged debug via the default path.
-
----
-
-## 9. Acceptance Criteria
-
-- `KitCgTypeKind` has no `ALIAS`/`SOURCE_BASE`; `KitCgTypeId` is storage/ABI only.
-- `api_unalias_type` and the per-backend alias/source-base prologues are gone.
-- `storage_id == id` everywhere (field kept as identity or removed).
-- No CG hot-path op resolves aliases; predicates read the exact kind.
-- DWARF output for the `244f1039` capabilities is byte-identical; typedef DIEs
- are additionally available.
-- Toy and Wasm are unchanged.
-- No global state, no VLAs; debug-type state hangs off `KitCg`.
-
----
-
-## 10. Open Questions
-
-1. **Keep or remove `KitCgTypeInfo.storage_id`?** It becomes vestigial (`== id`).
- Keeping it as a documented identity is zero-churn and leaves
- `kit_cg_type_resolve_alias` meaningful as a trivial accessor; removing it is
- cleaner but touches the public struct and every reader. Prefer **remove** for
- a true clean break, consistent with CG-TYPES.md's no-backcompat stance.
-2. **Should enum enumerators also move to the debug channel?** They are
- debug-only in principle, but they already live on the operational enum entry
- and cost no pervasiveness (nothing unaliases an enum). Prefer **leave on the
- operational enum** to minimize churn; revisit only if member-type debug work
- wants a uniform debug-graph representation.
-3. **`kit_cg_debug_of_type` vs a fully parallel debug graph.** The plan leans on
- `of_type` to reuse existing record/enum/func emission. If a future need arises
- to spell those differently from their operational shape (rare), the builder
- set would grow `kit_cg_debug_struct/union/enum/func`. Prefer the minimal
- `of_type`-based set until such a need is concrete.
diff --git a/doc/plan/CG-TYPES.md b/doc/plan/CG-TYPES.md
@@ -1,591 +0,0 @@
-# CG type and type-id system clean cutover
-
-**Goal.** Replace the current `KitCgTypeId` implementation and public type API
-with a simpler, explicit, and ABI-correct type table. Keep the good part: a
-small compiler-owned integer handle that can be stored cheaply throughout IR,
-ABI records, frontends, and backends. Change what that handle means, how void
-and aliases are represented, where layout facts are computed, and how nominal
-records are completed.
-
-This is a **clean-break rewrite, no backcompat**. The old constructors, query
-semantics, `KIT_CG_TYPE_NONE`-as-void convention, segmented id encoding, and
-frontend workarounds are removed as the cutover lands. There is no compatibility
-adapter and no byte-identity gate.
-
----
-
-## 1. Current State
-
-The public API uses an opaque `uint32_t KitCgTypeId`:
-
-- `0` is `KIT_CG_TYPE_NONE`.
-- Builtins are encoded in a reserved id segment.
-- User types are stored in a segmented vector with a biased segment id.
-- Pointers, arrays, and functions are structurally interned.
-- Aliases, records, and enums allocate fresh ids.
-
-Internally, `src/cg/type.c` owns `CgApiState`:
-
-- `CgType builtins[KIT_CG_BUILTIN_COUNT]`
-- `CgApiTypes types`
-- structural indexes for pointer, array, and function types
-- per-entry caches for alias resolution, predicates, type class, and ABI layout
-
-The C frontend lowers `Type*` to `KitCgTypeId` lazily and caches ids on C type
-nodes and parser stack slots. Incomplete records are handled outside CG: a
-temporarily incomplete record lowers to `void`, and record-field/function-param
-pointer lowering collapses pointees to `void*` so layout can proceed without a
-true incomplete nominal record id.
-
-The ABI layer reads `CgType` entries and caches target layout/classification
-facts by type id. Some storage facts live eagerly on `CgType` (`size`, `align`);
-some target facts live in `TargetABI`; record layout is currently computed by
-the CG type constructor and re-exposed by the ABI cache.
-
----
-
-## 2. Problems
-
-### 2.1 `NONE` Has Too Many Meanings
-
-`KIT_CG_TYPE_NONE` currently means:
-
-- invalid type id
-- absent function result, i.e. a void return
-- statement scope or no value in some control APIs
-
-There is also a real void builtin. Every caller must remember which API treats
-void as a real type and which treats `NONE` as absence. That is a correctness
-hazard and a permanent source of branchy special cases.
-
-### 2.2 Alias Semantics Are Implicit
-
-Some queries inspect the exact id. Others recurse through aliases. Examples:
-
-- integer and float width follow aliases
-- predicates follow aliases through a cached descriptor
-- pointer/array/function field queries are mostly exact-kind queries
-- many internal call sites remember to call `api_unalias_type`
-
-The result is not one type model; it is a collection of local conventions.
-
-### 2.3 ID Decode Complexity Does Not Buy Enough
-
-The segmented builtin/user encoding makes every id lookup decode segment bits.
-That helped keep builtin ids stable and user pointers stable, but the hot path
-now contains extra decode logic and special builtin handling. A direct table
-index keeps the same public handle shape with less machinery.
-
-### 2.4 Layout Authority Is Split
-
-`CgType` eagerly stores `size` and `align`, while `TargetABI` is supposed to be
-the single authority for target-dependent layout and calling convention facts.
-This is especially fragile for:
-
-- 32-bit ABIs and non-default pointer alignment
-- Windows and other ABI-specific scalar/aggregate rules
-- bit-field layout differences
-- `va_list` and other target-shaped builtins
-
-### 2.5 Incomplete Nominal Records Are Not Modeled
-
-The C frontend needs true identities for incomplete records so these are legal:
-
-```c
-struct S;
-struct T { struct S *p; };
-struct S { int x; };
-```
-
-Today CG has no incomplete record declaration/completion API, so the frontend
-works around it by erasing some pointee identity to `void*` while lowering
-record fields and function parameters. That preserves layout but loses type
-identity and pushes a CG responsibility into the frontend.
-
-### 2.6 Queries Are Too Fragmented
-
-The public API exposes many one-off queries (`kind`, `size`, `align`,
-`ptr_pointee`, `array_elem`, `func_param`, `record_field`, etc.). That is easy
-to call but expensive and ambiguous at scale: each query chooses exact vs
-resolved behavior separately, and hot call sites decode the same id repeatedly.
-
----
-
-## 3. Design Decisions
-
-| # | Decision | Choice |
-|---|---|---|
-| D1 | Public handle shape | Keep `typedef uint32_t KitCgTypeId`; `0` is invalid/none only |
-| D2 | Void representation | Void is always the real void builtin type id; `NONE` never means void |
-| D3 | ID layout | Builtin ids are `1..KIT_CG_BUILTIN_COUNT`; user ids are `KIT_CG_BUILTIN_COUNT + 1 + index` |
-| D4 | Cross-compiler stability | Type ids are stable only within one `KitCompiler`; never serialized as semantic identities |
-| D5 | Structural interning | Pointer, array, and function constructors intern by exact constructor shape |
-| D6 | Nominal identity | Records, enums, and aliases keep fresh source-facing ids |
-| D7 | Alias behavior | Exact queries inspect aliases; storage/semantic queries resolve aliases explicitly |
-| D8 | Function void result | Function result type is always a valid type id; use the void builtin for no value |
-| D9 | Layout authority | `TargetABI` computes all size/align/scalar layout facts; type entries only cache ABI results |
-| D10 | Records | Records are nominal and two-phase: declare first, complete once |
-| D11 | Incomplete use | Pointers to incomplete records are legal; sizing, arrays, fields, by-value params/results, locals, and memory accesses require complete/sized types |
-| D12 | Caches | Caches live on the owning type entry or ABI context and are filled only after the facts they depend on are final |
-| D13 | Implementation style | C11, no VLAs, no global type state; everything hangs off `KitCompiler`/`TargetABI` |
-
----
-
-## 4. New Public Shape
-
-### 4.1 IDs and Builtins
-
-The id remains opaque, but implementation decode is direct:
-
-```c
-#define KIT_CG_TYPE_NONE 0u
-
-/* implementation rule, not a public promise beyond nonzero/stable per compiler:
- * builtin id = 1 + KitCgBuiltinType
- * user id = 1 + KIT_CG_BUILTIN_COUNT + user_index
- */
-```
-
-The public builtin query becomes singular. The whole-table query is removed
-unless a real caller needs it after the cutover.
-
-```c
-KIT_API KitCgTypeId kit_cg_type_builtin(KitCompiler*, KitCgBuiltinType);
-```
-
-### 4.2 Function Signatures
-
-`KitCgFuncResult.type` is always valid. A void function uses the void builtin.
-
-```c
-typedef struct KitCgFuncResult {
- KitCgTypeId type; /* valid; void builtin means no value */
- KitCgAbiAttrs attrs;
-} KitCgFuncResult;
-
-typedef struct KitCgFuncSig {
- KitCgFuncResult result;
- const KitCgFuncParam* params;
- uint32_t nparams;
- KitCgCallConv call_conv;
- bool abi_variadic;
-} KitCgFuncSig;
-```
-
-The helper predicate is explicit:
-
-```c
-KIT_API int kit_cg_type_is_void(KitCompiler*, KitCgTypeId);
-KIT_API int kit_cg_func_result_has_value(KitCompiler*, KitCgFuncResult);
-```
-
-No caller tests `result.type == KIT_CG_TYPE_NONE` after this cutover.
-
-### 4.3 Exact and Storage Queries
-
-Exact identity and storage identity are separate operations.
-
-```c
-KIT_API KitCgTypeId kit_cg_type_resolve_alias(KitCompiler*, KitCgTypeId);
-KIT_API int kit_cg_type_same_storage(KitCompiler*, KitCgTypeId,
- KitCgTypeId);
-```
-
-Rules:
-
-- `kit_cg_type_kind` is exact. An alias id reports `KIT_CG_TYPE_ALIAS`.
-- `kit_cg_type_storage_info` resolves aliases before reporting layout/scalar
- facts.
-- `kit_cg_type_same_storage` recursively compares alias-resolved storage
- structure where needed, so `ptr(alias(i32))` and `ptr(i32)` can be storage
- equivalent even if their exact ids differ.
-
-### 4.4 Descriptor Query
-
-Add one structured query for hot callers and keep narrow helpers only where they
-remain clearly useful.
-
-```c
-typedef enum KitCgTypeFlag {
- KIT_CG_TYPEF_COMPLETE = 1u << 0,
- KIT_CG_TYPEF_SIZED = 1u << 1,
- KIT_CG_TYPEF_BUILTIN = 1u << 2,
- KIT_CG_TYPEF_NOMINAL = 1u << 3,
-} KitCgTypeFlag;
-
-typedef enum KitCgStorageKind {
- KIT_CG_STORAGE_VOID,
- KIT_CG_STORAGE_BOOL,
- KIT_CG_STORAGE_INT,
- KIT_CG_STORAGE_FLOAT,
- KIT_CG_STORAGE_PTR,
- KIT_CG_STORAGE_AGGREGATE,
-} KitCgStorageKind;
-
-typedef struct KitCgTypeLayout {
- uint64_t size;
- uint32_t align;
- uint16_t scalar_width;
- uint8_t storage_kind; /* KitCgStorageKind */
- uint8_t valid;
-} KitCgTypeLayout;
-
-typedef struct KitCgTypeInfo {
- KitCgTypeId id;
- KitCgTypeId storage_id; /* alias-resolved id, or id */
- KitCgTypeKind kind; /* exact kind */
- uint32_t flags; /* KitCgTypeFlag */
- KitCgTypeLayout layout; /* valid only for sized/storage queries */
-} KitCgTypeInfo;
-
-KIT_API KitStatus kit_cg_type_info(KitCompiler*, KitCgTypeId,
- KitCgTypeInfo*);
-```
-
-This descriptor is intentionally shallow. Shape-specific data still comes from
-shape-specific APIs or internal entry access:
-
-- pointer: pointee + address space
-- array: element + count
-- function: result/params/call convention
-- record: field count and field descriptors
-- enum: base and values
-- alias: name and base
-
-### 4.5 Record Declaration and Completion
-
-Records are true nominal objects.
-
-```c
-KIT_API KitCgTypeId kit_cg_type_record_decl(KitCompiler*, KitSym tag,
- int is_union);
-
-KIT_API KitStatus kit_cg_type_record_complete(KitCompiler*, KitCgTypeId record,
- const KitCgRecordDesc*);
-
-KIT_API int kit_cg_type_is_complete(KitCompiler*, KitCgTypeId);
-```
-
-Convenience construction stays possible, but it is just declare + complete:
-
-```c
-KIT_API KitCgTypeId kit_cg_type_record(KitCompiler*, const KitCgRecordDesc*);
-```
-
-Completion rules:
-
-- A record can be completed exactly once.
-- Field types must be valid.
-- A by-value field must be sized and complete.
-- A pointer field may point to an incomplete record.
-- A record cannot contain itself by value.
-- Layout caches for the record are invalid until completion succeeds.
-
-The C frontend no longer lowers record-context pointers to `void*`.
-
-### 4.6 Array Counts
-
-The public array count remains `uint64_t`. The structural key must use the full
-64-bit count or reject out-of-range counts with a documented status. Do not
-truncate the key. Prefer full-width support unless an object/backend limit
-requires a specific diagnostic.
-
-```c
-KIT_API KitCgTypeId kit_cg_type_array(KitCompiler*, KitCgTypeId elem,
- uint64_t count);
-```
-
----
-
-## 5. Internal Architecture
-
-### 5.1 Type Table
-
-Use one compiler-owned table. Builtins occupy the first nonzero ids; user entries
-append after them.
-
-```c
-typedef struct CgTypeEntry {
- KitCgTypeKind kind;
- uint32_t flags;
-
- KitCgTypeId id;
- KitCgTypeId storage_id; /* alias terminal once known */
-
- uint8_t pred_bits;
- uint8_t pred_valid;
- uint8_t class_bits;
- uint8_t class_valid;
- uint8_t layout_valid;
-
- KitCgTypeLayout layout;
-
- union {
- CgBuiltinType builtin;
- CgPtrType ptr;
- CgArrayType array;
- CgFuncType func;
- CgRecordType record;
- CgEnumType enum_;
- CgAliasType alias;
- };
-} CgTypeEntry;
-
-typedef struct CgTypeTable {
- Heap* heap;
- CgTypeEntries entries;
- CgPtrMap ptr_index;
- CgArrayMap array_index;
- CgFuncMap func_index;
-} CgTypeTable;
-```
-
-The table can still use a segmented vector if stable entry pointers are useful,
-but indexes are direct: `id - 1` maps to an entry. Hash indexes store ids, not
-entry pointers, so the implementation is free to move entries later if that
-becomes desirable.
-
-### 5.2 Structural Indexes
-
-Pointer key:
-
-```c
-{ pointee_id, address_space }
-```
-
-Array key:
-
-```c
-{ elem_id, uint64_t count }
-```
-
-Function key:
-
-```c
-{ result type+attrs, params type+attrs[], call_conv, abi_variadic }
-```
-
-The key is exact-id based. Alias preservation is therefore possible for source
-facing APIs and generated C/debug output. Storage equivalence is a separate
-query and must not depend on exact id equality.
-
-### 5.3 Layout Cache
-
-`CgTypeEntry.layout` is a cache of the ABI-computed layout, not an independent
-source of truth.
-
-Flow:
-
-```text
-kit_cg_type_size/align/info
- -> cg_type_require_layout
- -> TargetABI layout hook / shared ABI layout
- -> cache on CgTypeEntry after success
-```
-
-Builtins can be initialized with target facts, but they still follow the same
-layout contract. Target-shaped builtins such as `vararg_state` are computed
-through the ABI layer.
-
-Record completion asks the ABI/layout authority to compute source-facing record
-layout before marking the record complete. If a future ABI has different
-bit-field layout rules, it plugs in at this boundary rather than after the
-record is already committed.
-
-### 5.4 Predicates and Classes
-
-Predicate bits and codegen class bits remain valuable, but they should be
-computed from one storage descriptor:
-
-```text
-exact id -> resolve alias -> storage/layout descriptor -> pred/class bits
-```
-
-That avoids one-off predicate functions each re-decoding the id and each
-choosing its own alias behavior.
-
----
-
-## 6. Frontend Impact
-
-### 6.1 C Frontend
-
-The C frontend should map source types to CG types as follows:
-
-- C scalar type -> builtin storage id
-- C pointer type -> pointer to the exact lowered pointee id
-- C function type -> function id with a real void result type when appropriate
-- C record declaration -> `kit_cg_type_record_decl`
-- C record definition -> `kit_cg_type_record_complete`
-- C enum -> enum nominal id with integer base
-- C typedef -> source alias id only if CG/debug/C-backend output needs the
- source-facing name; storage semantics must use alias resolution
-
-The current incomplete-record workaround is deleted. The C type pool can cache
-the declared record id as soon as the tag exists, then complete the same id when
-the definition is parsed.
-
-### 6.2 Toy Frontend
-
-Toy mostly uses CG storage types directly. It should switch to:
-
-- real void result type in function signatures
-- explicit storage/equivalence queries where it currently relies on exact
- `kit_cg_type_kind`
-- record construction through the new declare/complete convenience API
-
-### 6.3 C Backend and Debug Info
-
-The C backend and debug producer are the main consumers that care about exact
-source-facing ids. They should use exact queries for names and aliases, and
-storage queries for codegen legality.
-
----
-
-## 7. Implementation Plan
-
-### Phase 1: Lock the New Contract in Tests
-
-Add focused `test/api/cg_type_test.c` cases that fail under the old model:
-
-- void function result uses the void builtin, never `NONE`
-- invalid ids stay invalid and never masquerade as void
-- alias exact kind is `KIT_CG_TYPE_ALIAS`
-- alias storage info reports the base storage kind/layout
-- `kit_cg_type_same_storage` succeeds for alias-equivalent storage shapes
-- record declaration creates an incomplete nominal id
-- pointer to incomplete record is legal
-- array of incomplete record is rejected
-- record completion fills fields/layout and cannot run twice
-- recursive by-pointer records are legal
-- recursive by-value records are rejected
-- full-width array count keys do not collide
-
-### Phase 2: Replace the Type Table
-
-Introduce the new direct-index table behind `src/cg/type.c`:
-
-- builtins are entries `1..KIT_CG_BUILTIN_COUNT`
-- user entries append after builtins
-- `cg_type_get` becomes direct index validation
-- structural maps store ids
-- remove segment/bias decode helpers
-
-Do this before converting all callers, accepting compile failures in the
-working tree during the phase. There is no compatibility shim.
-
-### Phase 3: Cut Over Public API and Call Sites
-
-Update `include/kit/cg.h` and every caller:
-
-- replace `kit_cg_builtin_type_id` with `kit_cg_type_builtin`
-- replace `result.type == KIT_CG_TYPE_NONE` void checks with void-builtin checks
-- update scope/block APIs that currently use `NONE` for statement/no-result so
- absence remains explicit and cannot be confused with the void type
-- replace ambiguous one-off queries with exact or storage queries
-- update function, intrinsic, call, local, memory, and data APIs to require real
- type ids where a type is required
-
-### Phase 4: Move Layout Authority to ABI
-
-Make `TargetABI` the only layout authority:
-
-- scalar/builtin layout through ABI helpers
-- pointer layout through target spec read by ABI
-- array layout from element ABI layout
-- record layout during completion through an ABI record-layout hook
-- `kit_cg_type_size/align/info` read cached ABI layout, computing on demand
-
-Delete independent eager `CgType.size` / `CgType.align` ownership. A type entry
-may cache layout but does not define it.
-
-### Phase 5: Add Two-Phase Records and Convert C Lowering
-
-Convert the C frontend record path:
-
-- create/get CG record id at tag declaration time
-- complete that same id at record definition time
-- preserve pointer-to-incomplete-record identity
-- delete `TYPE_CG_RECORD_FIELD` pointee erasure to `void*`
-- delete incomplete-record temporary lowering to `void`
-- keep source diagnostics in the C frontend for illegal incomplete uses
-
-### Phase 6: Delete Old Caches and Helpers
-
-Remove now-redundant pieces:
-
-- segment id constants and decode helpers
-- recursive alias helper conventions in random query functions
-- `api_unalias_type` call-site scatter where replaced by descriptor queries
-- duplicated eager layout caches in ABI/CG if the entry layout cache covers the
- use case
-- C frontend record memo workarounds that exist only because CG lacked
- incomplete nominal records
-
----
-
-## 8. Validation
-
-Use targeted runs during the cutover:
-
-- `make test-cg-api`
-- `make test-parse`
-- `make test-toy`
-- `make test-opt`
-- `make test-aa64-inline`
-- targeted smoke tests for one native 64-bit target and one non-aa64 backend
- touched by layout or call classification
-
-Broaden only after the API and frontend are compiling cleanly.
-
-Important scenario coverage:
-
-- C recursive structs by pointer
-- incomplete record misuse diagnostics
-- function pointers involving records/enums/aliases
-- varargs and `va_list`
-- bit-fields, packed records, explicit/max alignment
-- enum integer bases
-- generated C backend output for aliases/records
-- debug info typedef/record naming
-
----
-
-## 9. Acceptance Criteria
-
-- `KIT_CG_TYPE_NONE` means invalid/absent only; no function result or value type
- uses it to mean void.
-- Builtin and user ids decode by direct table index, with no segment/bias scheme.
-- Pointer, array, and function interning remains O(1)-average and collision-safe.
-- Aliases have explicit exact-vs-storage semantics.
-- Records can be declared incomplete and completed once.
-- The C frontend no longer erases record-context pointer pointees to `void*`.
-- Size/align/scalar facts come from the ABI layout path.
-- Existing type hot paths still have cached predicate/class/layout bytes, but
- those caches derive from the new descriptor model.
-- No global state, no VLAs, and all state hangs off `KitCompiler` or
- `TargetABI`.
-
----
-
-## 10. Open Questions
-
-1. Should `KIT_CG_TYPE_ALIAS` remain a true public kind, or should source aliases
- move entirely into debug/C-backend metadata after the cutover? The plan above
- keeps alias ids because they already serve source-facing consumers, but the
- storage semantics do not require them.
- **Resolved → move them out.** See [CG-TYPE-DEBUG-SPLIT.md](CG-TYPE-DEBUG-SPLIT.md):
- `KitCgTypeId` becomes storage/ABI-only and source spelling (now including the
- `KIT_CG_TYPE_SOURCE_BASE` added since this question was written) moves onto a
- separate optional debug-type channel, deleting `api_unalias_type` and the
- per-backend unalias boilerplate.
-2. Should function types reject incomplete by-value records at construction, or
- permit declaration-like signatures and reject only when defining/calling?
- Prefer rejection unless a frontend has a concrete declaration use case.
-3. Should `kit_cg_type_info` include shallow shape facts, or stay layout-only
- with shape-specific accessors? Prefer shallow layout-only initially to keep
- the public struct stable.
-4. Should array counts larger than target addressable object size be rejected by
- the type constructor or by object/data/local users? Prefer constructor
- rejection if every backend agrees on the same maximum; otherwise diagnose at
- the first sized-object use.
diff --git a/doc/plan/DEBUG.md b/doc/plan/DEBUG.md
@@ -1,48 +1,40 @@
-# Debugger, Debug Info, and Profiling (planned work)
+# Debugger and Debug Info (planned work)
This roadmap consolidates the remaining work across the interactive JIT
-debugger (`kit dbg`), the DWARF producer/consumer, and the not-yet-built
-sampling profiler (`kit prof`). Designs live one level up:
+debugger (`kit dbg`) and the DWARF producer/consumer. Designs live one level up:
[../DBG.md](../DBG.md) covers the `KitDebugSession` architecture, the
`KitDbgOs` host vtable, software breakpoints, and displaced single-step;
[../DWARF.md](../DWARF.md) covers the producer pipeline and the
-`kit_dwarf_*` consumer surface. This document is forward-looking: it states
-the baseline only as a starting point, then enumerates the open gaps, their
-rationale, and the next steps. Shipped items are noted as "done (baseline)".
+`kit_dwarf_*` consumer surface. The host-native sampling profiler (`kit prof`),
+which reuses this debugger's signal infrastructure, has its own roadmap in
+[PROF.md](PROF.md). This document is forward-looking: it states the baseline
+only as a starting point, then enumerates the open gaps, their rationale, and
+the next steps. Shipped items are noted as "done (baseline)".
## Baseline
-What already works and is not re-planned here:
-
-- The JIT debugger session (`src/dbg/session.c`, `bp.c`, `mem.c`, `step.c`,
- `displaced.c`) is real: worker thread, park/unpark, fault classification,
- refcounted software breakpoints with a read overlay, guarded memory access,
- displaced single-step, and the `STEP_LINE` / `NEXT_LINE` / `STEP_OUT` state
- machines. Done (baseline).
-- Displaced-step lifter implementations exist for all three backends. The
- lifter is arch-neutral (`dbg_displaced_prepare` drives an `ArchDbgOps`
- vtable); each backend ships `build_displaced_shim` + `decode_insn`:
- `src/arch/aa64/dbg.c`, `src/arch/x64/dbg.c` (INT3 + RIP-relative/rel8/rel32
- fixups), `src/arch/rv64/dbg.c` (EBREAK + AUIPC/JAL/branch fixups). Done
- (baseline).
-- Session-level integration of displaced-step is complete only on aarch64
- hosts. The x64 and rv64 backends decode and fix up instructions correctly in
- isolation, but their end-to-end REPL session loop (fault classification, trap
- PC normalization, register marshalling) has not been validated; closing that
- gap is §1 below. Done (baseline) for aarch64 only.
-- The POSIX host adapter (`driver/env/posix_dbg.c`, macOS/Linux/FreeBSD
- ucontext marshalling) and the Windows host adapter (`driver/env/windows.c`:
- `g_dbg_os_win` with `AddVectoredExceptionHandler`, `Set`/`GetThreadContext`
- interrupt path, `__try`/`__except` guarded copy) are both wired. Done
- (baseline).
-- The DWARF producer (`src/debug/debug*.c`: abbrev/form/emit, line program,
- type DIEs, `.eh_frame` CFI) and consumer (`src/debug/dwarf_*.c`:
- open/line/die/type/loc/query/cfi) are implemented and tested via `test-dwarf`
- / `test-debug`, including multi-input `kit_jit_view`,
- `kit_dwarf_line_to_addr` suffix matching, and graceful "no debug info for
- this frame" degradation. Done (baseline).
-
-The sections below are the work that remains.
+What already works and is not re-planned here (the sections below are the work
+that remains):
+
+- **JIT debugger session** (`src/dbg/session.c`, `bp.c`, `mem.c`, `step.c`,
+ `displaced.c`): worker thread, park/unpark, fault classification, refcounted
+ software breakpoints with a read overlay, guarded memory access, displaced
+ single-step, and the `STEP_LINE` / `NEXT_LINE` / `STEP_OUT` state machines.
+- **Displaced-step lifters** for all three backends, behind the arch-neutral
+ `ArchDbgOps` vtable (`dbg_displaced_prepare`): `src/arch/aa64/dbg.c`,
+ `src/arch/x64/dbg.c` (INT3 + RIP-relative/rel8/rel32), `src/arch/rv64/dbg.c`
+ (EBREAK + AUIPC/JAL/branch). But end-to-end session integration is validated
+ on **aarch64 only**; x64/rv64 decode correctly in isolation yet their REPL
+ session loop is unproven — that gap is §1 below.
+- **Host adapters**: the POSIX adapter (`driver/env/posix_dbg.c`,
+ macOS/Linux/FreeBSD ucontext marshalling) and the Windows adapter
+ (`driver/env/windows.c`: `g_dbg_os_win`, VEH interrupt path, `__try`/`__except`
+ guarded copy) are both wired.
+- **DWARF producer + consumer** (`src/debug/debug*.c`, `src/debug/dwarf_*.c`):
+ abbrev/form/emit, line program, type DIEs, `.eh_frame` CFI on the producer;
+ open/line/die/type/loc/query/cfi on the consumer. Tested via `test-dwarf` /
+ `test-debug`, including multi-input `kit_jit_view`, `kit_dwarf_line_to_addr`
+ suffix matching, and graceful "no debug info for this frame" degradation.
## 1. Bring x64 / rv64 debug sessions to full parity
@@ -176,82 +168,10 @@ Explicitly deferred until a client needs them (carried forward, not planned):
- Split DWARF / `.dwo`, `.debug_pubnames`, and any `LSDA` / exception tables
(C has none).
-## 7. Sampling profiler — `kit prof` (not yet built)
-
-A statistical CPU profiler that reuses the debugger's host signal
-infrastructure. Nothing exists yet: no `prof` subcommand in `driver/main.c`,
-no `src/dbg/prof.c`, no `on_sample` field on `KitDbgSignalOps`. Design
-intent: SIGPROF fires on the worker, the handler walks the frame-pointer chain
-into a pre-allocated ring buffer and returns **without parking** — the one
-property that keeps sampling cheap and guest timing undisturbed — and PCs are
-symbolicated after the guest exits.
-
-> **Scope — this is the *sampling* profiler, deliberately on the host-native
-> substrate.** It is one of two complementary profilers: this one measures real
-> wall/CPU time by statistically sampling natively-run kit code; the
-> *deterministic* instruction-counting profiler (callgrind-style — exact,
-> reproducible, any guest arch) lives on the emulator substrate
-> ([INSTRUMENT.md](INSTRUMENT.md) §9, §12.1). `kit prof` is **not** being
-> rebased onto the emulator: the two share a back-end (symbolication + folded /
-> flat output), not a collector. See INSTRUMENT.md §9 for the perf-vs-callgrind
-> split and the one-core-two-collectors factoring.
-
-Public API (`include/kit.h`):
-
-- Add `on_sample(void* session, void* ucontext)` to `KitDbgSignalOps` (NULL =
- ignore SIGPROF); it receives the raw `ucontext_t*`, not a marshalled frame,
- because it extracts only PC and FP on the hot path.
-- Declare `KitProfBuf` (fixed-capacity sample ring: `pcs[PROF_MAX_DEPTH]` per
- sample, `count`/`cap`/`dropped`) and `KitProfWriter` (post-run symbolication
- callback vtable), plus `kit_dbg_session_prof_attach(session, buf)` (before
- `session_call`) and `kit_dbg_session_prof_collect(session, buf, writer)`.
-
-Library (`src/dbg/prof.c`, freestanding C11):
-
-- `dbg_fp_walk(ucontext, sample)`: frame-pointer walk via
- `dbg_os->guarded_copy` for every dereference; terminate on NULL / misaligned
- / non-advancing FP or `PROF_MAX_DEPTH`. The three frame layouts are identical
- (`[FP]` = saved FP, `[FP+8]` = saved LR/return addr); FP is x29 / rbp /
- s0(x8). WHY: no DWARF or symbol lookup on the signal path — raw PCs only.
-- `on_sample` body: capacity check, walk, append or bump `dropped` (non-atomic;
- the single worker makes that safe). `prof_attach` / `prof_collect` bodies;
- `prof_collect` symbolicates each PC via `kit_jit_addr_to_sym` +
- `kit_dwarf_addr_to_line`, dispatching to the writer (may allocate freely).
-
-Host adapter (`driver/env/posix_dbg.c` and `windows.c`):
-
-- Add SIGPROF to the POSIX handler's signal set with an early-return path:
- `if (signo == SIGPROF && on_sample) { on_sample(...); return; }` — no
- park/unpark. SIGPROF joins the blocked cohort so it does not recurse. Timer
- arming (`setitimer(ITIMER_PROF)`) and thread targeting stay driver-side and
- do **not** belong behind `KitDbgOs`.
-- Decide the Windows sampling mechanism (no SIGPROF): a periodic
- `SuspendThread` + `GetThreadContext` sampler thread is the natural analog of
- the VEH interrupt path. WHY: the SIGPROF design has no direct Windows
- equivalent, so this is a genuine open design question, not a port.
-
-Driver (`driver/cmd/prof.c`, wired into the multi-call dispatch in
-`driver/main.c`):
-
-- Flags `--rate` (default 1ms), `--depth` (64), `--cap` (1M), `--output`
- (`prof.folded`), `--no-folded`, `--no-flat`. Input handling mirrors
- `kit run`, with `-g` forced on so symbolication always has DWARF.
-- Arm the timer before `session_call`, disarm after, then `prof_collect`.
- Emit folded stacks (sorted + RLE for `flamegraph.pl`) plus a flat
- self%/cumul% report to stdout and a dropped-sample warning.
-
-Tests: `test/smoke/prof_hello` (assert `prof.folded` is non-empty and `main`
-appears), `test/dbg/fp_walk_*` (canned frame chains per arch, assert the PC
-sequence and termination), and `test/dbg/prof_buf_overflow` (fill to capacity,
-assert `dropped` increments and `count` caps).
-
-Profiler follow-ons (deferred): per-thread timers via
-`timer_create(CLOCK_THREAD_CPUTIME_ID)` + `SIGEV_THREAD_ID` for multi-thread
-guests; an `ITIMER_REAL` wall-clock mode for I/O-bound programs; allocation
-profiling via a conditional breakpoint on the allocator; SpeedScope / pprof
-output.
+The host-native sampling profiler (`kit prof`) that reuses this debugger's
+signal infrastructure has moved to its own roadmap: see [PROF.md](PROF.md).
-## 8. Bigger follow-ons (cross-cutting)
+## 7. Bigger follow-ons (cross-cutting)
- **Watchpoints**, once `CGTarget` can express them without an ISA-specific
debug-register API. All breakpoints are software today; watchpoints need
@@ -259,4 +179,5 @@ output.
- **Multi-threaded guests.** The session assumes one worker. Concurrent guest
threads require widening `KitDbgOs` with thread enumeration and per-tid
stop/event slots; this is also the prerequisite for reliable per-thread
- profiler timer delivery in §7. Out of scope until a concrete need lands.
+ sampling-profiler timer delivery ([PROF.md](PROF.md) §6). Out of scope until a
+ concrete need lands.
diff --git a/doc/plan/FRONTEND-SHAPE.md b/doc/plan/FRONTEND-SHAPE.md
@@ -1,433 +0,0 @@
-# Frontend shared tagged cell
-
-**Goal.** Define the end-state shape for the C front half around one shared,
-compact cell used by the lexer, preprocessor, and parser input ring. This is a
-design document, not a phasing plan.
-
-The value-stack redesign lives in `doc/plan/CG-STACK-API.md`: the C parser drives
-`KitCg` directly, `KitCg` owns the single liveness-authoritative value stack, C
-type/value facts live in the CG language sidecar, and constant values live in the
-CG constant payload. This document deliberately stops at the front-half cell and
-the handoff from parser primary to CG stack.
-
-In one line: **one compact lexeme cell is shared by lexer, preprocessor, and the
-parser input ring; parser-only semantic tagging happens only on parser-owned
-cells; the preprocessor remains independently drainable; the CG stack remains the
-only live expression stack.**
-
-## Non-Goals
-
-- No parser-owned shadow value stack. `Parser.cg_slot_stack`, `PcgSlot`, and the
- `pcg_*` adapter are removed by `CG-STACK-API.md`, not replaced here.
-- No `ApiSValue` stored inside the frontend cell. A tagged cell is a short-lived
- lowering seed, not a durable value.
-- No scalar int-bitmask type lane in lex/pp cells. If scalar classification needs
- caching after measurement, the cache belongs in the CG language sidecar or a
- C type cache keyed by `const Type*`, not in every token.
-- No C parser dependency from the lexer or preprocessor headers. Shared cell
- helpers use `void*` and integer ids; C-specific wrappers live under `lang/c`.
-- No global state. Source registries, macro tables, macro-disabled state, parser
- rings, and CG side payloads hang off `Pp`, `Parser`, or `KitCg`.
-
-## Ownership Boundaries
-
-1. **Lexer and pp own the lexeme contract.** A lexeme cell is complete enough for
- macro expansion, directives, `-E` serialization, diagnostics, and replay. It
- carries lazy `LocRef`/`TextRef`, eagerly interned identifiers, punctuator codes
- in `aux`, and no C parser state.
-2. **The pp is independently drainable.** `cpp`, `cc -E`, and `KIT_PP_DRAIN`
- pull cells into caller-owned storage with no parser attached. The pp reads and
- writes only the lexeme view.
-3. **The parser owns semantic tagging.** The parser may tag only cells in its own
- input ring or short-lived scratch. It must never tag macro-definition storage,
- directive buffers, or pp-owned source buffers.
-4. **Macro replay copies before tagging.** Macro bodies are immutable lexeme-cell
- arrays. When replay emits a token, pp copies one lexeme into the caller's
- output cell, applies loc/first-token flag overrides, and returns. The parser
- may then tag that caller-owned copy.
-5. **`KitCg` owns values.** Once a primary is accepted, the parser lowers through
- `kit_cg_*`, stamps the CG top with `kit_cg_retag_*`, and advances. The cell can
- carry debug/lowering facts until the next `advance`, but expression lifetime is
- the CG stack slot.
-
-## Shared Cell
-
-The durable stream/storage unit stays the current lean-token shape: 32 bytes on
-LP64, trivially copyable, and without heap ownership. If the implementation
-renames the type, today's `Tok` should become an alias of this cell rather than a
-second representation.
-
-The shared header must not include C parser or CG internals.
-
-```c
-typedef enum CFeKind {
- /* 0..0x11ff remain TokKind / pp-internal token kinds. */
- CFE_SEM_FIRST = 0x8000,
- CFE_SEM_VALUE = CFE_SEM_FIRST, /* parser-only: current token lowered as value */
- CFE_SEM_PLACE, /* parser-only: current token lowered as place */
-} CFeKind;
-
-typedef struct CFeSem {
- const void* lang_type; /* C parser: const Type*; opaque to lex/pp */
- u32 cg_type; /* KitCgTypeId value, represented without cg includes */
- u32 lang_flags; /* same C value flags passed to kit_cg_retag_* */
-} CFeSem;
-
-typedef struct CFeCell {
- u16 kind; /* TokKind in lexeme view; CFeKind >= CFE_SEM_FIRST after tagging */
- u16 flags; /* lexeme: TF_*; semantic: parser-private flags */
- u32 aux; /* lexeme: Sym/Punct/PP_PARAM; semantic: small discriminator */
- LocRef loc; /* survives both views; line/col materialize lazily through Pp */
- union {
- TextRef text; /* lexeme spelling ref; lexer/pp/drain view */
- CFeSem sem; /* parser-only semantic seed; pp never reads this */
- } u;
-} CFeCell;
-
-_Static_assert(sizeof(CFeSem) <= sizeof(TextRef),
- "semantic overlay must not widen the lexeme cell");
-_Static_assert(sizeof(CFeCell) == 32,
- "cell must stay the compact token-stream storage unit");
-```
-
-Cell rules:
-
-- `kind < CFE_SEM_FIRST` means lexeme view. All pp helpers assert this when
- reading a cell.
-- `kind >= CFE_SEM_FIRST` means parser-private semantic view. Such a cell is
- illegal to feed back into pp, macro storage, directive evaluation, or replay.
-- The lexeme and semantic views do not co-live. The parser must finish all
- spelling, suffix, and encoding reads before overwriting `u.text`.
-- `loc` survives tagging. Diagnostics and debug locations can materialize full
- `SrcLoc` lazily through `Pp`.
-- Wide integer constants, float payloads, aggregate initializers, static
- relocation constants, and expression lifetime never live in the cell. They go
- to the CG constant payload or the existing initializer/static-data machinery.
-
-## Lexer Contract
-
-The lexer writes one caller-owned cell and returns nothing by value:
-
-```c
-void lex_next(Lexer*, CFeCell* out);
-```
-
-Lexeme shape:
-
-- `TOK_IDENT`: `aux = Sym`, `u.text = TEXT_SRC` for exact spelling. Identifiers
- remain eagerly interned because `Sym` is the macro, keyword, and binding key.
-- `TOK_PUNCT`, `TOK_PP_HASH`, `TOK_PP_PASTE`: `aux = Punct` or character code.
- Canonical punctuators use `TEXT_NONE`; digraphs may use `TEXT_SRC`.
-- `TOK_NUM`, `TOK_FLT`, `TOK_STR`, `TOK_CHR`, `TOK_HEADER`: `u.text` is a source
- span or synthetic symbol. Literal suffix and encoding facts stay in `flags`
- until the parser decodes the literal.
-- `TOK_NEWLINE`: produced only on raw/preprocessor-drain paths that need it for
- text reconstruction. Parser-feed mode uses `TF_AT_BOL` plus directive state.
-- `loc`: `(file_id, byte_off)` into the pp source registry. The lexer does not
- stamp line/column per token.
-- `TF_NO_EXPAND` is clear on lexer-produced identifiers. Only the pp sets it
- when an identifier token becomes unavailable for macro expansion.
-
-Materialization remains pp-owned because only `Pp` retains source buffers, splice
-tables, and `#line` overlays:
-
-```c
-SrcLoc pp_materialize_loc(Pp*, LocRef loc);
-KitSlice pp_text_slice(Pp*, const CFeCell* cell);
-Sym pp_text_intern(Pp*, const CFeCell* cell);
-int pp_text_eq_cstr(Pp*, const CFeCell* cell, const char* s);
-```
-
-## Preprocessor Contract
-
-The pp exposes two drainable pulls, both out-pointer only:
-
-```c
-void pp_next_parse(Pp*, CFeCell* out); /* expanded, directives consumed,
- * non-directive newlines suppressed */
-void pp_next_raw(Pp*, CFeCell* out); /* expanded, directives consumed,
- * TOK_NEWLINE preserved for -E/cpp */
-void pp_emit_text(Pp*, Writer* out); /* drains pp_next_raw */
-```
-
-Internal pp storage is lexeme-only:
-
-```c
-typedef struct CFeReplay {
- const CFeCell* cells;
- u32 n;
- Macro* disabled_owner; /* non-NULL while rescanning this macro replacement */
- LocRef loc_override;
- u16 first_flags_or;
-} CFeReplay;
-```
-
-Requirements:
-
-- Macro bodies, directive-line buffers, replay buffers, and argument slices store
- `CFeCell[]` in lexeme view only.
-- A live lexer source should write directly into the caller's output cell where
- possible. A replay source copies one immutable lexeme into the caller's output
- cell.
-- Directive processing, `#if` expression evaluation, paste, stringize,
- `__LINE__`, `__FILE__`, `_Pragma`, and `#include` consume and produce lexeme
- cells only.
-- Synthesized tokens use `TEXT_SYM` or `TEXT_NONE`; they never point at storage
- whose lifetime is shorter than `pp_free`.
-- The parser-feed path must not require newline tokens. `TF_AT_BOL` plus
- directive state is the line-boundary contract; raw `-E` keeps `TOK_NEWLINE` for
- text reconstruction.
-
-## Macro Expansion Availability
-
-Use the simpler cpplib-style availability model instead of a per-token hideset
-table: a macro is disabled while its own replacement-list frame is being
-rescanned, and an identifier token has one permanent unavailable bit.
-
-The unavailable bit is `TF_NO_EXPAND` in lexeme view. It means "this identifier
-token must not be macro-expanded if it is seen again." One bit is sufficient
-because the token's spelling names at most one macro at the time it is tested;
-the bit does not need to encode a set of macro names.
-
-Suggested pp-owned state:
-
-```c
-typedef struct Macro {
- Sym name;
- u16 disabled_depth;
- /* existing macro definition fields... */
-} Macro;
-
-typedef struct TokSrc {
- u8 kind;
- CFeCell* cells;
- u32 i;
- u32 n;
- Macro* disabled_owner; /* non-NULL for macro replacement-list frames */
- /* existing source/replay fields... */
-} TokSrc;
-```
-
-Frame rules:
-
-- Pushing a macro replacement-list frame increments
- `disabled_owner->disabled_depth`. Popping that frame decrements it.
-- The invoking macro is **not** disabled while its arguments are collected or
- pre-expanded. It becomes disabled only while the substituted replacement list
- is rescanned.
-- Nested expansions stack naturally: parent macro frames remain on the source
- stack while child macro frames are scanned, so parent macros remain disabled
- through nested replacement-list rescans.
-- Object-like no-copy replay, function-like substituted bodies, and paste
- results all use the same frame rule. Empty replacement lists need no frame.
-
-Identifier test in `pp_next_*`:
-
-```c
-if (cell.kind == TOK_IDENT && (cell.flags & TF_NO_EXPAND) == 0) {
- Macro* m = mt_get(pp, tok_ident(&cell));
- if (m && m->disabled_depth != 0) {
- cell.flags |= TF_NO_EXPAND;
- *out = cell;
- return;
- }
- if (m && macro_invocation_is_present(pp, m, &cell)) {
- push_replacement_frame(pp, m, &cell);
- continue;
- }
-}
-```
-
-Details that matter:
-
-- A token returned because its macro is disabled keeps `TF_NO_EXPAND` if it is
- later captured as an argument, substituted into another macro, or replayed.
- This is what makes `foo(foo)(1)` and `id(foo(foo)(1))` stay `foo(1)`.
-- A function-like macro name that is not followed by `(` is returned as a plain
- identifier **without** setting `TF_NO_EXPAND`. If it is later rescanned in a
- context where another macro has supplied the `(`, it may expand. This keeps
- cases like `id(f L 1))`, with `#define L (`, able to become `1`.
-- `defined` operands in `#if` expansion can reuse `TF_NO_EXPAND`: the source of
- the mark is different, but the effect is the same.
-- Tokens created by `##` are fresh lexeme cells with `TF_NO_EXPAND` clear. They
- are then rescanned under the active disabled-frame stack; if a pasted token
- spells a currently disabled macro, the normal identifier test marks it
- unavailable before returning it.
-- This removes the canonical hideset table and the per-token `HidesetId` side
- arrays from replay sources. The only per-token availability state that flows
- through arguments and replay is the `TF_NO_EXPAND` bit already in the cell.
-
-## Parser Input Ring
-
-The parser consumes pp output through a small fixed ring of parser-owned cells.
-APIs return pointers to parser-owned cells, never cells by value:
-
-```c
-typedef struct ParserInput {
- CFeCell cells[3]; /* fixed cur + LL(2) lookahead ring; no VLA */
- u8 cur; /* index of the current token in cells[] */
- u8 nvalid; /* number of valid slots from cur, 0..3 */
- CFeCell pending; /* string-literal fusion / one-token pushback */
- u8 has_pending;
-} ParserInput;
-
-void parser_advance(Parser*);
-void parser_fill_to(Parser*, u32 depth); /* cold path: fills through depth 0..2 */
-
-static inline u8 parser_ring_idx(const ParserInput* in, u32 depth) {
- return (u8)((in->cur + depth) % 3u); /* depth 0 == current */
-}
-
-static inline CFeCell* parser_cur(ParserInput* in) {
- return &in->cells[in->cur];
-}
-
-static inline const CFeCell* parser_cur_const(const ParserInput* in) {
- return &in->cells[in->cur];
-}
-
-static inline const CFeCell* parser_peek(Parser* p, u32 depth) {
- ParserInput* in = &p->input;
- if (in->nvalid <= depth) parser_fill_to(p, depth);
- return &in->cells[parser_ring_idx(in, depth)]; /* depth 1..2 */
-}
-```
-
-Rules:
-
-- `parser_advance` rotates `cur = (cur + 1) % 3` and fills the newly-free tail
- slot when needed. It does not copy or move live `CFeCell` values between
- current/lookahead slots.
-- `parser_peek(p, depth)` does the cheap inline validity check
- (`nvalid <= depth`) and calls `parser_fill_to` only on miss. `parser_fill_to`
- fills the ring tail with `pp_next_parse(p->pp, slot)`.
-- `parser_peek` returns a `const CFeCell*` valid until the next `advance` or replay
- source switch. Callers that need persistence copy the cell explicitly.
-- Adjacent string literal fusion writes the fused result into the destination
- parser cell in place: `TEXT_SYM`, combined encoding flags, and the preserved
- location of the first literal.
-- Parser replay sources store lexeme cells only. Before a cell is recorded for
- replay, it must be in lexeme view; tagged semantic cells are not replayable.
-- There are no VLAs. Fixed lookahead stays fixed; any variable replay buffer uses
- the parser arena or an existing vector helper.
-
-## Parser Tagging
-
-Tagging is local to a consumed primary. It is not a pp feature and it is not an
-expression stack.
-
-The shared-cell helper has no C or CG header dependency:
-
-```c
-static inline void cfe_tag_primary_raw(CFeCell* c, u16 sem_kind,
- const void* lang_type,
- u32 cg_type,
- u32 lang_flags) {
- c->kind = sem_kind; /* CFE_SEM_VALUE or CFE_SEM_PLACE */
- c->flags = 0; /* parser-private flags if needed */
- c->u.sem.lang_type = lang_type;
- c->u.sem.cg_type = cg_type;
- c->u.sem.lang_flags = lang_flags;
-}
-```
-
-The C parser may wrap it with C/CG-specific types:
-
-```c
-static inline void c_tag_primary(CFeCell* c, u16 sem_kind,
- const Type* type,
- KitCgTypeId cg_type,
- u32 c_flags) {
- cfe_tag_primary_raw(c, sem_kind, type, (u32)cg_type, c_flags);
-}
-```
-
-Identifier primary:
-
-1. `TOK_IDENT` carries `Sym` in `aux`.
-2. The parser checks `kw_map`; otherwise it reads `BindingTab` once.
-3. The parser emits the corresponding CG seed: local place, global/function
- address, enum constant, integer constant, or other binding-specific form.
-4. The parser calls `kit_cg_retag_top(p->cg, type, flags)` with the same semantic
- facts it wrote into the cell.
-5. The parser advances; the tagged cell is no longer live.
-
-Literal primary:
-
-1. The parser reads spelling, suffix, and encoding from lexeme view.
-2. The parser decodes only when the literal becomes a value.
-3. The parser pushes through the CG constant/value API. Width-complete integer
- constants use `KitCgConstInt` from `CG-STACK-API.md`.
-4. The parser retags the CG top with C type/value flags.
-5. The parser may tag the current cell as a debug/lowering seed until `advance`.
-
-Operators never tag tokens. They consume CG stack entries via `kit_cg_*`, query
-`KitCgSlotInfo`, and retag results according to `CG-STACK-API.md`. The parser
-input cell can be tagged; the live value is still the CG stack slot.
-
-## CG-Facing Dependencies
-
-This design depends on the CG stack contract from `CG-STACK-API.md`:
-
-```c
-void kit_cg_lang_enable(KitCg*);
-KitCgSlotInfo kit_cg_slot_info(KitCg*, u32 depth_from_top);
-void kit_cg_retag_top(KitCg*, const void* lang_type, u32 lang_flags);
-void kit_cg_retag_at(KitCg*, u32 depth_from_top,
- const void* lang_type, u32 lang_flags);
-void kit_cg_set_top_flags(KitCg*, u32 set, u32 clear);
-
-int kit_cg_top_const_int_ex(KitCg*, KitCgConstInt* out);
-void kit_cg_push_const_int(KitCg*, KitCgTypeId type,
- const KitCgConstInt* value);
-```
-
-`CFeSem` deliberately mirrors the non-constant part of `KitCgSlotInfo`:
-`{lang_type, cg_type, lang_flags}`. That keeps parser tagging and CG retagging
-the same shape without making the pp pay for parser-only facts.
-
-## End-State Data Flow
-
-```text
-source bytes
- -> lex_next(Lexer*, CFeCell*) writes lexeme view
- -> pp source/replay stack reads/writes lexeme view only
- -> pp_next_parse/raw(Pp*, CFeCell*) fills caller-owned cell
- -> cpp / cc -E / KIT_PP_DRAIN drains lexeme cells; no parser
- -> ParserInput ring parser-owned cells
- -> primary resolution optional in-place semantic tag
- -> kit_cg_* push/op + retag CG stack owns values
- -> CgTarget -> NativeTarget -> MC backend seam unchanged
-```
-
-The fusion point is the front-half representation and handoff: eliminate
-by-value token returns and duplicate token shapes, preserve pp drainability, and
-do not create a second value representation beside `KitCg`.
-
-## Correctness Checklist
-
-- A pp drain can run with no parser and never observes `CFE_SEM_*`.
-- Macro bodies and replay buffers are immutable lexeme-cell streams.
-- Parser tagging is limited to caller-owned parser cells.
-- Tagged cells are never recorded for replay or passed back to pp helpers.
-- Text and location materialization work after source pop and macro replay.
-- String-literal fusion produces a single lexeme cell without a second token type.
-- Parser current/lookahead is a three-cell ring with index rotation; `advance`
- does not copy or move live `CFeCell` values.
-- `TF_AT_BOL` is sufficient for parser-feed directive recognition; raw mode keeps
- `TOK_NEWLINE` for `-E`.
-- Macro replacement frames disable their owner only while the replacement list is
- being rescanned, and re-enable it exactly when that frame is popped.
-- A token skipped because its macro is disabled is returned with `TF_NO_EXPAND`,
- and that bit survives argument pre-expansion, substitution, replay, and later
- rescans.
-- A function-like macro name not followed by `(` is not marked `TF_NO_EXPAND`;
- it can expand if a later rescan sees a `(` supplied by another macro.
-- Pasted identifier tokens start with `TF_NO_EXPAND` clear and are judged by the
- active disabled-frame stack during normal rescan.
-- Identifier resolution does one `Sym`-keyed lookup after keyword classification.
-- Literal decoding is lazy and occurs before `u.text` is overwritten.
-- The CG stack, not `CFeCell`, owns expression lifetime, constants, liveness, and
- C type/value flags after primary lowering.
-- No lexer/pp header includes C parser or CG internals.
-- No VLAs and no global state.
diff --git a/doc/plan/KERNEL.md b/doc/plan/KERNEL.md
@@ -1,580 +0,0 @@
-# Kernel build and image pipeline
-
-This roadmap tracks the work needed for kit to build freestanding kernels from
-C and assembly sources and emit kernel images that can be passed to QEMU's
-direct loaders. It deliberately does not cover VM execution, bootloader
-generation, UEFI application layout, or kit-provided startup code. Kernel
-startup, privilege-mode entry, page-table setup, stack setup, TLS setup, and
-boot-protocol compliance are the kernel author's responsibility.
-
-Related: [../DRIVER.md](../DRIVER.md), [../LINK.md](../LINK.md),
-[../OBJ.md](../OBJ.md), [../RUNTIME.md](../RUNTIME.md),
-[LINKER-COMPAT.md](LINKER-COMPAT.md), [PORT.md](PORT.md).
-
-## Scope
-
-The supported path is:
-
-```
-C / asm sources + objects + archives
- -> kit build-obj / build-exe
- -> freestanding static kernel ELF
- -> kit image / objcopy
- -> ELF / flat binary / ROM-style payload / section-concatenated payload
-```
-
-The first targets are freestanding ELF:
-
-- `x86_64-none-elf`
-- `aarch64-none-elf`
-- `riscv64-none-elf`
-- `riscv32-none-elf`
-
-The output artifacts should be usable with QEMU features such as `-kernel`,
-`-bios`, or `-device loader,file=...`, depending on the target machine and the
-kernel's own entry contract. Kit should not decide or implement the boot
-protocol.
-
-## Current baseline
-
-Useful pieces already exist:
-
-- `build-obj` compiles C / asm / toy / wasm sources into objects, and can combine
- multiple source objects with `ld -r`.
-- `build-exe` compiles a source set and links it with object/archive inputs.
-- Freestanding triples resolve to non-PIE ELF/WASM targets by default.
-- `build-*` already accepts common freestanding and link flags such as
- `-ffreestanding`, `-nostdinc`, `-nostdlib`, `-nodefaultlibs`,
- `-nostartfiles`, `-static`, `-pie`, `-no-pie`, `-mcmodel=...`, `-T`, `-e`,
- `-Wl,...`, `--build-id=...`, `-ffunction-sections`, and `-fdata-sections`.
-- The linker has a structured linker-script subset with section placement,
- symbol assignment, `/DISCARD/`, and `KEEP` roots for `--gc-sections`.
-- The runtime provides freestanding headers and compiler-runtime-style support,
- but no `crt0`.
-- `objcopy` can transform object files and rewrite sections/symbols, but does
- not yet expose raw binary image output.
-
-The desired user-facing build shape stays on the existing tools:
-
-```
-kit build-exe -target x86_64-none-elf \
- -ffreestanding -nostdlib -static -no-pie \
- -mcmodel=kernel -mno-red-zone \
- -ffunction-sections -fdata-sections \
- -T kernel.ld -e _start \
- -Wl,--gc-sections \
- --map kernel.map \
- --symbols kernel.sym \
- -o kernel.elf \
- boot.S kernel.c mm.c
-```
-
-## Compile and build-driver support
-
-Do not add a separate `kit kernel compile` command. Keep `build-obj` and
-`build-exe` as the compile/link front doors, and make their flag surface complete
-enough for kernel authors.
-
-Required flag support and behavior:
-
-- `-ffreestanding` / `-fhosted`: select freestanding vs hosted assumptions,
- including whether sysroot-hosted profiles may be engaged.
-- `-nostdinc`: suppress all implicit non-resource include paths while still
- allowing explicit `-I` / `-isystem`.
-- `-nostdlib`, `-nodefaultlibs`, `-nostartfiles`: precisely control runtime
- archive and hosted CRT/libc insertion. No startup object is ever invented by
- kit for a freestanding kernel.
-- `-static`, `-no-pie`, `-fno-pic`, `-fno-pie`: produce static non-PIE kernel
- code and an ET_EXEC-style image unless the user deliberately opts into PIC/PIE.
-- `-mcmodel=...`: keep existing model selection and add kernel-relevant aliases
- where the target backend has meaningful behavior.
-- `-mno-red-zone`: disable the x86_64 SysV red zone for kernel code. This must
- affect backend frame selection, not merely be accepted as syntax.
-- `-mgeneral-regs-only` or equivalent target-feature spelling: give kernels a
- straightforward way to prevent accidental SIMD/FP codegen where an ABI or
- privilege-mode context does not save those registers.
-- `-fno-builtin`: accept in `build-*` and ensure the C frontend does not turn
- freestanding source into calls or assumptions the kernel did not request.
-- `-fno-stack-protector` / `-fstack-protector*`: either implement the supported
- subset or reject unsupported modes explicitly. Silently ignoring stack
- protector policy is not acceptable for kernels.
-- `-ffunction-sections` and `-fdata-sections`: compose with linker
- `--gc-sections` and script `KEEP`.
-- `--group`: continue to scope include/define/language/frontend flags to source
- subsets; do not let link-wide kernel policy drift into per-source groups.
-
-Driver parity to improve:
-
-- Accept common direct linker flags in `build-exe` where `ld` already accepts
- them, instead of requiring every flag to pass through `-Wl,`.
-- Keep `cc`, `ld`, and `build-exe` behavior aligned by routing shared policy
- through `driver/lib/link_flags.*` and `driver/lib/target.*`.
-- For freestanding executable links, default diagnostics should be strict:
- unresolved symbols and dynamic-link artifacts should be errors unless the user
- explicitly requests an escape hatch.
-
-## Linker work
-
-### Linker-script subset
-
-Kernel links need a larger structured GNU-ld-compatible subset. The goal is not
-to interpret arbitrary linker scripts blindly, but to support the constructs
-needed for deterministic kernel memory layouts with precise diagnostics.
-
-Add support for:
-
-- `MEMORY` with `ORIGIN`, `LENGTH`, attributes, and region-overflow diagnostics.
-- Output section placement into memory regions with `> REGION`.
-- Load-memory placement with `AT(expr)` and `AT> REGION`, so VMA/LMA split
- kernels can be represented.
-- `PHDRS`, `:phdr`, `FLAGS(...)`, and `FILEHDR` / `PHDRS` segment attributes,
- so kernels can control program headers and segment permissions.
-- `PROVIDE`, `PROVIDE_HIDDEN`, and `HIDDEN`.
-- `ASSERT(expr, "message")`.
-- `EXTERN(symbol)` as a GC root and undefined-symbol declaration.
-- Richer input section patterns: `*(.text .text.*)`, file-qualified patterns,
- and `EXCLUDE_FILE`.
-- Section fills: `=0x...` and `FILL(...)`.
-- Alignment and address helpers: `ALIGN`, `SUBALIGN`, `BLOCK`, `ADDR`,
- `LOADADDR`, `SIZEOF`, `SIZEOF_HEADERS`, `DEFINED`, and `ABSOLUTE`.
-- `OUTPUT_ARCH` and `OUTPUT_FORMAT` as validation directives initially. They do
- not need to drive target selection in the first pass, but mismatches should be
- reported clearly.
-
-Existing `KEEP(...)` support must continue to interact correctly with
-`--gc-sections`.
-
-### Link output side files
-
-Add linker-produced side outputs, available from both `ld` and `build-exe`:
-
-- `--map FILE`: write a deterministic link map.
-- `--symbols FILE`: write post-link absolute symbols.
-- `--symbols-format=nm|json`: start with an nm-like text format; JSON can land
- once the data model is stable.
-- `--cref FILE`: optional cross-reference table.
-- `--print-memory-usage`: summarize `MEMORY` regions once linker-script memory
- regions exist.
-
-The link map should include:
-
-- target triple and output kind,
-- entry symbol and address,
-- memory regions and usage,
-- program headers / segments,
-- output sections with VMA, LMA, file offset, size, and alignment,
-- input object/archive-member contributions,
-- linker-defined symbols,
-- discarded sections,
-- unresolved symbols,
-- section-GC roots and discarded-reason details where practical.
-
-### Linker flags and policy
-
-Add or normalize:
-
-- `--no-undefined`: reject unresolved symbols for executable/freestanding links,
- not only shared-library output.
-- `--allow-undefined`: explicit escape hatch.
-- `--defsym name=expr`.
-- `--section-start=.name=addr`.
-- `-Ttext`, `-Tdata`, and `-Tbss` through `build-exe` as well as `ld`.
-- `--orphan-handling=place|warn|error|discard`.
-- `--fatal-warnings`.
-
-Freestanding kernel validation should reject, by default:
-
-- DSO inputs,
-- dynamic interpreter paths,
-- dynamic sections and PLT/GOT imports,
-- unresolved relocations,
-- missing entry symbols when `-e` or `ENTRY(...)` names one,
-- target/object-format mismatches across inputs.
-
-## Image command
-
-Add a new `kit image` command for kernel-image emission. Also add a minimal
-`objcopy -O binary` path for compatibility, backed by the same lower-level image
-emitter.
-
-`objcopy` should remain an object transformer:
-
-```
-kit objcopy -O binary kernel.elf kernel.bin
-```
-
-`kit image` should own image-building policy:
-
-```
-kit image --format bin kernel.elf -o kernel.bin
-
-kit image --format bin --from segments --segment PT_LOAD \
- --addr paddr --base 0x80000000 --fill 0x00 --align 4096 \
- --pad-to 2M --max-size 8M \
- --metadata kernel.image.json \
- kernel.elf -o kernel.bin
-```
-
-### Image formats
-
-- `elf`: copy, normalize, strip, or split-debug an existing linked ELF.
-- `bin`: flat binary derived from loadable segments or selected sections.
-- `rom`: flat fixed-size binary with fill, padding, max-size checks, and later
- optional checksum hooks.
-- `sections`: concatenate explicitly named sections in user-specified order.
-
-Optional later embedded formats, such as Intel HEX, S-record, or UF2, are out of
-the first kernel-focused pass.
-
-### Flat kernel `Image` (arm64 / riscv64)
-
-QEMU's `-kernel` path on arm64 and riscv consumes the flat `Image` format: a raw
-loadable binary prefixed with a fixed **64-byte header** the kernel's loader
-reads to place and size the image. The first 32 bytes are common to both arches:
-
-| offset | field | notes |
-|--------|-------------------|---------------------------------------------------------|
-| 0 | `code0` (u32) | first instruction (branch to entry; `"MZ"` low half if EFI) |
-| 4 | `code1` (u32) | second instruction |
-| 8 | `text_offset` | u64 LE — load offset from a 2 MiB-aligned base |
-| 16 | `image_size` | u64 LE — effective image size **including BSS** |
-| 24 | `flags` | u64 LE — bit 0 endianness; arm64 bits 1-2 page size, bit 3 placement |
-
-The tails differ. arm64: three reserved u64s, `magic = "ARM\x64"` (`0x644d5241`)
-at offset 56, then a u32 PE-offset slot. riscv64: a `version` u32 (currently
-`0x2`), reserved words, a deprecated `"RISCV\0\0\0"` magic at 48, and
-`magic2 = "RSC\x05"` (`0x05435352`) at 56.
-
-There are two ways to support this, and they have different costs:
-
-- **Author-owned header (pass-through) — already supported.** In the
- Linux-native workflow the kernel's own startup code emits the 64-byte header
- (`head.S` places `code0`/`code1` and declares `image_size` from linker
- symbols). This matches the project stance that kernel startup is the author's
- responsibility. kit needs **nothing new**: `kit image --format bin` /
- `objcopy -O binary` copy PT_LOAD bytes verbatim, so the leading header survives
- byte-exact and the result is a loadable `Image`. Only a doc note and a fixture
- that asserts the header bytes and size are required.
-
-- **Kit-synthesized header — small add, the part worth doing.** The one field
- that is awkward for authors is `image_size`, the in-memory footprint including
- BSS, which kit already knows: `KitObjSegInfo.vsize` gives each loadable
- segment's memory size (the current emitter lays out `file_size` only and
- ignores it). So the add is: a ~64-byte header emitter, a new
- `--format arm64-image` / `--format riscv-image` (or an
- `--image-header=arm64|riscv` modifier on `--format bin`), and `vsize`-based
- computation of `image_size` and the memory span.
-
-Because `flags` encodes boot semantics (endianness, page size, placement),
-header synthesis is an explicit opt-in, never a default. Those fields are
-surfaced as explicit options (e.g. `--image-endian`, `--image-page-size`,
-`--image-text-offset`) rather than invented; kit fills magic/version and
-`image_size` deterministically and does not choose a boot policy.
-
-### Image flags
-
-Selection:
-
-- `--from=segments|sections`
-- `--segment=PT_LOAD` (repeatable)
-- `--only-section NAME` (repeatable)
-- `--remove-section NAME` (repeatable)
-- `--section NAME` (repeatable; explicit order for `--format sections`)
-
-Addressing:
-
-- `--addr=vaddr|paddr|lma`
-- `--base ADDR`
-- `--bias N`
-
-Holes and layout:
-
-- `--fill BYTE`
-- `--fail-on-holes`
-- `--max-hole SIZE`
-- `--align N`
-- `--pad-to SIZE`
-- `--max-size SIZE`
-
-ELF/debug:
-
-- `--strip-debug`
-- `--split-debug FILE`
-- `--keep-symbols`
-
-Validation:
-
-- `--require-entry`
-- `--require-symbol NAME` (repeatable)
-- `--require-section NAME` (repeatable)
-- `--no-dynamic`
-
-Reporting:
-
-- `--metadata FILE`: write a deterministic JSON sidecar containing target,
- object format, entry, build id, selected segments/sections, source ranges,
- output ranges, base/bias/fill policy, and warnings.
-
-### Image semantics
-
-Segment-based flat image emission should:
-
-- read loadable ranges from program headers,
-- sort by selected address kind,
-- detect overlaps,
-- fill or reject holes according to policy,
-- derive output offsets from `base` / lowest selected address,
-- preserve bytes exactly as they would be loaded,
-- include NOBITS memory ranges in metadata but not necessarily in output bytes
- unless a selected format requires padding.
-
-Section-based emission should:
-
-- operate on named sections in declared order,
-- reject missing sections unless a permissive option is added later,
-- concatenate section bytes exactly,
-- make address metadata explicit so users do not confuse section concatenation
- with a loadable memory image.
-
-ROM-style emission should:
-
-- require an explicit size or `--pad-to`,
-- fill unused bytes deterministically,
-- fail when selected payload bytes exceed the requested size,
-- leave target-specific checksums or reset-vector conventions as future,
- explicit options.
-
-## Initramfs and archive packaging
-
-The kernel-boot pipeline needs one packaging format kit does not yet emit: the
-SVR4 `newc` **cpio** archive the Linux kernel unpacks as its initramfs. An
-initramfs is a `cpio -H newc` archive (magic `070701`, or `070702` for the CRC
-variant), optionally compressed, that the kernel's built-in extractor reads at
-boot; the early-microcode convention is just an uncompressed cpio concatenated
-ahead of the compressed main archive. This is an archive format, not a boot
-protocol — the direct analogue of the existing `ar` and tar paths — so it
-belongs in kit's byte-utility tool family, not the image emitter. kit packages
-and inspects the archive; it does not build, mount, or boot it, and does not
-invent its contents.
-
-Add a `kit cpio` tool, reusing the `src/dist/tar.c` patterns and the public
-`kit/compress.h` codecs:
-
-- `newc` (SVR4 "portable") format only — the format the kernel requires. The
- legacy `bin`/`odc` cpio formats are out of scope.
-- Create from a directory or explicit file list, list (`-t`), and extract
- (`-i`), with deterministic ordering, normalized mode/uid/gid/mtime, and the
- closing `TRAILER!!!` record. Regular files, directories, and symlinks first;
- special/device nodes (which need explicit major/minor) can come later via a
- manifest if a use case appears.
-- Concatenation: build and accept already-concatenated archives so early-init
- cpio segments can be assembled and inspected.
-- Compression as a flag on `kit cpio` (e.g. `--compress=gzip|lz4`, with short
- `-z` / `--lz4`) that build-then-compresses in one step. An initramfs is just a
- compressed `newc` archive, so the flag is the whole story — no separate
- `initramfs` tool is warranted. gzip and lz4 **only**, the two initramfs
- compressors kit already ships; zstd and xz are out of scope, with a clear
- diagnostic rather than pretend support. On read (`-t` / `-i`), `-d` and
- auto-detection let a compressed archive round-trip with no separate
- decompress step.
-
-Gate the tool in `driver/main.c` (`KIT_TOOL_CPIO_ENABLED`) alongside the other
-archive utilities.
-
-## Implementation shape
-
-Add a shared image-emission layer rather than burying policy in `objcopy`:
-
-```
-driver/cmd/image.c CLI policy for kit image
-driver/cmd/objcopy.c simple -O binary compatibility path
-include/kit/image.h public image-emission API, if we want embedders to use it
-src/api/image.c public wrapper
-src/obj/image.c object/ELF-to-image implementation
-```
-
-The image API should consume already-read object bytes or an opened `KitObjFile`
-view, plus explicit options. It should not read the filesystem directly and
-should not run QEMU or inspect host bootloader installs.
-
-Map/symbol side outputs should be linker-owned rather than image-owned. The
-image metadata file can reference link-map facts, but it should be a report about
-the image transform, not a replacement for `--map`.
-
-## Phasing
-
-1. **Raw binary baseline**
- - Add `objcopy -O binary`.
- - Add `kit image --format bin --from segments`.
- - Support `--base`, `--fill`, `--fail-on-holes`, `--pad-to`, `--max-size`.
- - Add focused ELF fixtures for x64, aa64, rv64, and rv32.
-
-2. **Build/link parity for kernels**
- - Add missing `build-exe` direct flag parity with `ld`.
- - Add `-mno-red-zone`, `-mgeneral-regs-only`, builtin policy, and stack
- protector policy.
- - Add strict freestanding undefined/dynamic-artifact diagnostics.
-
-3. **Link map and symbols**
- - Add `--map FILE`.
- - Add `--symbols FILE`.
- - Add deterministic tests for section layout, symbols, and discarded
- sections.
-
-4. **Script growth**
- - Add `MEMORY`, region placement, VMA/LMA split, and region overflow checks.
- - Add `PHDRS` once memory regions are stable.
- - Add `ASSERT`, `PROVIDE`, `EXTERN`, richer input patterns, and orphan
- handling.
-
-5. **Image formats beyond bin**
- - Add `--format sections`.
- - Add `--format rom`.
- - Add `--metadata FILE`.
- - Add `--format elf` normalization/strip/split-debug behavior if it proves
- cleaner than routing those cases through `objcopy` and `strip`.
-
-## Acceptance criteria
-
-For each first-pass freestanding target, kit should be able to:
-
-- compile a kernel source set containing C and assembly with `build-exe`,
-- link it with a kernel-owned startup object and linker script,
-- emit a static freestanding ELF with a deterministic layout,
-- produce a link map and absolute-symbol side file,
-- convert the ELF to a flat binary image,
-- validate that the ELF/image has no accidental dynamic-loader dependencies,
-- reproduce byte-identical outputs from identical inputs and options.
-
-The validation suite should stay targeted:
-
-- one small kernel-link fixture per architecture,
-- one script-layout fixture per linker-script feature,
-- one image-conversion fixture per image format and hole policy,
-- negative tests for unresolved symbols, region overflow, dynamic artifacts, and
- overlapping image ranges.
-
-## Remaining work
-
-Phases 1-5 are landed. Phases 1-4 (flag parity, undefined-symbol policy,
-`--map`/`--symbols`, the linker-script parse surface, `kit image --format bin` /
-`objcopy -O binary`) plus all of the items below shipped; an adversarial review
-of the integrated diff found and fixed a further set of correctness bugs
-(coalesced-PT_LOAD bss/perms, `--defsym` ordering, exact `--section-start`
-addressing, NOLOAD relocations, memory-usage accounting, `--cref` imports, image
-metadata accuracy, and cc/ld report parity). The checklist below is closed out.
-
-### Image formats beyond `bin` (Phase 5)
-
-- [x] `--format rom`: fixed-size flat binary; requires `--pad-to`, deterministic
- fill, fails when payload exceeds the size.
-- [x] `--format sections`: concatenate `--section NAME` (repeatable) in declared
- order, reject missing sections, address metadata made explicit (new
- section-iteration path in `src/obj/image.c`).
-- [x] `--format elf`: evaluated and **deliberately delegated** to `objcopy` +
- `strip` — a standalone implementation would duplicate the object-rewrite /
- strip / split-debug machinery 1:1. The emitter returns a clear diagnostic
- pointing to those tools rather than half-building a parallel path.
-- [x] `--metadata FILE`: deterministic JSON sidecar (target, object format,
- entry, build id, selection, source/output ranges, policy, warnings); the
- `selection` field mirrors the emitter's actual segment policy.
-- [x] Image selection/validation flags: `--only-section` / `--remove-section` /
- `--section`; `--strip-debug` / `--split-debug` / `--keep-symbols` (gated to
- `--format elf`); `--require-entry` / `--require-symbol` / `--require-section` /
- `--no-dynamic`.
-
-### Linker-script layout (now honored at layout)
-
-- [x] `NOLOAD` with PROGBITS content (forces NOBITS / no file bytes; relocations
- into a NOLOAD section are skipped, not written through a null buffer).
-- [x] Inter-section `.` assignments apply at their textual position (sequence-
- stamped assignments interleaved with the section walk).
-- [x] Multi-byte fills (`=0x12345678` / `FILL(...)`) lay a repeating big-endian
- pattern.
-- [x] Multiple sections sharing one `:phdr` coalesce into one PT_LOAD; a section
- listing several phdrs appears under each (with R/W/X perms union).
-- [x] Recursion-depth guard in script expression parse and eval.
-
-### Linker flags and policy
-
-- [x] `--defsym name=expr` (can now satisfy an otherwise-undefined reference) and
- `--section-start=.name=addr` (lands at the exact requested vaddr).
-- [x] `-Tdata` / `-Tbss` through `build-exe` and `ld`.
-- [x] `--orphan-handling=place|warn|error|discard` and `--fatal-warnings`.
-- [x] `--cref FILE` (includes imported/undefined symbols) and
- `--print-memory-usage` (per-`MEMORY`-region usage, overflow-safe for high-half
- regions).
-- [x] Map completeness: LMA, discarded sections, and unresolved symbols in
- `--map`; input paths normalized to basenames (no absolute-path leak).
-
-### Freestanding strict validation (Phase 2 gap)
-
-- [x] Reject dynamic-interpreter paths, dynamic sections, and PLT/GOT imports.
-- [x] Reject cross-input target / object-format mismatches.
-- [x] Policy surfaced through `build-exe`, not just `ld` (both honor the same
- `freestanding_strict` trigger).
-
-### Tests and fixtures
-
-- [x] Per-arch kernel-link fixtures for `x86_64-none-elf`, `aarch64-none-elf`,
- `riscv64-none-elf`, and `riscv32-none-elf` (build → map/symbols → image →
- validate, reproducible).
-- [x] Byte-golden `--map` / `--symbols` fixtures (deterministic, committed
- goldens for a pinned link).
-- [x] Negative tests: region overflow, discarded sections, dynamic artifacts,
- NOLOAD-with-PROGBITS, and cross-arch freestanding rejection.
-
-### Newly scoped: initramfs and `Image` packaging
-
-Added on top of the closed-out Phase 1-5 work; not yet started.
-
-Initramfs / cpio (archive packaging, sibling to `ar`) — landed as `kit cpio`.
-The newc codec lives driver-local in `driver/cmd/cpio.c` (only the tool consumes
-it; the driver has no `-Isrc`, so it mirrors `tar.c`'s stateless append/finish/
-iter shape rather than living in the dist subsystem). Create needed two
-additive host shims: `driver_readlink` (read symlink targets) and
-`driver_path_lstat` (no-follow operand classification + the source executable
-bit). Bidirectionally interop-verified against host `bsdcpio`.
-
-- [x] `kit cpio` (newc/SVR4 only, magic `070701`/`070702`): create (`-o`) / list
- (`-t`) / extract (`-i`) with deterministic ordering (members sorted by path,
- a sorted DFS so each directory precedes its children) and normalized metadata
- (uid/gid 0, mtime 0, sequential inode, mode = type | perms keyed on the source
- exec bit), closing `TRAILER!!!` + a 512-byte tail pad. Regular files,
- directories, and symlinks; `..`/absolute names refused on both create and
- extract.
-- [x] Archive concatenation for early-init segments: the reader continues past a
- `TRAILER!!!`, skips inter-segment zero padding, and resumes on the next
- `070701`/`070702` magic (trailing non-cpio data is noted, not fatal). Building
- a concatenation is shell `cat` of 512-padded archives.
-- [x] Compression as a `kit cpio` flag (`--compress=gzip|lz4`, `-z`/`--lz4`;
- `-d` + always-on auto-detect on read) via the public `kit/compress.h` codecs —
- gzip + lz4 only, with a specific diagnostic for a zstd/xz magic or
- `--compress=zstd|xz`. An initramfs is just a compressed newc archive, so no
- separate `initramfs` tool.
-- [x] Tool gating in `driver/main.c` (`KIT_TOOL_CPIO_ENABLED`) + the
- `test/cpio/run.sh` round-trip fixture (pack → list → extract, byte-
- deterministic, newc-shape and 512-pad asserts, gzip/lz4/crc/concat coverage,
- security + usage negatives; optional `KIT_CPIO_TEST_HOST=1` cross-check against
- the host `cpio`).
-
-Flat kernel `Image` header (arm64 / riscv64) — landed via the `--image-header`
-modifier on `--format bin`/`rom` (not separate formats); it overlays the first
-64 bytes of the first loadable segment rather than prepending, so the entry
-branch (code0/code1) is preserved and the output stays the same size as a plain
-`bin`.
-
-- [x] Pass-through: an author-emitted 64-byte header survives
- `kit image --format bin` byte-exact (magic + image_size preserved); covered by
- the `image-hdr-pt-*` fixtures.
-- [x] `--image-header=arm64|riscv|auto` (AUTO infers the arch from the object's
- machine): synthesizes the 64-byte header; `image_size` is the in-memory span
- **including BSS**, computed from `KitObjSegInfo.vsize` over all selected
- loadable segments (BSS-only `file_size==0` segments included — the byte ranges
- skip them, the span must not).
-- [x] Explicit boot-semantic options (`--image-endian`, `--image-page-size`
- [arm64-only], `--image-text-offset`); magic/version filled deterministically;
- no invented boot policy. The sub-options error without `--image-header`.
-- [x] Per-arch fixtures (aarch64 / riscv64) in `test/tools/run.sh`: synthesized
- header is self-consistent (magic at 56, riscv version 2 at 32, `image_size`
- counts the BSS), deterministic, and the entry branch is preserved.
diff --git a/doc/plan/LEX-PP-API.md b/doc/plan/LEX-PP-API.md
@@ -1,571 +0,0 @@
-# Lexer and preprocessor API: a lean token boundary for the C frontend
-
-**Goal.** Redesign the lexer -> preprocessor -> parser boundary so a wholesale
-lexer/preprocessor rewrite can be high-performance without fusing the frontend
-layers. The lexer, preprocessor, and parser remain separately reusable modules:
-`cpp` / `cc -E` still use the preprocessor as a standalone component, and the C
-parser still consumes a preprocessed token stream. What changes is the data and
-the handoff contract.
-
-This is a design note for the new API and boundary types. It is not yet an
-implementation plan for the scanner algorithm itself.
-
----
-
-## 1. Current boundary
-
-Today the frontend has a simple but heavy token contract.
-
-### 1.1 C compile path
-
-`lang/c/c.c` wires the C frontend as:
-
-```text
-source bytes
- -> lex_open_mem / lex_skip_shebang
- -> pp_new / pp_push_input
- -> parse_c
- -> pcg adapter
- -> KitCg
- -> CgTarget / NativeDirectTarget
- -> NativeTarget
- -> MC emit
-```
-
-The current parser-feed path sets `pp_set_suppress_lexer_newlines(pp, 1)` before
-the primary lexer is pushed. Include lexers inherit the same policy. This avoids
-materializing non-directive newline tokens for the parser, while still surfacing
-directive-terminating newlines so directive reading works.
-
-### 1.2 Lexer input
-
-`lex_open_mem` takes:
-
-- `Compiler*`
-- source name as `const char*` or pre-interned `Sym`
-- borrowed source bytes and byte length
-
-Opening a lexer also registers a fresh compiler source file id. The lexer owns
-phase-2 line-splice folding. If a file contains no backslash-newline splices, it
-borrows the original bytes. If splices exist, it builds a folded copy and records
-fold points for physical line accounting.
-
-That means the true lexer input boundary is not just `(char*, len)`. It is:
-
-- source identity
-- source bytes
-- ownership/lifetime rule for those bytes
-- source registry side effect
-- newline policy
-- phase-2 splice policy
-- optional primary-file shebang handling
-
-### 1.3 Lexer output
-
-The lexer returns `Tok` by value:
-
-```c
-typedef struct Tok {
- u16 kind;
- u16 flags;
- SrcLoc loc;
- Sym spelling;
- union {
- Sym ident;
- Sym str;
- u32 punct;
- } v;
-} Tok;
-```
-
-Important properties:
-
-- Every content token carries a full `SrcLoc` (`file_id`, `line`, `col`).
-- Every token that needs text carries an interned exact `spelling`.
-- Identifiers are represented by interned `Sym`.
-- Punctuators carry both an integer punctuator code and an interned spelling.
-- Number/string/char literals carry interned spelling even though parsing later
- reads the spelling bytes again.
-- `TOK_NEWLINE` is a real token for PP mode and directive termination.
-- `TOK_PP_HASH`, `TOK_PP_PASTE`, and `TOK_HEADER` are lexer-visible PP concepts.
-
-### 1.4 PP internal source stack
-
-PP reads from a stack of token sources:
-
-- `SRC_LEX`: a `Lexer*`
-- `SRC_BUF`: a `Tok[]` replay buffer
-
-`SRC_BUF` is used for macro bodies, argument prescan, pushback, `#pragma`
-forwarding, and synthetic token sequences. Hidesets are internal to PP and are
-carried either as a per-token side array or as one uniform hideset id for a
-whole replay buffer.
-
-Current macro bodies store full `Tok` arrays. A no-`##` body can be pointer
-replayed with per-invocation loc/first-flag overrides; bodies with paste still
-copy/substitute/paste through scratch buffers.
-
-### 1.5 PP output to parser
-
-`pp_next()` returns a macro-expanded `Tok` by value. The parser wraps it with:
-
-- a `pending` token slot for string-literal fusion
-- a one-token lookahead slot
-- a replay buffer for recorded braced blocks
-
-The parser should not see non-directive newlines or directives. It does still
-see forwarded non-pragma `#` tokens in invalid cases, and it explicitly swallows
-forwarded pragmas.
-
-There are already out-pointer paths inside PP (`pp_next_raw_into`,
-`pp_next_into`) because returning a 24-byte `Tok` by value costs a hidden sret
-copy. The public boundary has not caught up to that shape.
-
----
-
-## 2. Design decision
-
-Use a lean token as the canonical internal token for the C preprocessor lane:
-
-- identifiers are interned eagerly
-- punctuators are represented by integer code only
-- exact spelling is a text reference, not always an interned `Sym`
-- source location is a compact location reference, with line/column recovered
- lazily when diagnostics or `__LINE__` need it
-- PP writes into caller-owned token slots
-- PP owns hidesets; hidesets do not cross to the parser
-
-This does not require control-flow fusion. The scanner can be rewritten as a
-tcc-class high-throughput scanner, the PP can keep a source stack and macro
-engine, and the parser can remain recursive descent. The boundary becomes cheap
-enough that keeping the layers separate is not itself the hot cost.
-
----
-
-## 3. Proposed core types
-
-The names here are provisional. The point is the contract.
-
-### 3.1 Source input
-
-```c
-typedef uint32_t CppSourceId;
-
-typedef enum CppSourceFlag {
- CPP_SRC_PRIMARY = 1u << 0, /* allow shebang skip */
- CPP_SRC_SYSTEM = 1u << 1, /* diagnostics/deps source property */
- CPP_SRC_PARSER_FEED = 1u << 2, /* suppress non-directive newlines */
- CPP_SRC_NO_SPLICES = 1u << 3, /* caller proves no backslash-newline */
-} CppSourceFlag;
-
-typedef struct CppSourceSpec {
- KitSlice name;
- const char* bytes;
- uint32_t len;
- uint32_t flags;
-} CppSourceSpec;
-```
-
-The lexer open API should make source identity explicit. It should return, or
-store in the lexer, the compiler file id registered for this source. Include
-resolution remains PP-owned; the lexer should not do filesystem work.
-
-`CPP_SRC_NO_SPLICES` is the safe version of today's paste fast path. It is only
-valid for synthetic buffers whose construction proves no line splice can exist.
-Normal source files and command-line definitions stay on the safe splice scan.
-
-### 3.2 Location reference
-
-```c
-typedef struct CppLocRef {
- uint32_t file_id;
- uint32_t byte_off;
-} CppLocRef;
-```
-
-The hot token carries a byte offset in the logical source stream, not an eager
-`line`/`col` pair. The source registry or lexer source object must have enough
-line-map data to turn `(file_id, byte_off)` into `KitSrcLoc` on demand.
-
-`#line` complicates this. The PP should keep the current file/line overlay on
-the source stack, as it does today, and provide a helper:
-
-```c
-KitSrcLoc cpp_pp_materialize_loc(CppPP* pp, CppLocRef loc);
-```
-
-Dynamic `__LINE__` expansion should use the overlay-aware helper. Diagnostics
-from parser/PP should also route through it. The parser should not know the
-details of `#line` state.
-
-### 3.3 Text reference
-
-```c
-typedef enum CppTextKind {
- CPP_TEXT_NONE = 0,
- CPP_TEXT_SOURCE = 1,
- CPP_TEXT_SYM = 2,
-} CppTextKind;
-
-typedef struct CppTextRef {
- uint32_t kind;
- uint32_t source_id;
- uint32_t off;
- uint32_t len_or_sym;
-} CppTextRef;
-```
-
-For source tokens, `CPP_TEXT_SOURCE` names a byte span in the source's logical
-post-splice buffer. For synthetic tokens, `CPP_TEXT_SYM` carries an interned
-symbol id in `len_or_sym`.
-
-Helpers:
-
-```c
-KitSlice cpp_text_slice(CppPP* pp, CppTextRef text);
-Sym cpp_text_intern(CppPP* pp, CppTextRef text);
-int cpp_text_eq_cstr(CppPP* pp, CppTextRef text, const char* s);
-```
-
-This keeps exact spelling available for `-E`, diagnostics, stringize, paste,
-literal decode, and macro identity checks without forcing every punctuator and
-literal spelling through the global symbol table on first lex.
-
-### 3.4 Token
-
-```c
-typedef struct CppTok {
- uint16_t kind;
- uint16_t flags;
- uint32_t aux;
- CppLocRef loc;
- CppTextRef text;
-} CppTok;
-```
-
-Field meaning:
-
-- `kind`: token kind (`EOF`, `IDENT`, `NUM`, `FLT`, `STR`, `CHR`, `PUNCT`,
- `PP_HASH`, `PP_PASTE`, `HEADER`, `NEWLINE`, plus PP-internal kinds).
-- `flags`: BOL, leading-space, no-expand, literal suffix/encoding flags,
- literal-bad.
-- `aux`: identifier `Sym`, punctuator code, macro parameter index, or small
- synthetic value depending on kind.
-- `loc`: compact source location.
-- `text`: exact spelling reference if the token has one.
-
-For identifiers, `aux` is the interned `Sym`. `text` may also be present as the
-exact spelling span, but normal identifier equality and macro lookup use `aux`.
-
-For punctuators, `aux` is the `Punct` code. `text` is optional for common
-canonical punctuators on the parser-feed path. The `-E`, stringize, and paste
-paths can still recover spelling by either source span or canonical punctuator
-table. Digraphs need an exact source span.
-
-For literals, `text` is the source or synthetic spelling. Numeric and string
-decoding should read via `cpp_text_slice` and only intern if a later operation
-requires a `Sym`.
-
----
-
-## 4. New API shape
-
-### 4.1 Lexer
-
-```c
-typedef struct CppLexer CppLexer;
-
-int cpp_lex_open(CppLexer* lx, KitCompiler* c, const CppSourceSpec* src);
-void cpp_lex_reset(CppLexer* lx, const CppSourceSpec* src);
-void cpp_lex_close(CppLexer* lx);
-
-void cpp_lex_set_mode(CppLexer* lx, uint32_t flags);
-void cpp_lex_next(CppLexer* lx, CppTok* out);
-
-uint32_t cpp_lex_file_id(const CppLexer* lx);
-KitSrcLoc cpp_lex_materialize_loc(const CppLexer* lx, CppLocRef loc);
-KitSlice cpp_lex_text_slice(const CppLexer* lx, CppTextRef text);
-```
-
-The lexer should fill a caller-provided token slot. It should not return token
-structs by value.
-
-The lexer should not track include keyword state as a hidden state machine.
-Instead PP should request header-name lexing while reading an include/embed
-directive. That can be a mode bit scoped to the next token, or an explicit
-entry point:
-
-```c
-void cpp_lex_next_header_name(CppLexer* lx, CppTok* out);
-```
-
-This removes `#include` knowledge from the normal scanner hot path.
-
-### 4.2 Preprocessor
-
-```c
-typedef struct CppPP CppPP;
-
-int cpp_pp_open(CppPP* pp, KitCompiler* c, const KitPreprocessOptions* opts);
-void cpp_pp_close(CppPP* pp);
-
-int cpp_pp_push_source(CppPP* pp, const CppSourceSpec* src);
-void cpp_pp_add_include_dir(CppPP* pp, const char* dir, int system);
-void cpp_pp_define(CppPP* pp, const char* name, const char* body);
-void cpp_pp_undef(CppPP* pp, const char* name);
-
-void cpp_pp_next_raw(CppPP* pp, CppTok* out);
-void cpp_pp_next_parse(CppPP* pp, CppTok* out);
-void cpp_pp_emit_text(CppPP* pp, KitWriter* out);
-
-KitSrcLoc cpp_pp_materialize_loc(CppPP* pp, CppLocRef loc);
-KitSlice cpp_pp_text_slice(CppPP* pp, CppTextRef text);
-Sym cpp_pp_text_intern(CppPP* pp, CppTextRef text);
-```
-
-`cpp_pp_next_raw` is the `-E` stream: macro-expanded, directives consumed,
-newlines preserved as needed for text reconstruction.
-
-`cpp_pp_next_parse` is the parser stream: macro-expanded, directives consumed,
-non-directive newlines absent, forwarded pragmas swallowed or represented by a
-parser-ignored event.
-
-Both write into caller-owned slots.
-
-### 4.3 Parser
-
-The parser should own a small token cursor:
-
-```c
-typedef struct CTokenCursor {
- CppPP* pp;
- CppTok cur;
- CppTok next;
- CppTok pending;
- uint8_t has_next;
- uint8_t has_pending;
-} CTokenCursor;
-```
-
-The parser's `advance`, `peek1`, pending string-literal fusion, and braced-block
-replay stay local to the parser. The difference is that they copy `CppTok`, not
-the old eager-interned `Tok`, and they ask PP/text helpers only when a spelling
-or materialized location is actually needed.
-
----
-
-## 5. Layer responsibilities
-
-### 5.1 Lexer owns
-
-- raw byte scanning
-- phase-2 splice folding and source line maps
-- physical BOL / leading-space flags
-- token kind recognition
-- identifier interning
-- numeric/string/char literal extent and suffix/encoding flags
-- punctuator code classification
-- header-name tokenization only when PP explicitly requests it
-
-### 5.2 PP owns
-
-- include search, include cache, include graph edges
-- source stack and source lifetime
-- directive recognition and directive-line reading
-- conditional inclusion stack and skipped-section scanning
-- macro table
-- hidesets
-- macro argument collection and prescan
-- macro replay buffers
-- token paste and stringize
-- dynamic predefined macros
-- `#line` overlays
-- raw stream vs parser stream policy
-- text emission for `-E`
-
-### 5.3 Parser owns
-
-- C keyword classification
-- token lookahead
-- string-literal fusion
-- braced-block replay used by current parser logic
-- literal value decoding
-- diagnostics through PP materialization helpers
-- semantic/type resolution and CG driving
-
-Hidesets must not cross into the parser. Include state must not cross into the
-lexer. C semantic keyword/type state must not cross into PP.
-
----
-
-## 6. Important invariants
-
-1. **Exact spelling remains available.**
- `#`, `##`, `-E`, diagnostics, literal decode, macro redefinition checks, and
- header parsing all require exact spelling. The change is lazy access, not
- lossy tokens.
-
-2. **Parser-feed tokens do not require newline materialization.**
- The parser stream should never allocate or return non-directive newline
- tokens. Directive termination remains an internal PP/lexer concern.
-
-3. **Identifier lookup stays `Sym`-based.**
- Macro lookup and parser binding/keyword lookup should be one indexed or
- table lookup on an interned identifier.
-
-4. **Punctuator spelling is not a hot-path symbol-table operation.**
- Punctuators are roughly half the stream on large C files. The parser needs
- the punctuator code, not a symbol table entry. Exact text is only needed for
- `-E`, stringize, paste, and diagnostics.
-
-5. **Line/column is lazy.**
- Hot tokens should not pay column arithmetic and `SrcLoc` construction unless
- a consumer needs a materialized location.
-
-6. **Synthetic tokens have stable text.**
- Macro expansions, dynamic predefined macros, paste output, stringize output,
- and command-line definitions must have text refs with lifetime at least until
- the returned token is consumed or until the owning PP arena is reset.
-
-7. **Macro replay can remain pointer-based.**
- Macro bodies should store `CppTok[]` plus compact metadata. Object-like
- no-paste bodies can still be pointer-replayed with loc/first-flag override.
-
-8. **The public compatibility API can exist during migration.**
- Old `Tok pp_next(Pp*)` can be implemented as an adapter while parser code is
- converted. This allows byte-identical gates at each step.
-
----
-
-## 7. Migration plan
-
-### Step 1: introduce the types and adapters
-
-Add `CppTok`, `CppTextRef`, `CppLocRef`, and text/location helper APIs next to
-the current `Tok`. Keep old `Tok` as the public compatibility type.
-
-Build adapter helpers:
-
-```c
-void cpp_tok_to_old_tok(CppPP* pp, const CppTok* in, Tok* out);
-void old_tok_to_cpp_tok(CppPP* pp, const Tok* in, CppTok* out);
-```
-
-The old adapter will eagerly intern text and materialize locations. That is
-acceptable as a temporary compatibility layer.
-
-### Step 2: make PP public pull slot-based
-
-Add slot-based public APIs over the existing implementation:
-
-```c
-void pp_next_into_public(Pp* pp, Tok* out);
-void pp_next_raw_into_public(Pp* pp, Tok* out);
-```
-
-Convert parser fetch to use the slot API before changing representation. This
-is a low-risk cleanup because PP already has internal out-pointer machinery.
-
-### Step 3: rewrite lexer behind a compatibility adapter
-
-Implement the new scanner to fill `CppTok`. Convert to old `Tok` at the lexer
-or PP boundary. Gate on:
-
-- PP golden tests
-- parser tests
-- sqlite object byte identity
-- sqlite `-E` token/text identity
-- diagnostic location parity on focused cases
-
-At this stage performance will understate the final win because the adapter
-still eagerly interns/materializes.
-
-### Step 4: convert PP macro storage and source stack to `CppTok`
-
-Move macro bodies, replay buffers, argument vectors, and directive lines to
-`CppTok`. Keep `pp_next()` compatibility at the outside edge.
-
-This is where lazy spelling starts to matter: macro expansion should carry
-source text refs until a PP operation demands interned text.
-
-### Step 5: convert parser cursor to `CppTok`
-
-Replace parser `Tok` slots with `CppTok` slots. Parser helpers should use:
-
-- `tok_ident(t)` for identifier `Sym`
-- `tok_punct(t)` for punctuator code
-- `cpp_pp_text_slice` for literal spelling
-- `cpp_pp_materialize_loc` for diagnostics and CG source loc
-
-After this step, the old `Tok` adapter is no longer on the compile hot path.
-
-### Step 6: delete or narrow the old token contract
-
-Keep an old-style token only where external API compatibility requires it, or
-delete it if no public API promises it. The C frontend hot path should be
-`CppTok` end to end through lexer, PP, and parser.
-
----
-
-## 8. Open questions
-
-1. **Line-map owner.**
- Should line maps live in the compiler source registry, the lexer source
- object, or PP's source cache? The answer affects how parser diagnostics
- materialize `CppLocRef` after the lexer for an include has been popped.
-
-2. **Text lifetime for folded source.**
- If a source needed splice folding, `CppTextRef` must point at the folded
- logical buffer, not the original bytes. That folded buffer must live long
- enough for any token text refs in macro bodies that outlive the source file.
-
-3. **Macro body text retention.**
- A macro body defined in an include may survive after that include source is
- popped. Either macro-body tokens must intern/copy their text at definition
- time, or source buffers referenced by macro bodies must be retained until
- `pp_free`.
-
-4. **Canonical punctuator text.**
- For canonical punctuators, can `CppTextRef` be omitted and reconstructed from
- the punctuator code? That is attractive for parser-feed and most PP paths,
- but `-E` must preserve digraph spelling where relevant.
-
-5. **Header-name mode.**
- The current lexer has directive state to emit `TOK_HEADER`. The new design
- should move this to PP, but the exact API should be chosen when directive
- reading is rewritten.
-
-6. **Token-paste validation.**
- Paste currently concatenates spellings and re-lexes a tiny buffer. The new
- scanner should preserve that validation path but route it through
- `CPP_SRC_NO_SPLICES` and a reusable lexer object.
-
-7. **Diagnostics parity.**
- Lazy location must preserve user-visible line/column behavior, including
- `#line`, includes, splices, comments, and shebang handling.
-
-8. **Public names.**
- The final names should probably not expose `Cpp` if this becomes the common
- C-family preprocessor substrate. The design uses `Cpp*` only to avoid
- colliding with current `Tok`/`Pp` names.
-
----
-
-## 9. Initial right-sized target
-
-The first useful deliverable is not the full PP rewrite. It is a measured
-compatibility lane:
-
-1. New source spec, loc ref, text ref, and token definitions.
-2. Slot-based public PP pull API over the current code.
-3. New scanner filling the lean token.
-4. Adapter to old `Tok`.
-5. Head-to-head sqlite measurement:
- - raw lex instructions per byte
- - compile `-c` instructions
- - `-E` byte identity
- - object byte identity
-
-If the adapter lane cannot produce a scanner win in isolation, the scanner
-algorithm is not yet good enough. If it does win in isolation but not through
-compile, the next bottleneck is PP token storage/replay and parser conversion.
diff --git a/doc/plan/LINKER-COMPAT.md b/doc/plan/LINKER-COMPAT.md
@@ -1,240 +0,0 @@
-# System-linker compatibility
-
-This roadmap tracks the remaining linker work needed for `kit ld` to act as the
-system linker over kit's supported target set. The near-term driver is Rust
-using `kit ld` through `rustc`, but the target is broader: C, Rust, assembler
-objects, archives, DSOs/dylibs/import libraries, PIE executables, shared
-libraries, and relocatable links should all behave like the platform linker for
-the supported arch/OS pairs.
-
-Related: [LINKER.md](LINKER.md), [../LINK.md](../LINK.md),
-[../OBJ.md](../OBJ.md), [../DRIVER.md](../DRIVER.md),
-[SYSROOTS.md](SYSROOTS.md).
-
-## Scope
-
-The support set is format-driven:
-
-- Mach-O: macOS `aarch64` and `x86_64`.
-- ELF: Linux glibc/musl `aarch64`, `x86_64`, `riscv64`; FreeBSD where Rust and
- sysroot artifacts exist; freestanding ELF for `aarch64`, `x86_64`, `riscv64`,
- and `riscv32`.
-- COFF/PE: Windows UCRT MinGW (`*-pc-windows-gnullvm`) for `x86_64` and
- `aarch64`.
-
-The linker should preserve format boundaries. ELF `--as-needed` and TLSDESC
-rules must not leak into Mach-O dylib or COFF import-library semantics. COFF's
-archive search behavior, Mach-O atom/GOT/TLV behavior, and ELF dynamic-section
-rules stay target-specific policy behind shared object and relocation APIs.
-
-## Current state
-
-The Rust-driven compatibility pass established these paths:
-
-- `kit ld` accepts the common system-linker flags Rust passes, including GNU
- `-Wl,`/`-z` forms, target/sysroot flags, linker scripts, rlibs, exact
- `-l:name` libraries, `-static-pie`, `-nodefaultlibs`, hosted/no-startfile
- combinations, PE/MinGW flags, and Mach-O version/platform flags.
-- Rust std links through `kit ld` for macOS `aarch64`/`x86_64`, Linux glibc
- `aarch64`/`x86_64`/`riscv64`, Linux musl `aarch64`/`x86_64`/`riscv64`,
- FreeBSD `x86_64`, and Windows UCRT MinGW `aarch64`/`x86_64`.
-- Rust no-std links through `kit ld` for `aarch64-unknown-none`,
- `x86_64-unknown-none`, `riscv64gc-unknown-none-elf`, and
- `riscv32imac-unknown-none-elf`.
-- ELF symbol-version imports can resolve explicit `name@VERSION` references,
- which FreeBSD Rust uses for libc compatibility symbols.
-- COFF archive libraries use global fixed-point search, matching the behavior
- Rust/MinGW expects from the library set.
-- RISC-V TLS-GD and x86_64 TLS-LD can relax local executable TLS cases; AArch64
- ELF TLSDESC relocation spellings are accepted and currently relax only for
- local defined TLS.
-
-This is a useful system-linker subset, not completion. The open work below is
-the gap between "Rust examples link" and "the platform linker can be replaced
-over the support set."
-
-## Workstreams
-
-### 1. Ordered input and dependency selection
-
-ELF `--as-needed` must become real linker semantics, not a driver/script
-special case.
-
-The linker needs to model, per ordered DSO input:
-
-- current link mode (`default`, `--as-needed`, `--no-as-needed`),
-- selected/not-selected state,
-- why it was selected,
-- SONAME or fallback identity for `DT_NEEDED`,
-- the exports made available to later inputs.
-
-ELF behavior:
-
-- default/no-as-needed DSOs are selected as explicit dependencies,
-- as-needed DSOs are selected only when they satisfy an eligible strong
- undefined reference at their position,
-- unselected as-needed DSOs do not satisfy later references,
-- selected DSOs are the source of `DT_NEEDED`,
-- duplicate SONAMEs are suppressed while preserving first selected order.
-
-GNU linker scripts should lower into the same ordered input model:
-
-- `INPUT(...)` inserts normal inputs with the current mode,
-- `GROUP(...)` preserves archive group behavior,
-- `AS_NEEDED(...)` temporarily pushes as-needed mode for nested DSO or `-l`
- tokens,
-- nested scripts inherit and restore mode correctly.
-
-Mach-O and COFF need separate parity checks rather than ELF semantics:
-
-- Mach-O dylib load commands and weak/imported symbol behavior should follow
- Darwin linker expectations.
-- COFF import libraries, auto-imports, weak externals, and library-set archive
- search should remain COFF-specific.
-
-### 2. ELF TLS model planning
-
-ELF TLS must be planned before final relocation application and dynamic-section
-synthesis. The planner should classify each TLS relocation or relocation
-sequence by:
-
-- symbol locality and visibility,
-- imported/preemptible vs. non-preemptible,
-- weak undefined behavior,
-- executable/PIE/shared/relocatable output,
-- static vs. dynamic link,
-- target ABI TLS variant and thread-pointer bias.
-
-The action should be explicit:
-
-- relax to local-exec,
-- synthesize initial-exec,
-- preserve or synthesize dynamic TLS/TLSDESC,
-- preserve relocations for `-r`,
-- reject with a precise diagnostic.
-
-This pass should own the decision; architecture code should own only the
-instruction byte rewrite once a sequence has been validated.
-
-### 3. TLSDESC completion
-
-The current AArch64 TLSDESC path accepts Rust/LLVM executable-local cases by
-relaxing the four instruction relocations to materialize the local-exec offset.
-Full TLSDESC support needs more:
-
-- complete per-architecture relocation surfaces in `RelocKind`, ELF mapping,
- relocation names, descriptor metadata, object read/write, and `objdump`;
-- descriptor GOT/data allocation, normally two machine words per unique
- `(symbol, addend)` descriptor;
-- TLSDESC dynamic relocation emission in `.rela.dyn`;
-- integration with `.dynsym`, symbol versions, `DT_NEEDED`, PIE address shifts,
- section GC, and DSO selection;
-- checked sequence relaxation for AArch64, then x86_64, then other ELF
- architectures as ABI support lands;
-- shared-library output that preserves or synthesizes dynamic TLS where final
- executable layout is not known.
-
-Imported TLSDESC references are real DSO uses and must select their provider
-under `--as-needed`.
-
-### 4. Architecture relocation and relaxation parity
-
-Each supported architecture needs both relocation coverage and checked
-relaxations for the code shapes produced by modern toolchains.
-
-Open areas include:
-
-- AArch64 ELF: complete TLSDESC dynamic path, validate local sequence
- relaxation as a unit, cover host-toolchain GOT/TLS/unwind relocation shapes.
-- x86_64 ELF: complete TLS-LD/GD/IE/LE relaxation behavior and add TLSDESC
- once modeled.
-- RISC-V ELF: keep expanding TLS-GD/LD pattern coverage conservatively, handle
- compressed-instruction variants, and preserve dynamic TLS paths where local
- relaxation is illegal.
-- Mach-O AArch64/x86_64: finish GOT/TLV/unwind relocation parity against
- Apple-produced objects and static archives.
-- COFF AArch64/x86_64: continue matching MinGW/LLVM relocation spellings,
- especially SECREL, unwind, pdata/xdata, and CRT object patterns.
-
-Relocation tests should pin both the descriptor table and representative byte
-patches so enum additions are covered immediately.
-
-### 5. Shared libraries and relocatable links
-
-Replacing a system linker requires more than executable links:
-
-- `-shared` should emit correct dynamic symbols, relocations, SONAME/install
- names/import tables, and TLS behavior for each format.
-- `-r` should preserve relocations and avoid making final TLS/layout decisions.
-- PIE and non-PIE executable behavior must stay distinct.
-- Copy relocations, protected visibility, weak imports, COMDAT/section groups,
- init/fini arrays, unwind tables, and build IDs must compose with GC and
- dynamic linking.
-
-The near-term Rust path mostly exercises final executables; shared-library and
-relocatable coverage should be broadened deliberately.
-
-### 6. Runtime/sysroot interoperability
-
-System-linker replacement depends on finding the same runtime inputs as the
-platform toolchain:
-
-- crt objects and default startup ordering,
-- libc/libm/libpthread/librt/libdl/libutil and libc-specific linker scripts,
-- compiler builtins and unwind libraries (`libgcc_s`, compiler-rt, Rust
- `compiler_builtins`),
-- platform dynamic loaders/interpreters,
-- MinGW UCRT CRT and import libraries,
-- FreeBSD versioned libc and auxiliary runtime libraries.
-
-The driver should prefer explicit command-line inputs, then sysroot/target
-layout, and finally host defaults only where that is safe. Runtime validation
-should distinguish linker failure from a too-minimal test container, such as an
-Alpine image missing `libgcc_s.so.1`.
-
-### 7. Diagnostics and tracing
-
-The linker needs precise diagnostics for unsupported system-linker cases:
-
-- target, input file/member, symbol, relocation kind, and output mode;
-- why a DSO was selected or skipped;
-- which symbol caused an as-needed DSO to be kept;
-- why a TLS access cannot be relaxed or represented dynamically;
-- which runtime library or sysroot path was searched.
-
-Use `KIT_TRACE` for opt-in structured tracing. Do not add global state.
-
-## Validation matrix
-
-For each supported arch/OS/libc family, track these lanes:
-
-- Rust std executable link,
-- Rust std runtime execution where a runner exists,
-- Rust no-std/freestanding link,
-- C/assembler object corpus link,
-- archive order and group behavior,
-- DSO/dylib/import-library dependency selection,
-- TLS local and imported access,
-- shared-library output,
-- relocatable `-r` output,
-- debug/unwind preservation.
-
-The first priority remains the verified Rust support set, because it is a good
-system-linker driver and produces real toolchain objects. The second priority is
-small focused fixtures for every feature above, so failures are explainable and
-do not depend on large external runtimes.
-
-## Acceptance criteria
-
-`kit ld` is complete enough to be the system linker for a target when:
-
-- Rust and C toolchains can use it directly with only target/sysroot/runtime
- configuration, not target-specific wrapper rewrites;
-- executables and shared libraries run under the target runtime loader;
-- `readelf`/`otool`/`llvm-readobj` dynamic metadata matches the platform
- linker's dependency and relocation shape for covered cases;
-- relocatable links preserve the relocation surface correctly;
-- local TLS relaxations are legal and checked, and dynamic TLS is emitted when
- required;
-- archive/DSO/import-library selection follows target format semantics;
-- negative cases fail early with actionable diagnostics.
diff --git a/doc/plan/LINKER.md b/doc/plan/LINKER.md
@@ -1,253 +1,317 @@
# Linker (planned work)
-This roadmap covers where the kit linker is headed beyond the static
-and JIT linking it does today. It is dominated by **incremental linking**:
-two related but distinct workstreams — append-only growth of a live JIT
-image (the `kit dbg` / `kit emu` consumer) and file-based incremental
-object linking (the build-system consumer, the "m2" redesign). Both rest
-on the same linker invariants — address stability, durable non-destructive
-relocation records, content-keyed reuse — and both fall back to a correct
-full link whenever a change cannot be proven local. For the linker's
-current architecture, passes, and invariants see [../LINK.md](../LINK.md);
-for how a resolved image runs in process see [../JIT.md](../JIT.md); for
-the object substrate see [../OBJ.md](../OBJ.md); for the build-system layer
-that consumes the file-incremental interface see [../BUILD.md](../BUILD.md)
-and the distribution CAS in [../DISTRIBUTE.md](../DISTRIBUTE.md).
-
-## Why incremental, and the shared invariants
-
-The full link is always available and always correct. Incremental linking
-is an *accelerator* gated on a soundness check: a correct-but-slow result
-always beats a fast-but-wrong one. Three invariants hold across both
-workstreams and must never be violated by any incremental path:
+This roadmap covers where the kit linker is headed beyond the static and
+JIT linking it does today. Two strands: **incremental linking** (making a
+relink cost `O(change)` instead of `O(program)`) and **system-linker
+compatibility** (making `kit ld` a drop-in replacement for the platform
+linker over kit's target set). For the linker's current architecture,
+passes, and invariants see [../LINK.md](../LINK.md); for the object
+substrate and per-format machinery see [../OBJ.md](../OBJ.md); for how a
+resolved image runs in process see [../JIT.md](../JIT.md). Design rationale
+lives in those docs — this file tracks open work, not background.
+
+## Shared invariants
+
+Everything below rests on three linker invariants that the full-link path
+already upholds and no incremental or compat path may violate:
- **Address stability.** Once a runtime/file vaddr is observable it never
moves. Unchanged atoms keep their bytes *and* their addresses, so their
- relocations are never reapplied — this is what makes a patch cost
- `O(change)`. Enforced by overwrite-in-slack / append-to-free-slot,
- **never compact**.
+ relocations are never reapplied. Enforced by overwrite-in-slack /
+ append-to-free-slot, **never compact**.
- **Relocations are durable, relative, and symbolic.** `LinkRelocApply`
- records survive as data and are not burned into bytes before emit.
- Persist each as `(atom, offset-within-atom, kind, target-name, addend)`;
- derive the absolute write address and target address from *current*
- placements at apply time. An atom that moves then needs zero reloc
- rewriting.
-- **Content-hash keying, not transient IDs.** `LinkInputId`/`LinkSymId`
- are stable only in-process. Persisted state is keyed by content hashes
- and symbol names, never by re-derived IDs, so determinism is a dedup
- nicety, not a correctness requirement.
-
-## Workstream 1 — append-only incremental JIT link
-
-Grow one live `KitJit` image with additional compiled objects while
-keeping every previously published runtime address stable. New code may
-reference old symbols; old debugger surfaces (`kit_jit_lookup`,
-`kit_jit_addr_to_sym`, symbol iteration, breakpoints, PC translation,
-the JIT debug view) must see new symbols. This is explicitly *not* hot
-reload: existing code is never replaced or repatched (see
-[../DBG.md](../DBG.md) for the debugger and the separate hot-reload
-design).
-
-### Done (baseline)
+ records survive as data, not burned into bytes before emit:
+ `(atom, offset-within-atom, kind, target-name, addend)`, resolved against
+ *current* placements at apply time. A moved atom needs zero reloc rewriting.
+- **Content-hash keying, not transient IDs.** `LinkInputId`/`LinkSymId` are
+ in-process only; any persisted state is keyed by content hashes and symbol
+ names. Determinism is a dedup nicety, not a correctness requirement.
+
+Incrementality is an *accelerator* gated on a soundness check: a
+correct-but-slow full link always beats a fast-but-wrong patch, and any path
+that cannot prove a change local falls back to a clean full link.
+
+---
+
+# Incremental linking
+
+## Baseline (done): append-only JIT link
The in-process append path is implemented and is the foundation the rest
-builds on: the `kit_jit_publish` surface (an append/replace batch driven
-by a `KitLinkSession`, reporting a bumped generation); append cursors with
-reserved RX/R/RW/TLS slack over one contiguous master
-VA reservation committed page-by-page; transactional rollback of cursors
-and symbol/section/reloc counts on failure; generation-bumped invalidation
-of the cached `kit_jit_view`; symbol resolution against the existing
-image, the append batch, and the external resolver with duplicate-strong
-detection; and a `dbg` REPL that drives compile → append → DWARF refresh
-with the worker stopped so line-table replacement never races a running
-thread.
-
-### Remaining
-
-- **Pending source-level breakpoints across appends.** Today a `b
- file:line` for a file not yet covered stays unresolved until retried.
- Maintain pending source breakpoint specs and arm them automatically
- after each append.
-- **Archive reselection on append.** v1 resolves a snippet against the
- already-linked image plus the external resolver only. A later cut can
- let appended inputs pull fresh archive members, sharing the gate logic
- from the file-incremental gate (below).
-- **`kit emu`'s append consumer.** Per-basic-block JIT translation wants
- to grow a single `LinkImage` as cold blocks land (see
- [../EMU.md](../EMU.md) §6). This is a separate consumer of the same
- append machinery and lands alongside the emu lifter cut. It is the
- motivation for the `link_resolve_at(Linker*, base_va)` /
- `link_resolve_extend(Linker*, LinkImage*)` entries in `src/link/link.c`,
- which are **panic stubs today** — see the shared surface below.
-- **Promote the API.** `kit_jit_publish` stays experimental until a
- second consumer (emu) exercises its append/replace batch, then settles
- as the stable extend surface.
-
-## Workstream 2 — file-based incremental object link (the "m2" redesign)
-
-The goal is "instant" relinks for dev builds: after editing one
-translation unit in a project of *N* TUs, the *link* cost should be
-`O(changed atoms + their relocations)`, not `O(whole program)`. Compile
-cost (caching, dependency scanning, the build graph and watch/daemon
-modes) is the build system's problem and is out of scope here — this
-workstream is the obj/link substrate that layer stands on. Incremental
-link is a `-O0`/`-O1` *dev* feature; release builds (`--incremental` off)
-always full-link, clean, and remain the canonical reproducible artifact.
-
-### Done (baseline) — "Done for ELF"
-
-The first cut landed on ELF as the reference format, with the acceptance
-suite (`test/link-incremental/`) green on **ELF/aa64 + ELF/x64**: atom
-content identity, per-atom reloc/symbol indices, the `LinkSession` with
-per-segment cursors/slack/free-list, append-only extend, patch-in-slack,
-the soundness gate with transactional rollback, per-segment build-id,
-per-changed-TU debug regen, and move-on-grow via thunk. This is the
-starting point; the rest of this section is what is *not* yet built.
-
-### The m2 redesign — design intent
-
-The redesign's central decision: **incrementality is not a parallel API —
-it is the existing link session made fully mutable.** A full link is the
-degenerate cold case (no prior state, nothing replaced); an incremental
-relink seeds prior state and replaces the changed inputs. The build system
-always drives *the same* session, and `resolve` internally decides
-patch-vs-full and reports which via an outcome enum
-(`FULL` / `PATCHED` / `FELL_BACK_FULL`). There is no separate "incremental"
-entry point that could drift from the full-link path. This directly
-matches the internal direction where `link_resolve` is "inputs → image"
-and the `link_resolve_at` / `link_resolve_extend` surface makes that
-resolve extend-capable.
-
-The atom is the patch unit — one function or one data object. Under
-`--incremental`, frontends emit one section per function/global (a
-`-ffunction-sections`/`-fdata-sections` equivalent) so each atom is
-independently placeable; kit already lays out kept atoms as individual
-`LinkSection`s. Each atom gets a BLAKE2b content id over its canonical
-form (`bytes || align || flags || canonical(relocs)`), the diff key.
-
-### The soundness gate
-
-Reuse is correct only when the change cannot alter symbol resolution. The
-edit is local only when the changed object's *interface* (defined global
-names + bindings, COMMON sizes/aligns, set of undefs) is unchanged and no
-archive pull-in changes; anything that can shift layout or resolution —
-symbol-set/binding flips, new archive members, COMDAT/COMMON merge changes,
-TLS-size shifts, import-set changes, slack/free-list exhaustion (data is
-never thunked), or layout-affecting flags — forces a fall-back. On
-fall-back the half-mutated session is discarded via the `LinkPatchTxn`
-watermark and a correct full link runs; the JIT append path's
-duplicate-global preflight is the precedent, but it **panics**, so
-converting "detect non-local" into "roll back + full link" is the new
-control flow at the heart of the redesign. See [../LINK.md](../LINK.md) for
-the full trigger set and rollback mechanics.
-
-### The move-on-grow primitive (swappable)
-
-When an atom outgrows its slot it must move, and callers must still reach
-it without their bytes changing. This is abstracted behind a single
-`LinkMoveOps.atom_moved` hook with two implementations; the rest of the
-design (atoms, slack, free-list, persisted session, the gate) is identical
-either way.
-
-- **Thunk-on-grow — ship first.** Calls stay direct (what codegen emits
- today). On a move, leave a jump island at the atom's *old* slot pointing
- to the new location; callers branch to the old address and hit the
- island. No codegen change, reachability is free by construction, and the
- tax is one extra jump only for functions that actually moved. Reuses the
- existing JIT call-stub island shape per arch. Data cannot be thunked, so
- a grown data atom that outgrows its slack falls back to a full link.
-- **GOT-cell — convergence target.** Under `--incremental`, codegen emits
- cross-unit calls and movable-data loads through a GOT cell; a move
- updates one cell. Costs a per-arch codegen change and a uniform extra
- indirect load, and needs reserved GOT slack + a GOT free-list (the GOT
- is one exactly-sized end segment today). Its strategic value is that it
- is the *same* primitive hot reload assumes, so one mechanism would serve
- both JIT hot reload and file incremental link. Build it when hot reload
- is scheduled, designed then to serve both — unifying earlier is
- speculative.
-
-### Persisted incremental state
-
-Side-band and content-addressed — **not** ELF-embedded incremental
-sections, because kit is multi-format. Store one blob in the existing
-`driver/dist` BLAKE2b CAS, recording per input and per atom: object + atom
-content ids, the `LinkAtomPlace` table (vaddr / file_offset / size /
-capacity / bucket), symbol→vaddr bindings keyed by *name*, relocations in
-relative+symbolic form, and free-list + per-segment cursor state. The
-session reads/writes it as opaque bytes through `KitWriter`; the build
-system owns the key, CAS storage, and lifetime — libkit stays IO/CAS-free.
-
-### Remaining work
+builds on. `kit_jit_publish` (an append/replace batch over a
+`KitLinkSession`, reporting a bumped `kit_jit_generation`) grows one live
+`KitJit` image with additional compiled objects while keeping every
+previously published runtime address stable: append cursors with reserved
+RX/R/RW/TLS slack over one master VA reservation, transactional rollback on
+failure, generation-bumped invalidation of the cached `kit_jit_view`,
+resolution against the existing image + append batch + external resolver
+with duplicate-strong detection, and a `dbg` REPL that drives
+compile → append → DWARF refresh with the worker stopped. This is *not* hot
+reload: existing code is never replaced or repatched. Mechanics live in
+[../LINK.md](../LINK.md) / [../JIT.md](../JIT.md) / [../DBG.md](../DBG.md).
+
+### Open
+
+- **Pending source-level breakpoints across appends.** A `b file:line` for a
+ file not yet covered stays unresolved until retried. Keep pending source
+ breakpoint specs and arm them automatically after each append.
+- **Archive reselection on append.** v1 resolves an appended snippet against
+ the already-linked image + external resolver only. Let appended inputs
+ pull fresh archive members, sharing the file-incremental soundness gate.
+- **`kit emu`'s append consumer.** Per-basic-block JIT translation wants to
+ grow a single `LinkImage` as cold blocks land (see [../EMU.md](../EMU.md)
+ §6). It is a second consumer of the same append machinery and the
+ motivation for the `link_resolve_at` / `link_resolve_extend` stubs below.
+- **Promote the API.** `kit_jit_publish` stays experimental until emu
+ exercises its append/replace batch, then settles as the stable extend
+ surface.
+
+## File-based incremental object link (the "m2" redesign)
+
+Not yet built. Goal: after editing one TU in an *N*-TU project, the *link*
+cost is `O(changed atoms + their relocations)`. Compile-side caching, dep
+scanning, and the build graph are the build system's problem
+([../BUILD.md](../BUILD.md)) — this is the obj/link substrate that layer
+stands on. Incremental link is a `-O0`/`-O1` *dev* feature; release builds
+always full-link and stay the canonical reproducible artifact.
+
+The current full-link path already preserves the invariants above and
+carries the hooks the redesign plugs into (see the comments threaded through
+`src/link/link.c` / `link_layout.c`), but none of the patch machinery
+exists yet. Design intent and the full trigger/rollback set live in
+[../LINK.md](../LINK.md); the actionable items:
- **Resolve the panic stubs.** `link_resolve_at` and `link_resolve_extend`
- in `src/link/link.c` are still `compiler_panic` stubs on the main path.
- They are the public extend-capable surface for both the file-incremental
- consumer and the emu append consumer; wiring them to the `LinkSession`
- patch/extend logic (and to graceful fallback rather than panic on
- exhaustion) is the remaining integration step to land the redesign on
- the main resolve path.
-- **Non-ELF formats.** The atom + slack + move-primitive core is
- format-agnostic; the difference is per-format machinery, so the order is
- ELF (done) → COFF/PE → Mach-O. COFF/PE is the incremental-friendly case
- (IAT-indirected imports, per-page base relocs, side-band PDB debug) and
- is gated mainly on kit's COFF maturity — see [../OBJ.md](../OBJ.md).
- Mach-O is heaviest but feasible last: each of `__LINKEDIT` fixups, the
- export trie, the indirect symtab, and the per-page code-signing
- CodeDirectory needs a bounded (not `O(image)`) incremental updater. Until
- a format's updater lands, that format falls back to the fast in-process
- full link.
-- **GOT-cell move primitive.** Deferred until hot reload is scheduled
- (above); the free-list, slack, session, and gate are reused verbatim
- when it lands — only `LinkMoveOps` changes.
-- **rv64 patch path.** The per-arch surface is small — the island/cell
- shape and the branch-into-island reloc kind. CI exercises ELF/aa64 +
- ELF/x64 first; rv64 follows by adapting its trampoline shape.
+ in `src/link/link.c` are `compiler_panic` today. They are the public
+ extend-capable surface for both the file-incremental and emu-append
+ consumers. Wiring them into a mutable `LinkSession` (patch-vs-full decided
+ internally, reported via a `FULL` / `PATCHED` / `FELL_BACK_FULL` outcome,
+ with graceful fallback rather than panic on exhaustion) is the central
+ integration step. Incrementality is *the same session made mutable*, not a
+ parallel API: a full link is the degenerate cold case.
+- **Atom model + soundness gate.** Under `--incremental`, frontends emit one
+ section per function/global so each atom is independently placeable; each
+ atom gets a BLAKE2b content id over `bytes || align || flags ||
+ canonical(relocs)`. Reuse is sound only when the changed object's
+ *interface* (defined global names/bindings, COMMON sizes/aligns, undef set)
+ is unchanged and no archive pull-in changes; any symbol-set/binding flip,
+ new archive member, COMDAT/COMMON-merge change, TLS-size shift, import-set
+ change, slack exhaustion, or layout-affecting flag forces a fall-back via a
+ `LinkPatchTxn` watermark + clean full link. The JIT preflight is the
+ precedent but it *panics*; converting "detect non-local" into "roll back +
+ full link" is the new control flow.
+- **Move-on-grow primitive (swappable behind `LinkMoveOps.atom_moved`).**
+ Ship **thunk-on-grow** first: calls stay direct, a moved atom leaves a jump
+ island at its old slot (reusing the per-arch JIT call-stub shape); no
+ codegen change, data that outgrows its slack falls back to full link. The
+ **GOT-cell** variant (one cell updated per move, the same primitive hot
+ reload needs) is the convergence target — build it when hot reload is
+ scheduled, so one mechanism serves both; unifying earlier is speculative.
+- **Persisted incremental state.** Side-band and content-addressed (kit is
+ multi-format — **not** ELF-embedded sections). One blob in the `driver/dist`
+ BLAKE2b CAS per input/atom: content ids, the `LinkAtomPlace` table,
+ name-keyed symbol→vaddr bindings, relative+symbolic relocations, free-list
+ + cursor state. libkit reads/writes it as opaque bytes through `KitWriter`;
+ the build system owns key, storage, and lifetime.
+- **Non-ELF formats.** The atom/slack/move core is format-agnostic; order is
+ ELF → COFF/PE (incremental-friendly: IAT-indirected imports, per-page base
+ relocs, side-band PDB) → Mach-O (heaviest — `__LINKEDIT` fixups, export
+ trie, indirect symtab, per-page code-sign CodeDirectory each need a bounded
+ incremental updater). Until a format's updater lands it falls back to the
+ fast in-process full link.
+- **rv64 patch path.** Small per-arch surface (island/cell shape +
+ branch-into-island reloc kind); follows ELF/aa64 + ELF/x64 by adapting the
+ trampoline shape.
- **Incremental build-id.** Per-segment FNV-1a subhashes combined
Merkle-style so a patch re-hashes only changed segments, replacing the
- current whole-image `O(image)` build-id. Keep this FNV-1a distinct from
- the BLAKE2b used for content/CAS keying.
-- **Determinism regression lock.** Object emission is already
- byte-deterministic; lock it with a two-compiles-equal regression test to
- enable cross-machine / shared-cache dedup. Content/name keying stays the
- correctness backbone so any future drift degrades dedup, never
- correctness.
-
-### Frontend contract and debug-info consistency
-
-All frontends converge to `ObjBuilder` and join the shared path at
-`obj_finalize`, so the machinery attaches once, frontend-agnostically —
-Toy, asm, and WASM get incremental link with no frontend-specific code. To
-be incrementally safe a frontend must produce deterministic output for
-identical `(source, flags, target, deps)`, declare its external dependency
-set (C reuses `KitDepIter`; single-source frontends report none), use
-stable source-derived symbol names, and expose a `frontend_id` +
-`schema_version` that salts the build-system key. Toy's durable-module REPL
-path is not a pure function of source, so it folds the module snapshot into
-the input key or opts out of caching; Toy's batch/file compile conforms
-like any other frontend.
+ current whole-image `O(image)` build-id (kept distinct from the BLAKE2b
+ content/CAS keying).
+- **Determinism regression lock.** Object emission is byte-deterministic;
+ lock it with a two-compiles-equal test so cross-machine / shared-cache
+ dedup is safe. Content/name keying stays the correctness backbone, so drift
+ degrades dedup, never correctness.
+
+### Frontend contract and debug info
+
+All frontends converge to `ObjBuilder` at `obj_finalize`, so the machinery
+attaches frontend-agnostically (Toy, asm, WASM included, no per-frontend
+code). To be incrementally safe a frontend must be a pure function of
+`(source, flags, target, deps)`, declare its external dependency set (C
+reuses `KitDepIter`; single-source frontends report none), use stable
+source-derived symbol names, and expose `frontend_id` + `schema_version` that
+salt the build-system key. Toy's durable-module REPL path is not pure → folds
+the module snapshot into the key or opts out of caching.
On debug info: on any changed atom, **re-emit that TU's full `.debug_*`**.
-kit emits one monolithic `.debug_line` program and one `.debug_info` CU
-with intra-CU `DW_FORM_ref4` offsets, so a function's rows cannot be spliced
-in isolation; and a body change rewrites the instruction→line mapping even
-when the atom did not move, so "keep stale `.debug_line`" is incoherent.
-Per-TU regen is `O(changed TU)`, cheap relative to the rest of the patch,
-and unchanged TUs' debug stays byte-stable because their atoms keep their
-addresses. Per-function CUs for `O(atom)` debug are a future option, not
-pursued now. See [../DWARF.md](../DWARF.md).
-
-## Acceptance: definition of done per format
-
-The executable spec lives in `test/link-incremental/`, authored test-first
-(red → green). Its synthetic multi-TU fixture (core TUs archived into a
-static library linked into two executables that share it; no third-party
-deps) covers an in-slack body edit (`PATCHED`, every vaddr stable,
-whole-program `link_resolve` counter does not increment), a grow-past-slack
-edit (`PATCHED`, atom moves, jump island at the old address, caller bytes
-byte-identical), the soundness gate (each non-local edit ⇒
+kit emits one monolithic `.debug_line` and one `.debug_info` CU with intra-CU
+`DW_FORM_ref4` offsets, so a function's rows can't be spliced in isolation,
+and a body change rewrites the instruction→line map even without a move.
+Per-TU regen is `O(changed TU)` and unchanged TUs stay byte-stable (their
+atoms keep their addresses). Per-function CUs for `O(atom)` debug are a
+future option, not pursued now. See [../DWARF.md](../DWARF.md).
+
+### Acceptance per format
+
+Author the suite test-first (`test/link-incremental/`, red → green) over a
+synthetic multi-TU fixture (core TUs archived into a static lib linked into
+two exes that share it; no third-party deps): in-slack body edit (`PATCHED`,
+every vaddr stable, the whole-program resolve counter does not increment),
+grow-past-slack edit (`PATCHED`, atom moves, jump island at old address,
+caller bytes byte-identical), the soundness gate (each non-local edit ⇒
`FELL_BACK_FULL` matching a from-scratch link), multi-output consistency,
-determinism, and a no-op relink. The two gates that define correctness are
+determinism, and a no-op relink. The two correctness gates are
vaddr-stability on a patch and fall-back on a non-local edit; both must be
-green before a format is "done." ELF/aa64 + ELF/x64 are done; COFF, Mach-O,
-and the rv64 patch path each repeat this bar. See
-[../TESTING.md](../TESTING.md).
+green before a format is "done." ELF/aa64 + ELF/x64 first; COFF, Mach-O, and
+rv64 each repeat the bar. See [../TESTING.md](../TESTING.md).
+
+---
+
+# System-linker compatibility
+
+Make `kit ld` act as the system linker over kit's support set. Near-term
+driver: Rust through `rustc`; target: C, Rust, asm objects, archives,
+DSOs/dylibs/import libs, PIE exes, shared libs, and relocatable links all
+behaving like the platform linker for the supported arch/OS pairs (Mach-O on
+macOS aa64/x64; ELF on Linux glibc/musl aa64/x64/rv64, FreeBSD, and
+freestanding aa64/x64/rv64/rv32; COFF/PE on Windows UCRT MinGW aa64/x64). The
+linker preserves format boundaries — ELF `--as-needed`/TLSDESC rules never
+leak into Mach-O dylib or COFF import-library semantics.
+
+## Covered (tested via `test-link`)
+
+These paths are landed and exercised by the linker harness; only widen as
+gaps surface:
+
+- **System-linker flag acceptance** — `kit ld` takes the common flags Rust
+ passes (GNU `-Wl,`/`-z`, target/sysroot, linker scripts, rlibs, `-l:name`,
+ `-static-pie`, `-nodefaultlibs`, hosted/no-startfile, PE/MinGW, Mach-O
+ version/platform), and Rust std + no-std link through it across the support
+ set.
+- **Relocatable `-r` links** — `link_relocatable.c` builds a fresh
+ `ObjBuilder` (preserving object structure + unresolved externals) for
+ `KIT_LINK_OUTPUT_RELOCATABLE` ET_REL / MH_OBJECT output.
+- **Shared-library output** — `-shared` / `KIT_LINK_OUTPUT_SHARED` emits a
+ loadable ET_DYN with `.dynsym`/`.dynstr`/`.dynamic`/`.rela.dyn`
+ (`src/obj/elf/link_dyn.c`).
+- **ELF symbol-version imports** — explicit `name@VERSION` resolution +
+ Verneed/Vernaux synthesis (FreeBSD libc compat); `elf_version_import_test.c`.
+- **COFF library-set search** — global fixed-point archive search +
+ weak-alias handling matching MinGW/LLVM; `coff_archive_fixpoint_test.c`,
+ `coff_weak_alias_test.c`.
+- **Relocation descriptor + byte-patch coverage** — the per-arch `RelocDesc`
+ table and representative byte patches are pinned (`reloc_desc_test.c`,
+ `reloc_apply_test.c`), including the AArch64 TLSDESC and RISC-V TLS-GD
+ relocation spellings.
+- **Local-exec TLS relaxation** — aa64 (the four TLSDESC relocs), x86_64
+ (TLS-LD/GD), and rv64 (TLS-GD, incl. the compressed gap) relax local
+ defined TLS; `jit_tls_relax_test.c`, relax paths in `src/obj/elf/link.c`.
+
+This is a useful subset, not completion. The work below is the gap between
+"Rust examples link" and "the platform linker can be replaced."
+
+## Open workstreams
+
+### 1. Ordered input and dependency selection (real `--as-needed`)
+
+The driver parses `--as-needed`/`--no-as-needed` into per-input `KitLinkMode`,
+but the resolver does **not** act on it: `collect_needed`
+(`src/obj/elf/link_dyn.c`) pulls *every* explicitly-supplied DSO into
+`DT_NEEDED` regardless of mode. Make as-needed real linker semantics by
+modeling, per ordered DSO input: current link mode, selected/not-selected
+state and why, SONAME/fallback identity for `DT_NEEDED`, and exports made
+available to later inputs. Then:
+
+- default/no-as-needed DSOs select as explicit deps; as-needed DSOs select
+ only when they satisfy an eligible strong undef at their position;
+- unselected as-needed DSOs do not satisfy later references;
+- selected DSOs are the `DT_NEEDED` source; duplicate SONAMEs suppressed
+ preserving first-selected order.
+- GNU linker scripts lower into the *same* ordered model: `INPUT`/`GROUP` as
+ today plus `AS_NEEDED(...)` pushing/restoring as-needed mode for nested DSO
+ or `-l` tokens, with nested scripts inheriting/restoring mode.
+- Mach-O (dylib load commands, weak/imported behavior) and COFF (import libs,
+ auto-imports, weak externals) get separate parity checks, not ELF semantics.
+
+### 2. ELF TLS model planning
+
+Plan ELF TLS *before* final reloc application and dynamic-section synthesis. A
+planner classifies each TLS reloc/sequence by symbol locality/visibility,
+imported-preemptible vs. non-preemptible, weak-undef, output kind
+(exe/PIE/shared/`-r`), static vs. dynamic, and target TLS variant +
+thread-pointer bias, then picks an explicit action: relax to local-exec,
+synthesize initial-exec, preserve/synthesize dynamic TLS/TLSDESC, preserve
+relocs for `-r`, or reject with a precise diagnostic. This pass owns the
+decision; arch code owns only the validated instruction-byte rewrite.
+
+### 3. TLSDESC completion
+
+Today only executable-local cases relax (materializing the local-exec offset).
+Full support needs: complete per-arch TLSDESC surfaces (`RelocKind`, ELF
+mapping, names, descriptor metadata, read/write, `objdump`); descriptor
+GOT/data allocation (two words per unique `(symbol, addend)`); TLSDESC dynamic
+relocations in `.rela.dyn`; integration with `.dynsym`, symbol versions,
+`DT_NEEDED`, PIE shifts, section GC, and DSO selection; checked sequence
+relaxation aa64 → x86_64 → other ELF arches; and shared-library output that
+preserves/synthesizes dynamic TLS where final layout is unknown. Imported
+TLSDESC references are real DSO uses and must select their provider under
+`--as-needed` (workstream 1).
+
+### 4. Architecture relocation and relaxation parity
+
+Per-arch reloc coverage + checked relaxations for modern-toolchain code
+shapes: aa64 ELF (dynamic TLSDESC path, local-sequence relaxation validated as
+a unit, host GOT/TLS/unwind shapes); x86_64 ELF (full TLS-LD/GD/IE/LE
+relaxation + TLSDESC once modeled); RISC-V ELF (expand TLS-GD/LD conservatively,
+compressed variants, preserve dynamic TLS where local relaxation is illegal);
+Mach-O aa64/x64 (GOT/TLV/unwind parity vs. Apple objects/archives); COFF
+aa64/x64 (SECREL, unwind, pdata/xdata, CRT object patterns). Every enum
+addition pins both descriptor table and a byte patch immediately.
+
+### 5. Shared libraries and relocatable links — broaden coverage
+
+Baseline `-shared`/`-r` emit (above) is exercised mostly on final
+executables. Deliberately broaden: `-shared` dynamic symbols/relocs/SONAME +
+install-names/import-tables + per-format TLS; `-r` preserving relocs and
+deferring final TLS/layout decisions; PIE vs. non-PIE staying distinct; and
+copy relocations, protected visibility, weak imports, COMDAT/section groups,
+init/fini arrays, unwind tables, and build IDs composing with GC and dynamic
+linking.
+
+### 6. Runtime/sysroot interoperability
+
+Find the same runtime inputs as the platform toolchain: crt objects + startup
+ordering; libc/libm/libpthread/librt/libdl/libutil + libc linker scripts;
+builtins/unwind libs (`libgcc_s`, compiler-rt, Rust `compiler_builtins`);
+dynamic loaders/interpreters; MinGW UCRT CRT + import libs; FreeBSD versioned
+libc. The driver prefers explicit command-line inputs, then sysroot/target
+layout, then host defaults only where safe. Runtime validation distinguishes a
+linker failure from a too-minimal container (e.g. Alpine missing
+`libgcc_s.so.1`). See [SYSROOTS.md](SYSROOTS.md).
+
+### 7. Diagnostics and tracing
+
+Precise diagnostics for unsupported cases — target, input file/member,
+symbol, reloc kind, output mode; why a DSO was selected/skipped and which
+symbol kept an as-needed DSO; why a TLS access can't relax or be represented
+dynamically; which runtime/sysroot path was searched. Opt-in structured
+tracing via `KIT_TRACE`; no global state.
+
+## Validation matrix
+
+Per supported arch/OS/libc family, track: Rust std link + runtime exec (where
+a runner exists), Rust no-std/freestanding link, C/asm object-corpus link,
+archive order/group behavior, DSO/dylib/import-lib selection, TLS local +
+imported access, shared-library output, relocatable `-r` output, and
+debug/unwind preservation. First priority is the verified Rust support set (a
+good driver producing real toolchain objects); second is small focused
+fixtures per feature so failures are explainable without large external
+runtimes.
+
+`kit ld` is the system linker for a target when: Rust and C toolchains use it
+directly with only target/sysroot/runtime config (no per-target wrapper
+rewrites); exes and shared libs run under the target loader;
+`readelf`/`otool`/`llvm-readobj` dynamic metadata matches the platform linker
+for covered cases; `-r` preserves the relocation surface; local TLS
+relaxations are legal and checked and dynamic TLS is emitted when required;
+archive/DSO/import-lib selection follows format semantics; and negative cases
+fail early with actionable diagnostics.
diff --git a/doc/plan/O1-PATTERNS.md b/doc/plan/O1-PATTERNS.md
@@ -1,554 +0,0 @@
-# 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.
-
-> **Update (2026-06-16): all of §2 (L1–L10) is landed.** Implemented in parallel
-> (worktree-isolated agents) and merged gated. Aggregate `__TEXT` **1.09× →
-> 1.059× clang** (lvm 2.86× → **2.57×** via L1; yyjson 0.86× → **0.80×** via
-> L6/L4/L2; sqlite 1.11× → **1.09×**; lparser 1.07× → **1.04×** via L9; tinyexpr
-> via L10). Per-item: **L1** store→load forwarding (`pass_combine.c`, lvm −9.95%),
-> **L2** cset→cmp_branch fusion (same-block subset; cross-block stays O2), **L3**
-> widen `aa_set_bytes` zero-init (`aa64/native.c`, cjson −5.4%), **L4** double-cset
-> drop, **L5** confirmed already-subsumed by `try_substitute`/`try_ret_retarget`
-> (no unsound new fold; the scratch-source residual is a miscompile, left out),
-> **L6** sxt-after-load drop (yyjson −5.45%), **L7** shift-into-ALU + **L8**
-> sxtw-index-into-addressing (Operand riders, aa64-only; x64/rv64 capability-gated
-> off), **L9** const div/mul fold (`pass_simplify.c`, lparser udiv 22→0), **L10**
-> frameless-leaf elision. L9 also surfaced + got fixed a **latent volatile-load
-> miscompile** (a volatile load of a register-resident local was served from the
-> value-residency fast path with no observable memory access emitted; fixed in
-> `src/cg/memory.c`). Gates: test-opt (18 guards) + test-toy 1392/0 + ecosystem
-> 28/0 + smoke-x64/rv64 3/0 + test-cg-api 289/289. The §3 (SSA/O2) and §4
-> (not-a-deficiency) findings below stand.
-
-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
@@ -1,876 +0,0 @@
-# O1 code quality: closing the gap to clang `-O1`
-
-**Goal.** Improve the *runtime quality* (density + efficiency) of kit's `-O1`
-emitted code **without making the `-O1` compile superlinear**. `-O1` is, by
-design, a no-SSA pipeline (Section 3 of [doc/OPT.md](../OPT.md)): local + linear-scan
-machinery only, no dominance-frontier phi insertion, no value numbering. Every
-item here must keep that property — a transform that needs SSA, a full
-interference graph, or any O(n²) analysis belongs in the parked **O2 mid-end**,
-not here.
-
-This doc is the forward-looking worklist. The design reference for the passes it
-touches is [doc/OPT.md](../OPT.md); the *compile-speed* roadmap (a separate axis —
-that doc explicitly does not care about generated-code quality) is
-[PERF.md](PERF.md). The broader optimizer roadmap (completing the O2 SSA mid-end,
-machine register-constraint work) is [OPTIMIZER.md](OPTIMIZER.md) — this doc is
-the narrow, near-term slice: `-O1` density within the linear/no-SSA budget. Read
-the first two before starting.
-
-Two rules, mirroring PERF.md:
-
-- **A superlinear axis is a bug.** `-O1` compile time must stay ≈ linear in
- function size/count. A small per-function sort over side tables (for example,
- frame slots) is acceptable only if it stays immaterial in sqlite timing; no
- transform should add an O(n²) or per-instruction superlinear axis. Validate
- with the synthetic sweep in OPT.md §8 and by timing sqlite `-O1` before/after
- (it is ~1.6 s today; a quality change should not move it materially).
-- **Gate every change on correctness, not byte-identity.** These changes
- deliberately alter emitted bytes, so the gate is: `make test-opt test-toy`,
- the ecosystem golden + vs-clang run at `-O0`/`-O1` (`make test-ecosystem`,
- 28/0), and `make test-smoke-x64 test-smoke-rv64`. Add a structural disasm
- guard per change under `test/opt/` (see `redundant_copy_ext.sh`).
-
----
-
-## 1. Methodology
-
-All numbers below: Apple-silicon arm64 / Darwin, `-O1`, release kit at
-**`build/release/kit`** (`make bin RELEASE=1`). Pitfalls that cost real time:
-
-- `make bin RELEASE=1` writes **`build/release/kit`**, *not* `build/kit`
- (`build/kit` may be a stale debug binary). Measure with the release binary.
-- A kit binary copied outside the build tree fails with `support dir not found`
- (the rt/support dir resolves relative to the exe path). To A/B two builds, keep
- both binaries **inside `build/release/`** (e.g. `kit_baseline`, `kit_fixed`).
-- clang on macOS needs `-isysroot "$(xcrun --sdk macosx --show-sdk-path)"`; kit
- needs `--sysroot "$SDK"`.
-
-Per-file comparison (text size + instruction mix):
-
-```sh
-SDK="$(xcrun --sdk macosx --show-sdk-path)"
-kit cc -O1 -c f.c -I... --sysroot "$SDK" -o k.o
-clang -O1 -c f.c -I... -isysroot "$SDK" -o c.o
-size -m k.o | awk '/__text/{print $NF}' # __TEXT bytes
-kit objdump -d k.o | grep -cE ':\t' # instruction count
-```
-
-The ecosystem sources are provisioned at `scripts/ecosystem.sh srcdir <name>`
-(lua, sqlite, cjson, lz4, miniz, yyjson, tinyexpr). **TODO:** codify the A/B
-harness as `scripts/o1_quality.sh` (build baseline+fixed into `build/release/`,
-diff text/mix across the corpus) — it was ad-hoc for this analysis.
-
----
-
-## 2. Current standings
-
-> **Update (2026-06-16): the §3 worklist is fully landed (see §5).** Aggregate
-> `__TEXT` is now **≈ 1.09× clang `-O1`** (down from 1.25×, a −12.8% reduction in
-> kit's emitted `__TEXT`). The dominant symptom below — spill-address `sub x29`
-> at 36% of lvm / 12% of sqlite — is **eliminated** on aa64 (lvm 8,774 → 0,
-> 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. **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:
-
-| file | kit/clang | note |
-|---------|----------:|------|
-| lz4 | **0.39×** | clang unrolls/inlines hot loops → *clang* is bigger |
-| lapi | 0.83× | kit smaller |
-| yyjson | 0.88× | kit smaller |
-| lparser | 1.09× | ~par |
-| sqlite | 1.27× | |
-| cjson | 1.33× | |
-| miniz | 1.94× | |
-| lvm | **4.66×** | pathological — the lua interpreter loop |
-
-On **small-function code kit is competitive or smaller** than clang (clang `-O1`
-trades size for speed via inlining + loop unrolling — not a kit deficiency). The
-real gap — *removable work kit emits that clang does not* — concentrates in
-**large, high-register-pressure functions**:
-
-- `luaV_execute` alone is **5.6× clang (22,175 vs 3,980 insns)** and is **92% of
- lvm's gap**. sqlite's big interpreter-style functions behave the same.
-
-### The dominant symptom: spilling + spill addressing
-
-| file | insns | `sub xN,x29,#` | % of insns | `[x17]` spill accesses |
-|--------|--------:|---------------:|-----------:|-----------------------:|
-| lvm | 24,197 | 8,774 | **36%** | 8,053 |
-| sqlite | 292,039 | 35,426 | **12%** | 29,361 |
-| miniz | 29,074 | 2,181 | 8% | 2,014 |
-| cjson | 5,302 | 33 | ~1% | 0 |
-| yyjson/lz4/lapi/lparser | — | ~0 | ~0% | ~0 |
-
-Two compounding problems, both visible in `luaV_execute`:
-
-1. **kit spills far more than clang.** lvm: ~8,053 stack accesses (kit) vs ~236
- (clang) — a ~34× difference in stack traffic. Root cause: a giant
- post-inlining function + a linear-scan allocator with no splitting/coalescing.
-2. **Each spill is addressed with two instructions.** Spill slots sit at
- *negative* offsets from the frame pointer `x29`, beyond `ldur`'s ±256 unscaled
- range, so kit emits:
-
- ```asm
- sub x17, x29, #552 ; recompute slot address
- stur x11, [x17]
- sub x17, x29, #552 ; ← recomputed again for the very next access
- ldur x9, [x17]
- ```
-
- clang addresses the same slots as `str [sp, #0x88]` — a single **scaled,
- positive** offset (`ldr`/`str` reach 0..32760). ~**2,478 of lvm's subs are
- immediately redundant** (same offset recomputed back-to-back).
-
-Instruction-mix delta that follows from this (lvm, kit vs clang): `sub` 8,887 vs
-78; everything else is within ~1.1–1.5× (`ldr` 1,072 vs 933, `mov` 820 vs 691,
-`add` 573 vs 512). **The gap is not broad — it is spill addressing on big
-functions, plus the spilling that feeds it.**
-
-Problem (2) is **arch-specific**, which shapes W1 (see §3): it is acute on aa64
-(`ldur` reaches only ±256, so any far slot needs an address recompute) and
-present-but-milder on rv64 (`ld` reaches ±2 KB). **x64 has it for free** — `mov
-[rbp-disp32]` always addresses any slot in one instruction (and auto-selects the
-1-byte `disp8` form when in range), so x64 emits no frame-address recompute at
-all. Problem (1), the over-spilling, is arch-neutral.
-
----
-
-## 3. Worklist (linear-feasible), in priority order
-
-Each item: mechanism · where · expected impact · why it stays linear · risks.
-
-### W1 — Optimal known-frame layout: hot-slot-low ordering + positive-offset addressing ★ highest leverage
-
-**The reframing.** At `-O1` the frame is *fully known before the body is emitted*
-— `*_func_begin_known_frame` fixes `frame_size_final` and sets `frame.frame_final`
-before any body instruction (aa64 `native.c:2070/2094`). That is a standing asset
-the `-O0` single-pass path lacks, and the optimal known-frame design exploits it
-two ways — one shared across all three arches, one aa64-specific:
-
-1. **(shared) Hot-slot-low frame ordering.** Order the body frame slots so the
- most-frequently-accessed spills get the *smallest final displacement from the
- addressing base used by that layout*. Small offsets are cheapest to encode on
- every arch: `disp8` vs `disp32` on x64 (3-byte vs 7-byte access), inside the
- scaled reach on aa64, inside the ±2 KB `imm12` window on rv64. This is a
- layout *choice* — zero added analysis; just do not confuse raw slot-allocation
- order with the final displacement when a backend uses a bottom-record or
- positive-offset layout.
-2. **(aa64) A uniform x29-at-bottom frame.** Anchor x29 below the static slots so
- every slot is a one-instruction positive `ldr/str [x29,#k]`, for every known
- frame — replacing the `sub x17,x29,#k; ldur` fallback. x29 is the (always
- reserved, alloca-stable) frame pointer, so this needs no register and no
- regalloc change, and alloca falls out for free (W1.1).
-
-The addressing story differs sharply per arch — **measure before assuming a gap**:
-
-| arch | in-range access | far-slot path today | W1 work |
-|------|-----------------|---------------------|---------|
-| aa64 | `ldur` ±256 only | `sub x17,x29,#k; ldur` (2–4 insns, recomputed each access) | **uniform x29-at-bottom (big)** + ordering |
-| rv64 | `ld off(s0)` ±2 KB | `lui;addi;add x; ld 0(x)` (2–4 insns, each access) | ordering (then assess positive layout) |
-| x64 | `mov [rbp-disp32]` (always 1 insn, auto disp8) | — none — | ordering only (disp8 density) |
-
-x64 has **no** address-recompute path (`x64_emit_mem`, `x64/native.c:638`; disp8/
-disp32 auto-selected in `x64/isa.h:414`), so for x64, W1 is *only* the shared
-ordering and purely a density micro-win.
-
-#### W1.0 — Hot-slot-low frame ordering (shared; all arches)
-
-**Mechanism.** Thread a per-slot priority from the allocator's spill-cost metric
-into frame layout, and present body slots to the backend in the order that gives
-hot slots the smallest final displacement. For today's x64/rv64/fp-relative
-layouts this is the same as handing the bump allocator
-(`native_frame_slot_alloc`, `cg/native_frame.c:45` — monotonic `cum_off`) hot
-slots first; for a positive/bottom-record layout, verify the final offset formula
-before assuming the same order.
-
-**Where + how.**
-- Add a priority field to `IRFrameSlot` (`src/opt/ir.h`) and to
- `NativeFrameSlotDesc` (`src/arch/native_target.h:41` — there is a spare `u8 pad`
- at line 49 to widen).
-- Stamp it from the allocator's final spill-slot assignment, not just first
- creation. `spill_slot_for` (`pass_lower.c:578`) sees the first PReg that creates
- a frame slot, but `alloc_assign_group_stack` can later reuse that same slot for
- non-overlapping PRegs. The slot priority should aggregate the traffic of every
- PReg/group assigned to that slot (saturating sum, or max if measurement shows
- sum is too noisy), using the metric `pass_live.c:844` computes:
- `2*use_freq + def_freq + live_across_call_freq + live_block_freq`. Homed
- (address-taken) locals keep priority 0 for v1 — the hot traffic is spills, not
- homed locals.
-- Copy `priority` through the desc-build loop in `opt_emit_native`
- (`pass_native_emit.c:~1513`).
-- Just before `t->func_begin_known_frame(...)` (`pass_native_emit.c:~1544`), build
- an index permutation over the slot descs, **stable-sort by descending
- priority** (skip any `NATIVE_FRAME_SLOT_FIXED_OFFSET` slot), and present descs
- in that order. Map each returned `NativeFrameSlot` back through the permutation
- so `e->slot_map[frame_slots[order[k]].id]` stays correct.
-
-The backend body list (`frame->slots[]`) holds only LOCAL/SPILL slots; the
-constrained slots (callee-saves, sret/va homes, aa64 atomic scratch,
-alloca/outgoing areas) are reserved by the backend *outside* this list and are
-untouched by the sort. No target analysis changes, but validate the final offset
-formula per backend (especially after W1.1's bottom-record aa64 layout) before
-claiming a slot is "low."
-
-**Impact.** rv64: keeps the hottest spills inside the ±2 KB `imm12` window so they
-stay single-instruction. x64: more accesses encode as disp8 (−4 bytes each) —
-density only. aa64: mostly subsumed by W1.1 (every slot ≤32 KB is already
-single-instruction there); ordering is insurance only for >32 KB frames.
-
-**Linear?** Effectively yes — one `O(nslots·log nslots)` sort per function, off
-the hot per-instruction path. If the synthetic sweep or sqlite timing shows this
-is visible, switch to a linear bucket/radix ordering over the integer priority.
-
-**Risk.** Low — the gate is correctness, not byte-identity, so reordering internal
-slot offsets is free. The hazards: the `slot_map` permutation must be exact (a
-transposed mapping silently miscompiles every spill), and priority aggregation
-must include reused spill slots rather than only the first PReg that allocated
-the slot. Add an assert that every IR slot id maps to a distinct native slot.
-
-#### W1.1 — aa64 uniform x29-at-bottom known-frame layout (the big win)
-
-**Root cause (measured).** aa64's only wide single-instruction memory form is the
-*unsigned scaled* `ldr/str [base,#pos]` (reach 0..32760 by size); its signed
-unscaled `ldur` reaches only ±256. So one-instruction slot access requires the
-base to sit **below** the slots (positive offsets). Today the known-frame path
-anchors x29 at the *top* (top-record), so slots are at *negative* x29 offsets past
-±256 → the `sub x17,x29,#k; ldur` fallback (`native.c:1009`) that is 36% of lvm /
-12% of sqlite. The existing positive forms don't cover the functions that matter:
-`fp_at_bottom` is gated to ≤504-byte, `out_stack==0` frames (`native.c:2085`), and
-the single-pass `slot_sp_base` far-slot patch can't run on the known-frame path
-(it *panics* if a patch is left pending, `native.c:1923`). `luaV_execute` — big
-frame + `out_stack>0` — qualifies for none of them.
-
-**Design: one stable base below the slots, for every known frame.** Anchor x29 at
-the bottom of the static slots (just above the outgoing-arg area), uniformly, so
-every slot is `ldr/str [x29,#pos]` — one instruction within the 32 KB scaled
-reach, with the `add x16,x29,#hi; ldr [x16,#lo]` build only past it. The payoff is
-that x29 is the **frame pointer**: already reserved (never in `aa_int_allocable`,
-`native.c:4128`) and **stable across alloca** (set once; the body never moves it).
-Two consequences make this the clean choice:
-
-- **No optimizer/regalloc change — contained in the aa64 backend.** Unlike an
- sp-base + x28-anchor scheme, there is no register to reserve and `machinize` is
- untouched. The known-frame contract is honored by construction:
- `aa_func_begin_known_frame` already has the *complete* frame
- (`frame_size_final`, `max_outgoing`, `has_alloca`, the slot list, callee-saves)
- and emits the final prologue in one pass — exactly what "known frame" means.
-- **alloca falls out for free.** x29 never moves, so `[x29,#k]` stays valid after
- `alloca` lowers sp. No anchor register, no special path, no v2.
-
-**Layout** (`os` = `out_stack` = max outgoing-arg bytes, `fs` = frame_size):
-
-```
-high addr
- incoming stack args (caller) ← CFA = x29 + (fs − os)
- saved x29, x30 ← at [x29] / [x29+8] (frame-pointer chain)
- static slots (locals/spills/saves/va-save) ← [x29+16 …], positive
- x29 ───────────────────────────── ← anchor = sp_entry + os
- outgoing args (os) ← [sp, #0 …], sp-relative
- sp
-low addr
-```
-
-The saved pair sits **at** x29 so `[x29]` = caller fp and `[x29+8]` = caller lr —
-the frame-pointer chain kit's unwinder / `__kit_backtrace` walks (uniform
-fp[0]/fp[1]). This invariant is non-negotiable; it is *why* the pair is
-co-located with the anchor rather than left at the top. Outgoing args stay
-sp-relative (`[sp,#k]`), so calls after an alloca still address their arg area at
-the current sp.
-
-**Where + how (aa64 backend only — `src/arch/aa64/native.c`).**
-1. **Generalize the bottom-record layout** beyond today's `fp_at_bottom` gate:
- make it the layout for *all* known frames except the Windows-variadic
- `top_home` case (its GP home area must sit above the saved pair → keep that one
- on top-record / fp-relative). Anchor `x29 = sp_entry + os`; `aa_fp_off_slot`
- already returns a positive value in this mode (`frame_size − slot_off`),
- generalized for the `+ os` shift.
-2. **Prologue** (`aa_build_prologue_words`): general form `sub sp,sp,#fs` (scratch-
- materialized / Windows-probed for huge `fs`) · `stp x29,x30,[sp,#os]` ·
- `add x29,sp,#os` · callee-save stores at positive x29 offsets. Keep today's
- folded `stp [sp,#-fs]!` + `mov x29,sp` as an encoding fast-path when
- `os==0 && fs≤504`.
-3. **Epilogue** — uniform sp recovery from the anchor, correct with or without
- alloca (replaces the current two-path teardown; the post-indexed `ldp [sp],#fs`
- can't be used once alloca floated sp): after callee-restores,
- `mov x16,x29` · `ldp x29,x30,[x16]` · `add sp,x16,#(fs − os)` · `ret`.
-4. **Addressing** (`aa_emit_mem`): a frame slot is always `ldr/str [x29,#base_off]`,
- `base_off = aa_fp_off_slot(slot) (+ extra)` (positive), emitted **directly**
- (frame is final) — scaled when `≤32760`, else `add x16,x29,#hi; ldr/str
- [x16,#lo]`. No `slot_sp_base`, no deferred `AA_PATCH_SLOT`, no `sub x17` on the
- known-frame path. (Single-pass `-O0` keeps `slot_sp_base` + the patch — its
- frame isn't final at emit time.)
-5. **CFI**: `CFA = x29 + (fs − os)`; saved fp/lr at `[x29]`/`[x29+8]`. Generalizes
- the existing bottom-record CFI (the `os==0` case, `native.c:1929–1936`) to
- `os > 0`.
-
-**Impact.** Removes essentially all of lvm's 8,774 and sqlite's 35,426
-frame-address `sub`s — for *every* function, alloca included. The single biggest
-`-O1` density lever; each slot access 2–4 insns → 1.
-
-**Linear?** Yes — a layout + per-access addressing choice; zero added analysis.
-
-**Risk — the highest blast radius in this doc.** It touches the prologue/epilogue,
-CFI, the bottom-record gate, tail calls (`AA_PATCH_TAIL`) and the slim tiers. Gate
-hard: aa64 smoke + toy corpus + ecosystem run-and-diff + `test/opt/prologue_tier.sh`
-+ an unwinding/backtrace check (the fp-chain invariant) + a `test/opt/` disasm
-guard asserting large *and* alloca functions emit `ldr/str [x29,#…]` (or the
-add-build) and **no** `sub xN,x29,#…` for spill access. Re-verify the slim/leaf
-tiers and tail-call teardown against the generalized epilogue. Include structural
-cases at the scaled-offset boundary, past the 32 KB scaled reach, signed narrow
-loads, under-aligned aggregate chunks, `top_home`/Windows-variadic fallback, and
-an alloca-after-call path that proves outgoing args remain addressed from the
-current sp.
-
-#### W1.2 — rv64: order first, assess positive layout
-
-**Assessment first.** rv64's `imm12` window is ±2 KB — 8× aa64's `ldur` ±256 — so
-a hot slot is single-instruction (`ld off(s0)`, `rv64/native.c:819`) for any
-realistic hot working set. After W1.0 lands, **measure** rv64 far-slot traffic
-(`lui;…;add` builds, `riscv/native.c:807`) in hot loops on the corpus before
-building more. Expectation: hot-low ordering captures most of the win; the
-residual is cold-slot tail.
-
-**If measurement shows a real gap:** rv64 has no positive-offset layout and no
-patch infra today (all `s0`-relative negative). Two options, in order: (a) the
-W1a per-access local CSE (don't rebuild the identical `lui;addi;add` for
-back-to-back accesses to the same far slot); (b) a known-frame positive layout
-(slots at `frame_size - off` above the saved pair, `s0 = sp`) gated like aa64's,
-with `rv_s0_off_slot` branching on a layout flag — heavier, lower ROI given the
-wide window. Do **not** build (b) speculatively.
-
-#### W1.3 — x64: ordering only
-
-W1.0 is the entire x64 story: more spill accesses fall in `disp8` range
-(−4 bytes/access). No addressing change — `x64_emit_mem` is already
-single-instruction. Skip if x64 density is not a priority.
-
-#### W1a — local frame-address-`sub` CSE (interim / harness bootstrap)
-
-**Status: largely subsumed by W1.1.** Once aa64 addresses far slots positively,
-the back-to-back `sub x17,x29,#k` recomputes vanish for spill slots. W1a keeps two
-narrow uses: (a) a **safe first PR** that stands up the gate + a `test/opt/`
-structural guard before the larger W1.1 change, and (b) mop-up for the *residual*
-recompute cases W1.1 doesn't touch — far slots past the scaled reach (huge
-frames), GOT/global address rebuilds, and the rv64 `lui;…;add` rebuild (W1.2a).
-
-**Mechanism.** A per-block post-emit peephole: track "x17 currently holds base−K";
-drop a re-`sub`/re-build of the same K while x17 is unclobbered. Natural home:
-`mir_combine` (`src/opt/pass_combine.c`) tracking, or a tiny aa64/rv64 post-emit
-peephole. Bounded per-block state → linear.
-
-### W2 — Rematerialization instead of spilling
-
-**Mechanism.** At a use of a spilled value whose single definition is *cheaper to
-recompute than to reload*, emit the recompute at the use instead of a slot
-reload; drop the spill store when every use rematerializes. Conservative v1 set
-(input-less or frame-only producers, recompute ≤ reload cost):
-- `IR_LOAD_IMM` whose immediate is ≤2 insns (`movz`, or `movz;movk`);
-- `IR_ADDR_OF` of an `OPK_LOCAL` (one `add` off the frame base).
-
-Defer `IR_ADDR_OF(global)` (2 insns + reloc; `addr_of_global_cse` already hoists
-it) and large `IR_LOAD_CONST` (a 4-insn `movz;movk;movk;movk` is *worse* than a
-1-insn reload) for v1.
-
-**Where + how.**
-1. Linear pre-pass `opt_mark_remat(f)` (new, in `pass_lower.c`): for each PReg
- record `remat_def[v] = InstId` iff it has exactly one def and that def is in
- the v1 set above (no register inputs). One scan over all insts.
-2. Add one shared helper for "materialize this spilled/remat PReg at this use",
- and route **every** spilled-use path through it. Do not only hook
- `rewrite_one_operand` (`pass_lower.c:1383`): call args
- (`rewrite_call_arg_operand`) and store values (`rewrite_store_value_operand`)
- currently turn spilled PRegs into direct frame operands, and would still read
- the slot if the defining store were removed.
-3. At ordinary reload/store insertion:
- - On a **use** of a spilled remat PReg: clone the recorded def into `c->before`
- with `dst = scratch` instead of appending an `IR_LOAD`.
- - On the **def** of a spilled remat PReg: rewrite the def operand to scratch.
- **Skip** the `IR_STORE` only when every later use is known to rematerialize;
- otherwise keep the store for the remaining frame-use sites. The original
- pure def then writes a dead scratch and `mir_dce` (already in the pipeline)
- removes it when the store is skipped.
-
-**Impact.** Cuts spill reloads and, when every use rematerializes, spill stores on
-high-pressure functions; compounds with W1. A v1 implemented during MIR rewrite
-does **not** reduce allocator pressure by itself — it attacks stack traffic after
-the spill decision, and frame size only if a later cleanup removes now-unused
-slots. Reducing spill count requires feeding remat costs back into allocation
-later.
-
-**Linear?** Yes — a per-def classification pre-pass + a local choice at each spill
-site. No global analysis.
-
-**Risk.** The recompute must be input-less so it is valid anywhere (the v1 set is).
-A constant bigger than its reload must *not* rematerialize — the ≤2-insn cost gate
-enforces it. The correctness trap is dropping the spill store while any direct
-frame use remains. Keep the set conservative; measure reload/store counts before/
-after, separately from total spill-slot count.
-
-### W3 — Linear move coalescing
-
-**The hook is already there.** The O1 allocator consults a union-find via
-`alloc_coalesce_root` / `alloc_group_member` (`pass_lower.c:742/755`) and assigns
-one location per class (`alloc_assign_group_hard/_stack`). At `-O1`
-`f->opt_coalesce_parent` is NULL, so every PReg is its own root (identity → no
-coalescing). W3 is a **linear pass that populates that parent array before
-`opt_assign_ranges`** — the allocator then merges copy-related values onto one
-register for free. The existing `opt_coalesce_ranges` (`pass_coalesce.c:257`) does
-this *correctly* but builds an O(n²) conflict **matrix** — that is the O2-only
-path; W3 must not call it.
-
-**Mechanism (linear).** Reuse unchanged: `collect_move` (eligible `IR_COPY`: same
-class+type, both have ranges, not `IRF_NO_COALESCE`; `pass_coalesce.c:238`),
-`coalesce_find`/`coalesce_union` (`:26/:209`), and `opt_ranges_overlap_kind` (`:54`,
-the cheap on-demand range-overlap test). New driver:
-1. Init `opt_coalesce_parent[v]=v`, `opt_coalesce_size[v]=1` (as `:265`).
-2. Per root, maintain its **merged live-range list** (sorted by `raw_start`,
- initially the PReg's own ranges) and **aggregated constraint masks** (`tied` /
- `forbidden` / `allowed`).
-3. Collect moves; sort hottest-first by block frequency.
-4. For each move (dst,src): `ra=find(dst)`, `rb=find(src)`; skip if equal. Test
- `can_merge(ra,rb)`:
- - merge-scan the two sorted range lists: any overlap wider than one point, or a
- *second* unit-length overlap, is a conflict (this mirrors
- `opt_ranges_overlap_kind`'s 0/1/2 result lifted to the group level — the one
- benign unit overlap is the copy itself);
- - constraint check: `forbidden_a | forbidden_b` compatible with the merged
- `allowed` and any `tied` (mirrors `group_constraints_compatible`, `:176`).
- If mergeable, `coalesce_union`, then merge the smaller root's range list and OR
- its masks into the larger (union-by-size).
-
-**Why this is linear (not the O2 matrix).** No `nrelated²` conflict bitmap. Each
-merge-scan is `O(|A|+|B|)`; union-by-size makes total work `O(N log N)` in ranges.
-**Conservative fallback** if the range-list bookkeeping proves fiddly: cap class
-size at a constant K and do the K²-bounded pairwise `opt_ranges_overlap_kind` per
-merge — still linear in moves, missing coalesces only in classes > K.
-
-**Impact.** Removes the cross-block `mov x,x` copies the per-block `mir_combine`
-cannot retire (~668 in lvm) *and* lowers pressure (→ fewer spills), compounding
-with W1/W2.
-
-**Linear?** Yes (above). Never coalesce an `IRF_NO_COALESCE` (phi-edge) copy.
-
-**Risk — bounded, but not zero.** Non-overlapping ranges mean the merged values are
-never simultaneously live, so the live set does not grow at any program point.
-But forcing two disjoint values to share one location can still make the merged
-group harder to place (combined constraints, one common hard register, callee-save
-costs) and can spill a group that two independent values would not have spilled.
-Mitigate by merging only when the combined constraints leave at least one plausible
-hard register, or by rolling back / splitting a coalesced class that falls to the
-stack. Gate on correctness + spill-count-not-worse on the corpus.
-
-Two implementation details must be made explicit:
-- `opt_verify_alloc` currently describes and checks the "O1 no-coalesce" shape;
- it must treat PRegs in the same coalesce root as one value, not as an
- interference violation.
-- `mir_combine` currently uses `!f->opt_coalesce_parent` as a proxy for "O1 may
- fold `load_imm` through copies". Once O1 has a union-find, replace that with an
- explicit mode/flag so enabling coalescing does not silently disable useful O1
- immediate folding.
-
-### W4 — Inline-pressure cap
-
-**Mechanism.** A size/pressure-aware inline cost cap — back off inlining into an
-already-large caller, or whose inlining pushes estimated live-set past the
-register file — trims pressure before regalloc. This is **not** the primary lvm
-fix in the current measurements: a targeted `lvm.c` metrics run showed
-`opt.inline.inlined=0` (75 candidates refused by shape), so `luaV_execute` is
-large from source structure rather than O1 inlining. The policy knob is still
-relevant for sqlite-style files, where the whole-program inliner does inline many
-small callees and there is no SSA post-inline cleanup at `-O1`.
-
-**Where.** `src/opt/pass_inline.c` (the `opt_inline` cost/growth/policy gates;
-see the policy table in OPT.md §8). A new "caller already huge / pressure" gate
-alongside the existing cost caps.
-
-**Impact.** Indirect; potentially useful on sqlite/inline-heavy files, near-zero
-on lvm as currently measured. Tune against the corpus — must not regress the
-files where inlining is currently a win (lz4/yyjson).
-
-**Linear?** Yes — a cheap size/pressure estimate per call site.
-
-**Risk.** Regressing runtime speed on hot inlined loops. This is a policy knob;
-measure both size *and* (ideally) a runtime proxy before committing thresholds.
-
-### W5 — Local (same-block) redundant-load + CSE elimination
-
-**Mechanism.** A forward scan within a block that reuses the last load of an
-address (and the last result of a pure expression) until a store / call / memory
-barrier invalidates it. Catches same-block reload and recompute redundancy.
-
-**Where.** `mir_combine` (`src/opt/pass_combine.c`) already tracks per-block
-producers and a clobber barrier (`inst_is_clobber_barrier`, `inst_writes_memory`)
-— extend it with an address→last-load map and a value→last-compute map. Keep this
-distinct from the existing adjacent spill compaction in `opt_combine_compact_block`
-(store/load, load/store, load/load, store/store pairs); W5 is the broader
-same-block map-based form.
-
-**Impact.** Modest at `-O1` (much redundancy in lvm is *cross-block* → needs GVN,
-out of scope), but cheap and broadly applicable.
-
-**Linear?** Yes — single forward pass, bounded per-block state.
-
-**Risk.** Alias correctness — only reuse a load when no intervening store may
-alias (reuse the existing `AliasRoot`/`inst_writes_memory` conservativism;
-calls/volatile/atomics are barriers).
-
-### W6 — Peephole tightening (modest, easy)
-
-- **cmp-immediate folding.** Register-compares like `movz w9,#k; cmp w8,w9` that
- fit the target immediate form should be `cmp w8,#k`. The current aa64 immediate
- policy already knows `NATIVE_IMM_CMP`; the missed cases are materialized too
- late for the existing operand-immediate path. A recent targeted audit found the
- opportunity mostly in sqlite (hundreds of small constants) rather than lvm after
- the latest copy/extension folds. Fold a `load_imm` feeding a `cmp` into the
- cmp's immediate operand when legal.
-- **Address-mode folding gaps.** `add xN,xM,#k; ldr [xN,#j]` →
- `ldr [xM,#k+j]` when `k+j` is a legal scaled offset. `mir_combine`'s
- addressing-mode synthesis already does most of this; close the constant-add +
- offset case.
-
-**Where.** `src/opt/pass_combine.c` (`try_addr_synth`, and a new const-into-cmp
-fold). **Linear?** Yes — per-instruction peephole.
-
-### W7 — Switch-chain immediate compares
-
-**Mechanism.** `IR_SWITCH` replay currently materializes every case value into a
-scratch register before `cmp_branch`:
-
-```c
-load_imm scratch, case_value
-cmp_branch EQ, selector, scratch, case_label
-```
-
-That loses the immediate path the normal `IR_CMP_BRANCH` emitter already has, and
-it differs from the CG fallback switch chain (`cg/control.c`), which passes the
-case value as an immediate operand. Replay should build an immediate operand per
-case and route it through the same `operand_imm_or_reg(..., NATIVE_IMM_CMP, ...)`
-logic as `IR_CMP_BRANCH`; only materialize to a scratch when the target says the
-compare immediate is illegal.
-
-**Where.** `IR_SWITCH` in `src/opt/pass_native_emit.c` (`emit_one`, around the
-case loop). This is an emitter-local change; no IR analysis required.
-
-**Impact.** Removes one constant materialization per case in non-jump-table switch
-chains, especially sparse switches and small chains below the jump-table
-threshold. On aa64 this is often a `movz` before every `cmp_branch`; x64 already
-has rich cmp-immediate forms and benefits from density; rv64 will still
-materialize when the immediate cannot fit the branch/compare lowering.
-
-**Linear?** Yes — still one pass over switch cases.
-
-**Risk.** Keep the fallback materialization path for targets/immediates that do
-not accept the case value directly. Preserve selector pinning: the selector should
-still be materialized once before the case chain, not reloaded per case.
-
-### W8 — Same-block stack dead-store elimination
-
-**Mechanism.** A forward MIR scan deletes a stack store when a later store in the
-same block fully overwrites the same stack slot before any possible read:
-
-```text
-store spill#17, r8
-... no read/barrier for spill#17 ...
-store spill#17, r9 ; first store is dead
-```
-
-This is deliberately narrower than O2 DSE. V1 should handle exact direct frame
-stores only:
-- `IR_STORE` with `opnds[0] == OPK_LOCAL(slot)`;
-- `!opt_mem_observable(&mem)`, `mem.alias.kind == ALIAS_LOCAL`, and
- `mem.alias.v.local_id == slot`;
-- exact same `{slot, size, addr_space}` and no bit-field rider;
-- start with `FS_SPILL` slots only. Optionally add `FS_LOCAL` later only when the
- slot is proven non-escaped (`!FSF_ADDR_TAKEN`) and every access remains direct.
-
-**Fast design.** Do not use a hash table per block. Frame slot ids are dense, so
-keep per-function side arrays indexed by `FrameSlot`:
-- `seen_gen[slot]`, `last_store_idx[slot]`, `last_size[slot]`,
- `last_addr_space[slot]`;
-- increment `gen` at each block start and on a full memory barrier instead of
- clearing `O(nslots)` state;
-- keep a touched-slot list only if compaction/reset bookkeeping needs it.
-
-On an accepted store:
-1. If `seen_gen[slot] == gen` and the stored key matches, mark the previous store
- `IR_NOP`.
-2. Record the current store as the new `last_store_idx[slot]`.
-3. If the same slot is accessed with a different size/address-space/bit-field
- shape, clear that slot's entry rather than reasoning about partial overlap.
-
-On an accepted direct load from the same slot, clear that slot's entry. On any
-unknown memory op, call, asm, intrinsic, atomic, volatile access, aggregate op, or
-non-direct local access, bump `gen` and forget all pending stores in O(1).
-
-**Where.** `src/opt/pass_combine.c`, either as a new helper called from
-`opt_combine_block`/`opt_combine_compact_block`, or as a separate same-block
-MIR cleanup immediately before the existing compact pass. Let the existing NOP
-compaction and `mir_dce` clean up the store's now-dead source producer.
-
-**Impact.** Smaller than W1/W2, but it directly cuts spill/local stack traffic and
-can expose dead rematerializations/copies to `mir_dce`. It compounds with W2:
-rematerialization removes reloads; stack DSE removes overwritten stores that
-remain.
-
-**Linear?** Yes — one scan over block instructions, O(1) per instruction, no
-per-block clear over all frame slots.
-
-**Risk.** The correctness boundary is aliasing and partial overlap. Keep v1 exact
-and spill-only; treat anything uncertain as a barrier. Do not delete volatile,
-atomic, bit-field, aggregate, or escaped-local stores. Add structural tests for:
-same-slot overwrite deleted, intervening load preserved, different size preserved,
-call/asm/unknown store barrier preserved.
-
-### W9 — One-pass branch cleanup subset
-
-**Mechanism.** O2 has `opt_jump_opt`, but its fixed-point loop is intentionally
-not an O1 fit. O1 can still import the single-pass, obviously-linear subset:
-1. Collapse `IR_CONDBR` / `IR_CMP_BRANCH` whose two successors are the same into
- a plain `IR_BR`.
-2. Forward successors through trivial pass-through blocks once:
- - unconditional `IR_BR` targets;
- - conditional taken targets (`succ[0]`);
- - conditional fallthrough targets (`succ[1]`) only when preserving fallthrough
- semantics is explicit: the old fallthrough block is physically next and is
- empty/pass-through, or the predecessor already had to emit an explicit jump
- to the false target.
-3. Optionally include `IR_SWITCH` successor forwarding through single-jump blocks,
- using the same label-address guard as existing jump cleanup.
-
-**Fast design.** Reuse `JumpCleanupCtx` and its memoized `forward_jump_target_ex`
-style, but run it once. No `for (iter < nblocks)` loop, no repeated CFG rebuilds.
-The shape should be:
-
-```text
-build_cfg
-one_pass_forward_branch_targets
-one_pass_collapse_same_target_branches
-build_cfg if changed
-existing cleanup_branch_targets / layout cleanup
-```
-
-The forwarding walk is bounded by `nblocks` per queried target today, but the
-memo table makes repeated queries amortized linear for the function. Keep the
-label-address guard (`has_label_addr_ref`) so computed-goto-visible blocks are
-not bypassed.
-
-**Where.** `src/opt/pass_jump.c`. Either extend `OPT_JUMP_CLEANUP_CFG` with this
-one-shot subset or add a named O1 cleanup helper called from the O1 prepare path
-after CFG construction. Do **not** call `opt_jump_opt` from O1.
-
-**Impact.** Mostly density/control-flow cleanup: fewer jump-only blocks survive
-to layout, fewer explicit jumps after branch forwarding, and better fallthrough
-shape for later MIR layout cleanup. It should also reduce verifier/debug noise by
-canonicalizing same-target branches early.
-
-**Linear?** Yes if it is one shot with memoized forwarding and at most one CFG
-rebuild after changes.
-
-**Risk.** Fallthrough correctness. Rewriting a conditional false edge can turn an
-implicit fallthrough into an explicit jump or skip a block; allow it only for
-empty/pass-through blocks with no label-address references. Accept missed
-multi-hop opportunities rather than adding a fixed-point loop.
-
-### W10 — Constant `cmp_branch` folding
-
-**Mechanism.** Fold branch conditions that are statically known without SSA:
-- `IR_CMP_BRANCH` with two immediate operands;
-- integer same-register comparisons (`x == x`, `x <= x`, `x < x`, etc.).
-
-Rewrite the terminator to `IR_BR` targeting the selected successor. This is the
-branch form of the existing local `IR_CMP x,x -> load_imm` simplification, but it
-must update CFG successors, so it belongs with branch cleanup rather than as a
-pure expression fold.
-
-**Where.** `src/opt/pass_jump.c` near the W9 one-pass cleanup. Use the shared
-integer compare evaluator/masking helpers where possible. V1 should not chase
-`IR_LOAD_IMM` definitions in PReg form; O1 PRegs are mutable, so direct operands
-and same-reg identities keep the pass local and safe.
-
-**Impact.** Low ceiling, but cheap. It removes dead conditional branches produced
-by macro/static-configuration code and exposes unreachable blocks to the existing
-CFG cleanup.
-
-**Linear?** Yes — a local terminator scan.
-
-**Risk.** Do not fold FP same-register comparisons: NaN makes `x == x` and
-ordered relations non-trivial. For immediate/immediate compares, use the operand
-type width and signedness semantics; if the width is unknown, skip.
-
----
-
-## 4. Out of scope (SSA-only — belongs in the O2 mid-end)
-
-These are where most of clang's *remaining* advantage on value-heavy code lives,
-and none is cheaply doable without SSA. They are already designed/parked in the
-O2 schedule (OPT.md §3, `src/opt/pass_o2.c`):
-
-- **Global GVN** — cross-block redundant load / value elimination (the bulk of
- the lvm redundancy that W5 can't reach).
-- **General DSE** — cross-block, alias-aware dead-store elimination. W8 is only
- exact same-block stack overwrite cleanup.
-- **LICM** — loop-invariant hoisting.
-- **Induction-variable strength reduction.**
-
-If a chunk of the gap turns out to *require* one of these, the answer is to wire
-up / ship the O2 path for that workload, not to bolt a non-linear analysis onto
-`-O1`.
-
----
-
-## 5. Already landed
-
-### The §3 worklist (2026-06-16) — all items shipped
-
-Implemented in parallel (worktree-isolated), merged one-at-a-time against the
-`scripts/o1_quality.sh` harness, each correctness-gated (`test-opt` structural
-guard + `test-toy` 1392/0 + ecosystem 28/0 + smoke-x64/rv64). **Cumulative:
-aggregate `__TEXT` 1.25× → 1.09× clang; lvm 4.66× → 2.86×; sqlite `sub x29`
-35,360 → 0; sqlite `-O1` compile ≈1.6s → ≈1.67s (linear).**
-
-- **W1a** `c6baf36f` — local frame-address `add`/`sub`-CSE (`pass_combine.c`):
- back-to-back rebuilds of the same frame address become a `mov`. Subsumed on
- aa64 spill slots by W1.1; still fires on the positive `add xN,x29,#k` form.
- Guard `redundant_frame_sub.sh`.
-- **W1.0** `d9902a7e` — hot-slot-low frame ordering (`pass_native_emit.c` +
- `ir.h`/`native_target.h` priority field + `pass_lower.c` aggregation): hottest
- spills get the smallest displacement. **aa64 −4.9% aggregate alone**; x64
- disp8 density; rv64 keeps hot slots in the imm12 window. Guard
- `hot_slot_order.sh`.
-- **W1.1** `9a99a0aa` — aa64 uniform x29-at-bottom known-frame layout
- (`aa64/native.c`): every spill slot is one-instruction positive `ldr/str
- [x29,#k]`; the `sub x17,x29,#k` fallback is gone for every known frame, alloca
- included; fp-chain/CFI/tail-call preserved. **The big win: lvm −36%, sqlite
- −11%, `sub x29` → 0.** Guard `aa64_x29_bottom.sh`.
-- **W3** `6f4cd59a` — linear move coalescing (`pass_coalesce.c`, populating
- `opt_coalesce_parent` before `opt_assign_ranges`; no O(n²) matrix). Taught
- `opt_verify_alloc` about coalesce roots and replaced the `mir_combine`
- `!opt_coalesce_parent` proxy with an explicit `opt_o1_coalescing` flag. −1.43%
- aggregate, spills not worse; +~7% sqlite `-O1` compile (the main cost). Guard
- `o1_coalesce.sh`.
-- **W2** `a6992971` — rematerialization instead of spilling (`pass_lower.c`):
- small `load_imm` / `addr_of[local]` recompute at the use; spill store dropped
- when every use remats. sqlite −0.58% (801 stores dropped). Guard `o1_remat.sh`.
-- **W7** `9f3f9a01` — switch-chain immediate compares (`pass_native_emit.c`):
- case values fold to `cmp #imm` instead of materializing a scratch; selector
- pinned; per-arch fallback. cjson −1.0%, lua subset −0.47%. Guard
- `o1_switch_imm.sh`.
-- **W6+W8** `fb2cb4c3` — cmp-imm + add-offset folds, and same-block spill
- dead-store elimination (`pass_combine.c`). sqlite −0.21%, lvm −0.61%. W8 is a
- correct/linear net that currently finds 0 sites (W2/DCE clear the pattern
- upstream — the "compounds with W2" case). Guards `o1_cmp_imm.sh`,
- `o1_stack_dse.sh`.
-- **W5** `235a72c7` — local same-block redundant-load + pure-expression CSE
- (`pass_combine.c`): reuse the last load of an address / last compute of a pure
- op until a may-alias store / call / barrier invalidates it (covers
- indirect/global loads; spill/frame loads ceded to W8/compaction). Small/flat as
- predicted (most redundancy is cross-block → GVN/O2). Found+fixed two MIR
- value-reuse traps: self-clobbering loads and reuse of native-emit scratch
- registers. Guard `o1_local_cse.sh`.
-- **W9+W10** `f97f2c08` — one-pass O1 branch cleanup + constant `cmp_branch`
- folding (`pass_jump.c`): same-target collapse, pass-through forwarding (one
- shot, ≤1 CFG rebuild), `x==x`/imm-imm fold; FP same-reg never folded; computed
- goto preserved. Low ceiling on real C as predicted. Guard `o1_branch_cleanup.sh`.
-- **W4** `d4eeb840` — inline-pressure cap (`pass_inline.c`): the whole-program
- inliner backs off into already-huge/high-pressure callers (cap 512), small +
- `always_inline` still fuse. Byte-identical on the win files (lz4/yyjson
- preserved); sqlite −324B / −75 spill insns; inliner phase faster. Guard
- `o1_inline_cap.sh`.
-- **W1.2** `a1ccdbcb` — rv64 far-slot assessment (no codegen change): measured
- **zero** far-slot traffic in lvm/sqlite hot loops (±2KB window + W1.0 ordering
- cover it), so no residual work warranted, per the doc. Guard `rv64_far_slot.sh`.
-- **W1.3** — x64 is W1.0 alone (disp8 density); validated, no separate change.
-- **Harness** `b1950955` — `scripts/o1_quality.sh`, the A/B `-O1` quality
- measurement (the §1 TODO).
-
-### Earlier
-
-- **commit 8826982d** — three local, target-agnostic copy/extension folds:
- `addr_of [base+0] → copy` and same-width same-class `convert → copy`
- (`pass_simplify.c`), and `ZEXT(zero-extending load) → copy`
- (`pass_combine.c`). **−2.4% `__TEXT`** across the corpus (lapi −9.1%, yyjson
- −4.3%, sqlite −1.9%, lvm −3.2%), compile time unchanged. Guarded by
- `test/opt/redundant_copy_ext.sh`. See OPT.md §4/§8.
-
----
-
-## 6. Suggested sequencing
-
-The structural set (the user-prioritized work) is W1a → W1 → W3 → W2. W4–W10
-remain the lighter / opportunistic sketches above, scheduled after or alongside
-that set when file ownership allows.
-
-1. **W1a** (local `sub`-CSE) — first PR: stands up the gate + a `test/opt/`
- structural guard with a small, safe aa64/rv64 peephole. Proves the harness.
- Keep it minimal — W1.1 subsumes most of it.
-2. **W1.0** (hot-slot-low ordering) — shared frequency-threaded layout; lands
- mostly in shared code, with backend offset validation. Immediate disp8/window
- wins and sets up W1.1.
-3. **W1.1** (aa64 uniform x29-at-bottom known-frame layout) — the big structural
- win (~36% lvm / ~12% sqlite `sub`s), and the highest blast radius
- (prologue/epilogue/CFI). Contained in the aa64 backend; alloca included, no
- regalloc change.
-4. **W3** (linear coalescing) — lowers pressure → fewer spills → fewer slots for
- W1 to place; removes surviving cross-block moves.
-5. **W2** (rematerialization) — attacks spill *traffic* directly; feed its costs
- into allocation later if spill count remains the bottleneck.
-6. **W1.2** (rv64) — assess far-slot traffic after W1.0; build the rv64 residual
- work only if measurement shows a gap. **W1.3** (x64) is W1.0 alone.
-
-Then, opportunistically: **W7** (switch immediates — tiny emitter PR), **W10**
-(constant branch folding), **W9** (one-pass branch cleanup), **W8** (same-block
-stack DSE), **W6** (peepholes — cheap polish, anytime, and a good small PR if
-W1.1 is too large to take first), **W4** (inline cap — needs threshold tuning
-against the corpus + ideally a runtime proxy), **W5** (local load/CSE — low
-ceiling at `-O1`).
-
-**Definition of done per item:** measurable `__TEXT`/instruction reduction on the
-affected corpus files, sqlite `-O1` compile time unchanged (linearity held), all
-correctness gates green, and a `test/opt/` structural guard added.
-
-### Parallel flow (agent fan-out)
-
-Parallelism is bounded by **file ownership** (kit's parallel-agent rule: disjoint
-files or worktree isolation), not by logical dependencies — the only hard
-ordering is W1.2 after W1.0.
-
-| item | exclusive owner | also touches (shared) |
-|------|-----------------|------------------------|
-| Harness (`o1_quality.sh`) | `scripts/` + `test/opt/` | — |
-| W1.1 aa64 | `src/arch/aa64/native.c` | — |
-| W1a sub-CSE | `src/opt/pass_combine.c` + `test/opt/` | — |
-| W3 coalescer | `src/opt/pass_coalesce.c` | `pass_lower.c` (1-line call site, ~:1887) |
-| W1.0 ordering | `src/opt/pass_native_emit.c` | `ir.h` + `native_target.h` (add field); `pass_lower.c` (`spill_slot_for` ~:578) |
-| W2 remat | `src/opt/pass_lower.c` (`rewrite_one_operand` ~:1383 + new fn) | — |
-| W1.2 rv64 | `src/arch/rv64/native.c` | — |
-| W7 switch immediates | `src/opt/pass_native_emit.c` | conflicts with W1.0 owner |
-| W8 stack DSE | `src/opt/pass_combine.c` | conflicts with W1a/W5/W6 owner |
-| W9/W10 branch cleanup | `src/opt/pass_jump.c` | — |
-
-**Wave 1 (up to 6 agents, worktree-isolated):** Harness, W1.1, W1a, W1.0, W2, W3.
-- Harness, W1.1, W1a own fully private files → trivially parallel. (W1a must take
- the `pass_combine.c` route, **not** an aa64 peephole, so W1.1 stays the sole
- owner of `native.c`.)
-- W1.0 / W2 / W3 share `pass_lower.c` but in **disjoint functions** → worktrees
- merge cleanly in any order; if you want zero friction, let W2 own `pass_lower.c`
- and fold in W1.0/W3's one-spot edits.
-
-**Wave 2 (gated):** W1.2 — only after W1.0 lands *and* a measurement shows rv64
-far-slot traffic worth removing (may be a no-op). W1.3 (x64) is W1.0 alone.
-
-**Opportunistic wave:** W7 and W9/W10 are file-disjoint from the structural work
-except W7's `pass_native_emit.c` overlap with W1.0; land them as small isolated
-PRs. W8 should share the `pass_combine.c` owner with W1a/W5/W6 to avoid churn in
-the same post-RA cleanup machinery.
-
-**Merge discipline:** parallelize *development*, but **merge + re-measure one at a
-time** against the harness — these change emitted bytes (gate = correctness, not
-byte-identity), so single-item integration keeps any regression attributable.
-Land the harness first.
diff --git a/doc/plan/OPTIMIZER.md b/doc/plan/OPTIMIZER.md
@@ -89,6 +89,29 @@ The path to flipping the switch:
rewrite the rest. `opt_pressure_relief` only sinks immediate/const candidates;
extend it to any single-cross-block-use move. Compute DSE memory liveness once
before the pass instead of an inline fixpoint per invocation.
+- **If-conversion (csel/csinc/ccmp selection).** kit emits **zero**
+ `csel`/`csinc`/`ccmp`; clang uses them heavily, so every `cond ? a : b`,
+ min/max clamp, and saturating cast becomes a compare-and-branch diamond with
+ the selected value threaded through the frame (feeding the spill gap). Doing it
+ well needs a select op in the IR plus a profitability/legality gate that proves
+ both arms side-effect-free and reasons about the merge of two definitions —
+ inherently the SSA domain. (A narrow peephole for the exact
+ `cmp;b.cc;mov-imm;b;mov-imm` diamond could fit `pass_jump.c`, but it is fragile
+ and low-value next to the linear peepholes; not a priority.)
+- **`tbz`/`tbnz` from `and #(1<<k); cbz/cbnz`.** A single-bit-AND feeding an
+ adjacent branch folds to one `tbz`/`tbnz`; kit emits zero. The narrow
+ single-bit, adjacent-branch case is arguably a linear `pass_native_emit`/
+ `pass_jump` peephole, but it sits inside the SSA-shaped if-conversion family, so
+ the narrow slice should be validated before it is committed.
+
+The cross-block redundancies that the linear `-O1` peepholes deliberately leave
+on the table all close here once these passes ship: cross-block redundant
+re-extension (repeated `sxtw` of an unchanged source across blocks/calls) closes
+under GVN; cross-block redundant loads close under the memory-aware GVN; and the
+dominant residual — values kept in registers across whole live ranges instead of
+spilled to the frame — closes under the SSA register allocator with live-range
+splitting (Section 2) plus global copy propagation. These are the bulk of clang's
+remaining advantage on value-heavy and interpreter-style code.
## 2. Live-range splitting and coalescing (the O2 allocator layer)
@@ -149,6 +172,46 @@ still trail MIR `O1`, and the gaps are specific and individually addressable.
codegen is at parity, so the loss is PLT/GOT indirection for `malloc`/`free`;
a `-fno-plt`-style direct-call or intra-image link resolution is a link-path
question more than a codegen one — see [LINKER.md](LINKER.md).
+- **Fold FP constants at the result width.** Constants like `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 — folding at the result width drops the widen. The fix is a
+ width-selection change in the constant-fold path (`cg_ir_lower.c`), not a
+ peephole, and it needs care that float→double of the folded constant is the
+ same value (canonical NaN payload); low priority.
+
+The linear `-O1` peepholes (OPT.md §"The shape of `-O1` quality work") close the
+*local* waste — redundant copies, extensions, and same-block reloads. The
+structural ceiling that remains on large, high-register-pressure functions is the
+**over-spilling itself**: kit's no-SSA linear-scan allocator (no splitting) falls
+to frame residency in big bodies where clang keeps loop-carried
+accumulators/pointers in callee-saved registers across the whole live range. That
+gap is not a `-O1` peephole — it is the SSA register allocator of Section 2 and
+the GVN/copy-propagation of Section 1, and it is where the bulk of the remaining
+quality gap on interpreter-style code lives.
+
+### Known non-deficiencies (do not chase)
+
+A disassembly audit of the ecosystem corpus turned up several `-O1` vs clang
+size deltas that are *not* kit gaps — recorded here so they are not mistaken for
+work:
+
+- **clang inlines/unrolls far more at `-O1`.** On lz4, yyjson, and lapi kit is
+ *smaller* than clang precisely because clang inlines aggressively; every
+ clang-heavy mnemonic on those files is an inline-site materialization artifact.
+ Tuning kit's inline-pressure cap *upward* would increase total `__TEXT`, not
+ decrease it.
+- **`ret` / function-count deltas are inlining shape.** kit emits exactly one
+ `ret` per function (optimal per-function, no duplicated epilogues); a higher
+ function count vs clang reflects clang's inlining, not kit waste. This belongs
+ to the parked O2 inliner / tail-call work, not to a size cleanup.
+- **NEON / vectorized columns.** clang's `umulh`/`smulh` magic-multiply divides,
+ `movi` vector zeroing, and `stp q` runs are vectorization / strength-reduction
+ kit does not do. (The scalar zero-init half is independently recoverable as a
+ wider `aa_set_bytes` expander; the magic-multiply half is the small-divisor
+ strength-reduction above.)
+- **Raw branch-mnemonic deltas.** A raw `cbz`/`cbnz` count difference favoring
+ clang on a given file does not mean kit over-emits branches — kit fuses many
+ compares directly into `cbz`/`b.cc` where clang materializes a bool first.
## 4. O0 generated-code quality
diff --git a/doc/plan/PERF.md b/doc/plan/PERF.md
@@ -1,657 +0,0 @@
-# Performance: the fastest C compiler
-
-**Goal.** kit should be *the fastest* C compiler at `-O0`, and emit code at least
-as dense as the bar. Generated-code *quality* (runtime speed) is irrelevant; only
-correctness, **compile+link throughput**, and **emitted code size** matter. The
-bar on both axes is **tcc** — the fastest mainstream C compiler. The structural
-bet is already in place (single-pass no-AST C frontend, single-pass emission with
-patch-ups, a format-neutral linker), so the work is measurement-driven: profile a
-real workload, find where the instructions/bytes go, remove them.
-
-Two rules hold everywhere:
-
-- **A superlinear axis is a bug.** It means an inner step is O(n²)-ish — a linear
- scan run per item, a table that rebuilds, a list re-walked. Catch these with the
- synthetic scaling benchmark (below).
-- **Gate every change.** A perf change must produce a **bit-for-bit identical**
- object (and `-E` / `-g` / diagnostics) vs a snapshot of the pre-change binary —
- *unless* it deliberately changes emitted bytes (a code-size lever), in which case
- the gate is **run-correctness + determinism** (compile twice + `cmp`, the test
- suites, sqlite e2e at `-O0`+`-O1` vs clang, per-arch clang-differential probes).
- ASan can't see a premature arena reuse or an uninit read; the output diff can.
-
-The real workload is the **sqlite amalgamation** — one ~9 MB / 263 K-line C file
-(`tmp/projects/sqlite-amalg/sqlite3.c`, v3.50.2): real, huge, declaration/macro/
-type-heavy, and doubling as a correctness test (compile + link the shell, run a
-query). All commands assume `SDK=$(xcrun --sdk macosx --show-sdk-path)` and a
-release kit at `build/release/kit` (`make bin RELEASE=1`).
-
----
-
-## 1. Current standings
-
-### Compile speed — the open frontier (~2.6× tcc)
-
-Apple-silicon arm64 / Darwin 25.3, refreshed 2026-06-15. **`instructions` is the
-metric to trust** — load-independent (`/usr/bin/time -l`, best-of-7);
-cycles/wall are low-load readings shown only for context. The kit row is **after
-the CG-type-interaction pass + the arena geometric/retain rework** (see §3). Each
-perf step left the sqlite object byte-identical, but the object has grown to
-1.75 MB (from ~1.70 MB) because aa64 codegen *correctness* fixes landed in the
-same window — the far-slot address-build fallback and the aggregate-return place
-fix — which add a fixed per-function reserved patch region (see *Code size* below).
-
-| compiler | instructions | cycles † | wall † | object |
-|---|--:|--:|--:|--:|
-| **tcc 0.9.28rc** | 0.663 B | 0.190 B | 0.06 s | 2.11 MB |
-| **kit** | 1.744 B | 0.522 B | 0.16 s | 1.75 MB |
-| Apple clang 21 | 8.824 B | 2.775 B | 0.88 s | 1.50 MB |
-
-† low-load; instructions is the figure to trust. kit instructions over the
-CG-type-interaction pass: **1.939 B → 1.864 B** (slot-query dedup + memory-align
-contract + builtin ABI-layout cache + resolve/predicate fast paths) **→ 1.760 B**
-(`abi_cg_type_info` inline builtin fast path + by-pointer layout cache) =
-**−179 M total (−9.2 %)**; then the arena geometric-block/retain-on-reset rework
-+ the delayed-arena block-size fix shaved another **−16 M → 1.744 B**, all
-byte-identical (each perf step left the sqlite object bit-for-bit unchanged — the
-object's growth to 1.75 MB is the separate codegen correctness fixes noted above).
-
-kit beats clang and is the fastest *general* backend, but **tcc is ~2.6×
-(instructions) ahead** — this is the whole game. **The gap is instructions, not
-cache:** kit's IPC (~3.3) is on par with tcc's (~3.5), so both are compute-bound
-on a wide core and there is no hidden cache-miss penalty to claw back. The payoff
-mechanism is **fewer retired instructions** (fewer copies, indirect calls,
-redundant recomputations) — denser structures help only because they cost fewer
-load/store/move *instructions*.
-
-> **Lean-token lexer/PP rewrite (landed, byte-identical, net win).** The
-> lexer/preprocessor boundary was rewritten to the `doc/plan/LEX-PP-API.md` "lean
-> token" design (compact `LocRef` byte-offset locations materialized on demand;
-> `TextRef` spellings; slot-based pulls). sqlite `-E`, `-c` object, `-g`, `-S`,
-> and diagnostics are **bit-for-bit identical** to the pre-rewrite snapshot
-> (`perf-gate` green incl. the splice/`#line`/diagnostic battery), so **code size
-> is unchanged**. The boundary-only landing was initially a −3.35 % `-c`
-> regression — lazy line/col was a *loss* because line numbers are needed in the
-> happy path anyway (sqlite expands `__LINE__` pervasively, and the parser stamps
-> a CG loc per statement) and the first cut built the line index with a scalar
-> `\n` rescan + a per-loc binary search. Fixing the *implementation* of laziness
-> (build the index with memchr; cursor the materializer — §4.1) turned it into a
-> campaign-local **−24 M `-c` win** (1.865 B → 1.841 B), since the scanner no
-> longer builds a `SrcLoc` per token. The remaining ~+19 M parse+CG residual
-> (per-statement loc lookups the old eager-on-token loc got for free) is the
-> optional Stage-2 target in §4.1.
-
-### Code size — below tcc (0.986×)
-
-The honest metric is **`.text` machine code** (the object file is format-skewed —
-Mach-O vs tcc's ELF — and not comparable; kit's *object* is actually smaller).
-
-| metric | kit | tcc | ratio |
-|---|--:|--:|--:|
-| **`.text`** | **1,352,356 B / 338,089 4-byte slots** | **1,370,940 B / 342,735 4-byte slots** | **0.986×** (−4,646 slots) |
-
-kit still clears the code-size bar overall, but the margin shrank from 0.951×:
-the recent aa64 far-slot/large-frame fix reserves a **branched-over 5-instruction
-patch region** in every qualifying function's prologue (a `b` over five `nop`s
-when the long address-build path is unused), and ~2,587 functions pay it. That
-alone adds **+12,897 `nop`s (~52 KB)** — i.e. the *entire* `.text` growth is this
-padding; net of nops, real instructions actually *shrank* slightly. The genuine
-excess remains the same instruction families as before (per-mnemonic, kit − tcc):
-
-| heavier in kit | Δ | what it is |
-|---|--:|---|
-| `stur`+`ldur`+`str` (spills) | **+47,095** | **#1: register pressure** — the single-pass NDT spills more than tcc keeps resident across statements / control-flow joins |
-| `mov` | **+13,666** | residual arg / value-stack copies the arg0-first placement can't reach (nested-call results already in x0, pressure spills) |
-| `nop` | **+12,897** | **new** — branched-over 5-`nop` patch region reserved per qualifying function by the far-slot/large-frame fallback; likely over-reserved (see §4.2) |
-| `sub`+`movk` | +3,474 | residual far-frame addressing (byte/half slots, `&local` in big frames) |
-
-…offset by where **kit already beats tcc** (structural wins — do not touch):
-`ldr` −24,214 (far slots fold into `[sp,#scaled]`), `add` −20,899 (folds offsets
-into displacements), `cset` −17,226 / `cbnz` −10,817 / `cmp` −9,421 (kit fuses
-compares straight into `cbz`/`b.cc`; tcc materializes a bool then tests — the cost
-side is `cbz` +6,294 and the `b.eq`/`b.ne`/`b.ge`/`b.le`/`b.lt` family +6,936,
-still a large net win), `movn` −6,720 / `movz` −4,644 / `stp` −3,722. The mnemonic
-histogram nets **−5,058 decoded instructions**; kit also has 412 4-byte padding
-slots, so the byte metric nets **−4,646 slots** — the +12,897 reserved-patch
-`nop`s are what eroded the old −16,897.
-
----
-
-## 2. Where the cost is (current profile)
-
-### Compile is frontend-bound — lex+pp ~2.0× tcc, post-PP ~3.6× tcc
-
-Phase split measured directly (instructions, best-of-7), not estimated from `-E`.
-`-E` is a **bad** lex+pp proxy: it re-serializes the token stream to text, work
-neither compiler does at `-c`, and the two serialize at wildly different cost
-(tcc's `-E` 1.17 B is ~0.79 B *serialization* via `get_tok_str` re-stringify +
-whitespace logic, so tcc `-E` even exceeds tcc `-c`; kit's `-E` serializes for
-~0.06 B since tokens carry `TEXT_SRC` spans → emit is a `memcpy`). To isolate
-lex+pp we **drain the token stream to EOF with no parse and no output** in both
-compilers (tcc: `-bench` hook, patched to use `-c` `parse_flags`; kit:
-`KIT_PP_DRAIN`, see §3):
-
-| phase | kit `-c` | tcc `-c` | kit / tcc |
-|---|--:|--:|--:|
-| **lex + pp** | **0.80 B** | **0.40 B** | **~2.0×** |
-| parse + sema + types + CG-drive | 0.71 B | ┐ 0.26 B | — |
-| native emit + object write | 0.24 B | ┘ (post-PP) | — |
-| **post-PP total** | **0.94 B** | **0.26 B** | **~3.6×** |
-| **total** | **1.74 B** | **0.66 B** | **~2.6×** |
-
-So **both halves are real frontiers** — lex+pp is *not* at parity (an earlier
-claim from the misleading `-E` proxy was wrong). The post-PP ratio is larger, but
-lex+pp is ~46 % of kit's `-c` and a clean 2.0× behind. Note kit's lex+pp drain
-does *less* than tcc's — kit defers number/string decode to the parser (tcc decodes
-in the lexer; bare tcc scan+expand is 0.38 B, +decode 0.40 B) — yet is still ~2.0×
-heavier, so the gap is pure per-token engine overhead, not extra work:
-
-- **tcc: one mutable global token** (`int tok` + `CValue tokc` + `tok_flags`) —
- the lexer and macro replayer write it in place; no per-token struct is built or
- copied. kit writes a 32-byte `Tok` (kind/flags/aux/`LocRef`/`TextRef`) per token
- and copies it lex→pp→parser through slots.
-- **tcc: no per-token location** (diagnostics read a `file->line_num` counter);
- kit stores a `LocRef` per token (cheap, but ≠ free) and the pp checks it.
-- **tcc: macro bodies are `int[]` token streams** replayed via a `macro_ptr`
- cursor with **no hidesets** (a cheap nested-macro guard); kit replays `Tok[]`
- with a Prosser hideset side-channel (hash-consed ids, union/dedup per expansion).
-- **tcc: `TokenSym` caches direct `Sym*` pointers** (define/ident/struct/label) on
- the interned token, so define-lookup is a pointer-follow; kit re-probes
- (`pool_intern_slice` 3.9 %, the Sym-indexed macro table 1 load).
-- **tcc: one `next()`** does lex + pp + feeds the parser — no inter-stage boundary;
- kit layers lex → pp (source stack, `src_next_raw_into`→`pp_pull_into`) →
- `pp_next_parse` → parser, and the pp re-checks directive/`defined`/dynamic-macro
- state per token. This is the [[frontend-instruction-halving-pathb]] lever: fewer
- per-token ops, not less work.
-
-The post-PP 3.6× is the bigger slice: tcc drives a thin `SValue[]` straight into a
-one-pass emitter, while kit routes through the `CgTarget`→`NativeTarget`→`MCEmitter`
-seam and a richer type/ABI layer (§4). Closing either is a campaign, not one hot
-function.
-
-In the lean-token rewrite + loc-materialization fix A/B (golden→candidate,
-best-of-7): **lex+pp −44 M** (the scanner no longer builds a `SrcLoc` per token —
-`loc` is a byte offset), **parse+sema+CG +19 M** (the residual: a per-statement
-cursor lookup at `pcg_set_loc` the old eager-on-token loc got for free), emit
-flat → **−24 M total (−1.3 %)**.
-
-**Linux callgrind** (inclusive, instruction-grounded — the tool that sees what
-wall-clock `sample` hides; total **1.547 B `Ir`**, glibc/ELF; self-`Ir` summed
-across callgrind's `'2` symbol splits). This is **after the CG-type-interaction
-pass** (slot-query dedup + memory-align contract + builtin ABI-layout cache +
-resolve/predicate fast paths + the `api_type_layout_get` follow-up) **and the
-arena geometric/retain rework**: the type cluster went **1.741 B → 1.608 B →
-1.566 B (−175 M, −10.1 %)**, then the arena change shaved heap/`memset` churn to
-**1.547 B (−19 M)**, all byte-identical for the perf steps (perf-identity gate
-green on every non-`-g` category; the Linux/ELF total is not comparable to the
-macOS hardware count — different libc/format — but the *distribution* is the point).
-
-| self % | function(s) | subsystem |
-|--:|---|---|
-| **12.3** | `lex_next` | scanner |
-| 6.4 | `malloc` | hosted heap |
-| 4.5 / 4.4 | `src_next_raw_into` / `pool_intern_slice` | pp + interning |
-| 3.9 | `pp_pull_into` | preprocessor |
-| 2.8 | `finish_ident` | scanner |
-| 2.0 / 1.6 / 0.3 | `__GI_memset` / `__GI_memchr` / `__GI_memcpy` | libc memory |
-| 1.9 / 1.8 | `abi_cg_type_info` / `api_type_pred_bits` | types/ABI |
-| 1.6 / 1.3 / 1.2 | `pp_materialize_loc` / `api_const_from_sv` / `api_sv_adjust_refs` | lazy-loc / value-stack |
-| 1.2 | `arena_alloc` | arena bump path |
-| 1.1 / 1.0 / 1.0 / 0.4 | `cg_type_get` / `api_type_layout_ref` / `api_type_class` / `resolve_type` | types |
-| 1.1 | `aa_emit_mem` | codegen |
-| 0.78 (Σ) | `kit_cg_slot_lang_type` 0.53 / `kit_cg_slot_lang_flags` 0.16 / `kit_cg_slot_cg_type` 0.09 | CG slot queries (zero-copy accessors) |
-
-**What moved (vs the pre-pass profile above each arrow):**
-`kit_cg_slot_info` **3.2 % → gone** (the by-value struct copy is retired; the C
-frontend now reads single facts through the narrow accessors above, Σ 0.75 %),
-`cg_type_get` 3.0 → 1.1, `resolve_type` 1.8 → 0.4, `__GI_memset` 3.0 → 1.9. The
-ABI-layout lookup was first **consolidated** into `api_type_layout_get` (1.3 →
-3.9 in the intermediate profile) — one memoized indexed load per type for
-builtins *and* user types instead of the `abi_cg_type_info_compute` →
-`cg_type_get` → per-call `resolve_type` spread — then the **follow-up** retired
-that 5-field out-param marshalling: `api_type_layout_ref` returns a *borrowed
-pointer* to the cached `ABITypeInfo`, and `abi_cg_type_info` resolves a builtin
-with **one inline indexed load** off `Compiler.cg_builtin_layout` (no cross-TU
-call). Combined `abi_cg_type_info` + layout cache: **5.5 % → 2.9 % (−43 M, the
-whole program-total drop this step)**; `api_type_layout_get` 3.9 → `_ref` 1.0,
-`abi_cg_type_info` 1.6 → 1.9 (it absorbed the inline load). Net of the targeted
-leaves: **15.0 % → ~9 %**.
-
-**Subsystem rollup:** scanner ~15 % (`lex_next`+`finish_ident`), pp+interning
-~13 % (`src_next_raw_into`+`pp_pull_into`+`pool_intern_slice`), CG/types/ABI
-metadata ~9 % (down from ~15 %), libc allocation/memory helpers ~10 %, lazy-loc
-(`pp_materialize_loc`) ~1.6 %, arena bump path (`arena_alloc`) ~1.2 %, direct
-codegen emit ~1 %. The type layer is no longer a top-tier frontier — the remaining
-leaves (`abi_cg_type_info` register-struct returns, `api_type_pred_bits`,
-`cg_type_get`) are already at/under ~2 % and were explicitly *not* micro-optimized.
-The next frontiers are the scanner (`lex_next` ~15 %) and the hosted heap (`malloc`
-~6 %, call frequency already addressed by the arena rework — see below). Trust
-callgrind for *where*; trust macOS instructions for *how much*.
-
-**Allocator (hosted heap).** `malloc` is ~6 % of `Ir` but only **~4,278 calls**
-for the whole sqlite TU (`KIT_METRICS=1` heap counters): the cost is in large,
-churned blocks, **not** call frequency. Most allocation never reaches the heap —
-arenas bump-allocate, and the variable-count structures (interner table/entries,
-vectors, segvecs) already grow geometrically (**351 reallocs total**, all grows).
-The one gap was **fixed-size 64 KiB arena blocks**. The arena now (a) grows
-blocks **geometrically** (64 KiB doubling to a 1 MiB cap, so a large arena needs
-O(log n) heap calls not O(n)) and (b) `arena_reset` **retains the high-water
-blocks** — it rewinds the bump cursor and frees nothing; only `arena_fini`
-returns memory, so reset/refill cycles (per-statement fold, per-function MC,
-per-expansion pp scratch) reuse their blocks with zero heap traffic. Effect on
-the sqlite compile (`KIT_METRICS`): large (≥32 KiB) block allocs **440 → 124
-(−72 %)**, total allocs 4,278 → 3,967, frees 4,571 → 4,260; **macOS instructions
-−0.22 %, byte-identical** (isolated golden-vs-candidate). Small on the
-instruction metric (Apple malloc is cheap) but it corrects the reset-vs-free
-semantics and cuts heap pressure/fragmentation. Heap counters reuse the
-`KitProfiler` machinery (embedder counter range), opt-in via `KIT_METRICS=1`.
-
-**SQLite `-O1` optimizer profile** (Linux callgrind, self `Ir`,
-optimizer-finalize only). Full sqlite `-O1` callgrind collection on the default
-2 GiB Podman VM overflows Valgrind's brk segment and dies with rc 137, so the
-useful profile toggles collection at `opt_on_finalize` and forces glibc malloc
-onto `mmap` (`MALLOC_MMAP_THRESHOLD_=1 MALLOC_ARENA_MAX=1`). That captures the
-whole-program O1 sweep (reachability/internalization, CGIR lowering, inlining,
-per-function O1 pipeline, native emit) but **excludes frontend recording**, so it
-is a distribution profile, not a total comparable to the `-O0` full-compile
-profile above. Command and raw output are in
-`build/linux-prof/cg.sqlite_o1.opt_finalize.mmap.*`.
-
-Profiled command:
-`kit cc -O1 -c sqlite3.c -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION -lc`.
-Total collected inside `opt_on_finalize`: **15.496 B Ir**.
-
-| self % | function(s) | subsystem |
-|--:|---|---|
-| **24.5** | `opt_regalloc_place` | physical register placement |
-| **18.7** | `opt_func_from_cg_ir` | CGIR -> optimizer Func lowering |
-| **7.8** | `alloc_assign_group_hard` | hard-register assignment grouping |
-| **7.4** | `ir_note_emit` | IR/lowering record construction |
-| 4.5 | `opt_build_loop_tree` | loop analysis |
-| 3.2 / 1.7 / 1.6 / 1.5 | `opt_bitset_has` / `opt_bitset_union` / `opt_bitset_copy` / `opt_bitset_union_and_not` | liveness/range bitsets |
-| 2.4 | `opt_live_ranges_build` | live-range construction |
-| 2.1 / 0.8 | `msort_with_tmp` / `u32_cmp` | sorting in optimizer hot paths |
-| 1.6 / 0.3 | `__GI_memset` / `__GI_memcpy` | libc memory |
-| 1.5 / 0.2 | `inline_call_site` / `opt_inline` | whole-program inliner |
-| 1.2 / 1.0 / 0.3 / 0.2 | `metrics_count` / `metrics_scope_from_name` / `metrics_scope_from_name'2` / `metrics_count'2` | profiling string-name lookup |
-| 1.1 | `arena_alloc` | arena bump path |
-| 1.0 | `opt_addr_xform_pregs` | address transform |
-| 0.1 | `opt_emit_native` | native emit self-time |
-
-Interpretation: sqlite `-O1` compile time is dominated by the optimizer's
-middle/back half, not native emission. Register allocation alone is at least
-**32 %** (`opt_regalloc_place` + `alloc_assign_group_hard`, excluding the shared
-bitset/range helpers), while CGIR lowering plus record construction is another
-**26 %**. Built-in metrics on the same TU report **2,522 functions**,
-**121,075 blocks**, **98,516 PRegs**, **1,049,079 ranges**, and **29.1 M live
-bitset words touched**, which explains why the liveness/range bitset helpers are
-visible. A cheap cleanup from this profile was the disabled metrics fast path:
-`metrics_scope_from_name` and `metrics_count` burned ~2.7 % of O1 finalization
-with `KIT_METRICS` unset because each hot scope/counter call string-decoded the
-name before discovering the profiler sink was null. `metrics_*` now checks the
-sink before name decoding; enum-specific wrappers would still make metrics-on
-collection cheaper.
-
-Follow-up cleanup on the same sqlite `-O1` workload made the hot shapes cheaper:
-CGIR lowering precomputes local address-use, param lookup, emit order, and
-fallthrough successors; regalloc builds coalesce-group member lists before
-candidate scoring and uses them in group scans; loop-tree construction
-precomputes backedges and uses generation-marked loop bodies. Bounded macOS
-release timing for the single amalgamation compile is now **0.93 s best / 0.96 s
-mean** (7 runs, 5 s per-run timeout); the ecosystem-style two-TU sqlite O1 build
-is **1.20 s best / 1.22 s mean** (5 runs, 8 s timeout), down from the previous
-**2.78 s** best row. `KIT_METRICS=1` ticks on the same single-amalgamation
-command: `opt.o1.total` **27.09 M → 16.78 M** (−38 %), `opt.regalloc`
-**13.81 M → 7.83 M** (−43 %), `opt.o1.cg_ir_lower` **4.94 M → 1.16 M** (−77 %),
-and `opt.build_loop_tree` **1.41 M → 0.20 M** (−86 %). Linux callgrind rerun was
-blocked in this pass because `podman machine` started then immediately stopped
-at 3 GiB, 2.5 GiB, and the restored 2 GiB setting; the VM was left configured at
-2 GiB/stopped.
-
-### Code size is still locally spill-bound
-
-Even with aggregate `.text` now smaller than tcc, the remaining per-mnemonic
-excess is dominated by **spills** (register pressure in the single-pass cache,
-+47 K) then residual **`mov`s** (+14 K). Both concentrate in the giant functions
-(`_sqlite3VdbeExec` etc.) where per-function pressure is highest.
-
----
-
-## 3. Reproducing the metrics
-
-**Build tcc** (the bar) from the mob mirror as an optimized binary:
-
-```sh
-git clone --depth 1 https://github.com/tinycc/tinycc tmp/tinycc && cd tmp/tinycc
-export SDKROOT=$(xcrun --sdk macosx --show-sdk-path) # else "stdio.h not found"
-sed -i 's|^CFLAGS=.*|CFLAGS=-Wall -O3 -DNDEBUG -Wdeclaration-after-statement|' config.mak
-./configure ; make clean ; make CC=cc # CC=cc = Apple clang (xcrun-aware)
-# binary at tmp/tinycc/tcc ; compile with: tcc -c sqlite3.c -o x.o
-```
-
-**Instructions / cycles / wall** — `/usr/bin/time -l` is the macOS `perf stat`;
-best-of-N filters scheduling noise:
-
-```sh
-m(){ local bi=9e18 bc=9e18 br=9e18; for i in $(seq 1 7); do
- /usr/bin/time -l "$@" >/dev/null 2>/tmp/m.txt
- local I=$(awk '/instructions retired/{print $1}' /tmp/m.txt)
- local C=$(awk '/cycles elapsed/{print $1}' /tmp/m.txt)
- local R=$(awk '/real/{print $1}' /tmp/m.txt)
- awk "BEGIN{exit !($I<$bi)}"&&bi=$I; awk "BEGIN{exit !($C<$bc)}"&&bc=$C
- awk "BEGIN{exit !($R<$br)}"&&br=$R; done; echo "instr=$bi cycles=$bc real=${br}s"; }
-cd tmp/projects/sqlite-amalg
-m build/release/kit cc -c sqlite3.c -o /tmp/k.o --sysroot "$SDK"
-m tmp/tinycc/tcc -c sqlite3.c -o /tmp/t.o
-```
-
-Caveat: Apple clang spawns a `cc1` that `/usr/bin/time` does **not** count —
-measure it with `cc -fintegrated-cc1 -c …`. kit and tcc are single-process.
-
-**Phase decomposition.** Do **not** use `-E` as the lex+pp number — it adds
-token→text serialization neither compiler does at `-c`, and tcc/kit serialize at
-very different cost (§2). Instead **drain the token stream to EOF** (lex+pp only,
-no parse, no output) in each compiler:
-
-```sh
-# kit: KIT_PP_DRAIN runs pp_next_parse to EOF then returns (lang/c/c.c)
-m env KIT_PP_DRAIN=1 build/release/kit cc -c sqlite3.c --sysroot "$SDK" -o /dev/null # lex+pp
-m build/release/kit cc -fsyntax-only sqlite3.c --sysroot "$SDK" # lex+pp +parse/sema/types/CG
-m build/release/kit cc -c -o /tmp/k.o sqlite3.c --sysroot "$SDK" # +emit+objwrite
-# tcc: the -bench hook drains next() to EOF; patched (tccpp.c tcc_preprocess)
-# to use -c parse_flags. TCC_DRAIN_FLAGS=c also decodes literals in the lexer.
-m env TCC_DRAIN_FLAGS=c tmp/tinycc/tcc -E -bench sqlite3.c -o /dev/null # tcc lex+pp
-m tmp/tinycc/tcc -c sqlite3.c -o /tmp/t.o # tcc full -c
-```
-
-**macOS hotspot sample** (self-time leaves only — see the callgrind caveat). One
-`-c` is too fast for `sample`; merge ~80 runs:
-
-```sh
-make RELEASE=1 PROFILE=1 CC=clang BUILD_DIR=build/bench bin # -g + frame ptrs, no strip
-cd tmp/projects/sqlite-amalg ; KIT=build/bench/kit ; rm -f /tmp/p_*.txt
-for i in $(seq 1 80); do
- $KIT cc -c sqlite3.c -o /tmp/kk.o --sysroot "$SDK" 2>/dev/null & pid=$!
- sample $pid 5 1 -file /tmp/p_$i.txt -mayDie >/dev/null 2>&1 ; wait $pid; done
-for f in /tmp/p_*.txt; do awk '/Sort by top of stack/{f=1;next}/Binary Images/{f=0}f' "$f"; done \
- | sed -E 's/\(in [^)]*\)//' | grep -oE '[A-Za-z_][A-Za-z0-9_]*[[:space:]]+[0-9]+' \
- | awk '{c[$1]+=$2;t+=$2} END{for(k in c) printf "%.1f%% %s\n",100*c[k]/t,k}' | sort -rn | head -25
-```
-
-**Linux callgrind** — the **inclusive, instruction-grounded** profiler (no
-valgrind on macOS). `scripts/perf_callgrind.sh` codifies the recipe; one-time
-`scripts/perf_callgrind.sh image` bakes the container, then:
-
-```sh
-scripts/perf_callgrind.sh run <tag> # builds kit in-container, runs callgrind, prints total Ir
- # → build/linux-prof/cg.<tag>.annot.txt (self Ir + callers)
-```
-
-For the sqlite `-O1` optimizer-finalize profile, use `podman machine` explicitly
-to give the VM enough memory for Valgrind, and force malloc away from brk:
-
-```sh
-podman machine stop || true
-podman machine set --memory 3072
-podman machine start
-podman run --rm --platform linux/arm64 -v "$PWD":/work:Z kit-prof sh -c '
- set -eu
- cd /work/tmp/projects/sqlite-amalg
- env MALLOC_MMAP_THRESHOLD_=1 MALLOC_ARENA_MAX=1 \
- valgrind --tool=callgrind --cache-sim=no --branch-sim=no --dump-instr=no \
- --collect-jumps=no --collect-atstart=no --toggle-collect=opt_on_finalize \
- --callgrind-out-file=/work/build/linux-prof/cg.sqlite_o1.opt_finalize.mmap.out \
- /work/build/linux-prof/kit cc -O1 -c sqlite3.c \
- -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION -lc -o /tmp/sqlite_o1.o \
- 2>/work/build/linux-prof/cg.sqlite_o1.opt_finalize.mmap.vg.log
- callgrind_annotate --threshold=99.5 \
- /work/build/linux-prof/cg.sqlite_o1.opt_finalize.mmap.out \
- >/work/build/linux-prof/cg.sqlite_o1.opt_finalize.mmap.annot.txt
-'
-# restore the default local VM shape after the run
-podman machine stop
-podman machine set --memory 2048
-podman machine start
-```
-
-Gotchas baked into the script (do not relearn): **(a)** pass `-lc` so kit finds
-the glibc sysroot even for `-c`; use **bookworm** (glibc 2.36), not ubuntu 24.04
-(glibc 2.39's `bits/math-vector.h` uses a vector-typedef attribute the C frontend
-rejects). **(b)** `strip --strip-debug` the kit binary in place before profiling
-(valgrind 3.19 chokes on clang DWARF5; callgrind needs only `.symtab`) — strip *in
-place*, kit resolves `support/rt` relative to its own path. **(c)** trust
-self/exclusive `Ir`, not inclusive % (recursion in the recursive-descent parser
-makes `--inclusive=yes` double-count to absurd numbers). The Linux/ELF total is
-not comparable to the macOS/Mach-O hardware figure (different libc, sysroot,
-format; counts glibc + loader + the `-lc` probe) — the *distribution* is the point.
-
-**Heap allocation counts** — `KIT_METRICS=1` prints the hosted-heap counters
-(allocs / large allocs ≥32 KiB / reallocs / frees) at exit; the counts are
-host-independent (kit issues the same `h->alloc` calls everywhere):
-
-```sh
-KIT_METRICS=1 build/release/kit cc -c sqlite3.c --sysroot "$SDK" -o /tmp/k.o
-# -> kit heap metrics: heap.allocs=… heap.large_allocs=… heap.reallocs=… heap.frees=…
-```
-
-**Code size** (`.text` machine code, the honest metric):
-
-```sh
-build/release/kit cc -c sqlite3.c --sysroot "$SDK" -o /tmp/k.o ; build/release/kit size /tmp/k.o
-tmp/tinycc/tcc -c sqlite3.c -o /tmp/t.o ; build/release/kit size /tmp/t.o
-# per-mnemonic excess:
-hist(){ build/release/kit objdump -d "$1" | grep -E '^[[:space:]]+[0-9a-f]+:' \
- | sed -E 's/.*\t([a-z][a-z0-9._]*).*/\1/' | grep -E '^[a-z]' | sort | uniq -c | sort -rn; }
-diff <(hist /tmp/k.o) <(hist /tmp/t.o)
-```
-
-**End-to-end correctness** (sqlite as a correctness test, both opt levels):
-
-```sh
-build/release/kit cc sqlite3.c shell.c -o /tmp/sq --sysroot "$SDK" -lc
-/tmp/sq :memory: "create table t(a,b); insert into t values(2,40),(10,32);
- select sum(a)+sum(b), count(*) from t;" # -> 84|2
-```
-
-**Synthetic scaling guard** (`make bench-cc`): sweeps each input dimension
-(`fn-count`, `body-size`, `global-decl`, `type-decl`, `locals-per-fn`, `pp-macro`,
-`pp-include`, `ref-density`, `obj-count`, `symbol-count`) over a geometric series,
-fits a scaling exponent (`p ≈ 1.0` goal, `p ≥ 1.4` flagged `SUPERLINEAR` — treat
-as a bug), samples hotspots, and records a clang reference. Output in
-`build/bench/cc/` (`scaling.md`, `hotspots.md`). Use it to catch a re-introduced
-O(n²); use the sqlite profile to decide *where* to optimize (the synthetic axes
-over-weight codegen vs real code). Sources of truth:
-`scripts/cc_bench_gen.py` (axis catalog), `scripts/cc_bench.sh` (harness),
-`scripts/cc_bench_report.py` (fit/verdicts).
-
----
-
-## 4. Ideas for improvement (forward-looking, ranked)
-
-Ordered by instruction/byte-payoff × structural leverage. The architecture's asset
-— the `CgTarget`→`NativeTarget` polymorphism (one frontend; aa64/x64/rv64/rv32 +
-wasm + c_target + check backends, plus the opt-IR recorder at `-O1`) — **stays**.
-Everything below lives in one of: the front half, the type subsystem, *inside* the
-concrete `NativeDirectTarget`/`MCEmitter` (the -O0 register machinery + byte sink,
-not the swappable vtable), or the code-size track (which *bytes* the NDT emits).
-
-### 4.1 Compile speed (frontend-bound)
-
-**Lean-token loc-materialization fix — Stage 1 DONE (`-c` −24 M net win).** The
-boundary rewrite's initial +62 M `-c` regression was two lazy-loc implementation
-costs, both now fixed byte-identically (frontend-only, no CG-API change):
-- ✅ **memchr the line-index build.** `srcinfo_build_lines` built the line-start
- index with a scalar `\n` byte loop; replaced with a memchr sweep (5.3 % → 0.6 %
- self-`Ir`). The build is genuinely needed in the happy path — `__LINE__` (which
- sqlite expands pervasively) and diagnostics need line numbers even without
- `-g` — so the lever was to make it cheap, not to skip it.
-- ✅ **cursor `pp_materialize_loc`.** Per-loc lookup was a fresh binary search;
- now a `SrcInfo.line_cursor` caches the last line index, so the near-monotonic
- access pattern (parser advances forward) is O(1), binary search only on a
- backward jump (2.6 % → 0.9 %).
-
-Result: lex+pp −44 M (the scanner no longer builds a `SrcLoc` per token),
-parse+CG +19 M residual, **net −24 M** (§1, §2), `perf-gate` byte-identical.
-
-**Stage 2 (optional, ~+19 M parse+CG residual).** The residual is the
-per-statement loc lookup at `pcg_set_loc` (CG source-loc) that the old
-eager-on-token loc got for free. `g->cur_loc` is consumed only by DWARF (gated on
-`g->debug`), descriptor locs, and cold CG `compiler_panic`s — so with `-g` off and
-no error it is discarded. Carrying it as a `LocRef` (a `kit_cg_set_locref` +
-a "resolve `LocRef`→`SrcLoc`" callback the frontend registers, so cold CG
-diagnostics still materialize exactly) would skip those per-statement lookups
-entirely. Smaller payoff than Stage 1, needs a contained CG-API addition + the
-parser loc-plumbing; gate: byte-identical (`-g` DWARF + diag battery unchanged).
-
-**Status (prior campaign).** Most of §4.1 has now landed, byte-identical, for a
-combined **−43.7 M `-c` instructions (−2.29 %)** on sqlite vs the pre-campaign
-snapshot (sqlite `.o` + `-E` bit-for-bit identical; 60/60 gate; full parse/pp/cg
-/toy/smoke-x64/rv64/debug/dwarf suites green). Item-by-item below: #1 ✅ (landed
-earlier, `5ff33ea3`); #2 ◐ (a+c landed `d07e43a3`+`c8ee9133`; b deferred —
-near-zero payoff, see note); #3 ✅ (object-macro replay `7ada3f62`, function-macro
-replay `d59dff08`, lexer-level newline elision `811351c2`); #4 ◐ (Cut A
-`f368c7f8` + Cut B `94e642e3` landed; the dead paste-file-id register, Cut C, left
-as a small follow-on); #5 ○ open.
-
-1. ✅ **[DONE — `5ff33ea3`]** **Symbol-binding cache on the interned `Sym`** — the highest-value front-half
- brick. `scope_lookup` is an N-scope chain walk per identifier (`lang/c/parse/
- parse.c`, ~12 call sites); tcc caches the binding on the interned token. A
- `Sym`-keyed binding stack with push/restore on scope enter/exit makes resolution
- a pointer load. Medium-high risk (save/restore + shadow/redef/typedef-vs-ident
- discipline); byte-identical gate, full parse corpus.
-2. ◐ **[a+c DONE; b deferred]** **Type subsystem (#2 self-`Ir` cluster, ~14 %).**
- *Done:* single-decode the per-op predicate gauntlet — hoist one
- `api_type_pred_bits` per id and test `API_PRED_*` masks locally instead of
- re-calling `cg_type_is_*` (each a fresh `id→entry`+unalias decode) on the
- load/store/convert hot paths (`d07e43a3`); fuse the redundant `resolve_type`
- into the following `api_unalias_type`, and route the hot `cg_adapter` helpers
- through the slot's cached `cg_id` instead of the uncached `pcg_tid` bridge
- (`c8ee9133`). *Deferred:* collapsing the duplicate `c_abi_record_layout`
- (frontend) vs `abi_cg_record_layout` (backend) memos — both are already
- memoized at O(#distinct records), so the collapse saves only the duplicate
- build (< 0.01 % of total) against real boundary/byte-identity risk; not worth
- it now. Optional remaining: cache `pred_bits` on the `ApiSValue` slot itself
- (free `ApiBitField.pad` bytes) — measure first, retype-invalidation surface.
-3. ✅ **[DONE]** **Token relay (lex+pp).** Macro bodies with no `##` now replay by
- pointer (object-like `7ada3f62`; function-like skips the `subst_phase2` copy,
- `d59dff08`). Non-directive newlines are no longer *materialized* by the lexer on
- the cc path (`811351c2`, −32 M `-c`): an `emit_newlines` lexer mode suppresses
- them while still emitting the one newline that terminates a directive line
- (Option B2 — no cross-frame coupling); `-E`/cpp keep them. Line numbers safe by
- construction (counter advances on byte-consume, `loc` rides every token);
- verified bit-identical on `-E`, `-c`, and `-c -g` DWARF.
-4. ◐ **[Cut A+B DONE; Cut C open]** **`lex_open_mem` per-open work** (~6,000 opens:
- 3,140 files + 2,796 macro-paste buffers). *Done:* paste buffers skip the
- `lex_fold_splices` memchr scan via a guarded splice-free fast path (`f368c7f8`,
- they are interned already-folded spellings — provably splice-free); the
- per-file-open full-struct `memset` is right-sized to just the `punct_sym`
- sentinel array (`94e642e3`). *Open (Cut C):* the dead paste file-id
- registration could become a bare `nfiles++` after auditing no diagnostic ever
- queries a paste file-id.
-5. ◑ **[arena churn LANDED; memset OPEN]** **`memset` / arena churn (~2–3 %).**
- Arena churn done: blocks now grow geometrically and `arena_reset` retains the
- high-water capacity instead of freeing all-but-head (large block allocs
- **−72 %**, byte-identical) — see §2 *Allocator*. *Still open:* right-size
- per-expression / per-emit struct zeroing (designated-init the per-op clears).
- (`memset` here is explicit zero-init, **not** `-ftrivial-auto-var-init` —
- proven by rebuilding with the flag off; attack call sites, keep the hardening
- flag.)
-
-Near-dead-ends for *instructions* (revisit only under a cache-stall study, not
-expected to pay): the `pool_intern_slice` probe/insert side (the self-sufficient
-`{hash,sym}` slot measured ~0 `Ir`); `lex_next` itself (raw-cursor scanner is
-optimal); `aa_emit_mem` typed-store micro-levers.
-
-### 4.2 Code size (run-correctness-gated; multiplicative — each byte cut shrinks emit + objwrite + assemble together)
-
-0. **Trim the over-reserved far-slot/large-frame patch region (new, +12,897 `nop`
- / ~52 KB — the whole 0.951×→0.986× erosion).** The aa64 far-slot address-build
- / large-frame fallback now reserves a fixed **branched-over 5-instruction patch
- region** in ~2,587 functions' prologues (a `b` over five `nop`s when the long
- path is unused), so every qualifying function pays 24 B of padding even when
- the short path suffices. This is correctness-first and *probably* over-reserved:
- reserve only when the frame/slot actually needs the long form, or shrink the
- reservation to the real worst case (and patch the branch away when unused).
- Investigate before touching spills — it's likely the cheapest byte win on the
- board. Gate: run-correctness + determinism (this is the codegen path the recent
- correctness fixes added).
-1. **Spill reduction — the #1 remaining local code-size excess (+47 K).** The
- single-pass NDT spills more than tcc keeps resident across statements and
- control-flow joins. The no-new-analysis wins (dead-operand drop,
- materialize-before-flush) are done; deeper residency = keeping a *live* value
- in a register across a join, which is register allocation and out of simple
- single-pass scope. Pursue as a separate `-O1`-style pass over the NDT, or a
- cheaper "pin the hot local across a straight-line run" heuristic.
-2. **Positive-offset frame addressing** (kills the residual `sub xN,x29`+`movk`,
- ~3.5 K). The machinery already exists — aa64 has a bottom-record layout
- (`fp_at_bottom`, `frame_size−off`) + an `AAPatch` deferred-patch list, currently
- gated to `-O1` known-frame. Extend to `-O0` by deferring per-slot offsets (emit
- scaled `str`/`ldr` with an imm12 placeholder, patch `(N−off)>>scale` at
- `func_end`; scaled imm12 reaches 32,760 B so the placeholder is always one
- word). alloca-safe via a stable base anchor (the fp-at-bottom / a callee-saved
- frame base plays tcc's stable-x29 role). Compile-speed risk (per-access
- bookkeeping) — gate it.
-3. **Residual arg/value `mov`s (+13.7 K).** Forward-order materialization places
- straightforward args directly in their ABI register; what's left is args it
- can't place — a nested-call result already in x0, or a value spilled under
- pressure. A per-arg **destination hint** (abstract arg-slot threaded
- frontend→CG-API→backend, resolved to the physical reg) would let the producer
- target the exact register in those cases. High plumbing, high correctness
- surface; the win is the residual only.
-4. **Signed load-with-extend** `ldrb;sxtb`→`ldrsb` (the unsigned zero-extend cases
- are done). Structurally **blocked**: needs a load-with-extend rider on
- `MemAccess` (kit's CG integer types are sign-agnostic, so `nd_load` can't know
- the value feeds a sign-extend) — a frontend widening signed load, not a register
- rename.
-5. Smaller, self-contained: **indexed-addressing fold** (collapse base+index+disp
- into one EA where a member offset currently forces an extra `add`),
- **NOP/alignment-pad trimming**, **call-argument stack-spill** reduction.
-
-### 4.3 The end-state bet (large lift, optional)
-
-tcc's *shape*: a shared mutable token slot fed by both scanner and macro replayer
-(no per-stage `Tok` structs), identifiers resolved through symbol pointers cached
-on the interned token, a thin `SValue[]` the parser drives by calling emit
-directly. This is the "clean structural redesign" the project prizes and subsumes
-§4.1. **But** the value-stack→direct-emit vtable collapse is **explicitly out of
-scope** here: the two real indirect calls per primitive are the price of the
-seven-backend modularity and are kept. The seam-preserving items above plus the
-code-size track are the campaign from here; the ~2× headroom a full tcc-shape
-collapse might reach is not pursued.
-
-### Measured dead ends — do not retry
-
-- **16-byte `Tok`** (LocId side-table / register return): byte-identical but a net
- `-c` regression (every token pays a LocId store+resolve that outweighs the
- smaller copy; `-c` dominates and is loc-heavy).
-- **Fused lex→pp→parse pull pipeline**: the pull wrappers are only ~5 % of frontend
- self-time; `pp_next`'s real cost (macro/directive logic) survives the rewrite.
-- **`nd_grow_*` non-zeroing**: uninit-read risk no sanitizer catches.
-- **Word-at-a-time intern hash · lower hash load factor · inline-prefix entry
- cache**: all slower or negligible for the short identifiers real code uses.
-
----
-
-## 5. Methodology notes
-
-- **Measure on a RELEASE build** (`make bin RELEASE=1`, or `PROFILE=1` for
- sampling), never the default `make bin` (`RELEASE=0` → ASan/UBSan): ASan
- amplifies *memory*-op savings and dilutes *compute* savings, inverting the
- ranking. Gates are build-mode-independent; timers are not.
-- **Byte-identity gate tooling.** `scripts/perf_identity_gate.sh` (60-category
- byte-identical gate, golden-vs-candidate) + `scripts/perf_axis_time.py` (focused
- A/B axis timer). For lexer changes the diff must include a line-splice battery
- (mid-token / mid-string / leading / trailing / consecutive continuations) plus a
- diagnostic whose line number falls across a splice. Flow: `make perf-golden`
- (snapshot pre-change RELEASE build) → edit → `make bin RELEASE=1` → `make
- perf-gate`. The golden carries its own `support/rt` sibling, so both binaries
- resolve their runtime from any cwd.
-- **Code-size gate** (for emitted-byte changes): determinism (compile twice +
- `cmp`) + `make test-toy test-cg-api test-opt test-smoke-x64 test-smoke-rv64
- test-parse-ok test-parse-err test-dwarf test-debug` + sqlite e2e at `-O0`+`-O1`
- vs clang + per-arch clang-differential probes. **Verify shared-NDT changes on
- x64 and rv64, not just aa64** — a register-residency change can be a win on the
- reference arch and a silent miscompile elsewhere (x86-64 RAX is an implicit
- div/mul operand; the result-stable capability is gated per-arch for this reason).
-- **Re-profile after each landing and re-rank** — the highest lever moves as work
- is removed.
diff --git a/doc/plan/PORT.md b/doc/plan/PORT.md
@@ -1,278 +0,0 @@
-# Portability test surface: `cross` + `selfhost` over one support set
-
-This doc is the spec for kit's portability test surface — the harmonized
-top-level make targets and the scripts beneath them that answer two questions:
-
-1. **cross** — can kit, running on the dev host, cross-compile a *correct*
- executable for every target in its support set?
-2. **selfhost** — can kit be built to *run on* each target, and then compile +
- run a program there?
-
-It supersedes the old scattered targets (`test-hosted*`, `test-libc*`,
-`test-freebsd*`, `test-coff-windows-*`, `test-toy-*-vm`, `test-link-x64`,
-`test-parse-rv64-wide`, `bootstrap-linux*`, `bootstrap-freebsd`, the standalone
-`windows_cross.sh`, and the freestanding smokes). See "Migration" at the end.
-
-Related: [BOOTSTRAP.md](BOOTSTRAP.md) (the 3-stage self-build mechanism),
-[SYSROOTS.md](SYSROOTS.md) (sysroot provisioning), [windows.md](windows.md).
-
-## Model: two modes over one matrix
-
-```
- support set (one canonical list in scripts/hosted.sh)
- ┌──────────────────────────────────────────────────────┐
- mode cross │ host kit builds for T → artifact runs correctly on T │
- mode selfhost│ build a kit that runs on T → it builds + runs a program │
- │ on T │
- └──────────────────────────────────────────────────────┘
- substrate (native / qemu-user / podman / VM / qemu-system) = IMPLICIT,
- resolved per target by the exec seam; never named in a target.
-```
-
-The support set, one token grammar everywhere: **`<os>[-<libc>]-<arch>`**.
-
-```
-cross matrix (16):
- linux-{glibc,musl}-{aa64,x64,rv64} (6)
- freebsd-{aarch64,amd64,riscv64} (3)
- windows-{aarch64,x64} (2)
- macos-aarch64 (1)
- freestanding-{aa64,x64,rv64,rv32} (4) ← rv32 lives here only
-
-selfhost matrix (12): the same, minus freestanding
-```
-
-Freestanding has **no OS**, so there is nothing to run a compiler *on*: it
-participates in `cross` only. `selfhost` of a `freestanding-*` target is a hard
-error.
-
-Arch tokens are the short forms **`aa64` / `x64` / `rv64`** (plus `rv32`,
-freestanding-only). The long forms (`aarch64`/`amd64`/`x86_64`/`riscv64`) are
-accepted as input aliases by `scripts/hosted.sh`.
-
-## Top-level make targets
-
-```
-make test-port # test-cross + test-selfhost
-make test-cross [TARGET=…] [DEPTH=…] [KIT_VM=…] [RUN=0|1]
-make test-selfhost [TARGET=…] [DEPTH=…] [KIT_VM=…]
-make kit-cross [TARGET=…] [CROSS_CC=kit|clang] [VERIFY=0|1]
-make provision [TARGET=…] [KIT_VM=…]
-```
-
-Defaults: `TARGET ?= all`, `DEPTH ?= smoke`, `KIT_VM ?= 1`, `RUN ?= 1`,
-`CROSS_CC ?= kit`, `VERIFY ?= 0`.
-
-`test-port` is **not** part of the default `make test` (it is heavy and needs
-provisioning). The native 3-stage `bootstrap` + `test-bootstrap-toy` stay in the
-default suite as the fast self-reproduction check.
-
-### `TARGET=` selector grammar
-
-Resolved centrally by `scripts/hosted.sh expand <selector> [--mode=cross|selfhost]`:
-
-| Selector | Expands to |
-|---|---|
-| `all` | every support-set token (mode- and `KIT_VM`-filtered) |
-| `linux` / `freebsd` / `windows` / `macos` / `freestanding` | all configs for that OS |
-| `linux-musl` / `linux-glibc` | that libc, all 3 arches |
-| `linux-glibc-x64`, `freebsd-aarch64`, `freestanding-rv32`, … | one exact config |
-| `a,b,c` | comma list of any of the above |
-
-`--mode=selfhost` drops `freestanding-*` from the expansion. `KIT_VM=0` drops
-`freebsd-*` and `windows-*`.
-
-### `DEPTH`
-
-`test-cross` accepts:
-
-- `coarse` — cross-compile and link the smoke artifacts across the expanded
- support set, but do not execute them. This is the named form of
- `make test-cross RUN=0`.
-- `smoke` (default) — two cases, building up platform coverage:
- 1. `exit` — an `#include`-free exit-code program (`test/cross/cases/exit.c`,
- returns 42): toolchain + link + crt + the exit-code path; on freestanding
- the bare-metal exit oracle. Runs on **every** config (the only case
- freestanding can run — no libc there).
- 2. `hello` — the libc hello-world (`hello.c`, prints + returns 0): the
- **sysroot headers + libc + stdout**. Hosted configs only.
-
- Each case runs once per **link mode** (`link_modes`): **musl exercises both
- `static` (libc.a) and `dynamic` (libc.so)** — the two ways the sysroot is
- consumed; lanes are suffixed `:static`/`:dynamic`. FreeBSD is static; glibc is
- dynamic-only (static glibc is discouraged / NSS-fragile); macOS/Windows use
- their single default shape. So a musl target yields four smoke lanes
- (exit/hello × static/dynamic), a glibc target two, freestanding one.
-- `full` — the two smoke cases + the toy and parse corpora (cross-compiled and
- run on the target) + the libc cases (linux only). Orchestrates the existing
- mature `test/{toy,parse,libc}` runners with the right arch/tag; it does not
- reimplement them.
-
-`smoke` is the default to honor "prefer targeted runs over mass runs"
-(CLAUDE.md). `DEPTH=full` is the deliberate heavy run, usually with a scoped
-`TARGET`.
-
-`test-selfhost` accepts `smoke` and `full`; it has no build-only `coarse` lane
-because its purpose is to run the newly built compiler on the target.
-
-### `kit-cross` — cross-build the compiler itself
-
-`test-cross` asks "can kit cross-compile a *program* for a target?";
-`kit-cross` asks "can kit (or clang) cross-compile *kit itself* to run on a
-target?". It is the general form of the old `windows_cross.sh`: pick any hosted
-token and a backend, and it produces a runnable `kit` binary.
-
-```
-make kit-cross TARGET=windows-x64 # kit.exe for windows-x64, via kit
-make kit-cross TARGET=linux CROSS_CC=clang VERIFY=1
-```
-
-- `CROSS_CC=kit` (default) dogfoods kit as the cross-compiler; `CROSS_CC=clang`
- uses an independent clang + lld (and llvm-mingw's compiler-rt for Windows)
- toolchain — a useful differential check, since clang's strictness surfaces
- source issues kit's frontend tolerates.
-- `VERIFY=1` runs the freshly built `kit` *on the target* through the same exec
- seam the tests use (`kit_cross_verify.sh`: bare `kit` prints its banner and
- exits 0 — exercises load + dynamic-linker + libc init + main).
-- `freestanding-*` is excluded (kit needs an OS to run on): the selector uses
- `expand --mode=selfhost`.
-- Output: `build/kit-cross/<backend>/<target>/kit[.exe]`. The per-target engine
- is `scripts/kit_cross.sh`; the makefile loop is in `mk/port.mk`.
-
-The two backends differ only in toolchain wiring, not source: the Windows
-sources are plain-clang-clean (no `__try`, no `_environ` extern, `__thread`
-rather than `__declspec(thread)`), and `kit cc` auto-appends `.exe` to an
-extension-less `-o` for a Windows target exactly as gcc/clang-mingw do, so both
-backends write `kit.exe` directly. The GC flag is the one linker-specific knob:
-GNU/lld and kit ld take `--gc-sections`, Apple ld (clang on darwin) takes
-`-dead_strip`.
-
-## Architecture
-
-### Source of truth — `scripts/hosted.sh`
-
-Already owns the support set and the `triple`/`path`/`tag` resolvers. Extended
-with:
-
-- `hosted.sh list [selector]` — emit tokens (one per line).
-- `hosted.sh expand <selector> [--mode=…]` — the selector grammar above.
-
-The Makefile reads it via `$(shell …)`; nothing else re-encodes the matrix.
-
-### Two orchestrators
-
-- `scripts/cross_test.sh <selector> [DEPTH]` — expand (mode=cross) → **fail-fast
- provisioning pre-flight** → compile/link all smoke artifacts → optional
- execution phase (`DEPTH=coarse`, `RUN=0`, or `KIT_CROSS_RUN=0` disables it)
- → aggregate report.
-- `scripts/selfhost.sh <selector> [DEPTH]` — expand (mode=selfhost) → pre-flight
- → per-token self-host (dispatch by OS) → on-target corpus → report.
-
-Both share `test/lib/kit_sh_report.sh` for the verdict/summary layer.
-
-### Provision-or-error
-
-Provisioning (network + VM prepare) is **separate** from running, and a requested
-target whose sysroot / image / VM is missing is a **hard error**, never a silent
-skip. The orchestrators do an aggregated pre-flight: they collect *all* missing
-provisioning for the expanded TARGET set, print each with the exact
-`make provision TARGET=…` (or host-tool install) to fix, and exit non-zero
-before running anything.
-
-`make provision [TARGET=…]` wraps `hosted.sh prepare`, `make test-images`, the
-glibc run images, the Windows UCRT sysroots, and VM prepare — scoped by TARGET
-and KIT_VM. rt archives are local build artifacts (not network): the runners
-build them on demand via idempotent `make rt-…`.
-
-### One exec front door, three backends
-
-`test/lib/exec_target.sh` is the single tag-dispatched front door. A tag is
-`<arch>-<os>[-<libc>]`. It routes to one of three backends:
-
-```
-exec_target.sh
-├── stateless os ∈ {linux, macos} → native / qemu-user / podman
-├── exec_vm.sh os ∈ {freebsd, windows} → boot VM, run
-└── exec_bare.sh os == freestanding → qemu-system bare-metal (NEW)
-```
-
-`exec_bare.sh` is the consolidated owner of all per-arch bare-metal scaffolding
-(reset stub + linker script + exit-code oracle + `qemu-system` invocation),
-generalizing the old `exec_rv32_bare.sh` to four arches and absorbing the stubs
-that were inlined in `freestanding_system.sh`. It exposes two contracts:
-
-- `exec_bare_run_image <arch> <kernel.elf> <out> <err> <rc>` — run a ready
- bootable image (what `test/link`'s `kernel_image` cases need; `exec_kernel.sh`
- is now a thin shim over this).
-- `exec_bare_setup <arch> <work>` + `exec_bare_run <arch> <obj> <work> <rc>` —
- link a corpus `.o` (entry `main`, returns the exit code) with the per-arch stub
- + rt into a bootable image, then run it (what toy/parse need).
-
-Exit-code oracle per arch (so callers compare `rc == expected` uniformly):
-
-| arch | mechanism | decode |
-|---|---|---|
-| aa64 | ARM semihosting `hlt #0xf000` + `ADP_Stopped_ApplicationExit` | qemu rc = guest code |
-| rv64/rv32 | SiFive test finisher MMIO at `0x100000` | stub writes `0x3333\|(code<<16)` (or `0x5555` for 0); qemu rc = code |
-| x64 | `isa-debug-exit` (iobase `0x501`) | qemu rc = `(code<<1)\|1` → code = `(rc-1)>>1` |
-
-## Self-host shapes (the substrate asymmetry)
-
-`selfhost.sh` dispatches by OS, because "build a kit that runs on T" differs:
-
-- **macos** → native 3-stage `bootstrap` here, then the corpus through stage3.
-- **linux** → native 3-stage in a podman container (emulated for non-host arch),
- then the corpus. Generalizes `scripts/linux_bootstrap.sh` to (arch, libc).
- **rv64 uses a hybrid seed**: an in-container clang stage1 would itself run
- emulated (slow), so the stage1 seed is cross-built on the host with clang
- (`kit_cross.sh linux-musl-rv64 --cc=clang`, native speed) and fed to
- `mk/bootstrap.mk` via `BOOTSTRAP_SEED`; only stages 2/3 run emulated. They are
- still byte-identical because the seed and stage2 are the same kit source, hence
- functionally identical compilers. Emulated builds write objects to
- container-local storage (the `:Z` virtiofs mount flakes new-file creates under
- sustained emulated write load) and copy the stage kits back.
-- **freebsd** → native 3-stage in the VM, then the corpus
- (`scripts/freebsd_bootstrap.sh`).
-- **windows** → **cross**-build `kit.exe` on the host (the VM has no seed
- compiler), then run it on the VM to cc + run a program
- (`scripts/windows_cross.sh`).
-
-Every selfhost leaf ends with the same contract: *a kit running on the target
-compiled and ran a program there.*
-
-## Status & phased backlog
-
-`cross`:
-
-| Config | coarse | smoke | full |
-|---|---|---|---|
-| linux-{glibc,musl}-{aa64,x64,rv64} | ready | ready | ready (toy X + parse E + libc) |
-| macos-aarch64 | ready | ready (native) | ready (native toy/parse) |
-| freebsd-{aarch64,amd64,riscv64} | ready | ready (VM) | ready (toy via VM) |
-| windows-{aarch64,x64} | ready | ready (VM) | ready (toy via VM) |
-| freestanding-rv32 | ready | ready (bare) | ready (toy X + parse E, bare) |
-| freestanding-{aa64,x64,rv64} | ready | ready (bare smoke) | **deferred** |
-
-`selfhost`: macos (native), linux musl/glibc aarch64 (container), linux x64/rv64
-(container under emulation), freebsd aarch64/amd64 (VM), windows aa64/x64 (cross
-+ VM).
-
-### Backlog: freestanding `DEPTH=full` for aa64 / x64 / rv64
-
-Running the toy/parse corpora bare-metal on these three arches is new capability
-beyond the smoke payload. The smoke stubs in `exec_bare.sh` are minimal; the
-corpus exercises TLS, soft-float, large frames, i128, etc., which need hardened
-per-arch stubs (the rv32 path already has this — its reset stub seeds a static
-TLS image and enables the FPU). Work items:
-
-1. Harden the aa64/rv64 `exec_bare` stubs (TLS image seed + thread-pointer setup,
- matching the rv32 stub and the toy X-lane `start.c`).
-2. Build the x64 long-mode corpus stub (the rv32/aa64 reset path + a full
- exit-code oracle via `isa-debug-exit`).
-3. Wire a bare lane into `test/{toy,parse}/run.sh` for aa64/x64/rv64 (the rv32
- "X / V" path generalized), gated on `qemu-system-<arch>`.
-4. Flip the table rows above from **deferred** to **ready** and drop this section.
-
-Until then, `test-cross TARGET=freestanding-{aa64,x64,rv64} DEPTH=full` runs the
-smoke payload and `log`s that corpus-depth is not yet wired for the arch (it does
-not silently claim full coverage).
diff --git a/doc/plan/PROF.md b/doc/plan/PROF.md
@@ -0,0 +1,85 @@
+# Sampling profiler — `kit prof` (planned work)
+
+Forward-looking design for `kit prof`, a host-native statistical CPU profiler.
+It reuses the interactive JIT debugger's host signal infrastructure
+([../DBG.md](../DBG.md), [DEBUG.md](DEBUG.md)): SIGPROF fires on the debugger's
+worker thread, the handler walks the frame-pointer chain into a pre-allocated
+ring buffer and returns **without parking** — the one property that keeps
+sampling cheap and guest timing undisturbed — and PCs are symbolicated after the
+guest exits.
+
+Nothing exists yet: no `prof` subcommand in `driver/main.c`, no
+`src/dbg/prof.c`, no `on_sample` field on `KitDbgSignalOps`. This document is the
+worklist for building it.
+
+> **Scope — this is the *sampling* profiler, deliberately on the host-native
+> substrate.** It is one of two complementary profilers, and it is **not** being
+> rebased onto the emulator. This one measures real wall/CPU time by
+> statistically sampling natively-run kit code; the *deterministic*
+> instruction-counting profiler (callgrind-style — exact, reproducible, any
+> guest arch) lives on the emulator substrate ([INSTRUMENT.md](INSTRUMENT.md)
+> §9, §12.1). The two share a back-end (symbolication + folded / flat output),
+> not a collector. See INSTRUMENT.md §9 for the perf-vs-callgrind split and the
+> one-core-two-collectors factoring.
+
+## 1. Public API (`include/kit.h`)
+
+- Add `on_sample(void* session, void* ucontext)` to `KitDbgSignalOps` (NULL =
+ ignore SIGPROF); it receives the raw `ucontext_t*`, not a marshalled frame,
+ because it extracts only PC and FP on the hot path.
+- Declare `KitProfBuf` (fixed-capacity sample ring: `pcs[PROF_MAX_DEPTH]` per
+ sample, `count`/`cap`/`dropped`) and `KitProfWriter` (post-run symbolication
+ callback vtable), plus `kit_dbg_session_prof_attach(session, buf)` (before
+ `session_call`) and `kit_dbg_session_prof_collect(session, buf, writer)`.
+
+## 2. Library (`src/dbg/prof.c`, freestanding C11)
+
+- `dbg_fp_walk(ucontext, sample)`: frame-pointer walk via
+ `dbg_os->guarded_copy` for every dereference; terminate on NULL / misaligned
+ / non-advancing FP or `PROF_MAX_DEPTH`. The three frame layouts are identical
+ (`[FP]` = saved FP, `[FP+8]` = saved LR/return addr); FP is x29 / rbp /
+ s0(x8). WHY: no DWARF or symbol lookup on the signal path — raw PCs only.
+- `on_sample` body: capacity check, walk, append or bump `dropped` (non-atomic;
+ the single worker makes that safe). `prof_attach` / `prof_collect` bodies;
+ `prof_collect` symbolicates each PC via `kit_jit_addr_to_sym` +
+ `kit_dwarf_addr_to_line`, dispatching to the writer (may allocate freely).
+
+## 3. Host adapter (`driver/env/posix_dbg.c` and `windows.c`)
+
+- Add SIGPROF to the POSIX handler's signal set with an early-return path:
+ `if (signo == SIGPROF && on_sample) { on_sample(...); return; }` — no
+ park/unpark. SIGPROF joins the blocked cohort so it does not recurse. Timer
+ arming (`setitimer(ITIMER_PROF)`) and thread targeting stay driver-side and
+ do **not** belong behind `KitDbgOs`.
+- Decide the Windows sampling mechanism (no SIGPROF): a periodic
+ `SuspendThread` + `GetThreadContext` sampler thread is the natural analog of
+ the VEH interrupt path. WHY: the SIGPROF design has no direct Windows
+ equivalent, so this is a genuine open design question, not a port.
+
+## 4. Driver (`driver/cmd/prof.c`)
+
+Wired into the multi-call dispatch in `driver/main.c`:
+
+- Flags `--rate` (default 1ms), `--depth` (64), `--cap` (1M), `--output`
+ (`prof.folded`), `--no-folded`, `--no-flat`. Input handling mirrors
+ `kit run`, with `-g` forced on so symbolication always has DWARF.
+- Arm the timer before `session_call`, disarm after, then `prof_collect`.
+ Emit folded stacks (sorted + RLE for `flamegraph.pl`) plus a flat
+ self%/cumul% report to stdout and a dropped-sample warning.
+
+## 5. Tests
+
+- `test/smoke/prof_hello`: assert `prof.folded` is non-empty and `main` appears.
+- `test/dbg/fp_walk_*`: canned frame chains per arch, assert the PC sequence and
+ termination.
+- `test/dbg/prof_buf_overflow`: fill to capacity, assert `dropped` increments
+ and `count` caps.
+
+## 6. Follow-ons (deferred)
+
+- Per-thread timers via `timer_create(CLOCK_THREAD_CPUTIME_ID)` +
+ `SIGEV_THREAD_ID` for multi-thread guests. This depends on widening
+ `KitDbgOs` for multi-threaded guests ([DEBUG.md](DEBUG.md) §7).
+- An `ITIMER_REAL` wall-clock mode for I/O-bound programs.
+- Allocation profiling via a conditional breakpoint on the allocator.
+- SpeedScope / pprof output.
diff --git a/doc/plan/README.md b/doc/plan/README.md
@@ -4,30 +4,31 @@ Forward-looking roadmaps: what is intended, why, the open problems, and the
design of features not yet built. These are distinct from the design docs one
level up (`../`), which describe the system *as it is* — when a feature here
ships, its durable design moves up to the matching design doc and the entry here
-shrinks to whatever remains open.
+shrinks to whatever remains open (and is deleted once nothing remains open).
| Roadmap | Scope | Design doc |
|---------|-------|------------|
-| [RELEASE.md](RELEASE.md) | Cross-cutting initial-release punchlist: release scope, deferred features, and per-subsystem completion/validation items. | — |
-| [OPTIMIZER.md](OPTIMIZER.md) | Completing the O2 SSA mid-end, expanded inlining, -O0/-O1 performance work, machine register-constraint improvements. | [../OPT.md](../OPT.md) |
-| [O1.md](O1.md) | Closing the `-O1` *generated-code quality* gap to clang `-O1` within the no-SSA/linear-compile constraint: measured gap, the spill-addressing root cause, and a ranked worklist (positive-offset spill addressing, rematerialization, linear coalescing, peepholes). | [../OPT.md](../OPT.md) |
-| [PERF.md](PERF.md) | Making kit the fastest `-O0` C compiler with code as dense as tcc: current compile-speed + code-size standings, how to reproduce them (macOS instruction counts, Linux callgrind, the `make bench-cc` scaling guard), and the ranked forward-looking levers on both axes. | [../ARCH.md](../ARCH.md) |
-| [CG-TYPES.md](CG-TYPES.md) | Clean-break redesign of the public CG type/type-id system: direct-index handles, real void types, explicit alias/storage semantics, ABI-owned layout, and two-phase nominal records. | [../CODEGEN.md](../CODEGEN.md), [../INTERFACES.md](../INTERFACES.md) |
-| [CG-TYPE-DEBUG-SPLIT.md](CG-TYPE-DEBUG-SPLIT.md) | Make `KitCgTypeId` storage/ABI-only and move source spelling (primitive sign+name, typedefs, enum underlying) onto a separate debug-type channel: deletes `ALIAS`/`SOURCE_BASE`, `api_unalias_type`, and the per-backend unalias boilerplate. Resolves CG-TYPES.md §10 Q1. | [../CODEGEN.md](../CODEGEN.md), [../DWARF.md](../DWARF.md) |
-| [CG-STACK-API.md](CG-STACK-API.md) | Refactoring the parser-to-CG expression seam: keep stack-based liveness, move the C frontend's duplicated `pcg` slot state onto CG-owned stack slots, and add parser-shaped place/load/store operations without backend fusion. | [../FRONTENDS.md](../FRONTENDS.md) |
-| [LEX-PP-API.md](LEX-PP-API.md) | Redesigning the lexer -> preprocessor -> parser boundary around lean tokens, lazy spelling/location materialization, and slot-based handoffs for a high-performance lexer/preprocessor rewrite without frontend layer fusion. | [../FRONTENDS.md](../FRONTENDS.md) |
-| [LINKER.md](LINKER.md) | Incremental linking: the file-based object-link redesign and remaining non-ELF format coverage. | [../LINK.md](../LINK.md) |
-| [LINKER-COMPAT.md](LINKER-COMPAT.md) | Completing system-linker compatibility across the support set: ordered DSO selection, ELF TLS/TLSDESC, shared libraries, relocatable links, runtime/sysroot interoperability, and Rust/toolchain validation. | [../LINK.md](../LINK.md), [../OBJ.md](../OBJ.md), [../DRIVER.md](../DRIVER.md) |
-| [KERNEL.md](KERNEL.md) | Freestanding kernel build and image pipeline: build-obj/build-exe support, linker-script growth, map/symbol side outputs, and image emission via kit image / objcopy -O binary. | [../DRIVER.md](../DRIVER.md), [../LINK.md](../LINK.md), [../OBJ.md](../OBJ.md), [../RUNTIME.md](../RUNTIME.md) |
-| [JIT.md](JIT.md) | Function-level hot reload, Go-runtime-style codegen support, and remaining JIT host-portability work. | [../JIT.md](../JIT.md) |
-| [DEBUG.md](DEBUG.md) | The Windows debugger host adapter, x64/rv64 displaced single-step, profiling, and DWARF gaps. | [../DBG.md](../DBG.md), [../DWARF.md](../DWARF.md) |
+| [RELEASE.md](RELEASE.md) | Cross-cutting initial-release punchlist: remaining release blockers and per-subsystem validation gaps (completed items are removed, not checked off). | — |
+| [OPTIMIZER.md](OPTIMIZER.md) | Completing the O2 SSA mid-end, live-range splitting/coalescing, the residual O1 generated-code-quality gaps that need SSA, -O0 quality, machine register-constraint improvements, and broader inlining. Carries the known non-deficiencies (what *not* to chase). | [../OPT.md](../OPT.md) |
+| [LINKER.md](LINKER.md) | Incremental linking (JIT append + the unbuilt file-based "m2" redesign) **and** system-linker compatibility (ordered DSO/`--as-needed`, ELF TLS planning + TLSDESC, shared/relocatable links, sysroot interop, toolchain validation). | [../LINK.md](../LINK.md), [../OBJ.md](../OBJ.md) |
+| [DEBUG.md](DEBUG.md) | The interactive JIT debugger + DWARF: x64/rv64 session parity, displaced-step instruction coverage, unit/smoke tests, REPL polish, Toy/C REPL frontends, and DWARF producer/consumer gaps (loclists, CFI register recovery, composite locations). | [../DBG.md](../DBG.md), [../DWARF.md](../DWARF.md) |
+| [PROF.md](PROF.md) | The not-yet-built host-native sampling profiler (`kit prof`): SIGPROF frame-pointer walk reusing the debugger's signal infrastructure, the `KitProfBuf`/`KitProfWriter` API, and folded/flat output. Complementary to (not rebased on) the emulator callgrind in INSTRUMENT.md. | [../DBG.md](../DBG.md) |
| [INSTRUMENT.md](INSTRUMENT.md) | Debugging/profiling tools in the Valgrind/callgrind/asan/ubsan tradition: the emulator as a dynamic-binary-instrumentation substrate (the `EmuToolHooks` ABI, shadow planes, the memory seam, a guest debugger), plus the cross-frontend compiler-sanitizer story. | [../EMU.md](../EMU.md) |
-| [WASM.md](WASM.md) | Completing the Wasm object backend and remaining parser/validator coverage. | [../WASM.md](../WASM.md) |
-| [ARCH.md](ARCH.md) | Remaining native-backend completeness for x64/rv64 relative to the aa64 reference, and per-call cost follow-ups. | [../ARCH.md](../ARCH.md) |
-| [BOOTSTRAP.md](BOOTSTRAP.md) | The 3-stage self-build reproducibility goal and the open `-O1` issues blocking it. | [../BUILD.md](../BUILD.md) |
-| [windows.md](windows.md) | Self-hosting kit on Windows: cross-built `kit.exe` runs `cc` + JIT on aarch64-windows; the open self-host miscompile crash, JIT printf, x64 parity, compile-on-VM lane, and the Windows 3-stage bootstrap. | [../WINDOWS.md](../WINDOWS.md) |
-| [BUILD.md](BUILD.md) | A new content-addressed build coordinator (Bazel/Nix-style incremental builds layered on the CAS) — storage state machine, caching algorithm, recipe protocol. Distinct from `../BUILD.md` (kit's own Makefile build). | — (new subsystem) |
-| [BUILD_COMMANDS.md](BUILD_COMMANDS.md) | The kit-native `build-exe`/`build-lib`/`build-obj` verbs that replace `compile`: polyglot, in-memory compile+link with `--group` flag scoping and full link-flag control. Distinct from `BUILD.md` (the CAS coordinator). | [../DRIVER.md](../DRIVER.md) |
-| [LLGEN_IMPORT.md](LLGEN_IMPORT.md) | Importing the standalone LL(1)/Pratt parser and lexer generator into libkit, including public API renames, file moves, build gates, and a `kit llgen` command. | — |
-| [ARM32.md](ARM32.md) | 32-bit ARM (`arm-none-eabi`, ARMv7-M/ARMv7E-M Thumb-2, Cortex-M3/M4/M7) freestanding backend: the supported ISA spec, AAPCS32 ABI, ARM ELF relocations, runtime reuse, and the `qemu-system-arm` cross-test lane. | [../ARCH.md](../ARCH.md), [PORT.md](PORT.md) |
-| [TODO.md](TODO.md) | Open deferred fixes and code smells only. Completed items are removed instead of checked off. Not a roadmap; a current backlog. | — |
+| [JIT.md](JIT.md) | Function-level hot reload, Go-runtime-style managed codegen support, and remaining JIT host-portability work. | [../JIT.md](../JIT.md) |
+| [ARM32.md](ARM32.md) | 32-bit ARM (`arm-none-eabi`, ARMv7-M/ARMv7E-M Thumb-2, Cortex-M3/M4/M7) freestanding backend: Phase 1 (walking skeleton) is landed; Phase 2 tracks the remaining ops, the -O1 known-frame path, 64-bit, atomics, TLS, and the `qemu-system-arm` cross-test lane. | [../ARCH.md](../ARCH.md), [../PORT.md](../PORT.md) |
+| [SYSROOTS.md](SYSROOTS.md) | Cross-compile sysroot packaging: minimal per-target stubs/headers/CRT objects distributed via `.kpkg` for the support set. Design complete, implementation not yet started. | — |
+| [BUILD.md](BUILD.md) | A new content-addressed build coordinator (Bazel/Nix-style incremental builds layered on the CAS) — storage state machine, caching algorithm, recipe protocol. Design, not yet built. Distinct from `../BUILD.md` (kit's own Makefile build). | — (new subsystem) |
+| [LLGEN_IMPORT.md](LLGEN_IMPORT.md) | Importing the standalone LL(1)/Pratt parser and lexer generator into libkit, including public API renames, file moves, build gates, and a `kit llgen` command. Not yet started. | — |
+| [TODO.md](TODO.md) | Open deferred fixes and code smells, plus terse backlog folded from retired plan docs (arch-backend parity, Wasm object backend, Windows x64 self-host, bootstrap breadth). Completed items are removed instead of checked off. A current backlog, not a roadmap. | — |
+
+Speculative, not-committed designs (no code, parked) live in [`../ideas/`](../ideas/)
+— currently the RQL query language and the RSN scripting language.
+
+Plan docs that shipped and were retired into the design set: the `-O1` quality
+worklist and frontend/CG redesigns (→ [../OPT.md](../OPT.md),
+[../CODEGEN.md](../CODEGEN.md), [../FRONTENDS.md](../FRONTENDS.md)); `build-exe`/
+`build-lib`/`build-obj` (→ [../DRIVER.md](../DRIVER.md)); the freestanding kernel/
+image pipeline (→ [../KERNEL.md](../KERNEL.md)); the bootstrap fixed point
+(→ [../BUILD.md](../BUILD.md)); aarch64 Windows self-host (→ [../WINDOWS.md](../WINDOWS.md));
+the portability test surface (→ [../PORT.md](../PORT.md)); and the compile-speed/
+code-size benchmarking methodology (→ [../BENCHMARKING.md](../BENCHMARKING.md)).
diff --git a/doc/plan/RELEASE.md b/doc/plan/RELEASE.md
@@ -78,11 +78,6 @@ For each target in the release set:
Target-specific work:
-- [x] Add/validate FreeBSD target parsing and serialization for arm64/x64/rv64.
-- [x] Add/validate FreeBSD hosted profiles against a real FreeBSD rootfs for
- arm64/x64/rv64.
-- [x] Add/validate runtime variants for arm64 FreeBSD, x64 FreeBSD, rv64
- FreeBSD, arm64 freestanding, and x64 freestanding.
- [ ] Validate Windows arm64/x64 object/link/runtime support, including UCRT
import libraries and large-frame stack probing.
- [ ] Validate Linux arm64/x64/rv64 hosted static and dynamic-executable links.
@@ -96,20 +91,12 @@ Target-specific work:
Release validation is orchestrated from a macOS/aarch64 host. Prepare each
execution environment explicitly so test skips mean "unsupported by this lane",
-not "runner missing".
-
-Verified native/VM run signoff:
-
-- [x] macOS arm64.
-- [x] Windows arm64/x64.
-- [x] FreeBSD arm64/rv64/x64.
-- [x] Linux arm64/rv64/x64, alpine (musl static and dynamic) and debian (glibc).
-- [x] Freestanding arm64/rv64/x64/rv32.
+not "runner missing". Native/VM run signoff is in place for macOS arm64, Windows
+arm64/x64, FreeBSD arm64/rv64/x64, Linux arm64/rv64/x64 (alpine musl
+static+dynamic, debian glibc), and freestanding arm64/rv64/x64/rv32.
## Runtime
-- [x] Fill the runtime variant table in `mk/rt.mk` and `driver/lib/runtime.c` for
- every release target.
- [ ] Validate compiler-rt integer/fp helpers for every release data model:
LP64, LLP64, and ILP32.
- [ ] Validate atomic helper coverage, including non-lock-free widths.
@@ -139,21 +126,16 @@ Verified native/VM run signoff:
## LTO and optimizer
-- [ ] Merge the LTO work to main without regressing the current build commands,
- RV32 work, or test layout.
-- [ ] Keep release optimization levels to `-O0` and `-O1`; LTO must be available
- at `-O1`.
+LTO is on main (`-flto` wired through `cc`/build verbs; preserved/export-set
+computation, non-preserved internalization, cross-TU inlining, and LTO tests
+landed). Remaining:
+
+- [ ] Keep release optimization levels to `-O0` and `-O1`; confirm LTO stays
+ available at `-O1` only (no `-O2` path).
- [ ] Decide the release spelling (`-flto`, plus any rejected aliases) and make
diagnostics precise.
-- [x] Finish link-picture preserved/export set computation for LTO:
- entry symbol, dynamic imports used by executable links, opaque object/asm
- references, `used`, init/fini, IFUNC, address-significant symbols, and
- visibility.
-- [x] Internalize non-preserved globals and re-run whole-module reachability.
-- [ ] Keep shared-library creation out of scope; make `-shared -flto` reject
- cleanly as part of the general dynamic-library creation policy.
-- [x] Validate cross-TU inlining and interposition safety on arm64/x64/rv64.
-- [x] Add LTO tests for `cc`, `build-exe`, `build-lib`, and `build-obj`.
+- [ ] Make `-shared -flto` reject cleanly as part of the general dynamic-library
+ creation policy (currently a "not supported yet" diagnostic).
- [ ] Refresh O0/O1 benchmark baselines and record LTO impact separately.
## Build coordinator
@@ -195,7 +177,7 @@ Verified native/VM run signoff:
- [ ] Add Wasm/WASI smoke tests that do not depend on Toy.
- [ ] Make unsupported Wasm proposals and unsupported WASI calls fail with
feature-naming diagnostics.
-- [ ] Update `doc/WASM.md`, `doc/plan/WASM.md`, driver help, and README to match
+- [ ] Update `doc/WASM.md`, driver help, and README to match
the v1 Wasm/WASI surface.
## Interactive debugger
@@ -208,7 +190,6 @@ Verified native/VM run signoff:
- [ ] Bring x64 and rv64 session integration to parity with arm64:
fault classification, trap PC normalization, register marshalling, and
displaced single-step.
-- [x] Add or extend ucontext/register marshalling for FreeBSD x64/arm64/rv64.
- [ ] Handle known declined displaced-step forms or diagnose them cleanly.
- [ ] Add scripted transcript tests and low-level unit tests for breakpoint patch
round-trip, guarded copy, displaced stepping, and source stepping.
@@ -231,29 +212,13 @@ Verified native/VM run signoff:
## C ecosystem gates
-The reproducible harness landed: `test/ecosystem/` + `scripts/ecosystem.sh`,
-driven by `make provision-ecosystem` (network: fetch + sha256-verify + extract
-pinned upstream sources into the kit cache dir) and `make test-ecosystem`
-(offline: build each project's TUs with `kit cc`, archive with `kit ar`, link a
-driver, run it, and diff the output against a checked-in golden AND against the
-same program built with clang). Both `-O0` and `-O1` run for every project.
-Green at both levels and byte-identical to clang: cJSON, Lua, LZ4, miniz,
-tinyexpr (and SQLite at `-O0`). Two real kit bugs are kept RED on purpose and
-documented in `test/ecosystem/known_bugs/`: (A) `MCEmitter: label … placed
-twice` compiling yyjson at `-O0`; (B) `kit cc src.c archive.a` at `-O1` drops a
-local `.Lkit_ro.N` symbol (hits SQLite + yyjson at `-O1`).
-
-- [x] Add a small, curated `examples/` or `test/ecosystem/` suite that proves kit
- can compile real C projects with ordinary headers, build scripts, archives,
- and hosted system libraries.
-- [x] Keep the suite deterministic: pinned upstream versions, checked hashes,
- patch files kept minimal and documented, no network access in default test
- runs.
-- [x] Add a source-built library gate for SQLite: compile the amalgamation,
- archive `libsqlite3.a`, build the shell or a focused smoke app, and validate
- basic SQL execution. (Green at `-O0`; `-O1` link kept red — known bug B.)
-- [x] Add a source-built compression/library gate such as zlib or libpng to cover
- common configure-style C and archive linking. (miniz + LZ4, both `-O0`/`-O1`.)
+The reproducible harness is landed (`test/ecosystem/` + `scripts/ecosystem.sh`,
+`make provision-ecosystem` / `make test-ecosystem`): build each project with
+`kit cc`/`kit ar`, run a driver, and diff against a checked-in golden AND against
+clang, at both `-O0` and `-O1`. All seven projects — cJSON, Lua, LZ4, miniz,
+tinyexpr, yyjson, and SQLite — are green at both levels and byte-identical to
+clang. Remaining gates extend hosted system-library coverage:
+
- [ ] Add a hosted SDL2 gate: compile a minimal C app and link it through the
platform mechanism (`pkg-config`/`sdl2-config`, framework, or import
library), with an offscreen/headless smoke mode where possible.
@@ -266,13 +231,8 @@ local `.Lkit_ro.N` symbol (hits SQLite + yyjson at `-O1`).
gate is still pending.)
- [ ] Add Windows hosted link gates for system/import libraries used by the
release support set.
-- [x] Run the ecosystem suite at `-O0` and `-O1`; add an LTO lane for at least
- SQLite once LTO lands. (Both opt levels run; LTO lane still TODO.)
-- [x] Decide which gates are required in default release CI and which are
- opt-in because they require host packages or graphics/display access.
- (Opt-in: needs provisioned sources + a host clang; not in DEFAULT_TEST_TARGETS.)
-- [x] Document the examples as supported smoke builds, with exact commands users
- can run after installing kit. (`test/ecosystem/README.md`.)
+- [ ] Add an LTO ecosystem lane for at least SQLite (both opt levels already run;
+ LTO lane still TODO).
## Release validation
diff --git a/doc/plan/TODO.md b/doc/plan/TODO.md
@@ -161,6 +161,64 @@ Add new deferred fixes below as they are discovered.
module-setup/opcode-switch split remains.)
- `c_emit_data_symbol` (`src/arch/c_target/c_emit.c`, ~306 lines; doubled reloc-walk).
+## Deferred subsystem work (folded from retired plan docs)
+
+Terse backlog rescued from plan docs that were deleted once their shipped design
+moved up to the design set. Fuller context for each is in git history (the named
+doc at `HEAD~`).
+
+### Arch-backend parity — x64/rv64 vs the aa64 reference (was `ARCH.md`)
+
+- **x64/rv64 tail-call realization.** `x64_no_tail` / `rv_no_tail` still bail on
+ `frame.ncallee_saves != 0`; aa64 gates on the outgoing-stack-arg size check
+ alone. The restore-before-jump machinery (`x64_emit_tail_site` /
+ `rv_emit_tail_site`) already exists — remove the callee-saves guard so it runs,
+ add callee-saves-live tail-call corpus cases, and validate Win64 callee-save
+ XMMs + forwarded sret and the rv64 s2–s11/fs2–fs11/s0/ra restore-then-`jr`.
+- **Cost-model alignment.** As the tail paths land, verify the optimizer's
+ per-call cost estimates (`signature_stack_bytes`/`call_stack_bytes`) reflect
+ the cheaper known-frame shapes. Also: `cleanup_layout_fallthrough_branches`
+ doesn't yet thread `b A; A: b B` chains (arch-shared optimizer pass fix).
+- **x64 debugger step-out / unwind.** `kit_dwarf_unwind_step` has no memory
+ provider and x64 has no link register, so step-out can't recover the return
+ address; also populate `.eh_frame` (or equivalent CFI) for JIT in-process
+ images.
+- **Niche `as` / inline-`asm()` encode-decode gaps** (blocks no build): aa64
+ `CASP`, LSE min/max (`ldsmax`/`ldsmin`/`ldumax`/`ldumin`), `LDAPR`/`STLLR` not
+ encoded; disasm rows missing for the new encode-only exclusive/LSE/reg-offset/
+ writeback forms (render as `.inst`); TLS reloc modifiers `:tprel_*:` (aa64) /
+ `%tls_*` (rv64) not accepted in operands; `.L`-prefixed local labels in operand
+ references.
+
+### Wasm object backend + linker (was `WASM.md`)
+
+- **Object backend** (largest gap): `src/obj/wasm` lacks relocatable-object +
+ linker-metadata support at ELF/Mach-O/COFF parity.
+- **Static linker**: no Wasm linker — `kit_link_exe` always builds a native
+ `Linker`; need multi-TU Wasm merge + relocation apply.
+- **Feature gaps**: cross-TU references, atomics, wrapper ABI; frontend lowering
+ beyond the staged MVP; validator diagnostics for unsupported proposals.
+- **wasm64 + WASI**: recognized-but-unsupported until the wasm32 object/link path
+ exists. **Cleanup**: move the shared Wasm core `lang/wasm/` → `src/wasm/`.
+
+### Windows x64 self-host + bootstrap (was `windows.md`)
+
+- **x64 self-host `kit.exe` crashes (`0xC0000005`)** on most subcommands
+ (`nm`/`size`/`cpp`/`as`); aarch64-windows self-host works (design in
+ `../WINDOWS.md`). Related: x64 `*sret` tail-call crash at `-O1`,
+ `118_decl_extra_attrs` ADRP-range link issue.
+- Open: a committed **compile-on-VM test lane**, **default sysroot/distribution**,
+ the **Windows 3-stage bootstrap**, and the **SEH fault-guard**
+ (`driver_run_with_crash_guard` is a no-op on Windows — a crashing `kit run`
+ takes down `kit.exe`).
+
+### Bootstrap breadth (was `BOOTSTRAP.md`; fixed point + triage playbook now in `../BUILD.md`)
+
+- Widen the 3-stage byte-identical self-build beyond aarch64 (done on macOS /
+ Linux musl+glibc / FreeBSD at `-O0` and `-O1`) to **x86-64 (ELF + Mach-O)** and
+ **rv64 (ELF)**, and run it as **CI on the reference host**. Cross-bootstrap is a
+ stretch goal.
+
## Cross-platform test failures
(Populated by the serial cross-platform run — macOS native, Linux, FreeBSD, Windows.)
diff --git a/doc/plan/WASM.md b/doc/plan/WASM.md
@@ -1,205 +0,0 @@
-# WebAssembly (planned work)
-
-kit treats WebAssembly as both an input language and an output target,
-sharing one binary/module layer between the two directions. The frontend path
-(Wasm in, native object/JIT out) and the minimal final-module backend (C/toy
-in, single-TU `.wasm` out) are working baselines. The remaining work is
-concentrated in three areas: completing the `obj/wasm` object backend so it can
-read and write tool-conventions *relocatable* objects (today it only mirrors
-raw section bytes); building the Wasm static linker; and closing the last
-frontend/backend feature gaps (relocations for cross-TU references, atomics
-sub-word coverage, the C-facing exported wrapper ABI, and the unsupported-proposal
-diagnostics). This doc is the forward-looking plan. Design rationale, the shared
-module model, and the API sketch live in [../WASM.md](../WASM.md); related
-design docs are [../OBJ.md](../OBJ.md) and [../LINK.md](../LINK.md), and the
-sibling plan is [LINKER.md](LINKER.md).
-
-Baseline already in place (do not re-plan): `src/wasm` core decode/validate/
-encode/wat; `lang/wasm` frontend with native lowering through `KitCg` and an
-explicit `KitWasmInstance*` ABI; `src/arch/wasm` with `arch_impl_wasm`, a
-wasm32 BasicCABI vtable, a structured-CG + CFG-structurer backend, and a
-single-TU `emit_wasm`; host-import binding via `kit_wasm_set_host_imports`.
-`read_wasm`/`emit_wasm` are real (no longer stubbed) but partial.
-
-## Object backend (largest gap)
-
-`src/obj/wasm` is far smaller than the ELF/Mach-O/COFF backends. `read.c`
-mirrors each Wasm section into an `ObjBuilder` section carrying raw payload
-bytes and synthesizes one function symbol per defined function (enough for
-`objdump -h/-s/-d/-t`). `emit.c` flushes a `WasmModule` attached under
-`OBJ_EXT_WASM` via `wasm_encode`, or an empty magic+version header. Neither
-understands tool-conventions object metadata: there is no symbol-table decode,
-no relocation decode/encode, no `linking`/`reloc.*` custom-section handling,
-and no `WasmObjMeta`.
-
-Work items:
-
-- Add the typed `WasmObjMeta` extension payload (module graph, symbol table,
- relocations, data-segment metadata, target features, init funcs) and hang it
- off the builder under `OBJ_EXT_WASM`. Today only a bare `WasmModule*` is
- stored there; the linker and relocatable emitter need the richer struct.
-- Extend `emit_wasm` to produce tool-conventions relocatable objects: a
- required `linking` custom section, `reloc.CODE`/`reloc.DATA` custom sections,
- symbol-info subsections, data-segment-info subsections, and target-feature
- metadata. Relocatable objects must use padded-LEB immediate encodings so
- relocations can rewrite immediates without re-disassembling the code section.
-- Extend `read_wasm` from raw-byte mirroring to a real relocatable-object
- reader: decode the symbol table into generic `ObjBuilder` symbols (so
- archives and generic symbol inspection keep working), decode `reloc.*` into
- generic relocations plus `WasmObjMeta`, and preserve unknown custom sections
- by name and bytes for lossless roundtrip.
-- Map the existing internal `RelocKind` values (`R_WASM_FUNCIDX`,
- `R_WASM_TABLEIDX`, `R_WASM_MEMOFS`, `R_WASM_TYPEIDX`) onto the
- tool-conventions wire relocation numbers, plus the data-symbol and
- table-index variants the cross-TU work needs
- (`R_WASM_MEMORY_ADDR_{LEB,SLEB,I32,I64}`, `R_WASM_TABLE_INDEX_{SLEB,I32}`).
- Add new `RelocKind` values only where the wire format needs a distinction the
- current names cannot express; unsupported kinds fail with a diagnostic naming
- the kind.
-- Add `make test-wasm-obj`: objects roundtrip through reader/writer/`objdump`
- without losing sections, symbols, relocations, or unknown custom sections.
-
-## Static linker
-
-There is no Wasm linker yet. `kit_link_exe` always builds a native `Linker`
-and emits a native `LinkImage`; a Wasm final module is not a virtual-addressed
-native image and must not go through that segment layout.
-
-Work items:
-
-- Add `WasmLinkImage`, `wasm_link_resolve(Linker*)`, `wasm_link_emit`, and
- `wasm_link_image_free`. Dispatch to this path from `kit_link_exe` when
- `target.obj == KIT_OBJ_WASM`, after `build_linker` and before
- `link_resolve`, so existing input handling, archive loading, entry selection,
- and diagnostics stay shared.
-- Merge index spaces across objects: renumber functions, globals, tables,
- memories, data segments, and types; merge type/import/function/table/global/
- element/data/custom sections.
-- Resolve undefined function/data/global/table symbols and apply Wasm
- relocations against the merged module without disassembling the code section.
-- Merge compatible target-feature sections; diagnose incompatible feature sets.
-- Synthesize `__wasm_call_ctors`, stack/memory symbols, and exports per the
- selected output mode; participate in archive demand loading.
-- Add `make test-wasm-link`: multiple objects link into one valid module;
- archives demand-load; imports, exports, ctors, and memory/table layout are
- deterministic.
-
-This is the path that unblocks multi-TU Wasm output. The single-TU final
-module the backend produces today needs no relocations; everything cross-TU
-depends on the object backend and linker above.
-
-## Frontend validator coverage
-
-The shared validator (`src/wasm/validate.c`) and WAT/binary readers already
-cover the accepted feature subset with typed operand/control stacks, section
-ordering and index-space checks, branch arity, `br_table`, limits, segment
-rules, and start-function signatures, plus the staged proposal gates (threads,
-typed function refs, tail calls, multi-memory, memory64, bulk memory,
-non-trapping float-to-int) behind `WasmFeatureSet`.
-
-Remaining work item:
-
-- Add explicit "outside the support plan" diagnostics for SIMD, exceptions/
- tags, GC, and the component model, so a module using them fails with a
- feature-naming message rather than a generic stack/opcode error. These
- proposals stay rejected, not lowered.
-
-## Frontend lowering gaps
-
-Native lowering covers the MVP numeric/control/memory subset plus the staged
-proposals (threads, typed refs, tail calls, multi-memory, memory64, bulk
-memory), all routed through the explicit `KitWasmInstance*` ABI with import
-slots and runtime table storage.
-
-Remaining work items:
-
-- Define and implement the C-facing exported wrapper ABI: host-callable thunks
- that keep the instance parameter explicit but use C-friendly scalar types and
- symbol names. Today only the internal `export_name(KitWasmInstance*, ...)`
- ABI exists. Embedders are a first-class use case; do not generate wrappers
- that hide or globalize the instance.
-- Once `read_wasm` handles relocatable objects, consider relaxing the frontend
- rule that rejects modules with a `linking` custom section (currently directed
- to be supplied as an object input instead).
-
-## Backend feature gaps
-
-The `wasm32-none` backend emits valid single-TU final modules: scalar +
-indirect-aggregate BasicCABI, structured control flow with a reducible-CFG
-structurer and `br_table` switches, linear memory + `__stack_pointer`,
-compact data layout with intra-module `R_ABS32` relocations, varargs via a
-caller-packed linear-memory buffer, the bit/overflow intrinsics, atomics via
-wasm-threads opcodes, `memory.copy`/`memory.fill` lowering, inline asm with
-WAT templates, conventional `"memory"` export, and `(import "env" ...)`
-declarations with `import_module`/`import_name` attribute overrides.
-
-Remaining work items, mostly blocked on the object/linker layer or on wider
-ABI support:
-
-- Cross-TU references. Address-of an undefined symbol and address-of a
- cross-TU function currently diagnose ("address of undefined symbol not yet
- implemented"). The fix is the object linking section plus relocations:
- data symbols via `R_WASM_MEMORY_ADDR_*`, address-taken functions placed in
- the indirect-call table via `R_WASM_TABLE_INDEX_*`, with the undefined symbol
- carrying `WASM_SYM_UNDEFINED` (and `WASM_SYM_BINDING_WEAK` for weak undefs).
- Resolved at link time. Shared machinery for any cross-TU function pointer.
-- `R_ABS64` and non-`R_ABS32` data relocation kinds in the linear-memory image
- (currently diagnosed explicitly).
-- `&&label` addresses in static-data initializers (diagnosed early today).
-- Wider scalar ABI: `__int128` (wasm32 ABI rejects 16-byte scalars) and
- `long double`/binary128 (advertised but fatals on materialization). These are
- phased-rollout SKIPs in the C corpus W path.
-- 64-bit checked-overflow multiply (`__builtin_*mul_overflow` on i64), lowerable
- as a software 64x64->128 product splitting each i64 into i32 halves and
- checking the high word; i32 already widens through i64.
-- Atomics: sub-word (8/16-bit) RMW and cmpxchg, and atomic NAND — blocked on
- the 8/16-bit atomic RMW opcodes not yet defined in kit's Wasm core. Full
- i32/i64 width is covered. Memory order is ignored (Wasm models seq_cst only).
-- TLS and bitfields diagnose-and-fail; WASI startup, and irreducible control
- flow / computed goto still produce raw rather than wasm-specific diagnostics
- — tighten the messages later.
-
-## Wasm-to-Wasm
-
-A Toy/C -> Wasm -> run roundtrip exists via `make test-wasm-toy`/`test-wasm-c`,
-but it routes the backend's output back through the lang/wasm frontend's native
-JIT (Wasm-to-native), not a direct Wasm-to-Wasm path.
-
-Work items:
-
-- Wire the lang/wasm frontend to the Wasm target backend instead of native CG
- for a direct Wasm-input -> Wasm-output normalization mode. Re-emit
- semantically (not byte-preserving; object roundtrip tests cover preservation);
- unsupported features still fail before emission.
-- Add an explicit validate-only pass over Wasm-backend output (the frontend
- validator currently only runs on Toy-produced `.wasm` during `kit run`).
-
-## wasm64 and WASI
-
-Both stay recognized-but-unsupported until the freestanding wasm32 object/link
-path is solid:
-
-- `wasm64`/memory64 parses and the frontend lowers it, but the wasm64 *target*
- ABI panics in `compute_func_info`. Keep 64-bit tool-convention relocations and
- the wasm64 backend behind explicit capability checks until the object/linker
- story is stable.
-- `wasm32-wasi` parses but is unsupported as a target: it must diagnose clearly
- until WASI imports, startup, argv/env, and libc policy are specified. Decision
- stands: freestanding `wasm32-unknown-unknown`/`wasm32-none` first.
-
-## Cleanup
-
-- Move the shared Wasm core from `lang/wasm/` to `src/wasm/` per the layout in
- [../WASM.md](../WASM.md). The core (decode/encode/validate/insn/module/wat)
- already lives under `src/wasm/`; the obj-layer glue is under `src/obj/wasm/`.
- Confirm no TU still reaches the core through `-Ilang/wasm` and retire that
- include path.
-
-## Test targets
-
-Present and green: `test-wasm-front`, `test-wasm-target`, `test-wasm-toy`,
-`test-wasm-c` (aggregated under `test-wasm`). Still to add as the object and
-link layers land: `make test-wasm-obj` and `make test-wasm-link`. Prefer small
-named fixtures over broad corpus runs; keep external validators (wasm-tools,
-WABT) as optional comparison oracles, never a hard dependency — kit's own
-validator is the semantic gate. See [TESTING.md](../TESTING.md).
diff --git a/doc/plan/windows.md b/doc/plan/windows.md
@@ -1,178 +0,0 @@
-# Windows Self-Host (current state and roadmap)
-
-This roadmap tracks bringing kit up as a self-hosted **Windows** toolchain:
-cross-compiling the kit binary into a PE/COFF `kit.exe` and running `kit cc`
-(AOT) and `kit run` (JIT) natively on Windows. The hosted target profile,
-sysroot mechanics, and VM are described in [../WINDOWS.md](../WINDOWS.md); the
-ABI work is in [windows-abi context]; this document tracks the self-host goal,
-the baseline already in tree, and the genuinely-open follow-ups.
-
-Targets are PE/COFF, 64-bit only: `aarch64-windows` (the reference, and what
-runs natively on the Apple-silicon ARM64 Win11 VM) and `x86_64-windows` (runs
-via the in-box x64 emulator on the same VM). The hosted profile is mingw-w64
-UCRT via llvm-mingw (not MSVC): kit advertises `__MINGW32__/__MINGW64__`, never
-`_MSC_VER`.
-
-## Baseline (done — context, not planned work)
-
-Cross-build + native cc + native JIT all work on aarch64-windows, verified on
-the VM.
-
-- **Cross-build**: `scripts/windows_cross.sh aarch64` builds `kit.exe`
- (PE32+ console, ~20 MB) with the host `build/kit` as the cross-compiler
- against the llvm-mingw UCRT sysroot, overriding `HOST_OS=windows
- HOST_ARCH=aarch64` so `mk/env.mk` selects `driver/env/windows.c`. (Windows
- can't bootstrap natively — no seed C compiler in the VM — so the binary is
- cross-produced on the dev host; contrast the native Linux/FreeBSD
- bootstraps.)
-- **`kit cc`**: compiles + links real C programs natively on the VM (`hello.c`
- → a `hello.exe` that prints and returns the right exit code), compiling
- `libkit_rt` on demand.
-- **`kit run` (JIT)**: executes self-contained programs (compute, data/globals),
- external calls (`rand`), and **libc I/O via dlsym** (`puts`).
-- **Toy corpus on the VM** (native `kit.exe`, compare `.expected`):
- - AOT (`kit cc` compile + link + execute): **166 / 166 pass**, 0 miscompile,
- 0 crash, at `-O0`.
- - JIT (`kit run`): **166 / 166** — the printf-family and thread-local gaps
- (§1) are fixed; the two thread-local cases (`141`, `142`) now return their
- values (43, 134).
-
-The items below are what is **not** yet done. The concrete open bugs/blockers
-are collected first; the numbered sections are the larger roadmap items.
-
-## Open bugs / blockers
-
-Concrete defects surfaced during bring-up, each blocking a roadmap item below.
-
-- **x64 self-host `kit.exe` crashes (`0xC0000005`) on most subcommands —
- OPEN.** The x64 `kit.exe` loads and dispatches (help, hash work), but
- `nm`/`size`/`cpp`/`as` crash. Suspected root cause: the kit.exe binary
- itself still imports `__intrinsic_setjmp` from
- `api-ms-win-crt-private-l1-1-0.dll` (a CRT-private api-set that may not
- load cleanly on Prism). Despite the two-archive rt placement fix (§2) that
- resolves the issue for kit-compiled programs, something in kit.exe's own
- link is still pulling the COFF WEAK_EXTERNAL `_setjmp → __intrinsic_setjmp`
- alias from libucrt.a. Exact trigger still under investigation (the strong
- `_setjmp` from libkit_rt.a does appear defined before libucrt.a scans, yet
- the import stub is pulled anyway — possibly via `__imp___intrinsic_setjmp`
- or a symbol not yet traced).
-
-## 2. x86_64-windows parity
-
-aarch64-windows is the reference. x64 codegen now shares the large-frame
-stack-probe gate (§Baseline) via `abi_stack_probe_interval`, and the cross-link
-now succeeds end to end.
-
-**What landed (this cycle):**
-
-- `rt/lib/coro/x86_64_win.c` now exports **strong** `_setjmp` and `_setjmpex`
- globals (in addition to the existing weak `setjmp`). mingw x64's
- `setjmp.h` expands `setjmp(BUF)` to `_setjmp(BUF, frame)` and declares
- `_setjmp` as a COFF WEAK_EXTERNAL aliasing `__intrinsic_setjmp` from
- `api-ms-win-crt-private-l1-1-0.dll` — a private CRT api-set that fails to
- load on some runtimes (e.g. `0xC0000139` on Prism). Providing a strong
- `_setjmp` in rt preempts the alias.
-
-- `driver/cmd/cc.c` places `libkit_rt.a` at **two** positions in the
- Windows link order:
- - **rt#1**: immediately before the hosted `after` group (before libmingw32.a,
- libucrt.a, …). This ensures `_setjmp` is in `defined` before libucrt.a's
- lazy-pull loop runs, so the WEAK_EXTERNAL member that introduces
- `__intrinsic_setjmp` is never pulled.
- - **rt#2**: before `crtend.o` only (the original position). libucrt.a pulls
- libc members (printf, malloc, …) that contain large GCC-compiled stack
- frames whose `___chkstk_ms` undefs appear only after libucrt.a's own scan
- completes; the second rt entry catches those late undefs. Because archive
- scanning is lazy, no symbol is ever defined twice.
-
-- `driver/env/windows.c`: fixed a forward-reference to `driver_join_path`
- (used at its call site before the definition appeared in the same file).
-
-**VM verification status (Prism, Win11 25H2 ARM64):**
-- x64 `kit.exe` loads and dispatches (`kit` with no args prints the multitool
- help). ✓
-- x64 `kit.exe` `hash` works (compute + file I/O, exit 0, correct digest). ✓
-- A kit-compiled x64 **non-setjmp** hosted program (printf `hello`) runs and
- returns its exit code. ✓
-- A kit-compiled x64 **setjmp** program runs end-to-end. ✓ (Fixed by the
- strong `_setjmp` in rt + two-archive placement.)
-- **x64 Toy AOT corpus** (`kit cc` compile + link + execute, compare
- `.expected`): **164 / 165** at both `-O0` and `-O1`. The one failure
- (`118_decl_extra_attrs`) is an aarch64-only ADRP-range link issue —
- pre-existing, not an x64 regression.
-- **The x64 self-host `kit.exe` itself** is not yet usable as a compiler on
- the VM — `nm`/`size`/`cpp`/`as` crash (see *Open bugs*). Running the Toy
- AOT corpus through a native x64 `kit.exe` is blocked on fixing the
- `__intrinsic_setjmp` import.
-- Known x64-windows codegen gaps already on file (from the toy VM lanes): `36/37`
- `*sret` tail-call crash at **-O1**, and `118_decl_extra_attrs` ADRP-range link
- is aarch64-only. Re-confirm against a native x64 `kit.exe` once the above are
- triaged.
-
-## 3. A committed "compile-on-VM" test lane
-
-`test/toy/vm.sh windows` today cross-compiles the toy cases on the *host* and
-only executes on the VM, so it cannot catch self-host compile bugs (the §Baseline
-stack-probe crash, for one, was invisible to it). The native self-host path
-(kit.exe compiling on the VM) has been exercised ad hoc. Generalize it into a
-committed lane (e.g. `test/toy/vm.sh windows --native` or a new harness) that
-ships the case sources + a `<bindir>/support/rt` + the mingw sysroot to the VM,
-compiles with `kit.exe` there, runs, and compares `.expected`. Gotchas to bake
-in:
-- PowerShell `Start-Process -PassThru` *without* `-Wait` reports `.ExitCode = 0`
- always — use a `[Diagnostics.Process]` with `WaitForExit(ms)` + `.ExitCode`,
- or `& exe; $LASTEXITCODE` with a Defender path exclusion. Likewise `cmd`
- `%ERRORLEVEL%` expands at parse time — use `cmd /v:on` + `!ERRORLEVEL!`.
-- macOS `tar` adds AppleDouble `._*` sidecars — ship case sources with
- `COPYFILE_DISABLE=1` (same trap as the FreeBSD bootstrap).
-- The mingw sysroot's `<arch>-w64-mingw32/include` is a symlink to
- `generic-w64-mingw32/include`; dereference (`cp -RL`) before shipping.
-
-## 4. Distribution + default sysroot
-
-- Ship the Windows distribution as `<bindir>/support/rt` (what `kit install`
- produces); rt now resolves from the real image path, so no cwd dependence.
-- The Windows hosted profile **requires** `--sysroot`/`KIT_SYSROOT` (no
- default). Windows ships the runtime DLLs (`ucrtbase.dll`, the
- `api-ms-win-crt-*` api-sets, `kernel32.dll`) but **no headers or import
- libs**, so a sysroot is mandatory. Consider: bundling the mingw UCRT
- headers + import libs into the kit distribution (a baked sysroot), and/or
- teaching the COFF linker to synthesize imports directly from a system DLL's
- export table (drops the import-lib half of the sysroot, but the `crt2.o`
- startup + `libmingwex` helpers — `__mingw_setjmp`,
- `__local_stdio_printf_options`, … — still have no DLL home and would have to
- move into `libkit_rt` for Windows).
-
-## 5. Windows 3-stage bootstrap
-
-The self-host milestone: use the cross-built `kit.exe` on the VM to compile
-kit's own sources into a stage-2 `kit.exe`, then stage-3, and assert stage-2 ==
-stage-3 byte-for-byte (cf. [BOOTSTRAP.md](BOOTSTRAP.md), and the
-native-bootstrap analogs `scripts/{linux,freebsd}_bootstrap.sh`). **Unblocked**
-now that the large-frame stack-probe crash *and* the `__FILE__` `malformed UCN`
-bug are fixed — kit.exe compiles + links + runs the full Toy AOT corpus
-(166/166) on the VM, and now compiles the rt tree on the VM cleanly (the
-on-demand `kit cc` rt build succeeds). The `malformed UCN` bug turned out to be
-a host-path stringization defect, **not** a self-compile codegen defect, so it
-does not threaten stage-2==stage-3 identity. Next: confirm kit.exe can compile
-the full libkit/driver source set on the VM, then add a
-`scripts/windows_bootstrap.sh` to drive the VM-side stages and the
-stage-2==stage-3 byte-identity check.
-
-## 6. Debugger fault-guard / SEH on Windows
-
-`driver/env/windows.c`'s `driver_run_with_crash_guard` is a no-op on Windows
-(the POSIX path uses `sigaction` + `sigsetjmp`); a crashing `kit run` program
-takes down kit.exe instead of reporting `on_crash`. The dbg `guarded_copy` also
-relies on the VEH backstop rather than `__try` (kit's C frontend has no SEH).
-A proper vectored-exception-handler port would give `kit run`/`kit dbg`
-fault isolation on Windows.
-
-## Operational notes
-
-- Build: `scripts/windows_cross.sh [aarch64|x64]` (needs `make bin`, the mingw
- sysroot via `scripts/llvm_mingw_sysroot.sh prepare <arch>`, and the rt
- variant). kit cc emits no `-MMD` depfiles, so the script wipes objects for a
- correct full rebuild each run.
-- VM: `scripts/windows_vm.sh boot|wait-ssh|run|ssh|stop` (see
- [../WINDOWS.md](../WINDOWS.md)). One ARM64 VM serves both arches.