# Lower phase — L7 (the flip): status and plan (2026-10-05) Branch **`ast-stage-c`**. L7 = "flip: lower from the AST instead of inline; delete the inline emit + `Fwd*` machinery". ## Why L7 is not incremental The flip makes `Lower` the sole emitter. The obstacle is that `QbeGen` holds **one** session state, used by the inline emit *during* parse; a per-unit `Lower` emit would be clobbered by the next unit's inline emit. So the flip is a **big-bang**: either remove/suppress the grammar's 533 `QbeGen.*` call sites, or give `QbeGen` two states — neither is a byte-verifiable step. The pragmatic path is therefore a **session re-emit**: collect every unit's AST and re-emit the whole session with `Lower` after parsing. That is byte-comparable against the legacy session image and becomes the flip once the inline emit is removed. ## Started (`d30abfd`, tag `v3-lower7-scaffold`) - `Lower.LowerSession/LowerOneUnit/MaterializeGlobals`. - Driver (`compiler.frm`) collects `GetUnit()` per file and calls `Lower.LowerSession` under `-lower`. - `SymTab.Materialize` makes a unit's own VAR/CONST visible in `globScope`, so `GlobalRef` resolves `"_"` at session end (the unit scopes are closed by then). - Removed `Lower.Reset` from the `M2` root so the owned tables persist across a session. Still scaffolding (inline emit untouched); suite **200/200**, FIXPOINT OK (3,401,129 bytes). ## Progress (`ce2fc67`, tag `v3-lower7-session`) - **`DefUnit` now builds an AST** (proc headings recorded as `NkProcDecl`); `LowerDecls` replays a bodyless proc as a heading. - **Session re-emit works** for small sessions: `lower_ok_files` added, and `DBasicProg`, `DQualProg`, `TQual` (def-CONST), `TQualP` (def-PROC) byte-match. Fixes: image named after the program module; `MaterializeGlobals` also materializes TYPE/CLASS names; `SymTab.ReenterModGlobal` keeps nested-module globals' `"_"` symbols; `AST.Init` once per session under `-lower`. Suite **204/204**; FIXPOINT OK (3,405,813 bytes). ## Remaining gaps 1. **AST-arena size (fundamental).** `LowerSession` needs every unit's AST alive at once, but `AST.MaxNodes = 65536` is sized for one unit; the compiler's session overflows it (`MakeNode` → `NoNode`, later units dump `(nil)`). Growing the arena past the 4 MiB `sessBuf` is not viable. So the session re-emit **cannot** handle the compiler — the flip must instead emit **per unit** at each unit's END (AST valid, scope open), which is the two-state / inline-emit-removal problem. 2. **External procs.** `storage_prog` differs: Lower emits `$malloc_6(l link, ...)` where the legacy emits `$malloc(...)` — the `ext` flag in `Lower.NoteProc` is hardcoded `FALSE` and `Mangled`'s `ProcLink` fails after scopes close. 3. Definition-module proc replay (done), nested globals (done). ## The "hang" was a Lower bug, not a miscompile `LowerDecls` emitted a procedure's parameter loop even when `ProcInfo` failed, using an uninitialised `np` — so the loop ran ~forever inside `FindProc`. (`MaxProc = 64` is far too small for a whole session, so `ProcInfo` failed for most of the compiler's procs.) **Fixed** (`a9a8ecd`): the proc is skipped unless `ProcInfo` succeeds, and each `ParamInfo` is guarded. Also grew the AST arena/pool (`caeeeb5`) — the arena/pool data lines are compact `z` runs, so this is nearly free. ## Hash index attempted — also trips V3 miscompiles Implemented an open-addressing name index (`HashName`/`HashGetSym`/ `HashGetProc`) for `FindSym`/`FindProc` in `Lower`. Results: - With the index, the session re-emit on the compiler's sources became **fast** (`MaxProc=512` completed in <300 s; before it hung). - But the tables must hold the whole session (the compiler has **713** functions, so `MaxProc ≥ 713`), and at `MaxProc ∈ {768,1024}` the same run is **slow/segfaults** (`CopySpan` via `ParamInfo`) — the size/layout-sensitive bug again. - Worse, the hash itself makes the **self-host fail**: with the index in `Lower`, `m2s2` (built by `m1s1`) produces no output on the compiler input (stage 3). So the index triggers *another* latent V3 miscompile. Reverted to the green state (`a9a8ecd`). Suite 204/204. ## Bisecting the hash miscompile Reproduced with a **proc-only** hash (`HashName`/`ProcEq`/`HashPutProc`/ `HashGetProc`; `FindSym` left linear): the fixpoint still fails at stage 3. - `m2s2` (V3-built) **works on small programs** (`t_exit`, `t_arith`, `t_lower5set`, `t_classmethod`) but **aborts (SIGABRT)** on `Lower.def + Lower.mod`. - Backtrace: `HashPutProc` ← `NoteProc` ← `ProcHeading` ← `DefUnit` — i.e. m2s2 crashes while parsing the definition module's proc headings. - The obvious cause — `(hashProc[s2] = 0) OR ProcEq(VAL(CARDINAL, hashProc[s2]-1), name)` evaluating the RHS (so `0-1` → huge index) — was restructured away, and a `ProcEq` bounds guard added; **neither fixed it**. So it is a deeper V3 miscompile of `HashPutProc`. Narrowing further needs many ~10-minute fixpoint cycles; reverted to green (`a9a8ecd`). Suite 204/204. --- ## Progress: per-unit flip unblocked (2026-10-05, after `a3ef897`) The `bad=T` that blocked per-unit emission was a **phantom**: it was the pre-existing `invalid call` errors in `Lower.mod` (forward references), now fixed by declaring the walkers in `Lower.def`. With that gone, the per-unit flip works end to end. ### Mechanism - Driver: `Lower.BeginSession` (open + `SetNoEmit`), `BeginUnit` (`SaveSession`) before each `Parse`, `EndSession` (write image). - `Lower.LowerUnitNow(unit, isProg)` after each `Parse`: `RestoreSession` → `SetNoEmit(FALSE)` → `LowerOneUnit` → `SetNoEmit(TRUE)`. - `QbeGen.EndModule` is a no-op under `NoEmit`, so the grammar's unit-end emit neither flushes nor writes. - The grammar's suppressed inline emit must be a **faithful shadow** of the legacy emit: `LocAdd`/`FuncParam` no longer skip `NewTemp` under `NoEmit`, so the temp/`NoteAddr` numbering aligns with Lower's emit. ### Name resolution - Whole-session tables: `MaxSym=4096`, `MaxProc=2048`, `MaxPar=32`. - Open-addressing hash index (`HashStr`/`SpanEq`/`HashPut`/`HashGet`, `HashSize=8192`) for `FindSym`/`FindProc`. The compiler session now lowers in ~1.5 s. (The earlier "hash miscompile" was a symptom of the miscompiled `Lower`; it is gone now that `Lower` is V3-legal.) - `CopySpan`'s source is a **VAR** open array: gm2 copies a value open-array formal to a stack VLA of its full length, which overflowed the stack for the multi-megabyte tables. - External procs: `Lower.MarkProcExternal` (called from the grammar's EXTERNAL branch) records `ext`; `LowerCall` binds external callees to their own name (no uid, no static link). - Indirect calls through procedure-typed variables (`CurrentCh`): `LowerCall` detects a proc-typed VAR/PARAM callee and uses `CallBeginInd`. ### Grammar AST fixes (also affect the two-phase AST generally) - `ActParam` saves/restores the enclosing call's `astArgs`/`astNArgs`: `Factor` reset `astNArgs` for designator arguments, so any call with two or more designator actuals kept only the last one. - `ImplUnit`/`ProgModule` reset `astStmt := AST.NoNode` after `DeclSeq`: the module-body child was the stale last procedure body, so modules without a module `BEGIN` (SysShim) grew a spurious `_init`. - `Lower.NoteConstVal`: const values are per-unit (every module has `EOL`), so they live in Lower's tables, not `SymTab` (which collided across modules after `EndUnit`). - `LEN`/`HIGH` added to `LowerBuiltinExpr`. ### Remaining gap The compiler session now lowers, but is **not byte-identical** to the legacy image yet: the first divergence is in `M2S.Get` (`$Get_61`), where Lower emits 148 labels vs. the legacy 430 — a large part of the `Get` body (a big `CASE ch OF …`) is missing. `lower_ok` single-file tests and the suite are green; the fixpoint is green (`-lower` is not used there). Suite **204/204**; FIXPOINT OK (3,695,508 bytes), session 0 errors. --- ## Progress: more Lower coverage (`132a981`, tag `v3-lower7-gaps`) First divergence moved to `AST.AddTxt` (line ~6961). Fixes: - **CASE chunking.** The arm list was capped at `AST.MaxChild` (8): `CaseStat`/`CaseAlt` appended arms with `SetChild(cnode, NChild, arm)`, which silently drops beyond 8. They now build a chunked arm sequence (`AstAppend`, container kind `NkBlock`), and `LowerCase` follows the continuation chunks. This was the big `M2S.Get` gap. - **`VAL(T, e)`** in `LowerBuiltinExpr`, same widen/narrow/convert chain as the grammar. - **Qualified calls** (`FileIO.ReadBytes`) resolve through Lower's proc tables (SymTab's scopes are closed); `CalleeAlias` returns the base name so the mangled symbol stays `_`. - **VAR actuals.** `LowerExprFull` records value→address with `NoteAddr`, mirroring the grammar's `Design`; `AddrOfVal` could not rely on the grammar's notes because its 256-entry ring wraps during a whole-session emit. - **Bare proc values** (`CurrentCh := CharAt`): `Resolve` recognises the proc table and `LowerProcAddr` emits the code address; sibling method names fall through to the THIS-binding path. ## Next blocker: symbol scoping `Lower`'s symbol table is a single **flat, latest-wins** table, so common names (`s`, `i`, `k`) collide across procedures. E.g. `HIGH(s)` in `AST.AddTxt` (an open-array parameter) resolved to a fixed array of 256 from another procedure. `lower_ok` tests are too small to collide. Two ways forward: 1. record a scope id per symbol and resolve within the active procedure chain, or 2. run `Lower` with `SymTab`'s scope open (the grammar-based call) and resolve locals via `SymTab` first — the earlier attempt only broke `EndModule`'s `ExitCode` lookup, fixable by materialising the program globals in `EndSession`. Suite **204/204**; FIXPOINT OK (3,711,028 bytes), session 0 errors.