Decision: no restart. The AST/two-phase change is a middle-of-the- compiler refactor, not a rewrite. We reuse everything and convert the frontend's pass structure on a dedicated branch.
This document is the plan of record; docs/session-handoff-2026-10-01.md
has the short version.
Everything painful in the last two sessions traced to one cause: lowering happens during parsing, so nothing can be looked at twice. An AST-first compiler fixes that — but a from-scratch restart would throw away the parts that are expensive and already correct:
| Component | Reuse on restart? | Reuse on refactor? |
|---|---|---|
Coco/R frame, scanner/parser driver (M2S/M2P/compiler.frm) |
mostly | yes |
SymTab — descriptors, scoping, checks, classes, vtables |
large, hard-won | yes |
QbeGen — emission, layouts, calls, sets, records, classes |
the whole backend | yes |
runtime shim, stdlib/, FileIO, fixpoint + corpus harnesses, tests |
yes | yes |
docs/ (language report, gaps, grammar) |
yes | yes |
AST.def/.mod (Stage A, tag v3-ast-stageA) |
n/a | already started |
M2.atg production grammar |
yes (recognition is fine) | yes |
M2.atg production actions |
rewritten | rewritten (same grammar) |
The one thing a restart legitimately replaces is the dialect/grammar — and the grammar is not the bottleneck. The pass structure is.
So: change what the grammar actions do (build nodes instead of emit), not what the grammar recognizes.
per unit:
1. Parse -> AST (declarations + bodies), SymTab entries for
names/kinds as declared (order-independent later)
2. Resolve/Check -> bind identifiers, complete types, 2xx diagnostics
3. Lower -> walk AST, call the existing QbeGen API
4. Emit -> unchanged (buffered image, EndModule)
Key property: step 3 runs after the whole unit's declarations are in
SymTab, so forward references resolve naturally. The text-patching
machinery and the Design placeholder path are deleted.
M2.atg flag TWO_PHASE (a grammar-level BOOLEAN or a
QbeGen.SetLower switch).Lower(n); run it in a test-only mode
(compile a unit, lower from the AST, compare to the old .ssa).ast-stage-c)Suggested slices, each ending in build.sh + run_tests.sh +
fixpoint.sh (revert the slice if either fails):
ast-stage-c,
docs/summary_two-phase-slice1.md): twoPhase flag; AST linked
into the compiler build; the integer leaf also builds NkIntLit.
Nothing consumes it. Green (178/178, fixpoint OK).ast-stage-c):
docs/summary_two-phase-slice2.md (spine, literals, parens, unary
minus via astCur + per-invocation astIsLit),
docs/summary_two-phase-slice3.md (Design: identifiers,
[i]/.f/^ selectors, qualified names), and
docs/summary_two-phase-slice4.md (calls/NkCall with actuals,
NOT, builtins as named NkCalls). Still NoNode: brace/set
literals, ResultComp suffixes; Lower + .ssa compare not
started. Green (178/178, fixpoint OK).ast-stage-c,
docs/summary_two-phase-slice5.md): StatSeq builds NkBlock;
assignment/call, IF/ELSIF/ELSE, WHILE, REPEAT, LOOP/
EXIT, FOR, RETURN, HALT build nodes. Deferred: CASE,
WITH, builtin statements (INCL/INC/NEW/…), brace/set
literals, ResultComp, and Lower. Green (178/178, fixpoint OK).
Finding: local arrays in a PROCEDURE are miscompiled
(docs/wip/local-array-bug.mod) — pre-existing, worked around here.ast-stage-c,
docs/summary_two-phase-slice6.md, docs/summary_two-phase-slice7.md):
DeclSeq builds NkDeclSeq; CONST/TYPE/VAR/PROC build
NkConstDecl/NkTypeDecl/NkVarDecl/NkProcDecl (types referenced
by the node ty index); astUnit holds NkUnit(name, decls, body)
for all three unit kinds; builtin statements build NkCalls.
Done. docs/summary_two-phase-slice8.md (CASE/WITH),
docs/summary_two-phase-slice9.md (brace/set literals),
docs/summary_two-phase-slice10.md (ResultComp + unbounded
NkBlock/NkDeclSeq), docs/summary_two-phase-slice11.md (imports),
docs/summary_two-phase-slice12.md (nested modules) and
docs/summary_two-phase-slice13.md (CLASS, + a TypeBlock
collection bug fix). Remaining: Lower + byte-compare, and the
MaxChild cap on CASE arms / WITH designators / call actuals.FwdPatchAll /
FixLoadClass / FixStoreClass, the Design forward-variable
placeholder, and pendVar* if superseded.docs/features.md, language-report.md,
analysis_gaps.md; tag../bootstrap/fixpoint.sh must print
FIXPOINT OK (byte-identical stage2/stage3). The compiler compiles
itself, so it exercises the new path on real, large input.run_tests.sh (178/178 today) after every slice.tools/v3-corpus/corpus.sh) — expect the undeclared
identifier / forward-reference buckets to shrink once the AST lands;
track the compile-OK counts..ssa vs old .ssa for
the test suite; they should match until a feature deliberately
changes.QbeGen.FwdPatchAll, FwdDesignator, FwdMangle,
FwdAddrOper, FwdPatch, FwdElemPatch, FixLoadClass,
FixStoreClass, BufReplaceAll; the grammar's FwdVarNote/
FwdVarFlush/nFvarRefs; the Design forward branch and
SymTab.FwdVar*; EXPR's IsFwdVar leniency.CONST (unblocks
ChanConsts/StreamFile, parked in docs/wip/); statement-context
result suffixes (F()[i] := x); GOTO/labels; typed CONST.M2.atg/QbeGen.mod must still compile under the
current compiler; keep declaration-before-use until the flip.Branch off v3-session-2026-10-02 (03f250e). Baseline: suite 178/178,
fixpoint OK (2,900,782 bytes). Start with §4 slice 1.