Kaynağa Gözat

fix: run-time init of large array-of-record inline length words

Completes the array-field fix.  Small record arrays (<= 131072 bytes)
are still expanded inline; larger ones (e.g. AST.arena, 262144 records)
now keep the compact zero blob and have their inline array-field length
words set at run time, so the image stays inside the 4 MiB sessBuf.

- QbeGen: RecArrayExpanded centralises the budget; RecArrayNeedsInit /
  InFunction; DeclRecTemplate emits a one-record template with the
  counts set; InitRecArray emits the copy-template-over-each-element
  loop (counter in a stack slot).
- Lower: EmitVarNames records module-level record-array globals that
  need it (declaring the template with their data); EmitCountInits runs
  the loops in the unit's init -- emitting an init even for a unit with
  no BEGIN body.

Regression: compiler/tests/t_arrrecbig.mod (large, exit 42) alongside
t_arrrecfld.mod (small, exit 42).
Suite 183/183; FIXPOINT OK (3,388,344 bytes).
Eric Streit 3 gün önce
ebeveyn
işleme
476f3eefa1

+ 1 - 0
compiler/run_tests.sh

@@ -94,6 +94,7 @@ expect_run t_record.mod 160
 expect_run t_recarr.mod 42
 expect_run t_nestarr.mod 42
 expect_run t_arrrecfld.mod 42
+expect_run t_arrrecbig.mod 42
 expect_run t_under.mod 42
 expect_run t_with.mod 114
 expect_run t_set.mod 131

+ 36 - 1
compiler/src/Lower.mod

@@ -48,6 +48,12 @@ VAR
   impProc  : ARRAY [0 .. MaxImp - 1] OF INTEGER;
   impName  : ARRAY [0 .. MaxImp * 64 - 1] OF CHAR;
   nImps    : CARDINAL;
