Part 1 (docs/plan-two-phase.md) gave the compiler an AST: parsing now
builds nodes and Lower emits from them. What is still not split
is semantic analysis: name binding, type resolution and all 2xx
diagnostics still run during parse, inside the grammar's actions.
This document designs the second split, which removes the last forward-reference machinery and makes forward references ordinary.
Forward references — a body naming a global, a CONST naming a later
type — are still special-cased:
Design tolerates an undeclared name inside a
procedure, creates a placeholder alias (SymTab.FwdVarRef) and emits
an @ marker;ClPtr-shaped, so the type checks must be made
lenient (IsFwdVar guards in Assignable, ArithCheck,
OrdCheck, Expr);FwdVarFlush) reports the ones that never
resolve (201).All of that exists only because checks run before the module's later
declarations are known. A resolve/check pass that runs after parsing
would let every reference bind first and check second, so the mechanism
could be deleted. It would also unlock what depends on it: aggregate
(SET/record) values in CONST, forward procedures without a
DEFINITION, and a cleaner statement-context story.
compiler/src/M2.atg is 3,489 lines with 171 SemError sites and
~1,061 SymTab.* call sites (190 distinct procedures) across its
production actions. Lower.mod (2,337 lines) emits from the AST and
already does some late binding (e.g. it resolves a forward
designator's name at lower time), but it is an emitter, not a
checker: it assumes the SymTab entries are already correct.
So the work is a relocation: move the semantic decisions out of the per-production actions into a walk over the AST, run after the unit's declarations are complete.
per unit:
1. Parse -> AST + a *declaration* skeleton in SymTab
(names and kinds only; no type checks, no forward
errors)
2. Resolve/Check-> walk the AST: bind identifiers to symbols, resolve
and complete types, check assignments/arity/2xx,
emit all diagnostics
3. Lower -> walk the AST (unchanged) and emit QBE
4. Emit -> unchanged (buffered image, EndModule)
The crucial ordering change: step 2 runs after the whole unit (and its imports) are parsed, so a forward reference simply binds to a symbol that now exists.
Grouped by where the check lives today:
| Category | Today (grammar actions) | Notes |
|---|---|---|
| Declaration entry | SymTab.Enter/EnterPending/FixPending in ConstDecl/TypeItem/VarDecl/ProcHeading |
keep in parse: names/kinds are the skeleton |
| Type constructor checks | Type/ArrayType/RecordType/SetType/PointerType actions |
move: bounds, element/base resolution, 230s |
| Expression typing | Expr/SimExpr/Term/Fact/Design sets t on every node |
move: fill AST.SetTy, ArithCheck/RelCheck/Assignable |
| Statement checks | AssOrCall/IfStat/WhileStat/ForStat/CaseStat/ReturnStat |
move: lvalue/type/232/214 checks |
| Call checks | ArgList/ActParam (arity 233, formal type, VAR-ness) |
move: against the bound callee |
| Class/method checks | MethodHeading/ClassItem/method binding in Design |
move: method lookup, vtable slots |
| Forward tolerance | Design placeholder + IsFwdVar + FwdVarFlush |
delete once 1–7 are late |
Lower currently re-derives a little of this (it calls ClassOf,
ArrayElem, ...). After the split, Lower should read the types the
checker stored (AST.Ty) instead of re-deriving, so the two agree.
NkIdent/NkDesignator gets its resolved symbol
recorded on the node (a new AST field, e.g. sym/uid), not
re-looked-up repeatedly. A designator that never binds is 201
here (replacing FwdVarFlush).ty slot; the walk sets
it. Aggregates get their descriptor type; sets their word count.Enter).SemError(n) for 210–222, 231–235 move to the
walk. Parse-time errors stay: 200 (duplicate), 201 for a declared
name that is malformed, 202 (module name).Keep the old (parse-time) check path until the new one passes, exactly as the L7 flip did.
Branch resolve-check off v3-generics-inc2.
proc Resolve(n: AST.Node) (and helpers) in a new
Check.def/Check.mod; call it from the driver between Parse and
Lower.LowerUnitNow, gated by a flag. It starts as a no-op.Type/Array/Record/
Set/Pointer) into Check, deleting them from the actions. One
constructor kind per commit.Expr-family typing; store AST.SetTy;
make Lower read AST.Ty.Design
forward branch, FwdVarRef/FwdVarName/FwdVarType/
FwdVarResolve, IsFwdVar and its guards, the grammar's
FwdVarNote/FwdVarFlush, and the @ marker.-resolve/legacy byte-compare over the suite to
prove equivalence, then remove the flag and the old path../bootstrap/fixpoint.sh — byte-identical stage2/stage3.
The hard gate, as always.run_tests.sh (190/190 today) after every increment.tools/v3-corpus/corpus.sh — the undeclared identifier /
forward-reference buckets should shrink; track compile-OK counts..ssa for the whole suite should match until a feature deliberately
changes.Check.mod
and M2.atg until the flip, so Check must respect
declaration-before-use and the array/VAR-formal limitations that bit
us in inc2.Lower reading unchecked types during the transition — keep
Lower deriving what the checker has not yet moved.SymTab.FwdVarRef, FwdVarName, FwdVarType, FwdVarResolve,
IsFwdVar; the grammar's FwdVarNote/FwdVarFlush/nFvarRefs and the
Design forward branch; the @ marker in Lower; the IsFwdVar
guards in SymTab.{Assignable,ArithCheck,UnaryCheck,OrdCheck,EqCheck}
and in Expr.
Branch resolve-check off v3-generics-inc2 (ba9d042). Baseline:
suite 190/190, fixpoint OK (3,472,663 bytes). Start with §6 step 1.