Delete SymTab.FwdVarRef/FwdVarName/FwdVarResolve, Check.NoteForward/
FlushForward and the Design forward branch, plus the Op=1 marker —
by binding names before they are checked.
Design defers a name it cannot find inside a procedure because it
may be a module-level declaration that appears later in the file.
The resolve/check pass happens after the parse, so it cannot help with
binding: binding happens during the parse, and the declaration simply
has not been read yet. The only fix is to read the declarations before
the bodies — a two-phase (or two-pass) parse.
The grammar/parser stays almost entirely as-is; only the order in which the parser visits text changes.
ProcDecl sees a
module-level PROCEDURE, record the body's source span and consume
its tokens without parsing them, then carry on with the remaining
declarations. Result: every module-level name is in SymTab once the
module's declarations are done.Block production. Every module-level name now
resolves, so no forward reference is ever created.Consequences:
Design's forward branch never fires → the whole FwdVar* path is
dead and removable.scanner.frm, and expose it in M2S.def's
template): PROCEDURE SeekTo (p: INT32) — set bp0/bp/nextPos to
p and recompute line/col (scan from offset 0 counting CHR(10);
CharAt already indexes the in-memory buffer, so this is O(p)).PROCEDURE keyword, consume tokens counting block openers
{PROCEDURE, IF, CASE, WHILE, FOR, LOOP, WITH, RECORD}; stop when the
count returns to 0 — that END closes the procedure. BEGIN is
not an opener (a block's BEGIN…END shares the declaration's
END), and REPEAT…UNTIL/EXIT/RETURN are not counted.
Verify afterwards that the token is END and the following identifier
equals the procedure name (guards a mis-skip).ProcDecl (module level only): skip + record the
span; the declaration node is a placeholder NkProcDecl.Block) driven at the
end of Unit, walking the recorded spans.-twophase (default off) until the suite
and the fixpoint are byte-identical, then flip.Make every expression/statement action resolution-free and bind/type/
check in Check, walking with scopes mirrored from the AST. This is the
"true" two-phase and is much larger: it touches every body action, and
parse decisions that need types (typed brace literals, method calls,
L.x qualified names, HIGH/LEN) would have to be deferred too.
END. The
post-skip check (END + name) turns this into a clear error rather
than silent corruption.line, col, lookahead) and error
positions inside re-parsed bodies.Lower ordering. Bodies are parsed after the declarations, so
node allocation order changes. Lower walks by structure (order-
independent), but any parse-time ordinal that assumes file order must
be re-checked.SeekTo + the skipper behind the flag; inert.FwdVarRef/FwdVarName/FwdVarResolve
and Check.NoteForward/FlushForward, the Design forward branch,
and the Op=1 marker.Pieces 1–2 are a few hundred lines across the scanner frame, the grammar
and the driver, and are the largest single change in the resolve/check
thread — the reason FwdVar* could not be deleted by moving checks
alone.
-twophase, inert).
Each outermost procedure/method body is shadow-skipped at the token
level and the skip verified before the body parses normally:
scanner.frm: PushPos/PopPos save and restore the full
scanner state (cursor + all published positions + errors, so a
re-scanned bad character cannot double-count). 1-deep use, 8-deep
stack.M2.atg: SetTwoPhase switch, procNest counter (hooked once in
Block, so ProcDecl and method bodies are both covered),
TokIs (compares token text, immune to token renumbering) and
ProbeBody (opener counting + END <name> check, silent on
success, twophase probe mismatch to StdOut otherwise — never
into the listing).compiler.frm + parser.frm: -twophase flag plumbed to
M2P.SetTwoPhase at both driver Parse sites.MODULE (nested modules), PROCEDURE, CLASS
(incl. CLASS IMPLEMENTATION), RECORD (incl. variant CASE),
IF, CASE, WHILE, FOR, LOOP, WITH. BEGIN is not an
opener; REPEAT…UNTIL needs none.run_tests.sh gains a twophase_same gate (3 procedure-heavy
files byte-compared flag-off vs flag-on)..ssa with
the flag on and zero mismatches; fixpoint OK (3,517,333 bytes).Get from the new atg helper. gm2 accepts it (whole-scope), but the
self-hosted V3 compiler resolves single-pass: the bare Get
bound the imported M2S.Get(VAR sym) and stage 2 failed with
233s (plus knock-on 201s at END M2P). Fix: the helper advances
via Adv (M2S.Get(sym); INC(errDist) — exactly the frame's Get
body for a pragma-free grammar, where the scanner never returns a
token above maxT). Rule going forward: atg helpers must never
call a later-declared frame procedure by a name that collides with
an import.scanner.frm: SeekTo(p) — replays NextCh from offset 0 over
the in-memory buffer (O(p), no file I/O), so curLine/lineStart/
lastCh match a fresh parse exactly. Published pos/len still
describe the previous token afterwards — caller must Get
immediately (same contract as the priming Get in Parse).M2.atg: span table (nSpans/spanName/spanStart, per-parse,
1024 entries) recorded in ProbeBody; a SeekTo(start)+Adv
round-trip check per body (sym/nextPos equality) whose side
effects the trailing PopPos discards — mismatch prints
twophase seek mismatch, silent otherwise.probe/seek mismatches, zero
.ssa diffs; suite 196/196 (gate now greps both mismatch
kinds); fixpoint OK (3,584,690 bytes). Positive controls: temp
print showed the check firing per body with the flag on (reverted),
and a truncated file reports probe mismatch.NoteSpan (defined after ProbeBody, not in M2P.def) and
stage 2 failed 233 + END M2P knock-ons. Bisected to the rule:
V3 has no forward procedure tolerance at all — even P; with
P declared two lines later is 233 (+201 at EOF). The existing
sources self-compile only because every .mod is paired with its
.def, which the driver parses first (so e.g. SemError is
already declared when NoteLabel's body compiles), plus strict
declaration-before-use inside modules. Fix was ordering (declare
NoteSpan before its caller), same as the CopyNm precedent.
The FwdVar tolerance covers module-level variables only.ses_ebc94df66ffeTZpjrrFnZS1GsF) plus the
closer-split (phase-1 skip must replicate CloseProc/EndFunc/
ScopeLeave effects without a body); then 2c deferral wiring
(skip + placeholder + end-of-unit re-parse worklist, nested spans
append during phase 2), byte-compared; then 2d flip + delete.ses_ebc7fa941ffeeUpNEqm1Crqav5): suppressed inline
emit = emission only — every QbeGen counter still advances and is
discarded per unit by RestoreSession, while Lower's tables are
permanent session state. Per-proc call shapes mapped for
heading/FORWARD/EXTERNAL/body/method/DefUnit cases.SymTab: DeferReset (per-parse, from the M2 action),
DeferBody() (saves curProc + curScope at skip time, returns
index / 65535), ReenterDeferred(idx) (restores saved proc scope —
params visible, locals entered once — and re-arms procStk),
ParamName (i-th formal name, for the re-parse QbeGen replay).
Index-keyed (no nested same-name ambiguity); no SymDesc layout
change (side table, like fwdVarScope).Lower: ReenterScopeProc(ty) — pushes current owner, resumes a
noted entry without re-registering, so the re-parse Block-end
ScopeLeave pops back and nested notes attach correctly.M2.atg: spanIdx table fed by DeferBody() in NoteSpan.defer <name> firing in order per body (reverted).AbortFunc close per the
FORWARD convention; re-parse replays BeginFunc+FuncParams and
pairs with EndFunc); registrations run once (skip pass) with
the re-parse on resume paths; nested spans append to the table
during phase 2 (worklist); DefUnit/EXTERNAL/FORWARD paths unchanged.-twophase).
DeferSkip (from Block, only for plain ProcDecl
bodies — methods stay inline) skips the body for real, leaves the
scanner at the matching END so Block's trailing END ident
consumes it, records the span, and ProcDecl closes like FORWARD
(AbortFunc, no body node). Phase 2: an inlined loop at both
module ends (ProgModule/ImplUnit) SeekTos each span, re-arms
SymTab/Lower/QbeGen, parses Block, attaches body + block
decls to the proc node, and closes with ScopeLeave/CloseProc/
EndFunc. Scanner/parser state and the unit's astDecl/astStmt
are saved and restored around the loop.astDecl/astStmt (now saved/restored), and PopPos restored
errors, wiping every diagnostic recorded during the repass (now
the post-loop count is re-instated — this had silently dropped
210/232 etc. in deferred bodies).-> 42 on t_fwdvar/t_fwdsel/
t_fwdprop, incompatible assignment on t_bad_fwd);
fixpoint OK (3,620,461 bytes).-twophase with
zero signals; 138/142 byte-identical. The 4 that differ are
exactly the forward-reference tests (t_fwdvar/t_fwdsel/
t_fwdprop/t_bad_fwd): deferral makes a forward variable ordinary,
so it compiles like a normally-ordered one — the legacy FwdVar
path is the divergent one (it drops a loadw). Deferred output for
those matches the normal codegen (verified against a reordered
fixture) and runs to 42. Sessions DBasicProg/TString/
Showcase12 byte-identical.Conversions.mod in the Library1
session), -twophase records a spurious ident expected whose
position lands inside a repassed body, even though the emitted
.ssa is identical to the default. So the repass parses to the
correct AST but trips one extra SynError. Next: locate it (the
candidates are the pos/len "previous token" model interacting
with SeekTo, or a construct where the repass consumes one token
more). Until then the pass stays opt-in and the default path (which
never defers) is what ships.ident expected: DeferSkip does a
PushPos for its failure-restore, but on its success path it
left the scanner at END and never popped — leaking one snapshot per
deferred body until PushPos started failing (8-deep), after which
the module-end PopPos restored a stale slot and the parser's
SynError read a stale nextPos (hence a bogus position inside an
earlier body). Fixed with a new scanner.DropPos (discard the top
snapshot without restoring), called on DeferSkip's success path.
Also removed the now-dead ProbeBody (superseded by DeferSkip).Library1, and
the whole suite passes with deferral on. doTwoPhase now
defaults to TRUE; -notwophase restores the old inline parse
(debug escape); -twophase is still accepted.t_bad_fwd still errors; -notwophase reproduces the legacy
output for forward-free code (gate).FwdVar*/NoteForward/FlushForward and the
Design forward branch. They remain exercised by nested-local
forwards (a nested proc's body naming a parent local declared
later), which the module-level deferral does not cover. That case
is a pre-existing limitation too (it fails to link under
-notwophase as well), so deleting FwdVar* needs either nested
deferral or accepting the limitation — a separate step.t_nestdeep/showcase10 produced wrong results), so
deferral stays module-level.SymTab.FwdVarRef/FwdVarName/FwdVarResolve and
their tables; Check.NoteForward/FlushForward (and the nFwd
bookkeeping); the Design forward branch (FwdVarRef + Check.
NoteForward + Op=1 marker) — an undeclared name inside a
procedure is now a plain 201; plus the dead Check.SelType/
LeafName.FORWARD needed) — the deferral
makes forward procedure calls ordinary too; a nested proc naming
a parent local declared later is a clean 201 (was a broken image).t_fwdproc -> 7,
t_bad_nestfwd undeclared identifier); fixpoint OK
(3,574,843 bytes); corpus 365 OK vs 358 with -notwophase
(+7 files, 0 regressions, 0 crashes).Check
is now mostly a vestigial walk (PropNode/EvalExpr/CheckAssign
re-type/ re-check nothing once no node is Ty = -1).Check removed, scaffolding dropped.
PropNode showed it did re-type
Ty = -1 nodes in 31 tests, but gutting Resolve left the emitted
.ssa byte-identical across all 141 suite files (and
201/201). So the walk was cosmetic: Lower recomputes what it
needs, and Check emitted no diagnostics (its only source,
CheckAssign, was reachable only via the deleted Op=1 marker).Check.def/Check.mod outright, its driver calls, the
-noresolve option (it only gated that pass), and the atg import.
build.sh drops Check; fixpoint.sh needs nothing (it stages
src/*.def/*.mod dynamically). Also removed the now-unused
procNest and the always-true L7 scaffolding flag twoPhase (13
sites in the atg, all inert guards around AST building)..ssa byte-identical on spot-checks and the 141-file set.Parse (builds AST; bodies deferred and
re-parsed at unit end) -> Lower -> Emit. There is no separate
resolve/check pass because binding is late by construction.