summary_lower_l7_flip.md 6.6 KB

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 <mod>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.