Browse Source

language: corpus triage — PIM arrays, typed subranges, ordinal FOR

Three conforming clusters from the 118 LANG entries (corpus 467 -> 479):

* ArrayType accepts the PIM/ISO repeated-group form
  "ARRAY [lo..hi], [lo..hi] OF T" (17 corpus files use it; V3 only had
  the non-standard one-bracket comma list).  Bounds from every group
  nest inside-out; the old form still works.  +6 (arraybool,
  assignment2, foo, recordarray2, tinyarray4/5).
* Subranges keep their ordinal base (NewSubB): ['A'..'Z'] is a
  character type and [red..blue] that enumeration, so base values
  assign to and compare with them (Assignable/EqCheck/OrdCheck);
  set spans already followed the bounds.  +3 (subrange16/17, enum3).
* FOR accepts any ordinal control variable (bounds assignment-
  compatible with it); MIN/MAX of a SET yields the set's base type.
  +3 (forloop2, smallset4, minmax).

Regressions t_multidim, t_subrange.  Suite 251/251, fixpoint OK
(3,890,738 bytes), corpus 479/612, 0 crashes.

Docs: session summary, STATUS/README/features, gaps triage and the
next buckets (libc prototypes ~48, ISO sized SYSTEM types ~8).
Eric Streit 4 hours ago
parent
commit
aa4f49b8b0

+ 4 - 5
README.md

