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.