소스 검색

language: implement fixed multi-dimensional array storage (M2)

Eric Streit 1 일 전
부모
커밋
4616e51c44
7개의 변경된 파일과 443개의 추가작업 그리고 31개의 파일을 삭제
  1. 1 0
      compiler/run_tests.sh
  2. 67 14
      compiler/src/Lower.mod
  3. 22 0
      compiler/src/QbeGen.def
  4. 287 3
      compiler/src/QbeGen.mod
  5. 24 0
      compiler/tests/t_multidim_local.mod
  6. 24 8
      docs/design-open-arrays.md
  7. 18 6
      docs/handoff-2026-10-23-next.md

+ 1 - 0
compiler/run_tests.sh

@@ -67,6 +67,7 @@ expect_run t_bool.mod 42
 expect_run t_real.mod 31
 expect_run t_array.mod 108
 expect_run t_multidim.mod 42
+expect_run t_multidim_local.mod 42
 expect_run t_subrange.mod 42
 expect_run t_emptychar.mod 42
 expect_run t_system.mod 42

+ 67 - 14
compiler/src/Lower.mod

@@ -58,6 +58,7 @@ VAR
   countTyp : ARRAY [0 .. 63] OF SymTab.TypeIndex;
   (* GOTO/labels: user label name -> QbeGen label, per procedure. *)
   nUserLab : CARDINAL;