@@ -45,14 +45,13 @@ vendor/            qbe toolchain pin
 docs/              STATUS.md, plan.md, plan-*.md, analysis_*.md, summary_*.md
 ```
 
-## Status (2026-10-18)
+## Status (2026-10-22)
 
-- Suite **239/239**; self-hosting **fixpoint OK** (byte-identical
-  stage2/stage3, 3,812,771 bytes); corpus **405/612 compile-OK,
+- Suite **251/251**; self-hosting **fixpoint OK** (byte-identical
+  stage2/stage3, 3,890,738 bytes); corpus **479/612 compile-OK,
   0 crashes** (`tools/v3-corpus`; the corpus is gm2's testsuite, so it
   mixes conforming Modula-2 with gm2 leniencies — see `docs/gaps/`).
-- Active branch **`resolve-check`** (holds the AST/`Lower`/parse
-  refactors and generics).
+- Active branch **`master`**.
 
 Lowers today: program modules; `CONST`/`VAR`/`TYPE` (scalars,
 subranges, enums, arrays, records incl. variants, sets, pointers);

+ 2 - 0
compiler/run_tests.sh

@@ -66,6 +66,8 @@ expect_run t_variantext.mod 42
 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_subrange.mod 42
 expect_run t_ctor1.mod 0
 expect_run t_ctor2.mod 0
 expect_run t_ctor3.mod 0

+ 60 - 20
compiler/src/M2.atg

@@ -1288,8 +1288,11 @@ PRODUCTIONS
                                            SymTab.ProcTypeAdd(t, isV, pt); .) ) .
   (* Arrays: "OF" without bounds is an open formal (allowed only
      where allowOpen); "[lo..hi, ...]" nests bounded levels inside
-     out. Bounds are folded literals (int/char); anything else 230.
-     Bare-type indices ("ARRAY Color OF") wait for enum ordinals. *)
+     out.  The PIM/ISO multi-index form repeats the bracket group
+     ("ARRAY [lo..hi], [lo..hi] OF T"); the bounds of every group
+     nest in the same inside-out order as the comma list.  Bounds are
+     folded literals (int/char); anything else 230.  Bare-type indices
+     ("ARRAY Color OF") wait for enum ordinals. *)
   ArrayType<VAR t: SymTab.TypeIndex; allowOpen: BOOLEAN>
                                         (. VAR elem: SymTab.TypeIndex;
                                              eb: SymTab.Name;
@@ -1317,19 +1320,39 @@ PRODUCTIONS
                                            ELSE SemError(230);
                                              t := SymTab.InvalidType
                                            END; .)
-      | "["                             (. SymTab.BoundBegin; ok := TRUE; .)
+      | "["                             (. SymTab.BoundBegin; ok := TRUE;
+                                           nb := 0; .)
         BoundPair<ok>
         { "," BoundPair<ok> }
-        "]"                             (. (* snapshot before the element
-                                              type, which reuses the bound
-                                              buffer for a nested ARRAY *)
-                                           nb := SymTab.BoundCount();
-                                           k := 0;
-                                           WHILE k < nb DO
-                                             bnds[k] := SymTab.BoundLo(k);
-                                             bndh[k] := SymTab.BoundHi(k);
-                                             INC(k)
+        "]"                             (. (* snapshot this bracket group
+                                              before the element type, which
+                                              reuses the bound buffer for a
+                                              nested ARRAY *)
+                                           IF nb + SymTab.BoundCount() >
+                                              HIGH(bnds) + 1 THEN
+                                             SemError(230); ok := FALSE
+                                           ELSE
+                                             k := 0;
+                                             WHILE k < SymTab.BoundCount() DO
+                                               bnds[nb] := SymTab.BoundLo(k);
+                                               bndh[nb] := SymTab.BoundHi(k);
+                                               INC(k); INC(nb)
+                                             END
                                            END; .)
+        { "," "["                       (. SymTab.BoundBegin; .)
+          BoundPair<ok>
+          { "," BoundPair<ok> }
+          "]"                           (. IF nb + SymTab.BoundCount() >
+                                              HIGH(bnds) + 1 THEN
+                                             SemError(230); ok := FALSE
+                                           ELSE
+                                             k := 0;
+                                             WHILE k < SymTab.BoundCount() DO
+                                               bnds[nb] := SymTab.BoundLo(k);
+                                               bndh[nb] := SymTab.BoundHi(k);
+                                               INC(k); INC(nb)
+                                             END
+                                           END; .) }
         "OF" Type<elem, FALSE>
                                         (. IF ok THEN
                                              k := nb;
@@ -1387,7 +1410,13 @@ PRODUCTIONS
                                              t := SymTab.NewSet(base)
                                            ELSE bcls :=
                                                   SymTab.ClassOf(base);
-                                             IF bcls = SymTab.ClBool THEN
+                                             (* a subrange keeps its own
+                                                span even when its base is
+                                                CHAR/BOOLEAN/an enum *)
+                                             IF SymTab.SubBounds(base,
+                                                     blo, bhi) THEN
+                                               bspan := bhi - blo + 1
+                                             ELSIF bcls = SymTab.ClBool THEN
                                                blo := 0; bspan := 2
                                              ELSIF bcls = SymTab.ClChar THEN
                                                blo := 0; bspan := 256
@@ -1395,9 +1424,6 @@ PRODUCTIONS
                                                blo := 0;
                                                bspan := VAL(INTEGER,
                                                  SymTab.EnumCount(base))
-                                             ELSIF SymTab.SubBounds(base,
-                                                     blo, bhi) THEN
-                                               bspan := bhi - blo + 1
                                              ELSE bspan := 0 END;
                                              IF (bspan <= 0)
                                                 OR (bspan > 65536) THEN
@@ -1541,7 +1567,13 @@ PRODUCTIONS
                                             OR (lo > hi) THEN
                                            SemError(230);
                                            t := SymTab.InvalidType
+                                         ELSE IF SymTab.SameType(tlo, thi) THEN
+                                           (* keep the base type: ['A'..'Z']
+                                              is character, [red..blue]
+                                              that enumeration *)
+                                           t := SymTab.NewSubB(lo, hi, tlo)
                                          ELSE t := SymTab.NewSubR(lo, hi)
+                                         END
                                          END; .)
       "]" .
   Enum<VAR t: SymTab.TypeIndex>         (. VAR n: SymTab.Name;
@@ -2586,16 +2618,18 @@ PRODUCTIONS
  AND (SymTab.SymKind(lv) #
                                                  SymTab.KindParam) THEN
                                              SemError(220); ok := FALSE
-                                           ELSIF NOT SymTab.IsIntFamily(
+                                           ELSIF NOT SymTab.IsOrdinal(
                                                    SymTab.SymType(lv)) THEN
                                              SemError(220); ok := FALSE
                                            END; .)
       ":=" Expr<tlo, qlo> (. astLo := astCur;
-                                           IF NOT SymTab.IsIntFamily(tlo) THEN
+                                           IF NOT SymTab.Assignable(tlo,
+                                                SymTab.SymType(lv)) THEN
                                              SemError(220); ok := FALSE
                                            END; .)
       "TO" Expr<thi, qhi> (. astHi := astCur;
-                                           IF NOT SymTab.IsIntFamily(thi) THEN
+                                           IF NOT SymTab.Assignable(thi,
+                                                SymTab.SymType(lv)) THEN
                                              SemError(220); ok := FALSE
                                            END; .)
       [ "BY" Expr<tby, qby> (. astBy := astCur;
@@ -3710,7 +3744,13 @@ PRODUCTIONS
                                                QbeGen.IntStr(hi, q)
                                              ELSE QbeGen.IntStr(lo, q)
                                              END;
-                                             t := SymTab.IntType()
+                                             (* a set's bounds are those
+                                                of its base type *)
+                                             IF SymTab.ClassOf(dt) =
+                                                SymTab.ClSet THEN
+                                               t := SymTab.SetBase(dt)
+                                             ELSE t := SymTab.IntType()
+                                             END
                                            ELSE SemError(230);
                                              t := SymTab.InvalidType;
                                              QbeGen.CopyOp("0", q)

File diff suppressed because it is too large
+ 2909 - 2869
compiler/src/M2.lst


+ 5 - 0
compiler/src/SymTab.def

@@ -396,6 +396,11 @@ PROCEDURE EnterTypeParamBound (name: ARRAY OF CHAR;
 PROCEDURE NewSubR (lo, hi: INTEGER): TypeIndex;
 (* Integer subrange [lo..hi] (no base descriptor needed). Bounds
    accessors arrive with step 3; the backend cannot lower them yet. *)
+PROCEDURE NewSubB (lo, hi: INTEGER; base: TypeIndex): TypeIndex;
+(* Subrange [lo..hi] anchored on an ordinal base type (CHAR, BOOLEAN,
+   enumeration): ClassOf follows the base so assignment compatibility
+   and set spans treat e.g. ['A'..'Z'] as a character type, not an
+   integer one. *)
 PROCEDURE NewEnum (): TypeIndex;
 PROCEDURE NewArrayB (elem: TypeIndex; lo, hi: INTEGER): TypeIndex;
 (* Bounded array (one dimension; nest for multi-dim). InvalidType

+ 42 - 5
compiler/src/SymTab.mod

@@ -770,6 +770,17 @@ PROCEDURE NewSubR (lo, hi: INTEGER): TypeIndex;
     RETURN t
   END NewSubR;
 
+PROCEDURE NewSubB (lo, hi: INTEGER; base: TypeIndex): TypeIndex;
+(* A subrange anchored on an ordinal base type: ClassOf follows the
+   base, so ['A'..'Z'] is a character type and [red..blue] an
+   enumeration of that type. *)
+  VAR t: TypeIndex;
+  BEGIN
+    t := NewDesc(FSub, base);
+    IF t # InvalidType THEN tlo[t] := lo; thi[t] := hi END;
+    RETURN t
+  END NewSubB;
+
 PROCEDURE NewEnum (): TypeIndex;
   BEGIN
     RETURN NewDesc(FEnum, InvalidType)
@@ -1230,8 +1241,7 @@ PROCEDURE SubBounds (t: TypeIndex; VAR lo, hi: INTEGER): BOOLEAN;
   BEGIN
     lo := 0; hi := -1;
     r := Resolve(t);
-    IF (r = InvalidType) OR (tform[r] # FSub)
-       OR (tref[r] # InvalidType) THEN
+    IF (r = InvalidType) OR (tform[r] # FSub) THEN
       RETURN FALSE
     END;
     lo := tlo[r]; hi := thi[r];
@@ -1264,7 +1274,7 @@ PROCEDURE SetSpan (t: TypeIndex; VAR lo, n: INTEGER): BOOLEAN;
       IF tenum[r] = 0 THEN RETURN FALSE END;
       lo := 0; n := VAL(INTEGER, tenum[r]); RETURN TRUE
     END;
-    IF (tform[r] = FSub) AND (tref[r] = InvalidType)
+    IF (tform[r] = FSub)
  AND SubBounds(b, blo, bhi) AND (bhi >= blo)
  AND (bhi - blo < 65536) THEN
       lo := blo; n := bhi - blo + 1; RETURN TRUE
@@ -2947,7 +2957,7 @@ PROCEDURE BaseSpanOk (b: TypeIndex): BOOLEAN;
     IF (tform[r] = FEnum) AND (tenum[r] >= 1) AND (tenum[r] <= 256) THEN
       RETURN TRUE   (* a set base over an enumeration *)
     END;
-    IF (tform[r] = FSub) AND (tref[r] = InvalidType)
+    IF (tform[r] = FSub)
  AND SubBounds(b, lo, hi) AND (hi >= lo) AND (hi - lo < 256) THEN
       RETURN TRUE
     END;
@@ -2962,6 +2972,18 @@ PROCEDURE SetBasesOk (a, b: TypeIndex): BOOLEAN;
     RETURN BaseSpanOk(a) AND BaseSpanOk(b)
   END SetBasesOk;
 
+PROCEDURE OrdBase (t: TypeIndex): TypeIndex;
+(* The ordinal base of a type: a subrange's declared base (or itself
+   when it is the base).  Used for assignment compatibility. Internal. *)
+  VAR r: TypeIndex;
+  BEGIN
+    r := Resolve(t);
+    IF (r # InvalidType) AND (tform[r] = FSub) THEN
+      r := Resolve(tref[r])
+    END;
+    RETURN r
+  END OrdBase;
+
 PROCEDURE Assignable (src, dst: TypeIndex): BOOLEAN;
   VAR rs, rd: TypeIndex;
   BEGIN
@@ -3019,6 +3041,14 @@ PROCEDURE Assignable (src, dst: TypeIndex): BOOLEAN;
     IF (ClassOf(src) = ClInt) AND (ClassOf(dst) = ClInt) THEN
       RETURN TRUE
     END;
+    (* CHAR/BOOLEAN/enumeration values assign to and from a subrange
+       of the same ordinal base ([A..Z] and CHAR, [red..blue] and
+       their enumeration). *)
+    IF (ClassOf(src) = ClassOf(dst))
+       AND ((ClassOf(src) = ClChar) OR (ClassOf(src) = ClBool)
+            OR (ClassOf(src) = ClEnum)) THEN
+      RETURN SameType(OrdBase(src), OrdBase(dst))
+    END;
     IF (ClassOf(src) = ClInt) AND (ClassOf(dst) = ClLong) THEN
       RETURN TRUE
     END;
@@ -3134,6 +3164,11 @@ PROCEDURE EqCheck (l, r: TypeIndex): BOOLEAN;
     IF (ClassOf(l) = ClBool) AND (ClassOf(r) = ClBool) THEN
       RETURN TRUE
     END;
+    IF (ClassOf(l) = ClEnum) AND (ClassOf(r) = ClEnum) THEN
+      (* an enumeration and a subrange of it compare; two different
+         enumerations are distinct types *)
+      RETURN SameType(OrdBase(l), OrdBase(r))
+    END;
     rl := Resolve(l); rr := Resolve(r);
     IF (rl = InvalidType) OR (rr = InvalidType) THEN RETURN TRUE END;
     IF (tform[rl] = FSet) AND (tform[rr] = FSet) THEN
@@ -3160,7 +3195,9 @@ PROCEDURE OrdCheck (l, r: TypeIndex): BOOLEAN;
     IF (ClassOf(l) = ClUChar) AND (ClassOf(r) = ClUChar) THEN
       RETURN TRUE
     END;
-    IF (ClassOf(l) = ClEnum) AND SameType(l, r) THEN RETURN TRUE END;
+    IF (ClassOf(l) = ClEnum) THEN
+      RETURN (ClassOf(r) = ClEnum) AND SameType(OrdBase(l), OrdBase(r))
+    END;
     RETURN FALSE
   END OrdCheck;
 

+ 27 - 0
compiler/tests/t_multidim.mod

@@ -0,0 +1,27 @@
+MODULE TMultiDim;
+// PIM/ISO multi-index array syntax: ARRAY [lo..hi], [lo..hi] OF T.
+// Both the repeated bracket groups and V3's earlier one-bracket comma
+// list must nest inside-out; indexing and whole-array copying follow
+// the same layout. Exit 42.
+TYPE Grid  = ARRAY [0..1], [0..2] OF INTEGER;
+TYPE Grid2 = ARRAY [0..1, 0..2] OF INTEGER;
+TYPE B2    = ARRAY [FALSE..TRUE], [FALSE..TRUE] OF BOOLEAN;
+VAR ExitCode : INTEGER;
+VAR g : Grid;
+VAR h : Grid2;
+VAR b : B2;
+VAR i, j, sum : INTEGER;
+BEGIN
+  sum := 0;
+  FOR i := 0 TO 1 DO
+    FOR j := 0 TO 2 DO
+      g[i, j] := i * 3 + j + 1;
+      h[i, j] := g[i, j];
+      sum := sum + h[i, j]
+    END
+  END;
+  b[TRUE, FALSE] := TRUE;
+  b[FALSE, TRUE] := FALSE;
+  IF b[TRUE, FALSE] AND NOT b[FALSE, TRUE] THEN sum := sum + 1 END;
+  ExitCode := sum + 20
+END TMultiDim.

+ 28 - 0
compiler/tests/t_subrange.mod

@@ -0,0 +1,28 @@
+MODULE TSubrange;
+// Subranges keep their base type: ['A'..'Z'] is character and
+// [red..green] is that enumeration, so base-typed values assign to
+// and compare with them.  MIN/MAX of a SET yields its base type, and
+// FOR accepts any ordinal control variable. Exit 42.
+TYPE Up     = ['A'..'Z'];
+TYPE Colour = (red, blue, green);
+TYPE Warm   = [red..green];
+TYPE CSet   = SET OF Colour;
+VAR ExitCode : INTEGER;
+VAR u : Up;
+VAR c : Colour;
+VAR w : Warm;
+VAR s : CSet;
+VAR i : INTEGER;
+BEGIN
+  u := 'H';
+  c := blue;
+  w := blue;
+  i := 0;
+  IF (u = 'H') AND (c = w) THEN i := i + 1 END;
+  IF (c >= red) AND (c <= green) THEN i := i + 1 END;
+  s := CSet{red..green};
+  IF MIN(CSet) = red THEN i := i + 10 END;
+  IF MAX(CSet) = green THEN i := i + 10 END;
+  FOR c := red TO green DO i := i + 1 END;
+  ExitCode := i + 17
+END TSubrange.

+ 3 - 0
docs/INDEX.md

@@ -32,6 +32,9 @@ bisecting *why* a decision was made, not for reading end-to-end).
 
 ## Recent session summaries (newest first)
 
+- **`summary_session-2026-10-22.md`** — corpus triage: PIM/ISO
+  multi-index arrays, typed CHAR/enum subranges, ordinal `FOR`
+  (corpus 467 → 479; the `libc` bucket identified).
 - **`summary_session-2026-10-19.md`** — the whole 2026-10-19 session
   (P0–P5, gap14, robustness, library; corpus 405 → 467).
 - **2026-10-19 (P0–P4):** generics double-instantiation fixed (lower each

+ 21 - 8
docs/STATUS.md

@@ -1,13 +1,13 @@
-# m2compiler-V3 — status (2026-10-19)
+# m2compiler-V3 — status (2026-10-22)
 
 This answers "where are we now?".
 
 ## Numbers
 
-- Suite **249/249** (`compiler/run_tests.sh`).
+- Suite **251/251** (`compiler/run_tests.sh`).
 - Self-hosting **FIXPOINT OK** — stage2 and stage3 images are
-  byte-identical (3,882,337 bytes).
-- Corpus **467 / 612** sources compile with 0 errors, **0 crashes**
+  byte-identical (3,890,738 bytes).
+- Corpus **479 / 612** sources compile with 0 errors, **0 crashes**
   (`tools/v3-corpus/corpus.sh` over the gm2 testsuite; the harness now
   resolves V3's own shipped libraries); the rest are genuine language
   gaps, **gm2 leniencies V3 deliberately rejects**, or unresolved
@@ -48,6 +48,13 @@ separate compilation (`FROM`/`IMPORT`, qualified `L.x`, opaque types);
 **generics** (ISO 10514-2 refinement): type/value/procedure
 parameters, constraints, nested generics, generic classes.
 
+Multi-dimensional arrays use the PIM/ISO repeated-bracket form
+(`ARRAY [lo..hi], [lo..hi] OF T`) as well as V3's earlier one-bracket
+comma list.  Subranges keep their ordinal base: `['A'..'Z']` is a
+character type and `[red..blue]` belongs to its enumeration, so
+base-typed values assign and compare; `FOR` accepts any ordinal
+control variable (`FOR c := red TO blue`).
+
 ## Landed recently (this thread)
 
 - AST + `Lower` (L0–L7) — `Lower` emits the whole session from the AST.
@@ -114,10 +121,16 @@ declarations still work (`t_fwdproc`, `t_fwdok`).
 
 ## What remains
 
-1. **Corpus** — genuine gaps remain (`not supported yet`,
-   `undeclared identifier`, `invalid call`); separate them from gm2
-   leniencies (`docs/gaps/`) and missing stdlib imports (`LIB`).  See
-   `docs/analysis_gaps.md`.
+1. **Corpus** — the 2026-10-22 triage fixed three conforming clusters
+   (+12 sources: PIM/ISO `ARRAY [..], [..] OF`, typed CHAR/enum
+   subranges, ordinal `FOR`).  The remaining `LANG` entries are mostly:
+   **48 files importing `libc`** that need real prototypes (V3's
+   `FROM libc IMPORT` declares a permissive no-result varargs external,
+   so using `printf`/`malloc` in an expression fails); **declaration
+   order** cases V3 rejects by policy (`docs/gaps/`); ISO
+   exceptions/`PACKEDSET`/SYSTEM sized types (`WORD32`, `BITSET32`,
+   qualified `SYSTEM.CARDINAL8`); and gm2-only modules
+   (`M2RTS`/`Builtins`/…).  See `docs/analysis_gaps.md`.
 2. ~~**The size-sensitive V3 miscompile**~~ — **root-caused and fixed**
    (2026-10-19; see `docs/summary_l3_selfhost_fix.md`).  It was never
    size-sensitive: the fixpoint gate matched the literal string

+ 13 - 7
docs/features.md

@@ -1,4 +1,4 @@
-# m2compiler-V3 — feature status (2026-10-08; suite 184/184 green)
+# m2compiler-V3 — feature status (2026-10-22; suite 251/251 green)
 
 Pipeline: Coco/R `M2.atg` (1748 lines, 73 productions) → `gm2`-built
 `M2` → QBE `.ssa` → `qbe` → `cc` → run. `SymTab.mod` 1623 lines,
@@ -19,8 +19,13 @@ Legend: ✅ done · 🔄 partial · ⏸ not started / deferred.
   `REAL`/`LONGREAL` (`d`, widened), `BOOLEAN`, `CHAR`.
 - ✅ Named types, aliases, integer/char subranges (folded bounds),
   enumerations (literals carry ordinals; usable in expressions,
-  `CASE` labels, `CONST` and as parameter/value types).
-- ✅ `ARRAY [lo..hi,…]` (multi-dim) and open `ARRAY OF` formals.
+  `CASE` labels, `CONST` and as parameter/value types).  A subrange
+  keeps its ordinal base (`['A'..'Z']` is character, `[red..blue]`
+  belongs to that enumeration), so base-typed values assign to and
+  compare with it.
+- ✅ `ARRAY [lo..hi,…]` (multi-dim, one bracket) and the PIM/ISO
+  repeated-group form `ARRAY [lo..hi], [lo..hi] OF T`, plus open
+  `ARRAY OF` formals.
 - ✅ Array constructors `T{…}` (GNU Modula-2): a `CONST` of an array
   type fills a pooled static descriptor (copied/indexed like a
   declared array); scalar elements, nested `Sub{…}`, string elements
@@ -34,8 +39,8 @@ Legend: ✅ done · 🔄 partial · ⏸ not started / deferred.
   string concatenation folded into a `CHAR`-array element, included.
 - ✅ Index-type arrays `ARRAY <ordinalType> OF T`; enum values usable
   as array indices.
-- ✅ `MIN`/`MAX` over any ordinal type, array or set; REAL/LONGREAL
-  and LONGINT bounds too.
+- ✅ `MIN`/`MAX` over any ordinal type, array or set (a set's bounds
+  have its base type); REAL/LONGREAL and LONGINT bounds too.
 - ✅ gm2 sized/scalar names: `LONGCARD`, `SHORTREAL`,
   `INTEGER8/16/32/64`, `CARDINAL8/16/32/64`.
 - ✅ `FROM libc IMPORT printf, exit, …`: intrinsic permissive external
@@ -80,8 +85,9 @@ Legend: ✅ done · 🔄 partial · ⏸ not started / deferred.
 
 ## Statements
 - ✅ Assignment, `IF/ELSIF/ELSE`, `CASE` (labels, ranges, `ELSE`),
-  `WHILE`, `REPEAT/UNTIL`, `LOOP/EXIT`, `FOR/TO/BY` (static sign),
-  `WITH` (single/multi/nested), `RETURN` (232-checked),
+  `WHILE`, `REPEAT/UNTIL`, `LOOP/EXIT`, `FOR/TO/BY` (static sign,
+  ordinal control variable: integer, CHAR, BOOLEAN, enum or a
+  subrange), `WITH` (single/multi/nested), `RETURN` (232-checked),
   `NEW`/`DISPOSE` (builtins), bare and parenthesized calls.
 - ✅ `HIGH`/`LEN`; ✅ `GOTO`/labels.
 

+ 24 - 0
docs/gaps/README.md

@@ -94,6 +94,30 @@ gap22_file_line.mod             invalid Fact            (__LINE__)
 
 ## Genuine gaps (worth fixing, standard Modula-2)
 
+- **Multi-index arrays — fixed** (2026-10-22): the PIM/ISO form is
+  `ARRAY [lo..hi], [lo..hi] OF T` (one bracket group per dimension).
+  V3 only accepted a non-standard single-bracket comma list; it now
+  takes both, nesting the groups inside-out.  Corpus **+6**:
+  `arraybool`, `assignment2`, `foo`, `recordarray2`, `tinyarray4`,
+  `tinyarray5`.
+- **Subranges keep their ordinal base — fixed** (2026-10-22):
+  `['A'..'Z']` was classified as an integer subrange, so assigning a
+  `CHAR` (or an enumeration literal to `[red..blue]`) was
+  `incompatible assignment`/`comparison`.  Subranges now carry their
+  base type (`SymTab.NewSubB`), and `CHAR`/`BOOLEAN`/enum values
+  assign to and compare with a subrange of the same base.  Corpus
+  **+3**: `subrange16`, `subrange17`, `enum3`.  *Not* covered yet: a
+  runtime range check on assignment (V3 remains lenient, as for the
+  integer family).
+- **`FOR` over any ordinal — fixed** (2026-10-22): the control
+  variable and bounds may be `CHAR`, `BOOLEAN`, an enumeration or a
+  subrange (bounds must be assignment-compatible with the variable),
+  not only the integer family.  `MIN`/`MAX` of a `SET` now has the
+  set's **base type**, so `FOR i := MIN(S) TO MAX(S)` works.  Corpus
+  **+3**: `forloop2`, `smallset4`, `minmax`.  The empty character
+  literal `''` (gm2's NUL, used by `forloopnulchar`) remains a
+  gm2 extension and is not accepted.
+
 - **`gap07` CONVERT — fixed** (2026-10-18): `CONVERT(T, x)` is accepted as
   a `VAL` sibling (`procconv2` passes).
 - **`gap08` set↔word `CAST`/`VAL` — fixed** (2026-10-18): `VAL`/`CAST`

+ 83 - 0
docs/summary_session-2026-10-22.md

@@ -0,0 +1,83 @@
+# Session 2026-10-22 — corpus triage: three conforming clusters
+
+Branch **`master`**.  Continues `summary_session-2026-10-19.md`; the
+only plan source was its "Remaining" list (corpus `LANG` triage).
+
+## Numbers
+
+| gate | session start | end |
+| --- | --- | --- |
+| suite (`compiler/run_tests.sh`) | 249/249 | **251/251** |
+| fixpoint (`bootstrap/fixpoint.sh`) | OK (3,882,337 B) | **OK (3,890,738 B)** |
+| corpus (`tools/v3-corpus/corpus.sh`) | 467/612 | **479/612, 0 crashes** |
+
+Reproduce:
+
+```sh
+cd compiler && ./build.sh && ./run_tests.sh          # 251/251
+cd .. && TMP="$PWD/.scratch/fixpoint" ./bootstrap/fixpoint.sh
+tools/v3-corpus/corpus.sh                            # 479 OK, 0 crash
+```
+
+## Triage method
+
+The 118 `LANG` entries were compiled one at a time (a single-file
+version of `corpus.sh`), and each first diagnostic was read against its
+source line.  They fall into these buckets:
+
+| bucket | count | verdict |
+| --- | --- | --- |
+| imports `libc` but uses a libc function as a value (`printf`/`malloc`) | ~48 | **libc prototypes missing**; V3's permissive `FROM libc IMPORT` has no result type, so expression use is `invalid call` |
+| declaration order (type/const/var used before its declaration) | ~25 | correctly rejected (`docs/gaps/`); not a target |
+| gm2 extensions (`ASM`, `''` NUL char, `666B`, absolute `VAR [addr]`) | ~7 | not a target unless chosen |
+| ISO features actually missing | ~20 | sized SYSTEM types (`WORD32`/`BITSET32`/qualified `SYSTEM.CARDINAL8`), `PACKEDSET`, exceptions/`UNIQUE`, `__FUNCTION__`, `PROC` as a result type |
+| three **conforming** clusters | 12 | **fixed this session** |
+
+## What landed
+
+### PIM/ISO multi-index arrays — `compiler/src/M2.atg`
+`ARRAY [0..1], [0..2] OF T` is the standard spelling (17 corpus files
+use it; none use V3's single-bracket comma list).  `ArrayType` now
+parses one bracket group per dimension (`{ "," "[" ... "]" }`) and
+appends every group's bounds before the element type; nesting stays
+inside-out, so the layout matches `ARRAY [0..1] OF ARRAY [0..2] OF T`.
+The old `ARRAY [0..1, 0..2] OF` form still works.  Corpus **+6**:
+`arraybool`, `assignment2`, `foo`, `recordarray2`, `tinyarray4/5`.
+Regression `t_multidim`.
+
+### Subranges keep their ordinal base — `SymTab`, grammar
+A subrange was always `FSub` with no base descriptor, so `ClassOf`
+returned `ClInt` for `['A'..'Z']` and `[red..blue]`: assigning a
+`CHAR`/enum literal (or comparing with one) failed.  `SymTab.NewSubB`
+stores the base when both bounds have the same type; `ClassOf`,
+`SubBounds`, `SetSpan`/`BaseSpanOk` and the set-span check now follow
+it.  `Assignable`/`EqCheck`/`OrdCheck` accept a base value against a
+subrange of the same `CHAR`/`BOOLEAN`/enum base (two different enums
+stay incompatible).  Corpus **+3**: `subrange16`, `subrange17`,
+`enum3`.  Regression `t_subrange`.  Assignment range checks remain
+(future) — V3 is already lenient for the integer family.
+
+### `FOR` over ordinals + typed `MIN`/`MAX` of sets — grammar
+`FOR` accepted only the integer family.  It now accepts any ordinal
+control variable (bounds must be assignment-compatible with it), which
+covers enums and `CHAR`.  `MIN`/`MAX` of a `SET` now yields the set's
+base type instead of `INTEGER`, so `FOR i := MIN(S) TO MAX(S)` binds
+(the bound was previously `INTEGER` against an enum `i`).  Corpus
+**+3**: `forloop2`, `smallset4`, `minmax`.
+
+## Remaining (for a next session)
+
+1. **`libc.def`** — the single biggest corpus bucket (~48 files).  A
+   minimal definition module (`exit`, `printf`, `malloc`, `write`, …)
+   needs the grammar to prefer it over the permissive `FROM libc
+   IMPORT` path, plus a varargs marker for `printf`.  This is library
+   work with a real payoff.
+2. **ISO SYSTEM sized types** — `WORD32`/`WORD16`/`WORD8` and
+   `BITSET8/16/32/64` are missing, and qualified `SYSTEM.CARDINAL8`
+   does not resolve (predefs live in the global scope, not a SYSTEM
+   module).  ~8 files.
+3. **`PACKEDSET`**, ISO exceptions/`UNIQUE`, `__FUNCTION__` — each
+   one or two files; larger or gm2-flavoured.
+4. **Runtime subrange range checks** — the remaining conformance
+   difference from this session's subrange work.
+5. `STATUS.md` "what remains" is the live list.

Some files were not shown because too many files changed in this diff