# Lower phase — L7 flip landed (2026-10-08) Branch **`ast-stage-c`**. Tag **`v3-lower7-flip`**. This session did three things: (1) **flipped the emitter** so `Lower` is the only active emitter, (2) **deleted the inline emit** from the statement and declaration productions, and (3) **started the value/emit decoupling** so expression emit could go too — in the process it found and fixed a **real latent bug** in the VAR-actual note ring. Everything is verified with `compiler/run_tests.sh` (**204/204**) and `bootstrap/fixpoint.sh` (**FIXPOINT OK**) after every step. ## 1. The switch is flipped (`857c221`) The driver (`compiler/src/compiler.frm`) now always runs the per-unit `Lower` emit: `BeginSession`/`BeginUnit`/`LowerUnitNow`/`EndSession`. The grammar's inline emit is still present but runs suppressed (`SetNoEmit`) and is discarded. `-lower` is now test-only — it does not change *what* is emitted, only names the image `L.ssa` for the byte-compare harness. This was a no-op flip because `Lower` had already been verified byte-identical to the legacy inline emit on every single-file test and every session. FIXPOINT OK, 3,553,701 bytes. ## 2. Inline emit deleted from statements and declarations | commit | production group | |---|---| | `e5830b2` | builtin statements (`EXIT`, `INCL`/`EXCL`, `INC`/`DEC`, `NEW`, `DISPOSE`) | | `ce37247` | control flow (`IF`/`WHILE`/`REPEAT`/`LOOP`/`FOR`/`CASE`), `RETURN`, `HALT` | | `acc4e91` | declarations/lifecycle (`DeclVar`/`DeclConst`, module/impl/nested/class `BeginInit`/`EndInit`/`BeginBody`/`EndModule`, `FlushPend`, `FwdPatch`) | Not mechanical: many `QbeGen.*` calls are semantic queries or parser state, not emit (`TopLoop`/`PushLoop`/`PopLoop`, `TopWith`/`PushWith`, `IsImm`/`Fold2`/`NormInt`, `LocFull`, `ConstInt`, …) and must stay. ## 3. A real bug found and fixed (`7de78bb`) Removing the grammar's `QbeGen.DeclVar` exposed a latent mis-resolution: `Strings.Replace`'s `src` VAR actual became `$Strings_nl`, so `Showcase11` no longer linked. Root cause: **`QbeGen.AddrOfVal` scans the whole 256-entry `nVal`/`nAddr` VAR-actual note ring, and `RestoreSession` restored `nn` but never cleared the ring.** The grammar's suppressed parse-time `NoteAddr` entries survived into `Lower`; their keys are temps that restart per function, so a stale entry could false-match. Removing `DeclVar` (fewer `AllocLocal`/`NewTemp` calls) shifted those keys and exposed it. Fix: `RestoreSession` clears the ring. `Lower` re-notes its own VAR actuals, so an empty ring is correct. The fix is output-neutral on `Showcase11` and corrected some latent resolutions in the compiler's own image. ## 4. Dead-code sweep (tree-wide, tests included) | commit | removed | |---|---| | `d730e80` | `LowerSession`, `LowerUnit`, the whole `Can*` family, `SetLower`/`lowerRequested` | | `e269828` | `QbeGen`'s `CloseModule`, `FwdPatch`, `FwdElemPatch`, `Remark` | | `241c2c9` | `SymTab`'s `CurModule`/`CurUnit`/`ProcDepth`/`ParamName`/`ModDefined`/`ModImplemented`/`Qual*`/`NestArray`/`NewSub`/`SetElemCheck`/`SetFor`/`UnaryCheck`/`FwdVarKind`, `QbeGen.StrLen` | | `b0ef2bf` | `AST.TxtLen`, `AST.SetKind` (`AST.AddChild` stays: `t_ast.mod` uses it) | A scan over `src/`, `tests/`, `runtime/`, `stdlib/`, `tools/` now reports no unreferenced procs in `AST`/`SymTab`/`QbeGen`/`Lower`. Still needed until the inline emit is gone: `SaveSession`/`RestoreSession`/`SetNoEmit`, and `MaterializeGlobals`/`MaterializeReplace` (live, from `LowerOneUnit`). ## 5. Value/emit decoupling (started) The expression value is also the front-end's constant value (`Expr`'s `q` feeds `SymTab.SetSymVal`/`Lower.NoteConstVal` and `IsImm`/`ConstInt`), so it cannot simply be dropped. The decoupling marks runtime results with a **non-constant sentinel `"@"`** (`IsImm`/`ConstInt` reject it), keeps constant folding (`NegFold`/`Fold2`/`StrFold`) and every check. | commit | production | |---|---| | `6001af6` | `SimExpr` (unary `-`, `OR`, UString/CHAR concat, set binops, arithmetic) | | `5fb26e7` | `Term` (`AND`, set intersection, `*` `/` `DIV` `MOD`) | | `4f2afa0` | `Expr` relation (`=`,`#`,`<`,`<=`,`>`,`>=`,`IN`) | | `d469c78` | `Fact`'s always-runtime branches (ResultComp `ElemLoad`, zero-arg calls, `ADR`, `SHIFT`/`ROTATE`, open-array `HIGH`/`LEN`, `CHR8` range check) | Constant-producing builtins (fixed-array `HIGH`/`LEN`, `SIZE`/`TSIZE`, `MAX`/`MIN`, `ORD`/`CHR`/`VAL`, …) keep their folding paths. ## The remaining coupled change `Design` → `Fact` → `ArgList` → `ActParam` plus the `NoteAddr`/ `AddrOfVal` chain must move **as one unit**: `Fact` records `NoteAddr(q, qd)`/`AddrOf(qn, qa)` and `ActParam` recovers the VAR-actual address with `AddrOfVal(qe, qa)`, and the note key is the actual's *emitted* value. Verified: dropping `ArgList`'s `CallBegin` alone (fewer `NewTemp`s) shifts the keys and breaks calls (75 suite failures). To finish: `Design` emits nothing (literal for a constant, `"@"` otherwise), `Fact` drops the notes, `ActParam` replaces `AddrOfVal` with an AST-kind addressability check and drops `CallArg`, then `ArgList` drops `CallBegin`/`CallEnd`; afterwards `NoteAddr`/`AddrOfVal`/`AddrOf` in `QbeGen` are dead. Then: retire the `-lower` test flag and the now-degenerate byte-compare tests, and delete `SaveSession`/`RestoreSession`/`SetNoEmit`. ## Fixpoint sizes across the session | step | bytes | |---|---| | flip | 3,553,701 | | statements + declarations + bug fix | 3,538,557 | | dead-code sweep | 3,420,733 | | value/emit decoupling (expr layer) | 3,406,951 | The functional goal — **`Lower` is the only emitter** — is met. The rest is architecture/cleanliness.