Просмотр исходного кода

lower L7: working per-unit flip, whole-session tables + hash index

The bad=T that blocked per-unit Lower was the pre-existing invalid-call
errors in Lower.mod (forward refs), not the flip.  With those fixed the
per-unit mechanism works end to end:

- Driver: BeginSession/BeginUnit/EndSession + LowerUnitNow per unit
  (RestoreSession, emit, re-suppress).  QbeGen.EndModule is a no-op
  under NoEmit so the grammar neither flushes nor writes.
- The suppressed inline emit is a faithful shadow of the legacy emit
  (LocAdd/FuncParam allocate temps under NoEmit), so NoteAddr and the
  temp/label counters line up with Lower's re-emit.
- Whole-session tables (MaxSym=4096, MaxProc=2048) plus an
  open-addressing hash for FindSym/FindProc (HashSize=8192): the
  compiler session lowers in ~1.5s.  The old "hash miscompile" was a
  symptom of the miscompiled Lower and is gone.
- CopySpan takes its source as a VAR open array: gm2 copies a value
  open-array formal to a stack VLA of its full length, which overflowed
  the stack for the multi-megabyte tables.
- External procs recorded via MarkProcExternal (no uid, no static link);
  indirect calls through proc-typed variables via CallBeginInd.
- Grammar AST fixes: ActParam preserves the enclosing call's argument
  list (Factor used to reset astNArgs for designator actuals); ImplUnit/
  ProgModule clear the stale astStmt so a module without BEGIN does not
  grow a spurious _init.
- Const values live in Lower's per-unit tables (every module has EOL,
  so SymTab collided across modules after EndUnit); LEN/HIGH added.

The compiler session lowers but is not yet byte-identical: M2S.Get
($Get_61) is missing a large CASE arm.  Suite 204/204; FIXPOINT OK
(3,695,508 bytes), session 0 errors.
Eric Streit 6 дней назад
Родитель
Сommit
4cc3a0e293
8 измененных файлов с 3158 добавлено и 2852 удалено
  1. 6 0
      compiler/src/Lower.def
  2. 212 47
      compiler/src/Lower.mod
  3. 24 5
      compiler/src/M2.atg
  4. 2806 2787
      compiler/src/M2.lst
  5. 4 0
      compiler/src/QbeGen.def
  6. 42 2
      compiler/src/QbeGen.mod
  7. 6 11
      compiler/src/compiler.frm
  8. 58 0
      docs/summary_lower_l7.md

+ 6 - 0
compiler/src/Lower.def

@@ -17,6 +17,10 @@ PROCEDURE CanLower (unit: AST.Node): BOOLEAN;
 PROCEDURE LowerUnit (unit: AST.Node);
 PROCEDURE LowerSession (units: ARRAY OF AST.Node; n: CARDINAL;
                         progName: ARRAY OF CHAR);
+PROCEDURE BeginSession (progName: ARRAY OF CHAR);
+PROCEDURE BeginUnit;
+PROCEDURE LowerUnitNow (unit: AST.Node; isProg: BOOLEAN);
+PROCEDURE EndSession;
 
 (* The expression/statement/declaration walkers are mutually recursive.
    gm2's link pass rejects FORWARD and V3 has no whole-module pass, so
@@ -33,10 +37,12 @@ PROCEDURE LowerDecls (seq: AST.Node);
 
 PROCEDURE Reset;
 PROCEDURE NoteVar (name: ARRAY OF CHAR; kind: INTEGER; t: SymTab.TypeIndex);
+PROCEDURE NoteConstVal (val: ARRAY OF CHAR);
 PROCEDURE ScopeEnter;
 PROCEDURE ScopeLeave;
 PROCEDURE NoteProc (name: ARRAY OF CHAR; uid: CARDINAL;
                     res: SymTab.TypeIndex; depth: CARDINAL; ext: BOOLEAN);
+PROCEDURE MarkProcExternal;
 PROCEDURE SetProcRes (t: SymTab.TypeIndex);
 PROCEDURE NoteParam (name: ARRAY OF CHAR; t: SymTab.TypeIndex;
                      isVar: BOOLEAN);

+ 212 - 47
compiler/src/Lower.mod

@@ -13,15 +13,17 @@ IMPORT AST, SymTab, QbeGen;
    Names live in flat CHAR arrays indexed manually at slot * 64. *)
 
 CONST
-  MaxSym  = 512;
+  MaxSym  = 4096;
   MaxName = 63;
-  MaxProc = 64;
+  MaxProc = 2048;   (* whole-session: the compiler declares >1200 procs *)
   MaxPar  = 32;
+  HashSize = 8192;  (* power of two > MaxSym, MaxProc: open addressing *)
 
 VAR
   symName  : ARRAY [0 .. MaxSym * 64 - 1] OF CHAR;
   symKind  : ARRAY [0 .. MaxSym - 1] OF INTEGER;
   symTy    : ARRAY [0 .. MaxSym - 1] OF SymTab.TypeIndex;
+  symVal   : ARRAY [0 .. MaxSym - 1] OF QbeGen.QVal;  (* const value *)
   nSyms    : CARDINAL;
   procName : ARRAY [0 .. MaxProc * 64 - 1] OF CHAR;
   procUid  : ARRAY [0 .. MaxProc - 1] OF CARDINAL;