+  lastView : BOOLEAN;   (* last LowerDesign result is a multi-array view *)
   userLabNm : ARRAY [0 .. 255 * 64 - 1] OF CHAR;
   userLabQ : ARRAY [0 .. 255] OF QbeGen.QVal;
   (* Scoping: each symbol records the procedure that owns it (-1 =
@@ -1020,7 +1021,7 @@ PROCEDURE LowerDesign (n: AST.Node; VAR t: SymTab.TypeIndex; VAR k: INTEGER;
   VAR nm, fn, mal: SymTab.Name; cv: QbeGen.QVal; cls, op: INTEGER;
       i: CARDINAL; sel: AST.Node; eT, it: SymTab.TypeIndex;
       iq, ql, qlo, qhi, qe: QbeGen.QVal;
-      lo, hi, fo: INTEGER; isOpen: BOOLEAN;
+      lo, hi, fo: INTEGER; isOpen, viewMode: BOOLEAN;
   BEGIN
     IF AST.Kind(AST.Child(n, 0)) = AST.NkCall THEN
       (* a call result used as a designator base: P()^, P()^.field,
@@ -1100,6 +1101,7 @@ PROCEDURE LowerDesign (n: AST.Node; VAR t: SymTab.TypeIndex; VAR k: INTEGER;
     ELSE QbeGen.CopyOp("0", q)
     END;
     END;
+    viewMode := SymTab.IsMultiArray(t);
     i := 1;
     WHILE i < AST.NChild(n) DO
       sel := AST.Child(n, i);
@@ -1130,7 +1132,11 @@ PROCEDURE LowerDesign (n: AST.Node; VAR t: SymTab.TypeIndex; VAR k: INTEGER;
           t := SymTab.FieldType(t, fn);
           QbeGen.FieldAddr(q, fo, qe);
           QbeGen.CopyOp(qe, q);
-          sfx := TRUE
+          sfx := TRUE;
+          (* Multi-dimensional record fields still use the legacy inline
+             nested layout; the flat view layout starts with independent
+             arrays.  M5 will migrate fields. *)
+          viewMode := FALSE
         END
       ELSIF op = AST.SelIndex THEN
         LowerExpr(AST.Child(sel, 0), it, iq);
@@ -1153,10 +1159,16 @@ PROCEDURE LowerDesign (n: AST.Node; VAR t: SymTab.TypeIndex; VAR k: INTEGER;
           END;
           QbeGen.CheckIndex(ql, qlo, qhi);
           eT := SymTab.ArrayElem(t);
-          QbeGen.ElemAddr(q, ql, qlo, t, qe);
-          IF SymTab.ClassOf(eT) = SymTab.ClArray THEN
-            QbeGen.ElemLoad(qe, eT, q)
-          ELSE QbeGen.CopyOp(qe, q)
+          IF viewMode THEN
+            QbeGen.ViewIndex(q, ql, qlo,
+              SymTab.ClassOf(eT) = SymTab.ClArray, qe);
+            QbeGen.CopyOp(qe, q)
+          ELSE
+            QbeGen.ElemAddr(q, ql, qlo, t, qe);
+            IF SymTab.ClassOf(eT) = SymTab.ClArray THEN
+              QbeGen.ElemLoad(qe, eT, q)
+            ELSE QbeGen.CopyOp(qe, q)
+            END
           END;
           t := eT; sfx := TRUE
         END
@@ -1165,11 +1177,14 @@ PROCEDURE LowerDesign (n: AST.Node; VAR t: SymTab.TypeIndex; VAR k: INTEGER;
         ELSIF SymTab.ClassOf(t) # SymTab.ClPtr THEN t := SymTab.InvalidType
         ELSE
           IF sfx THEN QbeGen.ElemLoad(q, t, qe); QbeGen.CopyOp(qe, q) END;
-          t := SymTab.PtrBase(t); sfx := TRUE
+          t := SymTab.PtrBase(t); sfx := TRUE;
+          viewMode := SymTab.IsMultiArray(t)
         END
       END;
       INC(i)
-    END
+    END;
+    lastView := viewMode AND (t # SymTab.InvalidType)
+                AND (SymTab.ClassOf(t) = SymTab.ClArray)
   END LowerDesign;
 
 PROCEDURE CompositeRes (t: SymTab.TypeIndex): BOOLEAN;
@@ -1220,13 +1235,17 @@ PROCEDURE CheckSub (t: SymTab.TypeIndex; q: ARRAY OF CHAR);
 PROCEDURE LowerCall (n: AST.Node; VAR t: SymTab.TypeIndex;
                      VAR q: QbeGen.QVal; want: BOOLEAN);
   VAR nm, pn, cqn: SymTab.Name; i, ai: CARDINAL; at, ft, res: SymTab.TypeIndex;
-      qa, qt, qe, mg, qres: QbeGen.QVal; conv, isV, qual, ind, isC, va: BOOLEAN;
+      qa, qt, qe, mg, qres, qb, qn: QbeGen.QVal;
+      conv, isV, qual, ind, isC, va: BOOLEAN;
       uid, depth, np: CARDINAL; ext: BOOLEAN; ck: INTEGER; pi: INTEGER;
       ct: SymTab.TypeIndex; csfx: BOOLEAN; cl: CHAR;
       seq, next, arg: AST.Node;
+      vaD, vaB, vaN: ARRAY [0 .. 63] OF QbeGen.QVal;
+      nVa, kva: CARDINAL;
   BEGIN
     qual := CalleeAlias(n, nm);
     ind := FALSE; isC := FALSE; va := FALSE; pi := -1; ext := FALSE;
+    nVa := 0;
     IF qual AND qualIsVar THEN
       (* M.Error where Error is a procedure-typed variable: indirect. *)
       LowerDesign(AST.Child(n, 0), ct, ck, qa, cqn, csfx);
@@ -1330,7 +1349,20 @@ PROCEDURE LowerCall (n: AST.Node; VAR t: SymTab.TypeIndex;
             (* permissive C variadic argument *)
             QbeGen.CArgAdd(qe, at, qa, cl)
           ELSIF isV THEN
-            IF (SymTab.ClassOf(at) = SymTab.ClChar)
+            IF lastView AND (SymTab.ClassOf(ft) = SymTab.ClArray)
+               AND (SymTab.ArrayDepth(ft) = 1) THEN
+              (* Row/multi view actual for a VAR 1-D open formal:
+                 materialise a legacy {count, data} descriptor, pass it,
+                 then copy its payload back after the call. *)
+              QbeGen.ViewToOpen1D(qe, ft, qt, qb, qn);
+              IF NOT QbeGen.CallArg(qt, "l") THEN END;
+              IF nVa <= HIGH(vaD) THEN
+                QbeGen.CopyOp(qt, vaD[nVa]);
+                QbeGen.CopyOp(qb, vaB[nVa]);
+                QbeGen.CopyOp(qn, vaN[nVa]);
+                INC(nVa)
+              END
+            ELSIF (SymTab.ClassOf(at) = SymTab.ClChar)
                AND (SymTab.ClassOf(ft) = SymTab.ClArray)
                AND (SymTab.ClassOf(SymTab.ArrayElem(ft)) = SymTab.ClChar)
                AND QbeGen.IsImm(qe) THEN
@@ -1365,6 +1397,15 @@ PROCEDURE LowerCall (n: AST.Node; VAR t: SymTab.TypeIndex;
               QbeGen.OpenScalarDesc(qe, at, ft, qa);
               IF NOT QbeGen.CallArg(qa, "l") THEN END
             ELSE
+              (* A rank-1/row view actual is adapted to the legacy 1-D
+                 descriptor by a payload copy.  VAR formals use the
+                 copy-in/copy-back path above; M3 replaces both with
+                 real view descriptors. *)
+              IF lastView AND (SymTab.ClassOf(ft) = SymTab.ClArray)
+                 AND (SymTab.ArrayDepth(ft) = 1) THEN
+                QbeGen.ViewToOpen1D(qe, ft, qt, qb, qn);
+                QbeGen.CopyOp(qt, qe)
+              END;
               (* A SET/PACKEDSET value actual with a different
                  representation is converted to a temporary of the
                  formal type before the call. *)
@@ -1401,6 +1442,11 @@ PROCEDURE LowerCall (n: AST.Node; VAR t: SymTab.TypeIndex;
       IF CompositeRes(res) THEN QbeGen.CopyOp(qres, q) END
     ELSE
       t := SymTab.InvalidType; QbeGen.CopyOp("0", q); QbeGen.CallEnd(FALSE, q)
+    END;
+    kva := 0;
+    WHILE kva < nVa DO
+      QbeGen.ViewCopyBack(vaD[kva], vaB[kva], vaN[kva]);
+      INC(kva)
     END
   END LowerCall;
 
@@ -2327,7 +2373,8 @@ PROCEDURE LowerExpr (n: AST.Node; VAR t: SymTab.TypeIndex;
                      VAR q: QbeGen.QVal);
   VAR k: INTEGER; qn: SymTab.Name; sfx: BOOLEAN;
   BEGIN
-    LowerExprFull(n, t, q, k, qn, sfx, FALSE)
+    LowerExprFull(n, t, q, k, qn, sfx, FALSE);
+    IF AST.Kind(n) # AST.NkDesignator THEN lastView := FALSE END
   END LowerExpr;
 
 PROCEDURE LowerStmt (seq: AST.Node);
@@ -2338,7 +2385,7 @@ PROCEDURE LowerStmt (seq: AST.Node);
       lTop, lBody, lEnd, lThen, lElse, lNext, lFalse, lDone, lx: QbeGen.QVal;
       qn, lv: SymTab.Name;
       kind, op, rop, cls: INTEGER;
-      sfx, folded, isR, conv, wconv, hasElse: BOOLEAN;
+      sfx, folded, isR, conv, wconv, hasElse, dView, sView: BOOLEAN;
   BEGIN
     IF seq = AST.NoNode THEN RETURN END;
     k := AST.Kind(seq);
@@ -2362,7 +2409,9 @@ PROCEDURE LowerStmt (seq: AST.Node);
       LowerStmt(AST.Child(seq, 0))
     ELSIF k = AST.NkAssign THEN
       LowerDesign(AST.Child(seq, 0), dt, cls, qd, qn, sfx);
+      dView := lastView;
       LowerExpr(AST.Child(seq, 1), et, qe);
+      sView := lastView;
       IF (dt # SymTab.InvalidType) AND (et # SymTab.InvalidType)
          AND SymTab.IsEmptyStr(et)
          AND (SymTab.ClassOf(dt) = SymTab.ClChar) THEN
@@ -2376,7 +2425,9 @@ PROCEDURE LowerStmt (seq: AST.Node);
                AND SymTab.IsIntFamily(et);
       IF sfx OR (cls = SymTab.KindField) THEN
         IF SymTab.ClassOf(dt) = SymTab.ClArray THEN
-          IF (et # SymTab.InvalidType) AND (SymTab.ClassOf(et) = SymTab.ClUStr)
+          IF dView OR sView THEN
+            QbeGen.CopyArrayEx(qd, qe, dt, dView, sView)
+          ELSIF (et # SymTab.InvalidType) AND (SymTab.ClassOf(et) = SymTab.ClUStr)
              AND SymTab.IsUCharArray(dt) THEN QbeGen.UAssign(qd, qe)
           ELSIF SymTab.StrCompat(dt, et) THEN QbeGen.StrAssign(qd, qe)
           ELSE QbeGen.CopyArray(qd, qe, dt)
@@ -2394,7 +2445,9 @@ PROCEDURE LowerStmt (seq: AST.Node);
         END
       ELSE
         IF SymTab.ClassOf(dt) = SymTab.ClArray THEN
-          IF (et # SymTab.InvalidType) AND (SymTab.ClassOf(et) = SymTab.ClUStr)
+          IF dView OR sView THEN
+            QbeGen.CopyArrayEx(qd, qe, dt, dView, sView)
+          ELSIF (et # SymTab.InvalidType) AND (SymTab.ClassOf(et) = SymTab.ClUStr)
              AND SymTab.IsUCharArray(dt) THEN QbeGen.UAssign(qd, qe)
           ELSIF SymTab.StrCompat(dt, et) THEN QbeGen.StrAssign(qd, qe)
           ELSE QbeGen.CopyArray(qd, qe, dt)

+ 22 - 0
compiler/src/QbeGen.def

@@ -367,6 +367,22 @@ PROCEDURE ElemAddr (base, idx, lo: ARRAY OF CHAR; elemT: INTEGER;
 (* q := base + 8 + (idx - lo) * elemSizeOf(elemT's array) in l.
    elemT is the array descriptor (open or fixed). *)
 
+PROCEDURE ViewIndex (view, idx, lo: ARRAY OF CHAR;
+                     nextIsArray: BOOLEAN; VAR q: QVal);
+(* Index a multi-dimensional ArrayView.  Scalar result: q := element
+   address.  Array result: q := a copied next-level view with base
+   adjusted by (idx-lo)*stride. *)
+
+PROCEDURE ViewToOpen1D (view: ARRAY OF CHAR; ft: INTEGER;
+                        VAR q, base, nbytes: QVal);
+(* Adapt a view to the legacy 1-D open descriptor by copying its payload
+   into a malloc'd {count, data} buffer.  base/nbytes report the original
+   view base and payload size so a VAR actual can be copied back after
+   the call.  Compatibility path until the M3 view ABI. *)
+
+PROCEDURE ViewCopyBack (desc, base, nbytes: ARRAY OF CHAR);
+(* Copy a temp descriptor's payload back into a view base after a call. *)
+
 PROCEDURE ElemLoad (addr: ARRAY OF CHAR; t: INTEGER; VAR q: QVal);
 PROCEDURE ElemStore (addr, v: ARRAY OF CHAR; t: INTEGER);
 (* Element access by element-type index (CHAR→loadub … nested→loadl). *)
@@ -379,6 +395,12 @@ PROCEDURE CopyArray (dst: ARRAY OF CHAR; src: ARRAY OF CHAR;
 (* Whole-array copy with runtime count check + blit, recursive for
    nested levels via a runtime loop. Counts mismatch traps. *)
 
+PROCEDURE CopyArrayEx (dst: ARRAY OF CHAR; src: ARRAY OF CHAR;
+                       t: INTEGER; dstIsView, srcIsView: BOOLEAN);
+(* Whole-array copy where either side may be a rank-1 ArrayView (for
+   row fields of multi-dimensional arrays) rather than the legacy
+   {count, data} descriptor. *)
+
 PROCEDURE HaltQ;
 (* HALT: terminate the process via exit(1) (noreturn). *)
 

+ 287 - 3
compiler/src/QbeGen.mod

@@ -3307,6 +3307,98 @@ PROCEDURE ElemAddr (base, idx, lo: ARRAY OF CHAR; elemT: INTEGER;
     Op3L("add", q, tb, t2)
   END ElemAddr;
 
+PROCEDURE ViewField (view: ARRAY OF CHAR; off: CARDINAL; VAR q: QVal);
+(* q := loadl(view + off), off one of 0,8,16,24,32. *)
+  VAR a, s: QVal;
+  BEGIN
+    NewTemp(a);
+    IntStr(VAL(INTEGER, off), s);
+    Op3L("add", a, view, s);
+    NewTemp(q);
+    Revive;
+    W("  "); W(q); W(" =l loadl "); WL(a)
+  END ViewField;
+
+PROCEDURE ViewIndex (view, idx, lo: ARRAY OF CHAR;
+                     nextIsArray: BOOLEAN; VAR q: QVal);
+  VAR t1, stride, off, base, addr, nv, next, tmp: QVal;
+  BEGIN
+    NewTemp(t1);
+    Op3L("sub", t1, idx, lo);
+    ViewField(view, 16, stride);
+    NewTemp(off);
+    Op3L("mul", off, t1, stride);
+    ViewField(view, 24, base);
+    NewTemp(addr);
+    Op3L("add", addr, base, off);
+    IF NOT nextIsArray THEN
+      CopyOp(addr, q)
+    ELSE
+      ViewField(view, 32, next);
+      NewTemp(nv);
+      Revive;
+      W("  "); W(nv); WL(" =l alloc8 40");
+      (* copy count, elemBytes, stride and next from the template *)
+      ViewField(next, 0, tmp);
+      Revive; W("  storel "); W(tmp); W(", "); WL(nv);
+      IntStr(8, off);
+      NewTemp(t1); Op3L("add", t1, nv, off);
+      ViewField(next, 8, tmp);
+      Revive; W("  storel "); W(tmp); W(", "); WL(t1);
+      IntStr(16, off);
+      NewTemp(t1); Op3L("add", t1, nv, off);
+      ViewField(next, 16, tmp);
+      Revive; W("  storel "); W(tmp); W(", "); WL(t1);
+      IntStr(32, off);
+      NewTemp(t1); Op3L("add", t1, nv, off);
+      ViewField(next, 32, tmp);
+      Revive; W("  storel "); W(tmp); W(", "); WL(t1);
+      IntStr(24, off);
+      NewTemp(t1); Op3L("add", t1, nv, off);
+      Revive; W("  storel "); W(addr); W(", "); WL(t1);
+      CopyOp(nv, q)
+    END
+  END ViewIndex;
+
+PROCEDURE ViewToOpen1D (view: ARRAY OF CHAR; ft: INTEGER;
+                        VAR q, base, nbytes: QVal);
+  VAR count, elem, total, n, desc, p: QVal;
+    cls: INTEGER;
+  BEGIN
+    ViewField(view, 0, count);
+    ViewField(view, 8, elem);
+    NewTemp(total);
+    Op3L("mul", total, count, elem);
+    cls := SymTab.ClassOf(SymTab.ArrayElem(ft));
+    IF (cls = SymTab.ClByte) OR (cls = SymTab.ClSByte) THEN
+      CopyOp(total, count)
+    END;
+    NewTemp(n);
+    Op3L("add", n, total, "8");
+    NewTemp(desc);
+    W("  "); W(desc); W(" =l call $malloc(l "); W(n); WL(")");
+    Revive;
+    W("  storel "); W(count); W(", "); WL(desc);
+    ViewField(view, 24, base);
+    CopyOp(total, nbytes);
+    NewTemp(p);
+    Op3L("add", p, desc, "8");
+    Revive;
+    W("  "); W(n); W(" =l call $memcpy(l ");
+    W(p); W(", l "); W(base); W(", l "); W(total); WL(")");
+    CopyOp(desc, q)
+  END ViewToOpen1D;
+
+PROCEDURE ViewCopyBack (desc, base, nbytes: ARRAY OF CHAR);
+  VAR p, r: QVal;
+  BEGIN
+    NewTemp(p);
+    Op3L("add", p, desc, "8");
+    NewTemp(r);
+    W("  "); W(r); W(" =l call $memcpy(l ");
+    W(base); W(", l "); W(p); W(", l "); W(nbytes); WL(")")
+  END ViewCopyBack;
+
 PROCEDURE ElemLoad (addr: ARRAY OF CHAR; t: INTEGER; VAR q: QVal);
 (* Loads one element of (element-)type t. Nested/pointer elements
    are addresses (loadl). *)
@@ -3363,6 +3455,45 @@ PROCEDURE ElemStore (addr, v: ARRAY OF CHAR; t: INTEGER);
     W(v); W(", "); WL(addr)
   END ElemStore;
 
+PROCEDURE CopyOldNestedToView (dst, src: ARRAY OF CHAR;
+                               t: INTEGER; rank: CARDINAL);
+(* Adapt an old nested-descriptor constructor (e.g. names{"Thu","Fri"})
+   into a flat multi-dimensional view payload.  Only static constructor
+   sources use this path. *)
+  VAR count, base, esz, row, a, off, off2, child, i, c: QVal;
+    ltop, lbody, lend: QVal;
+  BEGIN
+    IF rank <= 1 THEN
+      ViewField(dst, 24, base);
+      ViewField(dst, 0, count);
+      ViewField(dst, 8, esz);
+      NewTemp(a);
+      Op3L("mul", a, count, esz);
+      NewTemp(off); Op3L("add", off, src, "8");
+      NewTemp(c);
+      W("  "); W(c); W(" =l call $memcpy(l ");
+      W(base); W(", l "); W(off); W(", l "); W(a); WL(")")
+    ELSE
+      ViewField(dst, 0, count);
+      NewTemp(i);
+      W("  "); W(i); W(" =l copy 0"); WL("");
+      NewLabel(ltop); NewLabel(lbody); NewLabel(lend);
+      EmitLabel(ltop);
+      NewTemp(c); Op3("csltl", c, i, count, FALSE);
+      Jnz(c, lbody, lend);
+      EmitLabel(lbody);
+      ViewIndex(dst, i, "0", TRUE, row);
+      NewTemp(off); Op3L("mul", off, i, "8");
+      NewTemp(a); Op3L("add", a, src, "8");
+      NewTemp(off2); Op3L("add", off2, a, off);
+      NewTemp(child); W("  "); W(child); W(" =l loadl "); WL(off2);
+      CopyOldNestedToView(row, child, SymTab.ArrayElem(t), rank - 1);
+      Op3L("add", i, i, "1");
+      Jmp(ltop);
+      EmitLabel(lend)
+    END
+  END CopyOldNestedToView;
+
 PROCEDURE CopyArray (dst: ARRAY OF CHAR; src: ARRAY OF CHAR;
                       t: INTEGER);
 (* Whole-array copy with runtime count check + blit. Nested levels
@@ -3377,6 +3508,24 @@ PROCEDURE CopyArray (dst: ARRAY OF CHAR; src: ARRAY OF CHAR;
     i, c, de, se, di, si, dd0, doff, sd0, soff: QVal;
     ltop, lbody, lend: QVal;
   BEGIN
+    IF SymTab.IsMultiArray(t) THEN
+      IF (src[0] = "$") AND (src[1] = "c") AND (src[2] = "t")
+         AND (src[3] = "o") THEN
+        CopyOldNestedToView(dst, src, t, SymTab.ArrayDepth(t))
+      ELSE
+        (* src/dst are ArrayView pointers; copy the flat payload only. *)
+        ViewField(dst, 24, da);
+        ViewField(src, 24, sa);
+        ViewField(dst, 0, n);
+        ViewField(dst, 8, sb);
+        NewTemp(qr);
+        Op3L("mul", qr, n, sb);
+        NewTemp(n);
+        W("  "); W(n); W(" =l call $memcpy(l ");
+        W(da); W(", l "); W(sa); W(", l "); W(qr); WL(")")
+      END;
+      RETURN
+    END;
     NewTemp(dc);
     Revive;
     W("  "); W(dc); W(" =l loadl "); WL(dst);
@@ -3427,6 +3576,40 @@ PROCEDURE CopyArray (dst: ARRAY OF CHAR; src: ARRAY OF CHAR;
     END
   END CopyArray;
 
+PROCEDURE CopyArrayEx (dst: ARRAY OF CHAR; src: ARRAY OF CHAR;
+                       t: INTEGER; dstIsView, srcIsView: BOOLEAN);
+  VAR db, dc, de, sb, sc, se, total, srcData, dstData, r: QVal;
+  BEGIN
+    IF SymTab.IsMultiArray(t) THEN
+      CopyArray(dst, src, t); RETURN
+    END;
+    IF dstIsView THEN
+      ViewField(dst, 24, db);
+      ViewField(dst, 0, dc);
+      ViewField(dst, 8, de);
+      NewTemp(total); Op3L("mul", total, dc, de);
+      IF srcIsView THEN
+        ViewField(src, 24, sb)
+      ELSE
+        NewTemp(srcData); Op3L("add", srcData, src, "8"); CopyOp(srcData, sb)
+      END;
+      NewTemp(r);
+      W("  "); W(r); W(" =l call $memcpy(l ");
+      W(db); W(", l "); W(sb); W(", l "); W(total); WL(")")
+    ELSIF srcIsView THEN
+      ViewField(src, 24, sb);
+      ViewField(src, 0, sc);
+      ViewField(src, 8, se);
+      NewTemp(total); Op3L("mul", total, sc, se);
+      NewTemp(dstData); Op3L("add", dstData, dst, "8");
+      NewTemp(r);
+      W("  "); W(r); W(" =l call $memcpy(l ");
+      W(dstData); W(", l "); W(sb); W(", l "); W(total); WL(")")
+    ELSE
+      CopyArray(dst, src, t)
+    END
+  END CopyArrayEx;
+
 PROCEDURE DeclStr (text: ARRAY OF CHAR; VAR q: QVal);
 (* Records a quoted literal for top-level emission at EndModule and
    returns its address operand ($strN). Data definitions may only
@@ -3646,12 +3829,59 @@ PROCEDURE FlushStrings;
     END
   END FlushStrings;
 
+PROCEDURE DeclMultiArr (name: ARRAY OF CHAR; t: INTEGER);
+(* Static fixed multi-dimensional array: a chain of view descriptors
+   plus one contiguous payload block.  One-dimensional arrays still use
+   ArrData. *)
+  VAR rank, k, count, stride, payload: CARDINAL;
+    cur: SymTab.TypeIndex;
+    lab, base, nxt, bv, sv, pv: QVal;
+  BEGIN
+    IF NOT opened THEN RETURN END;
+    rank := SymTab.ArrayDepth(t);
+    IF rank < 2 THEN ArrData(name, t); RETURN END;
+    payload := SymTab.ArrayPayloadSize(t);
+    cur := t;
+    k := 0;
+    WHILE k < rank DO
+      count := SymTab.ArrayLen(cur);
+      stride := SymTab.ArrayStride(t, k);
+      Cpy(lab, name);
+      IF k > 0 THEN
+        App(lab, "_t"); IntStr(VAL(INTEGER, k), bv); App(lab, bv)
+      END;
+      IF k = 0 THEN
+        Cpy(base, "$"); App(base, name); App(base, "_data")
+      ELSE
+        Cpy(base, "0")
+      END;
+      IF k + 1 < rank THEN
+        Cpy(nxt, "$"); App(nxt, name); App(nxt, "_t");
+        IntStr(VAL(INTEGER, k + 1), bv); App(nxt, bv)
+      ELSE
+        Cpy(nxt, "0")
+      END;
+      W("data $"); W(lab); W(" = { l ");
+      IntStr(VAL(INTEGER, count), bv); W(bv);
+      W(", l "); IntStr(VAL(INTEGER, stride), sv); W(sv);
+      W(", l "); W(sv);
+      W(", l "); W(base);
+      W(", l "); W(nxt); WL(" }");
+      cur := SymTab.ArrayElem(cur);
+      INC(k)
+    END;
+    W("data $"); W(name); W("_data = { z ");
+    IntStr(VAL(INTEGER, payload), pv); W(pv); WL(" }")
+  END DeclMultiArr;
+
 PROCEDURE DeclArr (name: ARRAY OF CHAR; t: INTEGER);
   BEGIN
     IF SymTab.IsOpenArray(t) OR (SymTab.ArrayDepth(t) = 0) THEN
       DataLine(name, FALSE, "0"); RETURN
     END;
-    ArrData(name, t)
+    IF SymTab.IsMultiArray(t) THEN DeclMultiArr(name, t)
+    ELSE ArrData(name, t)
+    END
   END DeclArr;
 
 (* ---------------- array constructors (step: `T{...}`) ---------------- *)
@@ -4171,7 +4401,9 @@ PROCEDURE HeapSize (t: INTEGER): CARDINAL;
     n: CARDINAL;
   BEGIN
     cls := SymTab.ClassOf(t);
-    IF cls = SymTab.ClArray THEN
+    IF (cls = SymTab.ClArray) AND SymTab.IsMultiArray(t) THEN
+      RETURN SymTab.ArrayDepth(t) * 40 + SymTab.ArrayPayloadSize(t)
+    ELSIF cls = SymTab.ClArray THEN
       n := SymTab.ArrayLen(t) * ElemSize(t);
       IF SymTab.IsCharArray(t) THEN n := n + 1 END;
       IF SymTab.IsUCharArray(t) THEN n := n + 4 END;
@@ -4198,6 +4430,56 @@ PROCEDURE FreeHeap (v: ARRAY OF CHAR);
     W("  call $free(l "); W(v); WL(")")
   END FreeHeap;
 
+PROCEDURE InitMultiHeap (addr: ARRAY OF CHAR; t: SymTab.TypeIndex);
+(* Initialise the per-level view chain and zero the flat payload. *)
+  VAR rank, k, count, stride, payload: CARDINAL;
+    cur: SymTab.TypeIndex;
+    v, a, nb, off, pay: QVal;
+  BEGIN
+    rank := SymTab.ArrayDepth(t);
+    payload := SymTab.ArrayPayloadSize(t);
+    cur := t;
+    k := 0;
+    WHILE k < rank DO
+      IntStr(VAL(INTEGER, k * 40), off);
+      NewTemp(v); Op3L("add", v, addr, off);
+      count := SymTab.ArrayLen(cur);
+      IntStr(VAL(INTEGER, count), nb);
+      Revive; W("  storel "); W(nb); W(", "); WL(v);
+      stride := SymTab.ArrayStride(t, k);
+      IntStr(VAL(INTEGER, stride), nb);
+      NewTemp(a); Op3L("add", a, v, "8");
+      Revive; W("  storel "); W(nb); W(", "); WL(a);
+      NewTemp(a); Op3L("add", a, v, "16");
+      Revive; W("  storel "); W(nb); W(", "); WL(a);
+      IF k = 0 THEN
+        IntStr(VAL(INTEGER, rank * 40), off);
+        NewTemp(pay); Op3L("add", pay, addr, off);
+        NewTemp(a); Op3L("add", a, v, "24");
+        Revive; W("  storel "); W(pay); W(", "); WL(a)
+      ELSE
+        NewTemp(a); Op3L("add", a, v, "24");
+        Revive; W("  storel 0, "); WL(a)
+      END;
+      IF k + 1 < rank THEN
+        IntStr(VAL(INTEGER, (k + 1) * 40), off);
+        NewTemp(a); Op3L("add", a, addr, off);
+        NewTemp(nb); Op3L("add", nb, v, "32");
+        Revive; W("  storel "); W(a); W(", "); WL(nb)
+      ELSE
+        NewTemp(a); Op3L("add", a, v, "32");
+        Revive; W("  storel 0, "); WL(a)
+      END;
+      cur := SymTab.ArrayElem(cur);
+      INC(k)
+    END;
+    IntStr(VAL(INTEGER, rank * 40), off);
+    NewTemp(pay); Op3L("add", pay, addr, off);
+    IntStr(VAL(INTEGER, payload), nb);
+    NewTemp(a); W("  "); W(a); W(" =l call $memset(l ");
+    W(pay); W(", w 0, l "); W(nb); WL(")")
+  END InitMultiHeap;
+
 PROCEDURE InitHeap (addr: ARRAY OF CHAR; t: SymTab.TypeIndex);
   VAR cls, ecls: INTEGER;
     n, i: CARDINAL;
@@ -4215,7 +4497,9 @@ PROCEDURE InitHeap (addr: ARRAY OF CHAR; t: SymTab.TypeIndex);
       Revive;
       W("  storel "); W(eb); W(", "); WL(ea)
     END;
-    IF cls = SymTab.ClArray THEN
+    IF (cls = SymTab.ClArray) AND SymTab.IsMultiArray(t) THEN
+      InitMultiHeap(addr, t)
+    ELSIF cls = SymTab.ClArray THEN
       IntStr(VAL(INTEGER, SymTab.ArrayLen(t)), nb);
       Revive;
       W("  storel "); W(nb); W(", "); WL(addr);

+ 24 - 0
compiler/tests/t_multidim_local.mod

@@ -0,0 +1,24 @@
+MODULE TMultiDimLocal;
+(* M2: local fixed 2-D/3-D arrays use flat payload + per-level views. *)
+TYPE
+  Grid = ARRAY [0..1], [0..2] OF INTEGER;
+  Cube = ARRAY [0..1], [0..1], [0..1] OF INTEGER;
+VAR ExitCode : INTEGER;
+
+PROCEDURE P;
+VAR g : Grid;
+VAR c : Cube;
+BEGIN
+  g[1, 0] := 42;
+  c[1, 1, 0] := 7;
+  IF (g[1, 0] = 42) AND (c[1, 1, 0] = 7) THEN
+    ExitCode := 42
+  ELSE
+    ExitCode := 1
+  END
+END P;
+
+BEGIN
+  ExitCode := 0;
+  P
+END TMultiDimLocal.

+ 24 - 8
docs/design-open-arrays.md

@@ -1,10 +1,11 @@
 # Design — multi-dimensional open arrays
 
-**Status: design + M1 type/ABI helpers (2026-10-23).**  The view
-descriptor ABI is designed but no array layout/codegen change is
-implemented yet.  `SymTab` now exposes the rank/payload/stride queries
-needed by M2.  Baseline: suite **283/283**, fixpoint OK **4,198,600
-bytes**, corpus **512/612 compile-OK**, 0 crashes.
+**Status: design + M1 helpers + M2 fixed storage (2026-10-23).**
+Rank-2/3 fixed arrays now use a flat payload with per-level view
+descriptors; open arrays and builtins remain M3/M4.  Current gates:
+suite **284/284**, fixpoint OK **3,439,456 bytes**, corpus **512/612
+compile-OK**, 0 crashes.  The view descriptor ABI is designed but open
+view descriptors/call conversion are not implemented yet.
 
 Interactive decisions already taken:
 
@@ -369,10 +370,25 @@ Gates after each step: suite, fixpoint, corpus.
    - `MaxDefer` raised from 256 to 512 (the compiler now declares 257
      procedures) and the session SSA buffer raised from 4 to 8 MiB.
    - No array layout/codegen change.
-2. **M2 — fixed multi storage** (flat payload + static/local views;
-   keep 1-D unchanged).
+2. **M2 — fixed multi storage** — **done (2026-10-23)**
+   - Static and local rank-2/3 arrays use one contiguous payload and a
+     chain of 40-byte `{count, elemBytes, stride, base, next}` views.
+   - `ViewIndex` produces subviews for array selectors and element
+     addresses for the final scalar selector.
+   - Pointer-to-multi indexing and `NEW`/local initialization work.
+   - Whole-array copy handles new-view sources, plus old nested static
+     constructors via `CopyOldNestedToView`.
+   - A compatibility copy-in/copy-back path adapts rank-1 row views to
+     the legacy 1-D `{count, data}` descriptor for fixed/open formals;
+     this is intentionally temporary until M3.
+   - Multi-dimensional **record fields** still use the legacy inline
+     nested layout (M5); `t_nestarr` remains the regression.
+   - Tests: `t_multidim`, `t_multidim_local`, `t_array`, `t_ctor3`,
+     `t_nestidx`, `t_nestarr`, `t_ptrnest`, `showcase6`; suite 284/284,
+     fixpoint OK, corpus 512/612 with 0 crashes.
 3. **M3 — open view descriptors** (stack views, calls, row subviews,
-   flattening).
+   flattening).  Replace the M2 copy-in/copy-back adapter with real view
+   descriptors, including `VAR` writes and row/subarray lifetime.
 4. **M4 — builtins/indexing/assignment** (`SIZE`, `HIGH`, `ADR`,
    `ElemAddr`, `CopyArray`).
 5. **M5 — records/constructors/corpus**

+ 18 - 6
docs/handoff-2026-10-23-next.md

@@ -1,7 +1,7 @@
 # Next session — plan (from 2026-10-23)
 
-**Branch:** `master`.  **Feature state:** `v3-exceptions` + `v3-ptrchain` + `v3-function-exception` + `v3-finally`.
-**Gates:** suite **283/283**, fixpoint **OK (4,198,600 bytes)**,
+**Branch:** `master`.  **Feature state:** `v3-exceptions` + `v3-ptrchain` + `v3-function-exception` + `v3-finally` + open-array M1/M2.
+**Gates:** suite **284/284**, fixpoint **OK (3,439,456 bytes)**,
 corpus **512/612, 0 crashes**.
 
 Read first: `docs/STATUS.md`, `docs/features.md`,
@@ -80,14 +80,26 @@ records/constructors.
 
 **M1 — type/ABI helpers — done (2026-10-23).**  `ArrayRank`,
 `IsMultiArray`, `ArrayLeafBytes`, `ArrayStride`, `ArrayPayloadSize`;
-`MaxDefer` 512 and the session SSA buffer 8 MiB.  No layout/codegen
-change yet.  Next is M2: fixed multi-dimensional storage.
+`MaxDefer` 512 and the session SSA buffer 8 MiB.
+
+**M2 — fixed multi storage — done (2026-10-23).**  Static and local
+rank-2/3 arrays now use one contiguous payload plus a chain of 40-byte
+views `{count, elemBytes, stride, base, next}`.  `ViewIndex` yields
+subviews or final element addresses; pointer-to-multi indexing and
+`NEW`/local initialization work.  Whole-array copies handle new views
+and old nested static constructors (`CopyOldNestedToView`).  A temporary
+copy-in/copy-back adapter maps rank-1 row views to the legacy 1-D
+descriptor for fixed/open formals; this was required by the compiler's
+own `files[n]`/`symSet[...]` uses and is not the final M3 ABI.
+Multi-dimensional record fields still use the legacy nested layout;
+`t_nestarr` is unchanged.  Regression `t_multidim_local` was added.
+Suite **284/284**, fixpoint **OK**, corpus **512/612, 0 crashes**.
 
 Corpus: `unbounded`, `unbounded2/3/4/6/8`, `array9`, `adrunbounded3`.
 Do not just accept the files; the ABI and `ADR(m) = ADR(m[0])` checks
 must be real.  `unbounded5` remains a declaration-order case.  Next is
-M2: fixed multi-dimensional flat storage, then the open views/call
-conversion in M3.
+M3: real open view descriptors, stack views, calls and row/subarray
+conversion, replacing the M2 temporary copy adapter.
 
 ## 4. Runtime range checks — done, follow-ups