소스 검색

docs: design for a two-phase parse (deleting the forward mechanism)

The only way to delete FwdVar*/NoteForward is to bind names before they
are checked, i.e. read a module's declarations before its bodies.  Design
A (recommended): record and skip module-level procedure bodies, then
re-scan and parse them once the declarations are in SymTab, reusing the
existing body productions.  Records the pieces (scanner seek, token-level
body skipper, deferral, re-parse), a flag-gated milestone plan, risks and
cost.  Design B (syntax-only bodies + binding walk) is recorded and
rejected as much larger.
Eric Streit 2 일 전
부모
커밋
a63b3cfdf9
1개의 변경된 파일과 97개의 추가작업 그리고 0개의 파일을 삭제
  1. 97 0
      docs/plan-two-phase-parse.md

+ 97 - 0
docs/plan-two-phase-parse.md

@@ -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.