@@ -33,6 +35,11 @@ VAR
   parVar   : ARRAY [0 .. MaxProc * MaxPar - 1] OF BOOLEAN;
   parName  : ARRAY [0 .. MaxProc * MaxPar * 64 - 1] OF CHAR;
   nProcs   : CARDINAL;
+  (* Open-addressing indexes into symName/procName: entry = index+1,
+     0 = empty.  A whole-session linear scan is too slow (thousands of
+     lookups over thousands of entries), so FindSym/FindProc hash. *)
+  symHash  : ARRAY [0 .. HashSize - 1] OF INTEGER;
+  procHash : ARRAY [0 .. HashSize - 1] OF INTEGER;
   withRec  : ARRAY [0 .. 15] OF SymTab.TypeIndex;
   withTop  : CARDINAL;
   curMethCls : SymTab.TypeIndex;   (* class bound by the last obj.Method *)
@@ -47,8 +54,11 @@ PROCEDURE CopyN (src: ARRAY OF CHAR; VAR dst: ARRAY OF CHAR);
     dst[i] := CHR(0)
   END CopyN;
 
-PROCEDURE CopySpan (src: ARRAY OF CHAR; VAR dst: ARRAY OF CHAR;
+PROCEDURE CopySpan (VAR src: ARRAY OF CHAR; VAR dst: ARRAY OF CHAR;
                     srcOff, dstOff: CARDINAL);
+(* src is a VAR open array on purpose: gm2 copies a value open-array
+   formal to a stack VLA of its full length, which overflows the stack
+   for the multi-megabyte tables below. *)
   VAR i: CARDINAL;
   BEGIN
     i := 0;
@@ -58,18 +68,83 @@ PROCEDURE CopySpan (src: ARRAY OF CHAR; VAR dst: ARRAY OF CHAR;
     dst[dstOff + i] := CHR(0)
   END CopySpan;
 
+PROCEDURE HashStr (name: ARRAY OF CHAR): CARDINAL;
+  VAR h: CARDINAL; i: CARDINAL;
+  BEGIN
+    h := 5381; i := 0;
+    WHILE (i <= HIGH(name)) AND (name[i] # CHR(0)) DO
+      h := (h * 33 + VAL(CARDINAL, ORD(name[i]))) MOD HashSize; INC(i)
+    END;
+    RETURN h
+  END HashStr;
+
+PROCEDURE SpanEq (VAR arr: ARRAY OF CHAR; off: CARDINAL;
+                  name: ARRAY OF CHAR): BOOLEAN;
+  VAR j: CARDINAL;
+  BEGIN
+    j := 0;
+    WHILE (j < MaxName) AND (name[j] # CHR(0)) DO
+      IF arr[off + j] # name[j] THEN RETURN FALSE END;
+      INC(j)
+    END;
+    RETURN arr[off + j] = name[j]
+  END SpanEq;
+
+PROCEDURE HashPut (VAR htab: ARRAY OF INTEGER; VAR names: ARRAY OF CHAR;
+                   name: ARRAY OF CHAR; idx: CARDINAL);
+(* Insert idx, replacing an existing entry with the same name so the
+   latest declaration wins (the linear scan used to search backward). *)
+  VAR h: CARDINAL;
+  BEGIN
+    h := HashStr(name);
+    LOOP
+      IF htab[h] = 0 THEN htab[h] := VAL(INTEGER, idx) + 1; RETURN END;
+      IF SpanEq(names, VAL(CARDINAL, htab[h] - 1) * 64, name) THEN
+        htab[h] := VAL(INTEGER, idx) + 1; RETURN
+      END;
+      h := (h + 1) MOD HashSize
+    END
+  END HashPut;
+
+PROCEDURE HashGet (VAR htab: ARRAY OF INTEGER; VAR names: ARRAY OF CHAR;
+                   name: ARRAY OF CHAR): INTEGER;
+  VAR h: CARDINAL;
+  BEGIN
+    h := HashStr(name);
+    LOOP
+      IF htab[h] = 0 THEN RETURN -1 END;
+      IF SpanEq(names, VAL(CARDINAL, htab[h] - 1) * 64, name) THEN
+        RETURN htab[h] - 1
+      END;
+      h := (h + 1) MOD HashSize
+    END
+  END HashGet;
+
 PROCEDURE Reset;
+  VAR i: CARDINAL;
   BEGIN
-    nSyms := 0; nProcs := 0
+    nSyms := 0; nProcs := 0;
+    i := 0;
+    WHILE i < HashSize DO symHash[i] := 0; procHash[i] := 0; INC(i) END
   END Reset;
 
 PROCEDURE NoteVar (name: ARRAY OF CHAR; kind: INTEGER; t: SymTab.TypeIndex);
   BEGIN
     IF nSyms >= MaxSym THEN RETURN END;
     CopySpan(name, symName, 0, nSyms * 64);
-    symKind[nSyms] := kind; symTy[nSyms] := t; INC(nSyms)
+    symKind[nSyms] := kind; symTy[nSyms] := t;
+    HashPut(symHash, symName, name, nSyms);
+    INC(nSyms)
   END NoteVar;
 
+PROCEDURE NoteConstVal (val: ARRAY OF CHAR);
+(* Records the evaluated value of the CONST just entered by NoteVar.
+   Const values are per-unit and cross-module names collide (every
+   module has EOL), so they live here, not in SymTab. *)
+  BEGIN
+    IF nSyms > 0 THEN CopyN(val, symVal[nSyms - 1]) END
+  END NoteConstVal;
+
 PROCEDURE ScopeEnter;
   BEGIN
   END ScopeEnter;
@@ -86,9 +161,17 @@ PROCEDURE NoteProc (name: ARRAY OF CHAR; uid: CARDINAL;
     procUid[nProcs] := uid; procRes[nProcs] := res;
     procDep[nProcs] := depth; procExt[nProcs] := ext;
     procNpar[nProcs] := 0;
+    HashPut(procHash, procName, name, nProcs);
     INC(nProcs)
   END NoteProc;
 
+PROCEDURE MarkProcExternal;
+(* The grammar calls this from the EXTERNAL branch, after NoteProc, so
+   the walk records that the callee takes no static link. *)
+  BEGIN
+    IF nProcs > 0 THEN procExt[nProcs - 1] := TRUE END
+  END MarkProcExternal;
+
 PROCEDURE SetProcRes (t: SymTab.TypeIndex);
   BEGIN
     IF nProcs > 0 THEN procRes[nProcs - 1] := t END
@@ -108,37 +191,25 @@ PROCEDURE NoteParam (name: ARRAY OF CHAR; t: SymTab.TypeIndex; isVar: BOOLEAN);
 
 PROCEDURE FindSym (name: ARRAY OF CHAR; VAR kind: INTEGER;
                    VAR t: SymTab.TypeIndex): BOOLEAN;
-  VAR i: CARDINAL; j: CARDINAL; ok: BOOLEAN;
-  BEGIN
-    i := nSyms;
-    WHILE i > 0 DO
-      DEC(i);
-      ok := TRUE; j := 0;
-      WHILE ok AND (j < 64) AND (symName[i * 64 + j] # CHR(0)) DO
-        IF symName[i * 64 + j] # name[j] THEN ok := FALSE END;
-        INC(j)
-      END;
-      IF ok AND (symName[i * 64 + j] = name[j]) THEN
-        kind := symKind[i]; t := symTy[i]; RETURN TRUE
-      END
-    END;
-    RETURN FALSE
+  VAR i: INTEGER;
+  BEGIN
+    i := HashGet(symHash, symName, name);
+    IF i < 0 THEN RETURN FALSE END;
+    kind := symKind[i]; t := symTy[i]; RETURN TRUE
   END FindSym;
 
+PROCEDURE GetConstVal (name: ARRAY OF CHAR; VAR v: ARRAY OF CHAR): BOOLEAN;
+  VAR i: INTEGER;
+  BEGIN
+    i := HashGet(symHash, symName, name);
+    IF (i < 0) OR (symKind[i] # SymTab.KindConst) THEN RETURN FALSE END;
+    CopyN(symVal[i], v);
+    RETURN v[0] # CHR(0)
+  END GetConstVal;
+
 PROCEDURE FindProc (name: ARRAY OF CHAR): INTEGER;
-  VAR i: CARDINAL; j: CARDINAL; ok: BOOLEAN;
-  BEGIN
-    i := nProcs;
-    WHILE i > 0 DO
-      DEC(i);
-      ok := TRUE; j := 0;
-      WHILE ok AND (j < 64) AND (procName[i * 64 + j] # CHR(0)) DO
-        IF procName[i * 64 + j] # name[j] THEN ok := FALSE END;
-        INC(j)
-      END;
-      IF ok AND (procName[i * 64 + j] = name[j]) THEN RETURN VAL(INTEGER, i) END
-    END;
-    RETURN -1
+  BEGIN
+    RETURN HashGet(procHash, procName, name)
   END FindProc;
 
 PROCEDURE Resolve (nm: ARRAY OF CHAR; VAR k: INTEGER;
@@ -158,9 +229,10 @@ PROCEDURE ProcInfo (nm: ARRAY OF CHAR; VAR uid: CARDINAL;
   VAR i: INTEGER;
   BEGIN
     i := FindProc(nm);
-    IF i < 0 THEN RETURN FALSE END;
+    IF (i < 0) OR (i >= MaxProc) THEN RETURN FALSE END;
     uid := procUid[i]; res := procRes[i]; depth := procDep[i];
     ext := procExt[i]; npar := procNpar[i];
+    IF npar > MaxPar THEN npar := MaxPar END;
     RETURN TRUE
   END ProcInfo;
 
@@ -170,7 +242,8 @@ PROCEDURE ParamInfo (nm: ARRAY OF CHAR; i: CARDINAL;
   VAR k: INTEGER;
   BEGIN
     k := FindProc(nm);
-    IF (k < 0) OR (i >= procNpar[k]) THEN RETURN FALSE END;
+    IF (k < 0) OR (k >= MaxProc) THEN RETURN FALSE END;
+    IF (i >= procNpar[k]) OR (procNpar[k] > MaxPar) THEN RETURN FALSE END;
     t := parTy[VAL(CARDINAL, k) * MaxPar + i];
     isVar := parVar[VAL(CARDINAL, k) * MaxPar + i];
     CopySpan(parName, pn, VAL(CARDINAL, k) * MaxPar * 64 + i * 64, 0);
@@ -831,7 +904,7 @@ PROCEDURE LowerDesign (n: AST.Node; VAR t: SymTab.TypeIndex; VAR k: INTEGER;
         t := SymTab.BoolType(); QbeGen.CopyOp("0", q)
       ELSIF SymTab.Equal(nm, "NIL") THEN
         QbeGen.CopyOp("0", q)
-      ELSIF SymTab.GetSymVal(nm, cv) AND QbeGen.IsImm(cv) THEN
+      ELSIF GetConstVal(nm, cv) AND QbeGen.IsImm(cv) THEN
         QbeGen.CopyOp(cv, q)
       ELSE QbeGen.CopyOp("0", q)
       END
@@ -925,25 +998,52 @@ PROCEDURE LowerDesign (n: AST.Node; VAR t: SymTab.TypeIndex; VAR k: INTEGER;
 
 PROCEDURE LowerCall (n: AST.Node; VAR t: SymTab.TypeIndex;
                      VAR q: QbeGen.QVal; want: BOOLEAN);
-  VAR nm, pn: SymTab.Name; i: CARDINAL; at, ft, res: SymTab.TypeIndex;
-      qa, qt, qe, mg: QbeGen.QVal; conv, isV, qual: BOOLEAN;
-      uid, depth, np: CARDINAL; ext: BOOLEAN;
+  VAR nm, pn, cqn: SymTab.Name; i: CARDINAL; at, ft, res: SymTab.TypeIndex;
+      qa, qt, qe, mg: QbeGen.QVal; conv, isV, qual, ind: BOOLEAN;
+      uid, depth, np: CARDINAL; ext: BOOLEAN; ck: INTEGER;
+      ct: SymTab.TypeIndex; csfx: BOOLEAN;
   BEGIN
     qual := CalleeAlias(n, nm);
+    ind := FALSE;
     IF qual THEN
       uid := SymTab.ProcUid(nm); res := SymTab.ProcRes(nm);
       depth := SymTab.ProcDepthOf(nm); ext := SymTab.IsExternal(nm);
-      np := SymTab.ProcNPar(nm)
+      np := SymTab.ProcNPar(nm);
+      IF ext THEN QbeGen.CopyOp(nm, mg) ELSE QbeGen.Mangled(nm, uid, mg) END;
+      QbeGen.CallBegin(mg, res, depth, ext)
     ELSE
       CalleeName(n, nm);
-      IF NOT ProcInfo(nm, uid, res, depth, ext, np) THEN END
+      IF FindSym(nm, ck, ct)
+         AND ((ck = SymTab.KindVar) OR (ck = SymTab.KindParam))
+         AND (SymTab.ClassOf(ct) = SymTab.ClProc) THEN
+        (* indirect call through a procedure-typed variable *)
+        ind := TRUE;
+        LowerDesign(AST.Child(n, 0), ct, ck, qa, cqn, csfx);
+        res := SymTab.ProcTypeRes(ct);
+        np := SymTab.ProcTypeNPar(ct);
+        QbeGen.CallBeginInd(qa, res, FALSE)
+      ELSIF ProcInfo(nm, uid, res, depth, ext, np) THEN
+        IF ext THEN
+          (* External callee: bind to its C link name (the procedure's
+             own name); SymTab.ProcLink cannot be consulted after
+             EndUnit. *)
+          QbeGen.CopyOp(nm, mg)
+        ELSE
+          QbeGen.Mangled(nm, uid, mg)
+        END;
+        QbeGen.CallBegin(mg, res, depth, ext)
+      ELSE
+        res := SymTab.InvalidType;
+        QbeGen.CallBegin(nm, res, 0, FALSE)
+      END
     END;
-    QbeGen.Mangled(nm, uid, mg);
-    QbeGen.CallBegin(mg, res, depth, ext);
     i := 1;
     WHILE i < AST.NChild(n) DO
       LowerExpr(AST.Child(n, i), at, qe);
-      IF qual THEN
+      IF ind THEN
+        ft := SymTab.ProcTypeParamType(ct, i - 1);
+        isV := SymTab.ProcTypeParamIsVar(ct, i - 1)
+      ELSIF qual THEN
         ft := SymTab.ParamType(nm, i - 1);
         isV := SymTab.ParamIsVar(nm, i - 1)
       ELSE
@@ -977,6 +1077,7 @@ PROCEDURE IsBuiltin (nm: ARRAY OF CHAR): BOOLEAN;
     RETURN SymTab.Equal(nm, "ABS") OR SymTab.Equal(nm, "CAP")
         OR SymTab.Equal(nm, "CHR") OR SymTab.Equal(nm, "ORD")
         OR SymTab.Equal(nm, "SIZE") OR SymTab.Equal(nm, "TSIZE")
+        OR SymTab.Equal(nm, "LEN") OR SymTab.Equal(nm, "HIGH")
         OR SymTab.Equal(nm, "MIN") OR SymTab.Equal(nm, "MAX")
         OR SymTab.Equal(nm, "SHIFT") OR SymTab.Equal(nm, "ROTATE")
         OR SymTab.Equal(nm, "ADR")
@@ -1022,6 +1123,22 @@ PROCEDURE LowerBuiltinExpr (n: AST.Node; nm: SymTab.Name;
         t := SymTab.IntType()
       ELSE t := SymTab.InvalidType; QbeGen.CopyOp("0", q)
       END
+    ELSIF SymTab.Equal(nm, "LEN") OR SymTab.Equal(nm, "HIGH") THEN
+      LowerDesign(AST.Child(n, 1), dt, k, qd, qn, sfx);
+      IF dt = SymTab.InvalidType THEN
+        t := SymTab.InvalidType; QbeGen.CopyOp("0", q)
+      ELSIF SymTab.ClassOf(dt) = SymTab.ClArray THEN
+        IF SymTab.Equal(nm, "HIGH") THEN
+          IF SymTab.IsOpenArray(dt) THEN QbeGen.OpenHi(qd, q)
+          ELSE QbeGen.IntStr(SymTab.ArrayHi(dt), q) END
+        ELSE
+          IF SymTab.IsOpenArray(dt) THEN QbeGen.LoadCount(qd, q)
+          ELSE QbeGen.IntStr(VAL(INTEGER, SymTab.ArrayLen(dt)), q) END
+        END;
+        t := SymTab.IntType()
+      ELSE
+        t := SymTab.InvalidType; QbeGen.CopyOp("0", q)
+      END
     ELSIF SymTab.Equal(nm, "SHIFT") OR SymTab.Equal(nm, "ROTATE") THEN
       LowerExpr(AST.Child(n, 1), et, q);
       LowerExpr(AST.Child(n, 2), et2, q2);
@@ -1760,9 +1877,13 @@ PROCEDURE LowerDecls (seq: AST.Node);
         END
       ELSIF AST.Kind(c) = AST.NkConstDecl THEN
         Name(AST.Child(c, 0), nm);
-        Name(AST.Child(c, 1), txt);
-        QbeGen.NormInt(txt, qv);
-        QbeGen.DeclConst(nm, qv, AST.Ty(c))
+        (* The value is a constant expression (CHR(..), arithmetic, a
+           prior CONST): use the evaluated value recorded by NoteConstVal,
+           not the AST text.  Aggregates/strings store a descriptor
+           address and are not scalar data. *)
+        IF GetConstVal(nm, qv) AND QbeGen.IsImm(qv) THEN
+          QbeGen.DeclConst(nm, qv, AST.Ty(c))
+        END
       ELSIF AST.Kind(c) = AST.NkProcDecl THEN
         Name(AST.Child(c, 0), nm);
         IF ProcInfo(nm, uid, res, depth, ext, np) THEN
@@ -1863,6 +1984,50 @@ PROCEDURE LowerSession (units: ARRAY OF AST.Node; n: CARDINAL;
     QbeGen.EndModule(pn)
   END LowerSession;
 
+PROCEDURE BeginSession (progName: ARRAY OF CHAR);
+(* Per-unit Lower emission.  The grammar still runs its inline emit while
+   parsing, but under NoEmit it only mutates QbeGen's counters (nothing
+   reaches the buffer).  For each unit: BeginUnit snapshots the state
+   *before* parsing; after parsing, LowerUnitNow restores that snapshot
+   and lets Lower re-emit the unit from the same counter values the
+   legacy inline path would have used, so the image stays byte-identical.
+   EndSession writes the accumulated session image. *)
+  BEGIN
+    withTop := 0;
+    curImplCls := SymTab.InvalidType;
+    QbeGen.OpenModule(progName);
+    QbeGen.SetNoEmit(TRUE)
+  END BeginSession;
+
+PROCEDURE BeginUnit;
+(* Snapshot the pre-parse state so Lower can discard the grammar's
+   suppressed inline mutations for this unit. *)
+  BEGIN
+    QbeGen.SaveSession
+  END BeginUnit;
+
+PROCEDURE LowerUnitNow (unit: AST.Node; isProg: BOOLEAN);
+  BEGIN
+    IF unit = AST.NoNode THEN RETURN END;
+    QbeGen.RestoreSession;
+    QbeGen.SetNoEmit(FALSE);
+    LowerOneUnit(unit, isProg);
+    QbeGen.SetNoEmit(TRUE)
+  END LowerUnitNow;
+
+PROCEDURE EndSession;
+  VAR out: ARRAY [0 .. 255] OF CHAR; j: CARDINAL;
+  BEGIN
+    j := 0;
+    WHILE (j <= 254) AND (curUnitName[j] # CHR(0)) DO
+      out[j] := curUnitName[j]; INC(j)
+    END;
+    out[j] := "L"; INC(j); out[j] := CHR(0);
+    QbeGen.SetNoEmit(FALSE);
+    QbeGen.SetOutName(out);
+    QbeGen.EndModule(curUnitName)
+  END EndSession;
+
 PROCEDURE LowerUnit (unit: AST.Node);
   VAR nm: SymTab.Name; out: ARRAY [0 .. 255] OF CHAR; i: CARDINAL;
   BEGIN

+ 24 - 5
compiler/src/M2.atg

@@ -318,7 +318,7 @@ PRODUCTIONS
       ";"
       { Import                        (. AstAppend(AST.NkDeclSeq,
                                              astImp, astImpTail, astDecl); .) }
-      DeclSeq
+      DeclSeq                           (. astStmt := AST.NoNode; .)
       [ "BEGIN"                         (. FlushPend;
                                            k := 0;
                                            WHILE k < SymTab.FwdPending() DO
@@ -368,7 +368,7 @@ PRODUCTIONS
       ";"
       { Import                        (. AstAppend(AST.NkDeclSeq,
                                              astImp, astImpTail, astDecl); .) }
-      DeclSeq
+      DeclSeq                           (. astStmt := AST.NoNode; .)
       [ "BEGIN"                         (. FlushPend;
                                            k := 0;
                                            WHILE k < SymTab.FwdPending() DO
@@ -441,7 +441,7 @@ PRODUCTIONS
         { "," GetIdent<expNames[expCount]>
                                         (. INC(expCount); .) }
         ";" ]
-      DeclSeq
+      DeclSeq                           (. astStmt := AST.NoNode; .)
       [ "BEGIN"                         (. astStmt := AST.NoNode;
                                            QbeGen.BeginInit(m1); .)
         [ StatSeq ]                     (. QbeGen.EndInit; .) ]
@@ -1032,6 +1032,7 @@ PRODUCTIONS
                                            AST.SetTy(astNode, t);
                                            astDecl := astNode;
                                            Lower.NoteVar(n, SymTab.KindConst, t);
+                                           Lower.NoteConstVal(qv);
                                            SymTab.SetSymType(n, t);
                                            cls := SymTab.ClassOf(t);
                                            IF (cls = SymTab.ClArray)
@@ -1247,6 +1248,7 @@ PRODUCTIONS
                                            QbeGen.AbortFunc; .)
       | "EXTERNAL"                      (. AST.SetOp(astNode, 2);
                                            astDecl := astNode;
+                                           Lower.MarkProcExternal;
                                            Lower.ScopeLeave;
                                            SymTab.MarkExternal("");
                                            SymTab.CloseProc;
@@ -1760,8 +1762,25 @@ PRODUCTIONS
                                         (. VAR at, ft: SymTab.TypeIndex;
                                              qe, qa, qt: QbeGen.QVal;
                                              isV, conv, va: BOOLEAN;
-                                             cl: CHAR; .)
-    = Expr<at, qe> (. IF astNArgs <= HIGH(astArgs) THEN
+                                             cl: CHAR;
+                                             savedN, aj: CARDINAL;
+                                             savedArgs: ARRAY [0 .. 31]
+                                               OF AST.Node; .)
+    =                                   (. (* Parsing the actual can clobber
+                                             the enclosing call's argument
+                                             list (Factor resets astNArgs),
+                                             so save/restore it. *)
+                                           savedN := astNArgs; aj := 0;
+                                           WHILE aj <= HIGH(astArgs) DO
+                                             savedArgs[aj] := astArgs[aj];
+                                             INC(aj)
+                                           END; .)
+      Expr<at, qe>                      (. astNArgs := savedN; aj := 0;
+                                           WHILE aj <= HIGH(astArgs) DO
+                                             astArgs[aj] := savedArgs[aj];
+                                             INC(aj)
+                                           END;
+                                           IF astNArgs <= HIGH(astArgs) THEN
                                              astArgs[astNArgs] := astCur;
                                              INC(astNArgs)
                                            END;

Разница между файлами не показана из-за своего большого размера
+ 2806 - 2787
compiler/src/M2.lst


+ 4 - 0
compiler/src/QbeGen.def

@@ -62,6 +62,10 @@ PROCEDURE SetNoEmit (b: BOOLEAN);
 (* Suppresses all output while TRUE (nested procedures in 4.1:
    SymTab tracks, QbeGen stays silent). *)
 
+PROCEDURE SaveSession;
+PROCEDURE RestoreSession;
+(* Snapshot/restore the session counters (Lower per-unit emit). *)
+
 PROCEDURE SetModule (name: ARRAY OF CHAR);
 (* Names the module being compiled; its globals emit as
    "<mod>_<name>" so separate modules never collide. *)

+ 42 - 2
compiler/src/QbeGen.mod

@@ -108,6 +108,44 @@ VAR
   fwdSite : ARRAY [0 .. 255] OF SymTab.Name;  (* real name to patch in *)
   fwdOper : ARRAY [0 .. 255] OF QVal;         (* final load/store class *)
   fwdGlob : ARRAY [0 .. 255] OF BOOLEAN;
+  (* Session-state snapshot: Lower runs per unit while the grammar's
+     suppressed inline emit still mutates these, so Lower saves/restores
+     its own values around each unit. *)
+  sav_nTemp, sav_nLab, sav_loopTop, sav_nR, sav_nStr, sav_nLoc,
+  sav_nPar, sav_nArg, sav_nn, sav_nInit, sav_sessUsed, sav_delayTop,
+  sav_ustrN, sav_ustrUsed, sav_ctorN, sav_ctorTop, sav_funcDepth,
+  sav_scopeTop, sav_callDepth, sav_outSel, sav_withTop : CARDINAL;
+  sav_inBody, sav_inFunc, sav_useStack, sav_recvArmed : BOOLEAN;
+
+PROCEDURE SaveSession;
+  BEGIN
+    sav_nTemp := nTemp; sav_nLab := nLab; sav_loopTop := loopTop;
+    sav_nR := nR; sav_nStr := nStr; sav_nLoc := nLoc; sav_nPar := nPar;
+    sav_nArg := nArg; sav_nn := nn; sav_nInit := nInit;
+    sav_sessUsed := sessUsed; sav_delayTop := delayTop;
+    sav_ustrN := ustrN; sav_ustrUsed := ustrUsed;
+    sav_ctorN := ctorN; sav_ctorTop := ctorTop;
+    sav_funcDepth := funcDepth; sav_scopeTop := scopeTop;
+    sav_callDepth := callDepth; sav_outSel := outSel;
+    sav_withTop := withTop;
+    sav_inBody := inBody; sav_inFunc := inFunc;
+    sav_useStack := useStack; sav_recvArmed := recvArmed
+  END SaveSession;
+
+PROCEDURE RestoreSession;
+  BEGIN
+    nTemp := sav_nTemp; nLab := sav_nLab; loopTop := sav_loopTop;
+    nR := sav_nR; nStr := sav_nStr; nLoc := sav_nLoc; nPar := sav_nPar;
+    nArg := sav_nArg; nn := sav_nn; nInit := sav_nInit;
+    sessUsed := sav_sessUsed; delayTop := sav_delayTop;
+    ustrN := sav_ustrN; ustrUsed := sav_ustrUsed;
+    ctorN := sav_ctorN; ctorTop := sav_ctorTop;
+    funcDepth := sav_funcDepth; scopeTop := sav_scopeTop;
+    callDepth := sav_callDepth; outSel := sav_outSel;
+    withTop := sav_withTop;
+    inBody := sav_inBody; inFunc := sav_inFunc;
+    useStack := sav_useStack; recvArmed := sav_recvArmed
+  END RestoreSession;
 
 (* ---------------- small string utilities ---------------- *)
 
@@ -890,6 +928,10 @@ PROCEDURE EndModule (name: ARRAY OF CHAR);
     ref : QVal;
   BEGIN
     IF NOT opened THEN RETURN END;
+    (* Per-unit Lower emission: the grammar's own unit-end EndModule must
+       not flush/close/write while the legacy emit is suppressed.  The
+       real session image is written by Lower.EndSession. *)
+    IF noEmit THEN RETURN END;
     IF NOT inBody THEN BeginBody END;
     IF SymTab.Lookup("ExitCode")
  AND (SymTab.SymKind("ExitCode") = SymTab.KindVar)
@@ -1000,7 +1042,6 @@ PROCEDURE LocAdd (name: ARRAY OF CHAR; tag: INTEGER; rep: ARRAY OF CHAR;
     off : QVal;
     lk : QVal;
   BEGIN
-    IF noEmit THEN RETURN END;
     IF (scopeTop = 0) OR (nLoc > HIGH(locNames)) THEN RETURN END;
     idx := nLoc - scopeBase[scopeTop - 1];
     IF idx > 63 THEN RETURN END;
@@ -1167,7 +1208,6 @@ PROCEDURE FuncParam (name: ARRAY OF CHAR; isVar: BOOLEAN;
     tmp : QVal;
     buf : ARRAY [0 .. 71] OF CHAR;
   BEGIN
-    IF noEmit THEN RETURN TRUE END;
     IF nPar > HIGH(parCls) THEN RETURN FALSE END;
     IF isVar THEN c := "l" ELSE c := ResClass(t) END;
     parCls[nPar] := c;

+ 6 - 11
compiler/src/compiler.frm

@@ -288,8 +288,6 @@ MODULE -->Grammar;
     f: CARDINAL;
     bad: BOOLEAN;
     lowerAst: BOOLEAN;
-    units: ARRAY [0 .. 31] OF AST.Node;
-    nUnits: CARDINAL;
 
   BEGIN
     (* collect the session's source files (program module last) *)
@@ -313,12 +311,12 @@ MODULE -->Grammar;
     BaseName(files[nFiles - 1], progName);
     IF lowerAst THEN SetLower(TRUE) END;
     SymTab.Init;
-    IF lowerAst THEN AST.Init END;   (* once: -lower re-emits the session *)
     QbeGen.OpenModule(progName);
+    IF lowerAst THEN Lower.BeginSession(progName) END;
     Error := StoreError;
 
     (* parse each unit into the shared session *)
-    bad := FALSE; f := 0; nUnits := 0;
+    bad := FALSE; f := 0;
     WHILE f < nFiles DO
       Strings.Assign(files[f], sourceName);
       FileIO.Open(src, sourceName, FALSE);
@@ -334,11 +332,10 @@ MODULE -->Grammar;
 
       FileIO.WriteString(FileIO.StdOut, "Parsing");
       FileIO.WriteLn(FileIO.StdOut);
-      IF NOT lowerAst THEN AST.Init END;
+      AST.Init;
+      IF lowerAst THEN Lower.BeginUnit END;
       Parse;
-      IF lowerAst AND (nUnits <= HIGH(units)) THEN
-        units[nUnits] := GetUnit(); INC(nUnits)
-      END;
+      IF lowerAst THEN Lower.LowerUnitNow(GetUnit(), f = nFiles - 1) END;
       PrintListing;
       IF lst # FileIO.StdOut THEN FileIO.Close(lst) END;
       IF NOT Successful() THEN
@@ -354,9 +351,7 @@ MODULE -->Grammar;
       INC(f)
     END;
 
-    IF lowerAst AND (nUnits > 0) THEN
-      Lower.LowerSession(units, nUnits, progName)
-    END;
+    IF lowerAst THEN Lower.EndSession END;
 
     IF bad OR NOT SymTab.HaveProgram()
       THEN FileIO.WriteString(FileIO.StdOut, "Incorrect source")

+ 58 - 0
docs/summary_lower_l7.md

@@ -100,3 +100,61 @@ Reproduced with a **proc-only** hash (`HashName`/`ProcEq`/`HashPutProc`/
 
 Narrowing further needs many ~10-minute fixpoint cycles; reverted to
 green (`a9a8ecd`).  Suite 204/204.
+
+---
+
+## Progress: per-unit flip unblocked (2026-10-05, after `a3ef897`)
+
+The `bad=T` that blocked per-unit emission was a **phantom**: it was the
+pre-existing `invalid call` errors in `Lower.mod` (forward references),
+now fixed by declaring the walkers in `Lower.def`.  With that gone, the
+per-unit flip works end to end.
+
+### Mechanism
+- Driver: `Lower.BeginSession` (open + `SetNoEmit`), `BeginUnit`
+  (`SaveSession`) before each `Parse`, `EndSession` (write image).
+- `Lower.LowerUnitNow(unit, isProg)` after each `Parse`:
+  `RestoreSession` → `SetNoEmit(FALSE)` → `LowerOneUnit` → `SetNoEmit(TRUE)`.
+- `QbeGen.EndModule` is a no-op under `NoEmit`, so the grammar's unit-end
+  emit neither flushes nor writes.
+- The grammar's suppressed inline emit must be a **faithful shadow** of
+  the legacy emit: `LocAdd`/`FuncParam` no longer skip `NewTemp` under
+  `NoEmit`, so the temp/`NoteAddr` numbering aligns with Lower's emit.
+
+### Name resolution
+- Whole-session tables: `MaxSym=4096`, `MaxProc=2048`, `MaxPar=32`.
+- Open-addressing hash index (`HashStr`/`SpanEq`/`HashPut`/`HashGet`,
+  `HashSize=8192`) for `FindSym`/`FindProc`.  The compiler session now
+  lowers in ~1.5 s.  (The earlier "hash miscompile" was a symptom of the
+  miscompiled `Lower`; it is gone now that `Lower` is V3-legal.)
+- `CopySpan`'s source is a **VAR** open array: gm2 copies a value
+  open-array formal to a stack VLA of its full length, which overflowed
+  the stack for the multi-megabyte tables.
+- External procs: `Lower.MarkProcExternal` (called from the grammar's
+  EXTERNAL branch) records `ext`; `LowerCall` binds external callees to
+  their own name (no uid, no static link).
+- Indirect calls through procedure-typed variables (`CurrentCh`):
+  `LowerCall` detects a proc-typed VAR/PARAM callee and uses
+  `CallBeginInd`.
+
+### Grammar AST fixes (also affect the two-phase AST generally)
+- `ActParam` saves/restores the enclosing call's `astArgs`/`astNArgs`:
+  `Factor` reset `astNArgs` for designator arguments, so any call with
+  two or more designator actuals kept only the last one.
+- `ImplUnit`/`ProgModule` reset `astStmt := AST.NoNode` after `DeclSeq`:
+  the module-body child was the stale last procedure body, so modules
+  without a module `BEGIN` (SysShim) grew a spurious `_init`.
+- `Lower.NoteConstVal`: const values are per-unit (every module has
+  `EOL`), so they live in Lower's tables, not `SymTab` (which collided
+  across modules after `EndUnit`).
+- `LEN`/`HIGH` added to `LowerBuiltinExpr`.
+
+### Remaining gap
+The compiler session now lowers, but is **not byte-identical** to the
+legacy image yet: the first divergence is in `M2S.Get` (`$Get_61`),
+where Lower emits 148 labels vs. the legacy 430 — a large part of the
+`Get` body (a big `CASE ch OF …`) is missing.  `lower_ok` single-file
+tests and the suite are green; the fixpoint is green (`-lower` is not
+used there).
+
+Suite **204/204**; FIXPOINT OK (3,695,508 bytes), session 0 errors.

Некоторые файлы не были показаны из-за большого количества измененных файлов