# 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 moved **as one unit** (commit `90d2529`): `Fact` records no notes, `ActParam` checks VAR-actual addressability by AST kind (`NkDesignator`, or `NkStrLit`), and `ArgList` drops `CallBegin`/`CallEnd`. `Design`/`ResultComp` (`0667290`), assignment (`0ebe92e`), the remaining runtime value ops (`f59b784`) and the `ProcHeading` output (`02f2526`) followed. ## Completion (2026-10-08) The inline emit is gone from every production where it could be removed cleanly: statements, control flow, declarations, `SimExpr`/`Term`/ `Expr`/`Fact`, `Design`/`ResultComp`, `ArgList`/`ActParam`, assignment, the runtime builtins, and `ProcHeading`'s output. A runtime value is the non-constant sentinel `"@"`. What intentionally remains in the grammar is the **value layer** (`CopyOp`, `IsImm`, `IntStr`, `Fold2`, `NormInt`/`NormReal`/`NormLit`, `Conv*`, `CharVal`, `StrFold`/`StrEq`, `CapQ`/`AbsQ`/`NotQ`, `UStrLen`) that computes constants and the `q` `Lower.NoteConstVal` consumes, plus the **value/parser-state API** the checks depend on: the `ProcHeading` lifecycle (`BeginFunc` resets `nPar`/opens the scope, `FuncParam` is a check, `EndFunc`/`AbortFunc` close it), `PushWith`/`PopWith` (`WITH` field resolution), `PushLoop`/`PopLoop` (`EXIT` validity), and `TopWith`/`TopLoop`/`LocFull`. One trap worth remembering: an empty `(. .)` action in `ProcDecl` generated a bare `;` empty statement that **gm2 accepts but the V3 self-host grammar rejects** — the suite passed while the fixpoint failed. Then: retire the `-lower` test flag and the now-degenerate byte-compare tests (**done**: `-lower`/`IsLowerParam`/`lowerTest`/`EndSession`'s `testSuffix` removed, 23 `lower_ok` cases dropped, suite now 181), and delete `SaveSession`/`RestoreSession`/`SetNoEmit` (still used by the per-unit flow). ## 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 | | decoupling complete (Design/Fact/calls/assignment/ProcHeading) | 3,377,935 | The functional goal — **`Lower` is the only emitter** — is met. The rest is architecture/cleanliness.