+  (* Record-array globals too large to expand inline: their template is
+     emitted with their data and their elements are initialised by a
+     run-time loop (QbeGen.InitRecArray) in the unit's init.  Per unit. *)
+  nCountInit : CARDINAL;
+  countArr : ARRAY [0 .. 63] OF SymTab.Name;
+  countTyp : ARRAY [0 .. 63] OF SymTab.TypeIndex;
   (* Scoping: each symbol records the procedure that owns it (-1 =
      module level); FindSym searches the emitted procedure's chain
      innermost-first so common names (s, i, k) do not collide across
@@ -1983,7 +1989,17 @@ PROCEDURE EmitVarNames (c: AST.Node);
         vn := AST.Child(chunk, j);
         IF AST.Kind(vn) = AST.NkIdent THEN
           Name(vn, nm);
-          QbeGen.DeclVar(nm, AST.Ty(c))
+          QbeGen.DeclVar(nm, AST.Ty(c));
+          IF (NOT QbeGen.InFunction())
+             AND (SymTab.ClassOf(AST.Ty(c)) = SymTab.ClArray)
+             AND QbeGen.RecArrayNeedsInit(SymTab.ArrayLen(AST.Ty(c)),
+                                          AST.Ty(c))
+             AND (nCountInit <= HIGH(countArr)) THEN
+            QbeGen.DeclRecTemplate(nm, SymTab.ArrayElem(AST.Ty(c)));
+            countArr[nCountInit] := nm;
+            countTyp[nCountInit] := AST.Ty(c);
+            INC(nCountInit)
+          END
         END;
         INC(j)
       END;
@@ -1995,6 +2011,18 @@ PROCEDURE EmitVarNames (c: AST.Node);
     END
   END EmitVarNames;
 
+PROCEDURE EmitCountInits;
+(* Initialise the length words of record-array globals that were emitted
+   compactly (recorded by EmitVarNames). *)
+  VAR k: CARDINAL;
+  BEGIN
+    k := 0;
+    WHILE k < nCountInit DO
+      QbeGen.InitRecArray(countArr[k], countTyp[k]);
+      INC(k)
+    END
+  END EmitCountInits;
+
 PROCEDURE LowerDecls (seq: AST.Node);
   VAR i, j: CARDINAL; c: AST.Node; nm, txt: SymTab.Name; qv: QbeGen.QVal;
       mg: QbeGen.QVal; uid, depth, np: CARDINAL; res, pt: SymTab.TypeIndex;
@@ -2144,6 +2172,7 @@ PROCEDURE LowerDeferredVars (seq: AST.Node);
 PROCEDURE LowerOneUnit (u: AST.Node; isProg: BOOLEAN);
   VAR nm: SymTab.Name;
   BEGIN
+    nCountInit := 0;
     Name(AST.Child(u, 0), nm);
     CopySpan(nm, curUnitName, 0, 0);
     QbeGen.SetModule(nm);
@@ -2152,11 +2181,17 @@ PROCEDURE LowerOneUnit (u: AST.Node; isProg: BOOLEAN);
     LowerDeferredVars(AST.Child(u, 1));
     IF isProg THEN
       QbeGen.BeginBody;
+      EmitCountInits;
       LowerStmt(AST.Child(u, 2))
     ELSIF AST.Child(u, 2) # AST.NoNode THEN
       QbeGen.BeginInit(nm);
+      EmitCountInits;
       LowerStmt(AST.Child(u, 2));
       QbeGen.EndInit
+    ELSIF nCountInit > 0 THEN
+      QbeGen.BeginInit(nm);
+      EmitCountInits;
+      QbeGen.EndInit
     END
   END LowerOneUnit;
 

+ 17 - 0
compiler/src/QbeGen.def

@@ -446,6 +446,23 @@ PROCEDURE InSet (x: ARRAY OF CHAR; s: ARRAY OF CHAR; lo: INTEGER;
 PROCEDURE DeclRec (name: ARRAY OF CHAR; t: INTEGER);
 (* `data $name = { ... }` flat emission with per-field statics. *)
 
+PROCEDURE InFunction (): BOOLEAN;
+(* TRUE while a function body is open (module-level test). *)
+
+PROCEDURE RecArrayNeedsInit (n: CARDINAL; t: INTEGER): BOOLEAN;
+(* TRUE for an array of records/classes that (recursively) has array
+   fields and is too large to expand inline, so its inline length words
+   must be initialised at run time (DeclRecTemplate + InitRecArray). *)
+
+PROCEDURE DeclRecTemplate (varName: ARRAY OF CHAR; rec: INTEGER);
+(* Emits a one-record template ($<mod>_<varName>_tmpl) with the inline
+   array-field length words set; InitRecArray copies it over each
+   element of the compact array-of-record blob. *)
+
+PROCEDURE InitRecArray (varName: ARRAY OF CHAR; arrT: INTEGER);
+(* Emits the loop that value-initialises every element of
+   $<mod>_<varName> from its template. *)
+
 PROCEDURE FieldAddr (base: ARRAY OF CHAR; off: INTEGER; VAR q: QVal);
 (* q := base + off in l, fresh temp. *)
 

+ 82 - 4
compiler/src/QbeGen.mod

@@ -2034,6 +2034,16 @@ PROCEDURE HasArrayField (t: SymTab.TypeIndex): BOOLEAN;
     RETURN FALSE
   END HasArrayField;
 
+PROCEDURE RecArrayExpanded (n: CARDINAL; t: INTEGER): BOOLEAN;
+(* TRUE when a record-/class-array's per-element expansion fits the image
+   budget.  Larger tables keep the compact zero blob and are instead
+   initialised at run time (RecArrayNeedsInit/InitRecArray).  The budget
+   keeps the compiler's own huge tables (AST.arena: 262144 records) from
+   inflating the 4 MiB image. *)
+  BEGIN
+    RETURN VAL(INTEGER, n) * VAL(INTEGER, SymTab.TypeSize(t)) <= 131072
+  END RecArrayExpanded;
+
 PROCEDURE ArrBodyItems (prefix: ARRAY OF CHAR; t: INTEGER);
 (* Inline contents of an array descriptor (no "data $name = {" wrapper
    and no closing brace): "l <n>[, <elem>...]".  Nested levels are
@@ -2107,8 +2117,7 @@ PROCEDURE ArrBodyItems (prefix: ARRAY OF CHAR; t: INTEGER);
       END
     ELSIF ((ecls = SymTab.ClRecord) OR (ecls = SymTab.ClClass))
         AND HasArrayField(elem)
-        AND (VAL(INTEGER, n) * VAL(INTEGER, SymTab.TypeSize(elem))
-             <= 131072) THEN
+        AND RecArrayExpanded(n, elem) THEN
       i := 0;
       WHILE i < n DO
         Cpy(sub, prefix); App(sub, "_");
@@ -2210,8 +2219,7 @@ PROCEDURE ArrData (name: ARRAY OF CHAR; t: INTEGER);
     ELSIF ((SymTab.ClassOf(elem) = SymTab.ClRecord)
            OR (SymTab.ClassOf(elem) = SymTab.ClClass))
         AND HasArrayField(elem)
-        AND (VAL(INTEGER, SymTab.ArrayLen(t))
-             * VAL(INTEGER, SymTab.TypeSize(elem)) <= 131072) THEN
+        AND RecArrayExpanded(SymTab.ArrayLen(t), elem) THEN
       (* Sub-descriptors referenced (as $name_i_field_k) from the inline
          items emitted by ArrBodyItems; one set per element. *)
       n := SymTab.ArrayLen(t);
@@ -3847,6 +3855,76 @@ PROCEDURE InitHeap (addr: ARRAY OF CHAR; t: SymTab.TypeIndex);
     END
   END InitHeap;
 
+PROCEDURE InFunction (): BOOLEAN;
+(* TRUE while a function body is open (module-level test). *)
+  BEGIN
+    RETURN inFunc
+  END InFunction;
+
+PROCEDURE RecArrayNeedsInit (n: CARDINAL; t: INTEGER): BOOLEAN;
+(* TRUE for an array of records/classes that (recursively) has array
+   fields and is too large to expand inline: its inline length words
+   must be initialised at run time (InitRecArray). *)
+  VAR elem: INTEGER;
+  BEGIN
+    IF SymTab.ClassOf(t) # SymTab.ClArray THEN RETURN FALSE END;
+    elem := SymTab.ArrayElem(t);
+    IF (SymTab.ClassOf(elem) # SymTab.ClRecord)
+       AND (SymTab.ClassOf(elem) # SymTab.ClClass) THEN RETURN FALSE END;
+    IF NOT HasArrayField(elem) THEN RETURN FALSE END;
+    RETURN NOT RecArrayExpanded(n, elem)
+  END RecArrayNeedsInit;
+
+PROCEDURE DeclRecTemplate (varName: ARRAY OF CHAR; rec: INTEGER);
+(* "data $<mod>_<varName>_tmpl = { <one record with counts set> }" plus
+   any nested sub-descriptors; InitRecArray copies it over each element
+   of the compact array-of-record blob. *)
+  VAR g: QVal; first: BOOLEAN;
+  BEGIN
+    IF NOT opened THEN RETURN END;
+    Cpy(g, curModName); App(g, "_"); App(g, varName); App(g, "_tmpl");
+    RecStatics(g, rec);
+    W("data $"); W(g); W(" = { ");
+    first := TRUE;
+    RecItems(rec, g, first);
+    WL(" }")
+  END DeclRecTemplate;
+
+PROCEDURE InitRecArray (varName: ARRAY OF CHAR; arrT: INTEGER);
+(* value-initialise $<mod>_<varName>'s element length words: copy the
+   template record over each element (element i is at base + 8 +
+   i*stride). *)
+  VAR base, tmpl, slot, i, il, off, tb, p, c, one, two, nStr, stStr: QVal;
+      elem: INTEGER; lTop, lBody, lDone: QVal;
+  BEGIN
+    IF NOT opened THEN RETURN END;
+    elem := SymTab.ArrayElem(arrT);
+    Cpy(base, "$"); App(base, curModName); App(base, "_"); App(base, varName);
+    Cpy(tmpl, base); App(tmpl, "_tmpl");
+    IntStr(VAL(INTEGER, SymTab.ArrayLen(arrT)), nStr);
+    IntStr(VAL(INTEGER, SymTab.TypeSize(elem)), stStr);
+    Slot4(slot);
+    StoreW(slot, "0");
+    NewLabel(lTop); NewLabel(lBody); NewLabel(lDone);
+    EmitLabel(lTop);
+    LoadW(slot, i);
+    NewTemp(c); Op3("csltw", c, i, nStr, FALSE);
+    Jnz(c, lBody, lDone);
+    EmitLabel(lBody);
+    LoadW(slot, i);
+    WidenIndex(i, il);
+    NewTemp(off); Op3L("mul", off, il, stStr);
+    NewTemp(tb); Op3L("add", tb, base, "8");
+    NewTemp(p); Op3L("add", p, tb, off);
+    CopyRecord(p, tmpl, elem);
+    LoadW(slot, i);
+    NewTemp(one); CopyOp("1", one);
+    NewTemp(two); Op3("add", two, i, one, FALSE);
+    StoreW(slot, two);
+    Jmp(lTop);
+    EmitLabel(lDone)
+  END InitRecArray;
+
 BEGIN
   opened := FALSE;
   inBody := FALSE;

+ 36 - 0
compiler/tests/t_arrrecbig.mod

@@ -0,0 +1,36 @@
+MODULE TArrRecBig;
+(* Regression for the run-time path of the array-of-record inline
+   length-word fix: a record array larger than the inline-expansion
+   budget is emitted compactly and its element length words are set by a
+   generated init loop (QbeGen.InitRecArray).  Exit 42. *)
+VAR ExitCode : INTEGER;
+
+TYPE
+  ProcEnt = RECORD
+    name : ARRAY [0..63] OF CHAR;
+  END;
+VAR
+  procs  : ARRAY [0..4999] OF ProcEnt;
+  nProcs : CARDINAL;
+
+PROCEDURE CopyN (src : ARRAY OF CHAR; VAR dst : ARRAY OF CHAR);
+  VAR i : CARDINAL;
+BEGIN
+  i := 0;
+  WHILE (i < 63) AND (src[i] # CHR(0)) DO dst[i] := src[i]; INC(i) END;
+  dst[i] := CHR(0)
+END CopyN;
+
+PROCEDURE NoteProc (name : ARRAY OF CHAR);
+BEGIN
+  CopyN(name, procs[nProcs].name);
+  INC(nProcs)
+END NoteProc;
+
+BEGIN
+  NoteProc("hello");
+  IF (procs[0].name[0] = "h") AND (procs[0].name[4] = "o") THEN
+    ExitCode := 42
+  ELSE ExitCode := 1
+  END
+END TArrRecBig.

+ 12 - 14
docs/wip/arrrec-field-openarray.mod

@@ -5,21 +5,19 @@ MODULE ArrRecFieldOpenArray;
    traps.  V3-built compilers miscompile this; gm2-built ones happen to
    work.  Expected: exit 0.  Actual (buggy V3): SIGABRT (134).
 
-   Status (2026-10-08): FIXED for record arrays whose data is at most
-   131072 bytes — `QbeGen.ArrBodyItems`/`ArrData` now expand record/
-   class elements per element (predicate `HasArrayField`, not just
-   nested ARRAY OF ARRAY) so each inline array field's length word is
-   initialised.  This case used to be a single zero blob, so the field's
-   count stayed 0 and the callee trapped.  Regression: compiler/tests/
-   t_arrrecfld.mod (exit 42).
+   Status (2026-10-08): FIXED, both paths.
 
-   Larger tables (e.g. AST.arena, 262144 records) keep the compact zero
-   blob on purpose: expanding them (even interleaved `l <len>, z <n>`)
-   would push the compiler image past the 4 MiB QbeGen.sessBuf.  The
-   principled general fix is to initialise such counts at runtime (a
-   generated init loop), not in the static data; until then the latent
-   0-count bug remains for very large record arrays.  See
-   docs/summary_l3_selfhost_fix.md. *)
+   Small record arrays (data <= 131072 bytes) are expanded per element
+   by `QbeGen.ArrBodyItems`/`ArrData` (predicate `HasArrayField`, not
+   just nested ARRAY OF ARRAY), so each inline array field's length word
+   is in the static data.  Larger ones (e.g. AST.arena, 262144 records)
+   are kept compact and their element length words are set at run time
+   by a generated init loop (`QbeGen.DeclRecTemplate` +
+   `QbeGen.InitRecArray`, driven from `Lower`), which is what keeps the
+   image inside the 4 MiB `QbeGen.sessBuf`.
+
+   Regression: compiler/tests/t_arrrecfld.mod (small, exit 42) and
+   compiler/tests/t_arrrecbig.mod (large, exit 42). *)
 
 TYPE
   ProcEnt = RECORD