# Lower phase — slice 3 (procedures): groundwork + a blocking finding Branch `ast-stage-c`. Suite **182/182**; fixpoint **OK** (**3,147,849 bytes**; L2 was 3,123,487). L3 is **not** enabled — see the finding; it stays out of `CanLower`, so the comparisons cannot false-fail. ## What landed (groundwork, inert) - **Call support** in `Lower`: `NkCall` in expressions (value) and statements (discard), mirroring `ArgList`/`ActParam` — `CallBegin`, per-actual `CallArg` (VAR via `AddrOfVal`, value via `ArgClass` + real widening), `CallEnd`. `CanCallArgs` checks arity. - **`SymTab.ParamName`** (new, read-only) — the i-th formal's name, for the header. - **Procedure headers** in `LowerDecls`: `Mangled`/`BeginFunc`/`FuncParam` for each formal/`SetFuncRes`/`EndFuncHeader`/`EndFunc`. - **`Block` now records its local `NkDeclSeq`** (`astBlkDecls`), stored as `NkProcDecl` child 2 — so a proc's `VAR` locals are in the AST. - **Callee extraction fix**: `NkCall` child 0 is an `NkDesignator`, not a bare `NkIdent`; `CalleeName` unwraps it (this fixed calls mangling as `$_0`). ## The blocking finding — scopes close before `Lower` runs The L3 comparison does **not** match, and the reason is structural, not a missing emit: `SymTab.CloseProc` (and `EndUnit`) **close the scope** that the inline actions lowered *inside of*. `Lower` runs at the end (module end), by which time a procedure's locals/params are no longer resolvable — so `LowerDesign("s")` fails and the body emits nothing. - Module-level names survive until `EndUnit`, so L0–L2 (program body) work. - Procedure bodies do **not**: they must be lowered while the procedure scope is still open. This is exactly the "flip" the plan anticipated (§3 phase 3): the walk cannot be one end-of-unit pass for *everything*; body lowering has to happen at each procedure while `SymTab` still has its scope. ## Option 1 was attempted and is *not* a small hook The chosen route — grammar calls `Lower.LowerProc(node)` right before `SymTab.CloseProc` — requires the `L` image to be built **concurrently** with the legacy image. `QbeGen` has a **single** session buffer and a single set of counters (`nTemp`, `nLoc`, `scopeTop`, …), so two emitters interleaved **corrupt the legacy path** — the very path that must stay byte-identical for self-hosting. A second image buffer (`altBuf` + `SetAlt`/`WriteAlt`) was added and then **reverted**: it does not solve the shared-counter problem, only the buffer. Concurrent emit needs either: - **two full independent backend states** (a second `QbeGen` instance / duplicated counters — invasive), or - **not running both at once**: under `-lower`, suppress the legacy emit for the unit and lower only from the AST — i.e. the **flip** (L7) applied to the test path. Lowering then happens *during* the parse, so every scope is open at the right time and the whole class of "closed-scope" problems disappears. The second is the clean destination and subsumes L3. Recommendation: make the flip the next dedicated step, rather than fake option 1 with two entangled emitters. ## Verified this slice - L0–L2 comparisons still pass (`t_exit`, `t_arith`, `t_lower1`, `t_lower2`); `t_lower3` is **gated out** of `CanLower`, so the tree stays green. - The call-mangling and callee-designator fixes are exercised by the parse (and will be by the flip). ## Next Start the flip: a backend-level emit gate so `-lower` suppresses the legacy emit and the grammar lowers the whole unit from the AST during the parse. That completes L3 and is the basis for L4+.