Przeglądaj źródła

two-phase parse milestone 1: body-skip probe behind -twophase (inert)

Each outermost procedure/method body is shadow-skipped at the token
level and the skip verified (END + name) before the body parses
normally. Scanner gains PushPos/PopPos (exact state save/restore);
grammar gains SetTwoPhase/probe helpers hooked once in Block;
driver plumbs -twophase to both Parse sites. Silent on success;
mismatches go to StdOut, never the listing. run_tests.sh gains a
twophase_same gate (3 files, .ssa byte-compared off vs on).

Suite 196/196; 142 single-file tests plus session spot-checks and
the compiler's own sources byte-identical with the flag on, zero
mismatches; fixpoint OK (3,517,333 bytes).
Eric Streit 2 dni temu
rodzic
commit
6c64b46f4f

+ 24 - 0
compiler/run_tests.sh

@@ -461,6 +461,30 @@ expect_fail_files() {
 expect_fail_files d_badimp_prog "undeclared identifier" d_basic.def d_basic.mod d_badimp_prog.mod
 expect_fail_files sig "implementation does not match definition" sig.def sig.mod sig_prog.mod
 
+# --- two-phase probe gate (milestone 1): -twophase shadow-skips each
+#     outermost body and must be output-neutral (identical .ssa, no
+#     mismatch reports). Spot-checks procedure-heavy files. ---
+twophase_same() {
+  # $1 = test file (no dir)
+  name="$1"; mod=$(modof "$name")
+  rm -f "gen_ssa/$mod.ssa" "gen_ssa/$mod.off.ssa"
+  ./M2 "tests/$name" >/dev/null 2>&1
+  cp "gen_ssa/$mod.ssa" "gen_ssa/$mod.off.ssa"
+  if ./M2 -twophase "tests/$name" 2>&1 | grep -q "twophase probe mismatch"; then
+    fail=$((fail+1)); echo "FAIL(twophase): $name probe mismatch"; return
+  fi
+  if cmp -s "gen_ssa/$mod.off.ssa" "gen_ssa/$mod.ssa"; then
+    pass=$((pass+1)); echo "PASS(twophase): $name identical"
+  else
+    fail=$((fail+1)); echo "FAIL(twophase): $name .ssa differs"
+  fi
+  rm -f "gen_ssa/$mod.off.ssa"
+}
+
+twophase_same t_fwdvar.mod
+twophase_same t_nestedproc.mod
+twophase_same t_classmethod.mod
+
 # --- step 7 gate: compiler sources free of gm2-only imports.
 #     FileIO is the last module to port (see docs/summary_step7.md);
 #     it is excluded here so the gate guards everything else. ---

+ 84 - 2
compiler/src/M2.atg

@@ -60,6 +60,14 @@ VAR
      the emitted output is unchanged. *)
   twoPhase: BOOLEAN;
 
