summary_two-phase-lower3-finding.md 3.5 KB

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