|
|
@@ -0,0 +1,97 @@
|
|
|
+# Two-phase parse (design) — eliminating the forward mechanism
|
|
|
+
|
|
|
+## Goal
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+## Why post-parse checking is not enough
|
|
|
+
|
|
|
+`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*.
|
|
|
+
|
|
|
+## Design A (recommended): module-level body deferral + re-scan
|
|
|
+
|
|
|
+The grammar/parser stays almost entirely as-is; only the *order* in which
|
|
|
+the parser visits text changes.
|
|
|
+
|
|
|
+1. **Skip module-level procedure bodies.** When `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.
|
|
|
+2. **Re-scan and parse the bodies.** At the end of the module, for each
|
|
|
+ recorded span: seek the scanner to its start and parse the body with
|
|
|
+ the existing `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.
|
|
|
+- The body productions are **reused unchanged** (both the skip and the
|
|
|
+ re-parse reuse existing machinery), unlike Design B below.
|
|
|
+
|
|
|
+### Pieces
|
|
|
+
|
|
|
+1. **Scanner seek** (`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)).
|
|
|
+2. **Token-level body skipper** (grammar helper): starting at the
|
|
|
+ `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).
|
|
|
+3. **Deferral** in `ProcDecl` (module level only): skip + record the
|
|
|
+ span; the declaration node is a placeholder `NkProcDecl`.
|
|
|
+4. **Body re-parse**: a new grammar entry (reusing `Block`) driven at the
|
|
|
+ end of `Unit`, walking the recorded spans.
|
|
|
+5. **Flag**: all of this behind `-twophase` (default off) until the suite
|
|
|
+ and the fixpoint are byte-identical, then flip.
|
|
|
+
|
|
|
+## Design B (rejected): syntax-only bodies + binding walk
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+## Risks / open questions
|
|
|
+
|
|
|
+- **Skipper exactness.** A mis-count mis-skips to the wrong `END`. The
|
|
|
+ post-skip check (`END` + name) turns this into a clear error rather
|
|
|
+ than silent corruption.
|
|
|
+- **Scanner state after a seek** (`line`, `col`, lookahead) and error
|
|
|
+ positions inside re-parsed bodies.
|
|
|
+- **AST/`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.
|
|
|
+- **Nested procedures** are inside the deferred span, so they are
|
|
|
+ re-parsed with the same mechanism (their own locals are declared
|
|
|
+ before their statements, so they need no deferral themselves).
|
|
|
+
|
|
|
+## Milestones (each with the fixpoint green)
|
|
|
+
|
|
|
+1. `SeekTo` + the skipper behind the flag; inert.
|
|
|
+2. Module-level deferral + body re-parse; byte-compare against the
|
|
|
+ default across the suite.
|
|
|
+3. Flip the default; delete `FwdVarRef`/`FwdVarName`/`FwdVarResolve`
|
|
|
+ and `Check.NoteForward`/`FlushForward`, the `Design` forward branch,
|
|
|
+ and the `Op=1` marker.
|
|
|
+
|
|
|
+## Cost
|
|
|
+
|
|
|
+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.
|