+  (* Two-phase parse (docs/plan-two-phase-parse.md), milestone 1.  When
+     TRUE each outermost procedure/method body is shadow-skipped at the
+     token level and the skip verified before the body parses normally.
+     Set by the driver (-twophase); default off.  `procNest` counts the
+     Blocks being parsed so only outermost bodies probe. *)
+  twoPhaseParse: BOOLEAN;
+  procNest: CARDINAL;
+
   (* Result slot for the expression-AST builder (slice 2).  Every
      Expr/SimExpr/Term/Fact leaves its node here; the combination
      productions save it into locals before parsing the next operand.
@@ -306,6 +314,75 @@ PROCEDURE GetUnit (): AST.Node;
     RETURN astUnit
   END GetUnit;
 
+PROCEDURE SetTwoPhase (b: BOOLEAN);
+(* Driver switch for the two-phase parse probe (milestone 1). *)
+  BEGIN
+    twoPhaseParse := b
+  END SetTwoPhase;
+
+PROCEDURE TokIs (kw: ARRAY OF CHAR): BOOLEAN;
+(* The current lookahead token's source text equals kw.  Compares text,
+   not token codes, so grammar renumbering cannot desync the skipper. *)
+  VAR tok: ARRAY [0 .. 63] OF CHAR;
+  BEGIN
+    M2S.GetName(M2S.nextPos, M2S.nextLen, tok);
+    RETURN SymTab.Equal(tok, kw)
+  END TokIs;
+
+PROCEDURE Adv;
+(* Advance one token: exactly the parser frame's Get for a grammar
+   without pragmas (its pragma branch is empty and the scanner never
+   returns a token above maxT, so the repeat loop runs once).  A
+   separate name because the frame's Get is declared later in this
+   module: a bare Get here would bind the imported M2S.Get (which takes
+   a parameter) under single-pass compilers. *)
+  BEGIN
+    M2S.Get(sym);
+    INC(errDist)
+  END Adv;
+
+PROCEDURE ProbeBody (pn: SymTab.Name);
+(* Shadow-skip the body starting at the current token: consume tokens
+   counting block openers until the matching END, check the procedure
+   name after it, then restore the exact scanner/parser state.  The
+   body then parses normally, so a correct skipper is output-neutral;
+   a wrong one reports to StdOut (never into the listing). *)
+  VAR savedSym, savedErr, depth, guard: CARDINAL;
+    nm: SymTab.Name;
+    ok: BOOLEAN;
+  BEGIN
+    savedSym := sym; savedErr := errDist;
+    M2S.PushPos;
+    depth := 0; guard := 0; ok := TRUE;
+    LOOP
+      IF sym = 0 THEN ok := FALSE; EXIT END;
+      IF TokIs("END") THEN
+        IF depth = 0 THEN EXIT END;
+        DEC(depth)
+      ELSIF TokIs("MODULE") OR TokIs("PROCEDURE") OR TokIs("CLASS")
+         OR TokIs("RECORD") OR TokIs("IF") OR TokIs("CASE")
+         OR TokIs("WHILE") OR TokIs("FOR") OR TokIs("LOOP")
+         OR TokIs("WITH") THEN
+        INC(depth)
+      END;
+      Adv;
+      INC(guard);
+      IF guard > 1000000 THEN ok := FALSE; EXIT END
+    END;
+    IF ok THEN
+      Adv;
+      M2S.GetName(M2S.nextPos, M2S.nextLen, nm);
+      IF (sym = 0) OR NOT SymTab.Equal(nm, pn) THEN ok := FALSE END
+    END;
+    M2S.PopPos;
+    sym := savedSym; errDist := savedErr;
+    IF NOT ok THEN
+      FileIO.WriteString(FileIO.StdOut, "twophase probe mismatch in ");
+      FileIO.WriteString(FileIO.StdOut, pn);
+      FileIO.WriteLn(FileIO.StdOut)
+    END
+  END ProbeBody;
+
 CHARACTERS
   eol      = CHR(13) .
   lf       = CHR(10) .
@@ -337,7 +414,8 @@ PRODUCTIONS
   M2
     = (. twoPhase := TRUE; astCur := AST.NoNode; astStmt := AST.NoNode; astDecl := AST.NoNode; astImp := AST.NoNode; astImpTail := AST.NoNode;
                                            astImp := AST.NoNode;
-                                           astImpTail := AST.NoNode; .)
+                                           astImpTail := AST.NoNode;
+                                           procNest := 0; .)
       Unit "." .
   (* Units: program modules compile fully; DEFINITION and
      IMPLEMENTATION modules parse + check now but lower in step 4
@@ -1432,7 +1510,10 @@ PRODUCTIONS
                                            QbeGen.EndFunc(
                                              SymTab.ProcRes(pn)); .) ) .
   Block<pn: SymTab.Name>                (. VAR m2: SymTab.Name; .)
