boot2

Playing with the boostrap
git clone https://git.ryansepassi.com/git/boot2.git
Log | Files | Refs | README

commit cbb65ee09fdd8116fd5d2b029c8a535a96dd015e
parent 9bc9e556cd159f382927e9712aa24f2c55285fef
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 28 Apr 2026 11:42:57 -0700

rm impl plan doc

Diffstat:
Ddocs/CC-SCRATCH.md | 356-------------------------------------------------------------------------------
1 file changed, 0 insertions(+), 356 deletions(-)

diff --git a/docs/CC-SCRATCH.md b/docs/CC-SCRATCH.md @@ -1,356 +0,0 @@ -# Parse arena via scratch heap - -Plan to bound parse-phase heap residency by routing parser allocations -through a separate scratch heap that resets at top-level decl -boundaries. Companion to [TCC-TODO.md](TCC-TODO.md) (the heap-blocker -that motivates this) and [CC.md](CC.md) (the C subset). - -## Why this, not mark/rewind alone - -The streaming pipeline ([commit 257fdd5](../cc/cc.scm)) reduced -intermediate materialization but didn't bound steady-state heap. -Investigation pinned the cost on per-token interpreter overhead: -every `cond` arm, `let*`, named-let recursion, and CPS closure -allocates env-cons cells in scheme1's bump arena, with no GC to -reclaim them. Measured at ~4.5 KB allocated per consumed token after -removing the O(decls²) `alist-ref` walk leak — so the full TU still -needs ~700 MB at minimum, far above the 256 MiB cap. - -`heap-mark` / `heap-rewind!` can't fix this directly. Per-decl -survivors (newly-bound sym, ctype, name-bv) are constructed *during* -the parse, so they live above the mark; rewinding them as scratch -defeats their purpose. Pre-allocating survivor cells doesn't work -either — the parser doesn't know the shape of what it's building -until it's done. - -A second heap solves it cleanly: parse runs in scratch, and at each -decl boundary we deep-copy the surviving roots into the main heap and -reset scratch. The copy-out lands in main, so it's safe to reset -scratch wholesale. - -## Plan overview - -Three phases, each independently shippable: - -1. **Pre-refactors** — semantically null, but make the boundary - contract clearer and shrink the deep-copy surface. -2. **scheme1 two-heap primitives** — `current-heap-ptr` indirection, - `(use-scratch-heap!)` / `(use-main-heap!)` / `(reset-scratch-heap!)`. -3. **cc parse-decl-or-fn boundary** — scratch-by-default, promote-and-reset - at each top-level decl; subsumes parse-fn-body's bespoke arena. - ---- - -## Phase 1 — Pre-refactors - -### 1.1 Drop `ps-typedefs` - -Redundant index. A name is a typedef iff `scope-lookup` returns a sym -with `(eq? (sym-kind sm) 'typedef)`. - -```scheme -(define (typedef? ps n) - (let ((sm (scope-lookup ps n))) - (and sm (eq? (sym-kind sm) 'typedef)))) -``` - -`parse-decl-spec` (cc.scm:4112–4117) currently calls `(typedef? ps n)` -and then `(scope-lookup ps n)` — collapses to one walk. - -Removes one root from the boundary work and one slot from `pstate`. - -### 1.2 Drop `cg-globals` - -Only reader is `parse-fn-body` (5338, 5344) — the canary for "did the -body add a global?", gating the heap-rewind. With Phase 3 that gate -goes away, and the alist becomes write-only. - -`cg-emit-global` / `cg-emit-extern` keep emitting bytes into -`cg-data` / `cg-bss` (fixed-storage); they just stop maintaining the -alist. - -If anything later wants "all emitted globals", iterate ps-scope's -file-scope frame filtered to `kind ∈ {fn,var}` with `defined?` set. - -Removes another root and a `cg` slot. - -### 1.3 Eliminate non-essential ctype mutation - -Three mutation sites today: - -- **`complete-agg!` (4208–4210)** — forward struct/union completion. - Cross-decl, structurally necessary for forward-decl identity. - **Keep.** This is the one mutation the promote walker has to - handle. -- **`parse-enum-spec` (4226)** — same-decl. Build the members list - first, then construct the enum ctype with that ext directly. - Remove the `ctype-ext-set!`. -- **`%init-fix-array-size!` (4882–4886)** — same-decl. Build the - array ctype with the right size after the initializer count is - known instead of fixing it up. Remove both `ctype-ext-set!` / - `ctype-size-set!`. - -After: ctype has exactly one mutation site, and the doc comment on -the record (321–327) shrinks to "size/align/ext mutate only on -forward struct/union completion." - -### 1.4 Group cross-boundary state into a `world` record - -Today's persistent-across-decl state is split across `pstate` (scope, -tags, typedefs) and `cg` (globals, str-pool). After 1.1/1.2 it -collapses to three roots — make them one record: - -```scheme -(define-record-type world - (%world scope tags str-pool) - world? - (scope world-scope world-scope-set!) - (tags world-tags world-tags-set!) - (str-pool world-str-pool world-str-pool-set!)) -``` - -`pstate` and `cg` both reference the same `world`. The boundary -contract reads as one record definition. If macros come back -later, `world-macros` joins the same record. - -### 1.5 Stop precomputing mangled names - -`handle-decl` does `(bytevector-append "cc__" n)` at every var/fn -binding (4727, 4736–4738, 4755) and stores the result in `sym-slot`. -But the same mangling can happen at emit time — it's deterministic -from `(sym-name sm)`. - -Drop the precompute. `cg-push-sym` / `cg-emit-global` / -`cg-emit-extern` mangle on demand. `sym-slot` becomes a clean union: -fixnum (auto local), #f (fn / non-static var), bv only for the -block-static mangled case (4736–4738). - -Saves a bv per fn/var from being copied across the boundary; shrinks -the `promote-sym` walker. - -### 1.6 Note `sym` immutability as load-bearing - -Already true (no `sym-*-set!` exists). Add a doc-comment line so it -stays true. After 1.3, the only mutable record in cc is `ctype`. -Promotion is dramatically easier when records can't be mutated -post-construction. - ---- - -## Phase 2 — scheme1 two-heap primitives - -Add a second heap region and an indirection. All Lisp-level -allocation in scheme1 goes through three sites: `cons`, `alloc_hdr`, -`alloc_bytes`. Each currently bumps `heap_next`. Route them through -`current_heap_ptr` instead. - -### 2.1 New globals - -``` -heap_buf # main heap -heap_next -heap_end - -scratch_buf # scratch heap -scratch_next -scratch_end - -current_heap_ptr # &heap_next or &scratch_next -current_heap_end # &heap_end or &scratch_end -``` - -Default at `heap_init`: main. - -### 2.2 New primitives - -| Form | Effect | -|---|---| -| `(use-scratch-heap!)` | Set `current_heap_ptr = &scratch_next`, `current_heap_end = &scratch_end`. | -| `(use-main-heap!)` | Set `current_heap_ptr = &heap_next`, `current_heap_end = &heap_end`. | -| `(reset-scratch-heap!)` | `scratch_next = scratch_buf` (round-aligned). | -| `(in-main-heap?)` | bool — true iff currently allocating in main. (Optional; useful for assertions.) | -| `(heap-in-main? obj)` | bool — true iff `obj`'s pointer falls inside `[heap_buf, heap_buf+heap_size)`. Used by promote walkers to short-circuit on already-promoted / interned objects. | - -`heap-mark` / `heap-rewind!` continue to operate on whichever heap is -current. Mostly used in scratch from now on. - -### 2.3 Sizing - -Scratch needs to cover the worst-case decl + per-token churn between -boundaries. Estimate from the per-decl probe data: max ~1 MB per -decl observed in the test inputs, well below a 16 MiB scratch. Start -with 16 MiB scratch, 256 MiB main (current cap). - -ELF `p_memsz` grows by the scratch size — bumped from 512 MB to -`512 + scratch` MB. - -### 2.4 Fault behavior - -If an alloc would overflow scratch: `runtime_error` ("scratch -exhausted"). Cleaner than silently stomping into main. The "live -data per decl" is bounded by parser state; if scratch runs out it's -a real bug or pathological input, not a sizing oversight. - -### 2.5 Error path discipline - -`die` (sys-exits) is the only abnormal exit. It doesn't need to -restore heap selection — process exits. So mid-parse errors with -the allocator pointed at scratch are fine. - -### 2.6 Test - -A scheme1-level test under `tests/scheme1/` exercises the prims: -allocate in scratch, switch to main, allocate, verify both ptrs in -their respective ranges, reset scratch, verify scratch ptrs become -dangling. Same shape as `93-heap-mark-rewind.scm`. - ---- - -## Phase 3 — cc parse-decl-or-fn boundary - -### 3.1 Driver shape - -```scheme -(define (parse-translation-unit ps) - (cond - ((eq? (tok-kind (peek ps)) 'EOF) #t) - (else - (use-scratch-heap!) - (let ((R (snapshot-roots (ps-world ps)))) - (parse-decl-or-fn ps) - (use-main-heap!) - (promote-roots! (ps-world ps) R) - (promote-iter-buffers! (ps-iter ps))) - (reset-scratch-heap!) - (parse-translation-unit ps)))) -``` - -`snapshot-roots` captures the `eq?`-heads of the three world alists -(plus any peek-buffer survivors that crossed in from the previous -decl, which are already in main). `promote-roots!` walks each alist -from its current head down to the snapshotted head, deep-copying -each new entry into main and rebuilding the chain. - -### 3.2 Promote-map for sharing preservation - -Single alist mapping `scratch-ptr → main-ptr`, cleared at each -boundary. Used by all `promote-*` walkers to preserve `eq?` identity: - -- Forward struct/union ctype completed in this decl: tags promotion - finds the (already-in-main) ctype, walks its now-scratch `ext` and - rewrites it in place via `ctype-ext-set!` — the one allowed - ctype mutation, now happening from scratch context to a main-heap - record (safe). -- Same struct ctype referenced by multiple ptrs in the same decl: - map ensures all copies point at one main-heap ctype. - -### 3.3 Promote walkers - -``` -promote-bv ; bytevector-copy -promote-loc ; new record, copy file-bv via map -promote-tok ; new record, recurse on value (if bv) + loc + hide -promote-ctype ; map-cached, branches on kind -promote-sym ; new record, copy name-bv, promote type, copy slot if bv -promote-fields ; (name-bv ctype offset) list -promote-fn-params ; (name-bv-or-#f . ctype) list -promote-enum-members ; (name-bv . fixnum) list -``` - -All short, all leaf-recursive. Scratch context calls main-heap -allocators after `(use-main-heap!)` is in effect. - -### 3.4 Promotion order - -`world-tags` first (struct/union/enum identity anchors), then -`world-scope` and `world-str-pool`. The map preserves identity -across the second pass. - -### 3.5 Iter buffer copy - -`promote-iter-buffers!` walks `(tok-iter-buf pp-iter)`, -`(tok-iter-buf lex-iter)`, and pp-state's `up-pending` / `out-buf` / -`cur-file` / `macros`. Toks in those buffers are scratch-allocated by -lex; promote each. - -For tcc.flat.c (no macros, no #line): typically 0–2 toks in pp-iter -peek buf, 0–2 in lex-iter peek buf. Bounded constant. - -### 3.6 Delete `parse-fn-body`'s arena dance - -Lines 5328–5357 collapse to: - -```scheme -(define (parse-fn-body ps name dt) - (scope-bind! ps name - (%sym name 'fn 'extern dt #f #t)) - (%parse-fn-body-inner ps name dt)) -``` - -The hoisted `scope-bind!` / heap-mark / state-eq? gate / fn-meta -cleanup all go away. The body runs in scratch like everything else. -Block-statics, string literals, block-scope tags created inside the -body get promoted at the enclosing parse-decl-or-fn boundary along -with everything else. - -The doc block at 5305–5327 (the A→B→C arena pattern) shrinks to a -pointer at this doc. - -### 3.7 cc-init runs in main - -`cg-init`, `make-pstate`, the iter constructors, `%lex-scratch`, -the cg bufs — all the long-lived top-level state — must be allocated -in main. The default heap is main, so this is automatic provided -`use-scratch-heap!` is only called inside `parse-translation-unit`. - ---- - -## Expected impact - -For tcc.flat.c (608 KB, 18 896 lines): - -| state | predicted high water | -|--------------------------------|---------------------:| -| current | heap-exhausted >256 MB | -| post Phase 1 (refactors only) | unchanged — still exhausted | -| post Phase 2 (prims, unused) | unchanged | -| post Phase 3 (boundary live) | scratch ≤ 16 MB peak; main ≤ 30 MB (cg bufs + persistent decls). Total ~45 MB. | - -The fixed cost is `cg-init`'s pre-allocated bufs (~12 MiB) plus -linear-in-decls persistent state in main heap. Per-decl peak in -scratch is bounded by the most-allocating single decl (deep-nested -declarators, large struct bodies) — well under 16 MiB on observed -inputs. - -## Migration order - -Phase 1 lands as 4–6 small commits, each independently testable. -None touch heap behavior; existing tests cover them. - -Phase 2 lands as one commit to scheme1 + a scheme1-level test. cc -is unaffected (default heap is main; cc never calls the new prims). - -Phase 3 lands as one commit to cc.scm. The cc test suites and -tcc.flat.c probe (TCC-TODO.md repro) are the validation. - -## Open questions - -- **Do we want to also wrap `pp-iter-pull` per-token?** Inside - scratch, it costs nothing — the env-cons cells go to scratch and - reset at the next decl boundary. Probably no, simpler. -- **Promote-map as alist vs. hash-table.** Per-decl entry count is - small (< 100 typical, < 500 worst case), so alist is fine. Hash - is a scheme1 capability change anyway. -- **Do we need an `(in-main-heap thunk)` wrapper** that - save/use-main/run/restore for inline copy-out points? - Probably yes — makes promote walkers callable from inside other - contexts without manual juggling. Decide during Phase 3 build. - -## Related - -- [TCC-TODO.md](TCC-TODO.md) — the heap-explosion blocker this - plan resolves. -- [CC.md](CC.md) — the C subset the cc accepts. -- The deleted [CC-STREAM.md] (visible at git rev `67e43e7`) — prior - plan that turned the pipeline streaming. This plan addresses what - remained after streaming.