-    = DeclSeq                           (. astBlkDecls := astDecl;
+    =                                   (. IF twoPhaseParse AND (procNest = 0) THEN
+                                            ProbeBody(pn) END;
+                                          INC(procNest); .)
+      DeclSeq                           (. astBlkDecls := astDecl;
                                            astStmt := AST.NoNode;
                                            nLab := 0; nGot := 0; .)
       [ "BEGIN"                         (. (* an empty body is still a
@@ -1447,6 +1528,7 @@ PRODUCTIONS
       "END"
       GetIdent<m2>                      (. IF NOT SymTab.Equal(pn, m2) THEN
                                              SemError(202) END;
+                                           DEC(procNest);
                                            CheckGotos; .) .
   StatSeq                               (. VAR astSeq, astTail: AST.Node; .)
     =                                   (. astSeq := AST.NoNode;

Plik diff jest za duży
+ 3451 - 3369
compiler/src/M2.lst


+ 7 - 1
compiler/src/compiler.frm

@@ -10,7 +10,7 @@ MODULE -->Grammar;
     InInst, InstBegin, InstAddActual, InstEnd, InstNameGet, InstActualGet,
     UnitNameGet, UnitIsGeneric, UnitIsDefModule,
     RefineSeen, RefineIsDef, RefineInst, RefineGen,
-    RefineActualCount, RefineActualGet;
+    RefineActualCount, RefineActualGet, SetTwoPhase;
   IMPORT
     Strings, Storage, SYSTEM, FileIO, SymTab, QbeGen, AST, Lower, Check;
     (* and any others needed *)
@@ -287,6 +287,7 @@ MODULE -->Grammar;
     f: CARDINAL;
     bad: BOOLEAN;
     doCheck: BOOLEAN;   (* run the resolve/check pass (Check.Resolve) *)
+    doTwoPhase: BOOLEAN; (* probe the two-phase body skipper (ProbeBody) *)
 
     (* generics (Increment 2): module name -> source path, def and impl *)
     genDefNm:  ARRAY [0 .. 31] OF ARRAY [0 .. 63] OF CHAR;
@@ -405,6 +406,7 @@ MODULE -->Grammar;
     AST.Init;
     Lower.BeginUnit;
     Check.BeginUnit;
+    SetTwoPhase(doTwoPhase);
     Parse;
     InstEnd;
     IF doCheck THEN Check.Resolve(GetUnit()) END;
@@ -419,11 +421,14 @@ MODULE -->Grammar;
     (* collect the session's source files (program module last) *)
     nFiles := 0;
     doCheck := TRUE;
+    doTwoPhase := FALSE;
     LOOP
       FileIO.NextParameter(sourceName);
       IF (sourceName[0] = CHR(0)) OR (nFiles > HIGH(files)) THEN EXIT END;
       IF EqNm(sourceName, "-noresolve") THEN
         doCheck := FALSE
+      ELSIF EqNm(sourceName, "-twophase") THEN
+        doTwoPhase := TRUE
       ELSE
         Strings.Assign(sourceName, files[nFiles]);
         INC(nFiles)
@@ -461,6 +466,7 @@ MODULE -->Grammar;
       AST.Init;
       Lower.BeginUnit;
       Check.BeginUnit;
+      SetTwoPhase(doTwoPhase);
       Parse;
       UnitNameGet(uname);
       IF doCheck THEN Check.Resolve(GetUnit()) END;

+ 5 - 0
compiler/src/parser.frm

@@ -130,6 +130,11 @@ IMPORT AST, SymTab;
 
 PROCEDURE Parse;
 
+PROCEDURE SetTwoPhase (b: BOOLEAN);
+(* Enable (or disable) the two-phase parse probe: with it on, each
+   outermost procedure body is shadow-skipped at the token level and
+   the skip is verified before the body parses normally. *)
+
 PROCEDURE Successful (): BOOLEAN;
 
 PROCEDURE GetUnit (): AST.Node;

+ 62 - 0
compiler/src/scanner.frm

@@ -34,6 +34,29 @@ VAR
   buf:       Buffer;     (*source buffer for low-level access*)
   start:     StartTable; (*start state for every character*)
   CurrentCh: GetCH;
+  (* Two-phase parse probe (milestone 1): exact save/restore of the
+     scanner state around a token-level shadow skip.  The pushed state
+     covers the low-level buffer cursor plus every published position
+     variable, so PopPos resumes the parse bit-exactly.  `errors` is
+     included: re-scanning a bad character must not double-count. *)
+  posDepth: CARDINAL;
+  saveLastCh: ARRAY [0 .. 7] OF CHAR;
+  saveCh: ARRAY [0 .. 7] OF CHAR;
+  saveCurLine: ARRAY [0 .. 7] OF INTEGER;
+  saveLineStart: ARRAY [0 .. 7] OF INT32;
+  saveApx: ARRAY [0 .. 7] OF INT32;
+  saveOldEols: ARRAY [0 .. 7] OF INTEGER;
+  saveBp: ARRAY [0 .. 7] OF INT32;
+  saveBp0: ARRAY [0 .. 7] OF INT32;
+  saveLine: ARRAY [0 .. 7] OF INTEGER;
+  saveCol: ARRAY [0 .. 7] OF INTEGER;
+  saveLen: ARRAY [0 .. 7] OF CARDINAL;
+  savePos: ARRAY [0 .. 7] OF INT32;
+  saveNextLine: ARRAY [0 .. 7] OF INTEGER;
+  saveNextCol: ARRAY [0 .. 7] OF INTEGER;
+  saveNextLen: ARRAY [0 .. 7] OF CARDINAL;
+  saveNextPos: ARRAY [0 .. 7] OF INT32;
+  saveErrors: ARRAY [0 .. 7] OF INTEGER;
 
 PROCEDURE Err (nr, line, col: INTEGER; pos: INT32);
   BEGIN
@@ -154,9 +177,42 @@ PROCEDURE Reset;
     buf[i-1]^[read] := EOF;
     curLine := 1; lineStart := -2; bp := -1;
     oldEols := 0; apx := 0; errors := 0;
+    posDepth := 0;
     NextCh;
   END Reset;
 
+PROCEDURE PushPos;
+(* Save the full scanner state (milestone 1 probe). *)
+  BEGIN
+    IF posDepth > HIGH(saveBp) THEN RETURN END;
+    saveLastCh[posDepth] := lastCh; saveCh[posDepth] := ch;
+    saveCurLine[posDepth] := curLine; saveLineStart[posDepth] := lineStart;
+    saveApx[posDepth] := apx; saveOldEols[posDepth] := oldEols;
+    saveBp[posDepth] := bp; saveBp0[posDepth] := bp0;
+    saveLine[posDepth] := line; saveCol[posDepth] := col;
+    saveLen[posDepth] := len; savePos[posDepth] := pos;
+    saveNextLine[posDepth] := nextLine; saveNextCol[posDepth] := nextCol;
+    saveNextLen[posDepth] := nextLen; saveNextPos[posDepth] := nextPos;
+    saveErrors[posDepth] := errors;
+    INC(posDepth)
+  END PushPos;
+
+PROCEDURE PopPos;
+(* Restore the state saved by PushPos. *)
+  BEGIN
+    IF posDepth = 0 THEN RETURN END;
+    DEC(posDepth);
+    lastCh := saveLastCh[posDepth]; ch := saveCh[posDepth];
+    curLine := saveCurLine[posDepth]; lineStart := saveLineStart[posDepth];
+    apx := saveApx[posDepth]; oldEols := saveOldEols[posDepth];
+    bp := saveBp[posDepth]; bp0 := saveBp0[posDepth];
+    line := saveLine[posDepth]; col := saveCol[posDepth];
+    len := saveLen[posDepth]; pos := savePos[posDepth];
+    nextLine := saveNextLine[posDepth]; nextCol := saveNextCol[posDepth];
+    nextLen := saveNextLen[posDepth]; nextPos := saveNextPos[posDepth];
+    errors := saveErrors[posDepth]
+  END PopPos;
+
 BEGIN
   -->initializations
   Error := Err; lastCh := EOF;
@@ -199,4 +255,10 @@ PROCEDURE CharAt (pos: INT32): CHAR;
 PROCEDURE Reset;
 (* Reads and stores source file internally *)
 
+PROCEDURE PushPos;
+(* Save the full scanner state for a shadow skip (two-phase probe) *)
+
+PROCEDURE PopPos;
+(* Restore the state saved by PushPos *)
+
 END -->modulename.

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

@@ -95,3 +95,44 @@ 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.
+
+## Progress
+
+- **2026-10-15, milestone 1 (probe behind `-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.
+  - Openers counted: `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).
+  - Verified: suite **196/196** (193 + 3 probe gates); all 142
+    single-file tests plus session spot-checks (incl. Library1) and
+    the compiler's own sources produce **byte-identical `.ssa`** with
+    the flag on and zero mismatches; **fixpoint OK (3,517,333 bytes)**.
+- **Finding (V3 name resolution).** The first cut called the frame's
+  `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.
+- **Next**: milestone 2 — record body spans during the probe, add the
+  body-entry production, and re-parse bodies after the declarations
+  (driver-level restart), byte-compared against the default.

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików