فهرست منبع

feat: use sorted range lists for dynamic sets

Eric Streit 1 روز پیش
والد
کامیت
34cd0a9ce7

+ 3 - 3
README.md

@@ -87,13 +87,13 @@ docs/              STATUS.md, plan.md, plan-*.md, analysis_*.md, summary_*.md
 
 ## Status (2026-10-24)
 
-- Suite **307/307**; self-hosting **fixpoint OK** (byte-identical
-  stage2/stage3, 3,809,843 bytes); corpus **521/612 compile-OK,
+- Suite **309/309**; self-hosting **fixpoint OK** (byte-identical
+  stage2/stage3, 3,845,439 bytes); corpus **521/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/`).
   The remaining `except*`/`onebyte*`-style corpus files still hit V3's
   declaration-order policy.
-- Active branch **`master`**, feature state **`v3-wide-sets`**.
+- Active branch **`master`**, feature state **`v3-dynamic-sets`**.
   Recent milestones: `PACKEDSET`, ISO exceptions (`EXCEPT`/`RETRY`,
   `EXCEPTIONS`, `M2EXCEPTION`, catchable language traps including
   `functionException`, module `FINALLY`), self-referential pointer

+ 2 - 2
TUTORIAL.md

@@ -10,7 +10,7 @@ From the repository root:
 ```sh
 cd compiler
 ./build.sh          # M2.atg -> scanner/parser/driver -> ./M2
-./run_tests.sh      # 307/307
+./run_tests.sh      # 309/309
 ```
 
 `M2` reads Modula-2 sources and writes QBE IR to `gen_ssa/`.
@@ -149,7 +149,7 @@ TMP="$PWD/.scratch/fixpoint-separate" ./bootstrap/fixpoint-separate.sh
 tools/v3-corpus/corpus.sh
 ```
 
-Current gates: suite **307/307**, fixpoint **OK (3,809,843 bytes)**,
+Current gates: suite **309/309**, fixpoint **OK (3,845,439 bytes)**,
 corpus **521/612 compile-OK, 0 crashes**.
 
 ## 7. Current limits

+ 3 - 1
compiler/run_tests.sh

@@ -95,6 +95,7 @@ expect_run t_packedset.mod 42
 expect_run t_ptrchain.mod 42
 expect_run t_addreq.mod 42
 expect_run t_wide_set.mod 42
+expect_run t_big_ops.mod 42
 expect_run t_finally.mod 43
 expect_run t_finally_return.mod 43
 expect_run t_finally_except.mod 42
@@ -242,8 +243,9 @@ expect_fail t_bad_field.mod "unknown field"
 expect_fail t_bad_with.mod "not a RECORD type"
 expect_fail t_bad_recassign.mod "incompatible assignment"
 expect_fail t_bad_setelem.mod "set operand mismatch"
-expect_fail t_bad_setbase.mod "not supported yet"
+expect_run t_bad_setbase.mod 42
 expect_fail t_bad_setconst.mod "not supported yet"
+expect_fail t_bad_bigconst.mod "not supported yet"
 expect_fail t_bad_in.mod "set operand mismatch"
 expect_fail t_bad_index.mod "array index must be integer"
 expect_fail t_bad_bounds.mod "not supported yet"

+ 63 - 15
compiler/src/Lower.mod

@@ -1794,7 +1794,11 @@ PROCEDURE LowerBuiltinStmt (n: AST.Node; nm: SymTab.Name);
       IF at = SymTab.InvalidType THEN
       ELSIF SymTab.ClassOf(at) # SymTab.ClSet THEN
       ELSE
-        IF SymTab.IsPackedSet(at) THEN
+        IF SymTab.IsBigSet(at) THEN
+          IF isInc THEN QbeGen.BigSetBit(qd, qe, SymTab.BigSetSigned(at))
+          ELSE QbeGen.BigSetClearBit(qd, qe, SymTab.BigSetSigned(at))
+          END
+        ELSIF SymTab.IsPackedSet(at) THEN
           IF isInc THEN QbeGen.PackedSetBit(qd, at, qe,
                                              SymTab.SetBaseLo(at),
                                              SymTab.SetCount(at))
@@ -2169,17 +2173,29 @@ PROCEDURE LowerExprFull (n: AST.Node; VAR t: SymTab.TypeIndex;
       END
     ELSIF AST.Kind(n) = AST.NkSetLit THEN
       t := AST.Ty(n); IF t = -1 THEN t := SymTab.NewSet(SymTab.NewSubR(0, 255)) END;
-      QbeGen.NewSetTemp(8, q); QbeGen.SetZero(q, 8);
+      IF SymTab.IsBigSet(t) THEN
+        QbeGen.NewSetTemp(2, q); QbeGen.SetZero(q, 2)
+      ELSE
+        QbeGen.NewSetTemp(8, q); QbeGen.SetZero(q, 8)
+      END;
       i := 0;
       WHILE i < AST.NChild(n) DO
         el := AST.Child(n, i);
         IF AST.Kind(el) = AST.NkSubrange THEN
           LowerExpr(AST.Child(el, 0), t2, q2);
           LowerExpr(AST.Child(el, 1), t2, qt);
-          QbeGen.SetRange(q, q2, qt, SymTab.SetBaseLo(t), SymTab.SetCount(t))
+          IF SymTab.IsBigSet(t) THEN
+            QbeGen.BigSetRange(q, q2, qt, SymTab.BigSetSigned(t))
+          ELSE
+            QbeGen.SetRange(q, q2, qt, SymTab.SetBaseLo(t), SymTab.SetCount(t))
+          END
         ELSE
           LowerExpr(el, t2, q2);
-          QbeGen.SetBit(q, q2, SymTab.SetBaseLo(t), SymTab.SetCount(t))
+          IF SymTab.IsBigSet(t) THEN
+            QbeGen.BigSetBit(q, q2, SymTab.BigSetSigned(t))
+          ELSE
+            QbeGen.SetBit(q, q2, SymTab.SetBaseLo(t), SymTab.SetCount(t))
+          END
         END;
         INC(i)
       END
@@ -2189,7 +2205,9 @@ PROCEDURE LowerExprFull (n: AST.Node; VAR t: SymTab.TypeIndex;
          constructors are not lowered here yet. *)
       t := AST.Ty(n);
       IF (t # SymTab.InvalidType) AND (SymTab.ClassOf(t) = SymTab.ClSet) THEN
-        IF SymTab.SetWords(t) = 0 THEN
+        IF SymTab.IsBigSet(t) THEN
+          QbeGen.NewSetTemp(2, q); QbeGen.SetZero(q, 2)
+        ELSIF SymTab.SetWords(t) = 0 THEN
           QbeGen.NewSetTemp(8, q); QbeGen.SetZero(q, 8)
         ELSE
           QbeGen.NewSetTemp(SymTab.SetWords(t), q);
@@ -2204,11 +2222,19 @@ PROCEDURE LowerExprFull (n: AST.Node; VAR t: SymTab.TypeIndex;
               IF AST.Kind(el) = AST.NkSubrange THEN
                 LowerExpr(AST.Child(el, 0), t2, q2);
                 LowerExpr(AST.Child(el, 1), t2, qt);
-                QbeGen.SetRange(q, q2, qt, SymTab.SetBaseLo(t),
-                  SymTab.SetCount(t))
+                IF SymTab.IsBigSet(t) THEN
+                  QbeGen.BigSetRange(q, q2, qt, SymTab.BigSetSigned(t))
+                ELSE
+                  QbeGen.SetRange(q, q2, qt, SymTab.SetBaseLo(t),
+                    SymTab.SetCount(t))
+                END
               ELSE
                 LowerExpr(el, t2, q2);
-                QbeGen.SetBit(q, q2, SymTab.SetBaseLo(t), SymTab.SetCount(t))
+                IF SymTab.IsBigSet(t) THEN
+                  QbeGen.BigSetBit(q, q2, SymTab.BigSetSigned(t))
+                ELSE
+                  QbeGen.SetBit(q, q2, SymTab.SetBaseLo(t), SymTab.SetCount(t))
+                END
               END
             END;
             INC(i)
@@ -2287,7 +2313,9 @@ PROCEDURE LowerExprFull (n: AST.Node; VAR t: SymTab.TypeIndex;
         END;
         rop := SymTab.OpEq; folded := RelSym(op, rop);
         IF op = AST.OpIn THEN
-          IF SymTab.IsPackedSet(t2) THEN
+          IF SymTab.IsBigSet(t2) THEN
+            QbeGen.BigInSet(q, q2, SymTab.BigSetSigned(t2), qt)
+          ELSIF SymTab.IsPackedSet(t2) THEN
             QbeGen.PackedInSet(q, q2, SymTab.SetBaseLo(t2),
               SymTab.SetCount(t2), t2, qt)
           ELSE QbeGen.InSet(q, q2, SymTab.SetBaseLo(t2),
@@ -2300,12 +2328,20 @@ PROCEDURE LowerExprFull (n: AST.Node; VAR t: SymTab.TypeIndex;
           t := SymTab.BoolType(); QbeGen.CopyOp(qt, q)
         ELSIF (SymTab.ClassOf(t) = SymTab.ClSet)
            OR (SymTab.ClassOf(t2) = SymTab.ClSet) THEN
-          IF SymTab.IsPackedSet(t) OR SymTab.IsPackedSet(t2) THEN
-            QbeGen.PackedCmpSet(rop, q, t, q2, t2, qt)
+          IF SymTab.IsBigSet(t) OR SymTab.IsBigSet(t2) THEN
+            QbeGen.BigSetEqual(q, q2, qt);
+            IF (rop = SymTab.OpNeq1) OR (rop = SymTab.OpNeq2) THEN
+              QbeGen.NotQ(qt, q)
+            ELSE QbeGen.CopyOp(qt, q)
+            END
+          ELSIF SymTab.IsPackedSet(t) OR SymTab.IsPackedSet(t2) THEN
+            QbeGen.PackedCmpSet(rop, q, t, q2, t2, qt);
+            QbeGen.CopyOp(qt, q)
           ELSE QbeGen.CmpSet(rop, q, q2, SymTab.SetWords(t),
-                 SymTab.SetWords(t2), qt)
+                 SymTab.SetWords(t2), qt);
+            QbeGen.CopyOp(qt, q)
           END;
-          t := SymTab.BoolType(); QbeGen.CopyOp(qt, q)
+          t := SymTab.BoolType()
         ELSIF SymTab.StrCompat(t, t2) THEN
           QbeGen.StrEq(rop, q, q2, qt);
           t := SymTab.BoolType(); QbeGen.CopyOp(qt, q)
@@ -2372,7 +2408,15 @@ PROCEDURE LowerExprFull (n: AST.Node; VAR t: SymTab.TypeIndex;
           ELSIF op = AST.OpMul THEN idx := 1
           ELSE idx := 3
           END;
-          IF SymTab.IsPackedSet(t) OR SymTab.IsPackedSet(t2) THEN
+          IF SymTab.IsBigSet(t) OR SymTab.IsBigSet(t2) THEN
+            QbeGen.NewSetTemp(2, qt);
+            QbeGen.SetZero(qt, 2);
+            QbeGen.BigSetBin(idx, qt, q, q2);
+            IF SymTab.IsBigSet(t) THEN
+              t := SymTab.NewBigSet(SymTab.SetBase(t))
+            ELSE t := SymTab.NewBigSet(SymTab.SetBase(t2))
+            END
+          ELSIF SymTab.IsPackedSet(t) OR SymTab.IsPackedSet(t2) THEN
             QbeGen.PackedSetBinOp(idx, q, t, q2, t2, qt);
             t := SymTab.NewSet(SymTab.NewSubR(0, 31))
           ELSE
@@ -2693,8 +2737,12 @@ PROCEDURE LowerStmt (seq: AST.Node);
           IF SymTab.ClassOf(procRes[emitProc]) = SymTab.ClArray THEN
             QbeGen.CopyArray(qt, q, procRes[emitProc])
           ELSIF SymTab.ClassOf(procRes[emitProc]) = SymTab.ClSet THEN
-            QbeGen.CopySet(qt, q, SymTab.SetWords(procRes[emitProc]),
+            IF SymTab.IsBigSet(procRes[emitProc]) THEN
+              QbeGen.BigSetCopy(qt, q)
+            ELSE
+              QbeGen.CopySet(qt, q, SymTab.SetWords(procRes[emitProc]),
                                     SymTab.SetWords(procRes[emitProc]))
+            END
           ELSE QbeGen.CopyRecord(qt, q, procRes[emitProc])
           END;
           QbeGen.EmitRet("0", FALSE)

+ 83 - 23
compiler/src/M2.atg

@@ -1585,7 +1585,8 @@ PRODUCTIONS
      words + zero-check extras), out-of-span literals are 222. *)
   SetType<VAR t: SymTab.TypeIndex>      (. VAR base: SymTab.TypeIndex;
                                              blo, bhi, bspan: INTEGER;
-                                             bcls: INTEGER; .)
+                                             bcls: INTEGER;
+                                             big: BOOLEAN; .)
     = "SET"                             (. allowFwdType := FALSE; .)
       "OF" Type<base, FALSE>
                                         (. IF base = SymTab.InvalidType THEN
@@ -1597,6 +1598,7 @@ PRODUCTIONS
                                              t := SymTab.NewSet(base)
                                            ELSE bcls :=
                                                   SymTab.ClassOf(base);
+                                             big := FALSE;
                                              (* a subrange keeps its own
                                                 span even when its base is
                                                 CHAR/BOOLEAN/an enum *)
@@ -1619,11 +1621,15 @@ PRODUCTIONS
                                                blo := 0;
                                                bspan := VAL(INTEGER,
                                                  SymTab.EnumCount(base))
-                                             ELSIF (bcls = SymTab.ClInt) AND
-                                                   SymTab.IsCardinalType(base) THEN
-                                               blo := 0; bspan := 1000000
+                                             ELSIF bcls = SymTab.ClInt THEN
+                                               (* unbounded integer sets use
+                                                  a sorted range list *)
+                                               t := SymTab.NewBigSet(base);
+                                               big := TRUE
                                              ELSE bspan := 0 END;
-                                             IF (bspan <= 0)
+                                             IF big THEN
+                                               (* t already set *)
+                                             ELSIF (bspan <= 0)
                                                 OR (bspan > 1000000) THEN
                                                SemError(230);
                                                t := SymTab.InvalidType
@@ -2077,7 +2083,12 @@ PRODUCTIONS
                                            Lower.NoteConstVal(qv);
                                            SymTab.SetSymType(n, t);
                                            cls := SymTab.ClassOf(t);
-                                           IF cls = SymTab.ClSet THEN
+                                           IF (cls = SymTab.ClSet) AND
+                                              SymTab.IsBigSet(t) THEN
+                                             (* a dynamic range-list set
+                                                has no static layout yet *)
+                                             SemError(230)
+                                           ELSIF cls = SymTab.ClSet THEN
                                              (* a SET CONST needs static
                                                 data: fold its mask now
                                                 (the grammar sees the
@@ -3681,12 +3692,19 @@ PRODUCTIONS
            ELSIF (t # SymTab.InvalidType) AND (t2 # SymTab.InvalidType)
  AND (SymTab.ClassOf(t) = SymTab.ClSet)
  AND (SymTab.ClassOf(t2) = SymTab.ClSet) THEN
-              lw := SymTab.SetWords(t); rw := SymTab.SetWords(t2);
-              mw := lw;
-              IF rw > mw THEN mw := rw END;
-              t := SymTab.NewSet(
+              IF SymTab.IsBigSet(t) OR SymTab.IsBigSet(t2) THEN
+                IF SymTab.IsBigSet(t) THEN
+                  t := SymTab.NewBigSet(SymTab.SetBase(t))
+                ELSE t := SymTab.NewBigSet(SymTab.SetBase(t2))
+                END
+              ELSE
+                lw := SymTab.SetWords(t); rw := SymTab.SetWords(t2);
+                mw := lw;
+                IF rw > mw THEN mw := rw END;
+                t := SymTab.NewSet(
                      SymTab.NewSubR(0,
-                       VAL(INTEGER, mw) * 32 - 1));
+                       VAL(INTEGER, mw) * 32 - 1))
+              END;
               QbeGen.CopyOp("@", q)
            ELSE
              IF SymTab.ArithCheck(t, t2, FALSE, res2) THEN
@@ -3747,12 +3765,19 @@ PRODUCTIONS
          ELSIF (t # SymTab.InvalidType) AND (t2 # SymTab.InvalidType)
  AND (SymTab.ClassOf(t) = SymTab.ClSet)
  AND (SymTab.ClassOf(t2) = SymTab.ClSet) THEN
-            lw := SymTab.SetWords(t); rw := SymTab.SetWords(t2);
-            mw := lw;
-            IF rw > mw THEN mw := rw END;
-            t := SymTab.NewSet(
+            IF SymTab.IsBigSet(t) OR SymTab.IsBigSet(t2) THEN
+              IF SymTab.IsBigSet(t) THEN
+                t := SymTab.NewBigSet(SymTab.SetBase(t))
+              ELSE t := SymTab.NewBigSet(SymTab.SetBase(t2))
+              END
+            ELSE
+              lw := SymTab.SetWords(t); rw := SymTab.SetWords(t2);
+              mw := lw;
+              IF rw > mw THEN mw := rw END;
+              t := SymTab.NewSet(
                    SymTab.NewSubR(0,
-                     VAL(INTEGER, mw) * 32 - 1));
+                     VAL(INTEGER, mw) * 32 - 1))
+            END;
             QbeGen.CopyOp("@", q)
          ELSE
            IF SymTab.ArithCheck(t, t2,
@@ -4530,6 +4555,24 @@ PRODUCTIONS
       [ "BY" Expr<et2, q2>              (. hasB := TRUE; .) ]
                                         (. IF braceCls = SymTab.ClSet THEN
                                              IF hasB THEN SemError(230) END;
+                                             IF SymTab.IsBigSet(vt) THEN
+                                               cl := SymTab.ClassOf(et);
+                                               IF hasR THEN
+                                                 cl2 := SymTab.ClassOf(et2)
+                                               ELSE cl2 := SymTab.ClInt
+                                               END;
+                                               IF NOT SymTab.SetElemClassOk(cl)
+                                                  OR (hasR AND NOT
+                                                     SymTab.SetElemClassOk(cl2))
+                                               THEN SemError(222)
+                                               ELSIF hasR THEN
+                                                 QbeGen.BigSetRange(sq, qe, q2,
+                                                   SymTab.BigSetSigned(vt))
+                                               ELSE
+                                                 QbeGen.BigSetBit(sq, qe,
+                                                   SymTab.BigSetSigned(vt))
+                                               END
+                                             ELSE
                                              lo := SymTab.SetBaseLo(vt);
                                              span := SymTab.SetCount(vt);
                                              IF (et = SymTab.InvalidType)
@@ -4573,6 +4616,7 @@ PRODUCTIONS
                                                   lo, span)
                                                END
                                              END
+                                           END
                                            ELSIF (braceCls = SymTab.ClArray)
                                               OR (braceCls =
                                                  SymTab.ClRecord)
@@ -4615,16 +4659,32 @@ PRODUCTIONS
       [ ".." Expr<et2, q2>              (. astEl := AST.MakeBin(
                                              AST.NkSubrange, 0, astEl, astCur);
                                            hasR := TRUE; .) ]
-                                        (. lo := SymTab.SetBaseLo(st);
-                                           span := SymTab.SetCount(st);
-                                           IF (et = SymTab.InvalidType)
+                                        (. IF (et = SymTab.InvalidType)
                                               OR (hasR AND (et2 =
                                                  SymTab.InvalidType)) THEN
-                                           ELSE cl :=
-                                                  SymTab.ClassOf(et);
+                                           ELSIF SymTab.IsBigSet(st) THEN
+                                             cl := SymTab.ClassOf(et);
+                                             IF hasR THEN
+                                               cl2 := SymTab.ClassOf(et2)
+                                             ELSE cl2 := SymTab.ClInt
+                                             END;
+                                             IF NOT SymTab.SetElemClassOk(cl)
+                                                OR (hasR AND NOT
+                                                   SymTab.SetElemClassOk(cl2))
+                                             THEN SemError(222)
+                                             ELSIF hasR THEN
+                                               QbeGen.BigSetRange(sq, qe, q2,
+                                                 SymTab.BigSetSigned(st))
+                                             ELSE
+                                               QbeGen.BigSetBit(sq, qe,
+                                                 SymTab.BigSetSigned(st))
+                                             END
+                                           ELSE
+                                             lo := SymTab.SetBaseLo(st);
+                                             span := SymTab.SetCount(st);
+                                             cl := SymTab.ClassOf(et);
                                              IF hasR THEN
-                                               cl2 :=
-                                                 SymTab.ClassOf(et2)
+                                               cl2 := SymTab.ClassOf(et2)
                                              ELSE cl2 := SymTab.ClInt
                                              END;
                                              IF NOT SymTab.SetElemClassOk(cl)

+ 14 - 0
compiler/src/QbeGen.def

@@ -463,6 +463,20 @@ PROCEDURE PoolSetNamed (name: ARRAY OF CHAR; mask: ARRAY OF CARDINAL;
 PROCEDURE NewSetTemp (words: CARDINAL; VAR q: QVal);
 (* Fresh alloc8 stack slot for a set value; q is its address. *)
 
+PROCEDURE BigSetBit (addr: ARRAY OF CHAR; val: ARRAY OF CHAR;
+                     signed: BOOLEAN);
+PROCEDURE BigSetClearBit (addr: ARRAY OF CHAR; val: ARRAY OF CHAR;
+                          signed: BOOLEAN);
+PROCEDURE BigSetRange (addr: ARRAY OF CHAR; a: ARRAY OF CHAR;
+                       b: ARRAY OF CHAR; signed: BOOLEAN);
+PROCEDURE BigInSet (x: ARRAY OF CHAR; s: ARRAY OF CHAR;
+                    signed: BOOLEAN; VAR q: QVal);
+PROCEDURE BigSetEqual (l: ARRAY OF CHAR; r: ARRAY OF CHAR; VAR q: QVal);
+PROCEDURE BigSetBin (sel: INTEGER; dst: ARRAY OF CHAR;
+                     l: ARRAY OF CHAR; r: ARRAY OF CHAR);
+PROCEDURE BigSetCopy (dst: ARRAY OF CHAR; src: ARRAY OF CHAR);
+(* Sorted-range-list sets (FBigSet): runtime helpers in shim.c. *)
+
 PROCEDURE SetZero (addr: ARRAY OF CHAR; words: CARDINAL);
 (* Zeroes W words at addr. *)
 

+ 72 - 1
compiler/src/QbeGen.mod

@@ -708,7 +708,8 @@ PROCEDURE DeclVar (name: ARRAY OF CHAR; t: INTEGER);
   BEGIN
     IF inFunc THEN DeclLocal(name, t); RETURN END;
     Cpy(g, curModName); App(g, "_"); App(g, name);
-    IF SymTab.ClassOf(t) = SymTab.ClReal THEN DataLine(g, TRUE, "0")
+    IF SymTab.IsBigSet(t) THEN DataLineL(g, "0")
+    ELSIF SymTab.ClassOf(t) = SymTab.ClReal THEN DataLine(g, TRUE, "0")
     ELSIF SymTab.IsByteType(t) THEN DataLineB(g, "0")
     ELSIF SymTab.IsWord16Type(t) THEN DataLineH(g, "0")
     ELSIF SymTab.ClassOf(t) = SymTab.ClArray THEN DeclArr(g, t)
@@ -1167,6 +1168,9 @@ PROCEDURE AllocLocal (t: INTEGER; VAR slot: QVal);
       ELSIF SymTab.SetBytes(t) = 2 THEN W("  storeh 0, "); WL(slot)
       ELSE W("  storew 0, "); WL(slot)
       END
+    ELSIF SymTab.IsBigSet(t) THEN
+      W(" =l alloc8 8"); WL("");
+      W("  storel 0, "); WL(slot)
     ELSIF cls = SymTab.ClSet THEN
       IntStr(VAL(INTEGER, SymTab.SetWords(t) * 4), nb);
       W(" =l alloc8 "); WL(nb);
@@ -2837,6 +2841,70 @@ PROCEDURE SetZero (addr: ARRAY OF CHAR; words: CARDINAL);
     END
   END SetZero;
 
+PROCEDURE BigSetBit (addr: ARRAY OF CHAR; val: ARRAY OF CHAR;
+                     signed: BOOLEAN);
+  VAR sv: QVal;
+  BEGIN
+    IF NOT opened OR noEmit THEN RETURN END;
+    Revive;
+    W("  call $m2bs_incl(l "); W(addr); W(", l "); W(val);
+    W(", l "); IntStr(VAL(INTEGER, signed), sv); W(sv); WL(")")
+  END BigSetBit;
+
+PROCEDURE BigSetClearBit (addr: ARRAY OF CHAR; val: ARRAY OF CHAR;
+                          signed: BOOLEAN);
+  VAR sv: QVal;
+  BEGIN
+    IF NOT opened OR noEmit THEN RETURN END;
+    Revive;
+    W("  call $m2bs_excl(l "); W(addr); W(", l "); W(val);
+    W(", l "); IntStr(VAL(INTEGER, signed), sv); W(sv); WL(")")
+  END BigSetClearBit;
+
+PROCEDURE BigSetRange (addr: ARRAY OF CHAR; a: ARRAY OF CHAR;
+                       b: ARRAY OF CHAR; signed: BOOLEAN);
+  VAR sv: QVal;
+  BEGIN
+    IF NOT opened OR noEmit THEN RETURN END;
+    Revive;
+    W("  call $m2bs_incl_range(l "); W(addr); W(", l "); W(a);
+    W(", l "); W(b); W(", l "); IntStr(VAL(INTEGER, signed), sv);
+    W(sv); WL(")")
+  END BigSetRange;
+
+PROCEDURE BigInSet (x: ARRAY OF CHAR; s: ARRAY OF CHAR;
+                    signed: BOOLEAN; VAR q: QVal);
+  VAR sv: QVal;
+  BEGIN
+    NewTemp(q); Revive;
+    W("  "); W(q); W(" =w call $m2bs_in(l "); W(s); W(", l "); W(x);
+    W(", l "); IntStr(VAL(INTEGER, signed), sv); W(sv); WL(")")
+  END BigInSet;
+
+PROCEDURE BigSetEqual (l: ARRAY OF CHAR; r: ARRAY OF CHAR; VAR q: QVal);
+  BEGIN
+    NewTemp(q); Revive;
+    W("  "); W(q); W(" =w call $m2bs_equal(l "); W(l); W(", l "); W(r);
+    WL(")")
+  END BigSetEqual;
+
+PROCEDURE BigSetBin (sel: INTEGER; dst: ARRAY OF CHAR;
+                     l: ARRAY OF CHAR; r: ARRAY OF CHAR);
+  VAR sv: QVal;
+  BEGIN
+    IF NOT opened OR noEmit THEN RETURN END;
+    Revive;
+    W("  call $m2bs_bin(l "); W(dst); W(", l "); W(l);
+    W(", l "); W(r); W(", l "); IntStr(sel, sv); W(sv); WL(")")
+  END BigSetBin;
+
+PROCEDURE BigSetCopy (dst: ARRAY OF CHAR; src: ARRAY OF CHAR);
+  BEGIN
+    IF NOT opened OR noEmit THEN RETURN END;
+    Revive;
+    W("  call $m2bs_copy_to(l "); W(dst); W(", l "); W(src); WL(")")
+  END BigSetCopy;
+
 PROCEDURE SetBit (addr: ARRAY OF CHAR; val: ARRAY OF CHAR;
                    lo: INTEGER; span: CARDINAL);
 (* ORs one element in: off = val - lo trapped in [0, span). *)
@@ -3154,6 +3222,9 @@ PROCEDURE CopySetVal (dst: ARRAY OF CHAR; dt: INTEGER;
                       src: ARRAY OF CHAR; et: INTEGER);
   VAR q: QVal;
   BEGIN
+    IF SymTab.IsBigSet(dt) THEN
+      BigSetCopy(dst, src); RETURN
+    END;
     IF SymTab.IsPackedSet(dt) THEN
       IF SymTab.IsPackedSet(et) THEN LoadPacked(src, et, q)
       ELSE LoadW(src, q)

+ 3 - 0
compiler/src/SymTab.def

@@ -448,6 +448,7 @@ PROCEDURE IsVariant (rec: TypeIndex): BOOLEAN;
 (* TRUE when rec has a variant part. *)
 PROCEDURE NewSet (base: TypeIndex): TypeIndex;
 PROCEDURE NewPackSet (base: TypeIndex): TypeIndex;
+PROCEDURE NewBigSet (base: TypeIndex): TypeIndex;
 (* ISO PACKEDSET: same base/span as SET but 1/2/4-byte storage
    (spans up to 32 bits in V3). *)
 PROCEDURE NewPtr (base: TypeIndex): TypeIndex;
@@ -604,6 +605,8 @@ PROCEDURE SetWords (t: TypeIndex): CARDINAL;
 PROCEDURE SetBytes (t: TypeIndex): CARDINAL;
 (* Storage bytes: SET = SetWords*4, PACKEDSET = ceil(count/8) (1..4). *)
 PROCEDURE IsPackedSet (t: TypeIndex): BOOLEAN;
+PROCEDURE IsBigSet (t: TypeIndex): BOOLEAN;
+PROCEDURE BigSetSigned (t: TypeIndex): BOOLEAN;
 (* Mask words for a set type from its base span (bool 1, char 8,
    subrange by bounds); 0 when the base is unsuitable. *)
 PROCEDURE SetBaseLo (t: TypeIndex): INTEGER;

+ 46 - 6
compiler/src/SymTab.mod

@@ -20,7 +20,7 @@ CONST
   FInt = 9; FReal = 10; FChar = 11; FBool = 12;
   FClass = 13; FOpenArr = 14; FNil = 15; FLong = 16; FProc = 17;
   FUChar = 18; FUStr = 19; FByte = 20; FSByte = 21;
-  FWord16 = 22; FSWord16 = 23; FPackSet = 24;
+  FWord16 = 22; FSWord16 = 23; FPackSet = 24; FBigSet = 25;
 
   (* signatures compared by the strict 231 check are bounded to this many
      formals (self-hosting uses far fewer). *)
@@ -979,6 +979,12 @@ PROCEDURE NewPackSet (base: TypeIndex): TypeIndex;
     RETURN NewDesc(FPackSet, base)
   END NewPackSet;
 
+PROCEDURE NewBigSet (base: TypeIndex): TypeIndex;
+(* Unbounded set: a pointer to a sorted linked list of uint32 ranges. *)
+  BEGIN
+    RETURN NewDesc(FBigSet, base)
+  END NewBigSet;
+
 PROCEDURE NewPtr (base: TypeIndex): TypeIndex;
   BEGIN
     RETURN NewDesc(FPtr, base)
@@ -1174,7 +1180,7 @@ PROCEDURE ClassOf (t: TypeIndex): INTEGER;
     | FArray : RETURN ClArray
     | FOpenArr : RETURN ClArray
     | FRecord : RETURN ClRecord
-    | FSet, FPackSet : RETURN ClSet
+    | FSet, FPackSet, FBigSet : RETURN ClSet
     | FPtr : RETURN ClPtr
     | FStr : RETURN ClStr
     | FClass : RETURN ClClass
@@ -1382,7 +1388,7 @@ PROCEDURE SetBase (t: TypeIndex): TypeIndex;
   BEGIN
     r := Resolve(t);
     IF (r = InvalidType) OR ((tform[r] # FSet)
-       AND (tform[r] # FPackSet)) THEN
+       AND (tform[r] # FPackSet) AND (tform[r] # FBigSet)) THEN
       RETURN InvalidType
     END;
     RETURN tref[r]
@@ -1419,9 +1425,17 @@ PROCEDURE SetSpan (t: TypeIndex; VAR lo, n: INTEGER): BOOLEAN;
     RETURN FALSE
   END SetSpan;
 
+PROCEDURE IsBigSet (t: TypeIndex): BOOLEAN;
+  VAR r: TypeIndex;
+  BEGIN
+    r := Resolve(t);
+    RETURN (r # InvalidType) AND (tform[r] = FBigSet)
+  END IsBigSet;
+
 PROCEDURE SetWords (t: TypeIndex): CARDINAL;
   VAR lo, n: INTEGER;
   BEGIN
+    IF IsBigSet(t) THEN RETURN 2 END;  (* pointer-sized slot *)
     IF NOT SetSpan(t, lo, n) THEN RETURN 0 END;
     RETURN VAL(CARDINAL, (n + 31) DIV 32)
   END SetWords;
@@ -1429,6 +1443,7 @@ PROCEDURE SetWords (t: TypeIndex): CARDINAL;
 PROCEDURE SetBytes (t: TypeIndex): CARDINAL;
   VAR lo, n: INTEGER;
   BEGIN
+    IF IsBigSet(t) THEN RETURN 8 END;
     IF NOT SetSpan(t, lo, n) THEN RETURN 0 END;
     IF IsPackedSet(t) THEN
       RETURN VAL(CARDINAL, (n + 7) DIV 8)
@@ -1443,6 +1458,15 @@ PROCEDURE IsPackedSet (t: TypeIndex): BOOLEAN;
     RETURN (r # InvalidType) AND (tform[r] = FPackSet)
   END IsPackedSet;
 
+PROCEDURE BigSetSigned (t: TypeIndex): BOOLEAN;
+  VAR b, r: TypeIndex;
+  BEGIN
+    b := SetBase(t);
+    IF b = InvalidType THEN RETURN FALSE END;
+    r := Resolve(b);
+    RETURN r = Resolve(dInt)
+  END BigSetSigned;
+
 PROCEDURE SetBaseLo (t: TypeIndex): INTEGER;
   VAR lo, n: INTEGER;
   BEGIN
@@ -1472,6 +1496,7 @@ PROCEDURE TypeBounds (t: TypeIndex; VAR lo, hi: INTEGER): BOOLEAN;
     r := Resolve(t);
     IF r = InvalidType THEN RETURN FALSE END;
     IF tform[r] = FSub THEN RETURN SubBounds(r, lo, hi) END;
+    IF tform[r] = FBigSet THEN RETURN FALSE END;
     IF tform[r] = FArray THEN lo := alo[r]; hi := ahi[r]; RETURN TRUE END;
     c := ClassOf(r);
     IF c = ClChar THEN lo := 0; hi := 255; RETURN TRUE END;
@@ -1736,6 +1761,7 @@ PROCEDURE TypeSizeD (t: TypeIndex; depth: CARDINAL): CARDINAL;
     | FReal : RETURN 8
     | FEnum, FSub : RETURN 4
     | FSet : RETURN SetWords(t) * 4
+    | FBigSet : RETURN 8
     | FPackSet : RETURN SetBytes(t)
     | FPtr, FOpenArr, FArray, FProc, FUStr : RETURN 8
     | FRecord, FClass :
@@ -3418,6 +3444,9 @@ PROCEDURE Assignable (src, dst: TypeIndex): BOOLEAN;
     rs := Resolve(src); rd := Resolve(dst);
     IF rs = rd THEN RETURN TRUE END;
     IF (rs = InvalidType) OR (rd = InvalidType) THEN RETURN TRUE END;
+    IF (tform[rs] = FBigSet) AND (tform[rd] = FBigSet) THEN
+      RETURN SameType(SetBase(rs), SetBase(rd))
+    END;
     IF ((tform[rs] = FSet) OR (tform[rs] = FPackSet))
        AND ((tform[rd] = FSet) OR (tform[rd] = FPackSet)) THEN
       RETURN SetBasesOk(tref[rs], tref[rd])
@@ -3587,8 +3616,14 @@ PROCEDURE EqCheck (l, r: TypeIndex): BOOLEAN;
       RETURN TRUE
     END;
     (* gm2 ext: any two set types compare =/# (word-wise equality);
-       a set CASE selector uses this too. *)
-    IF (ClassOf(l) = ClSet) AND (ClassOf(r) = ClSet) THEN RETURN TRUE END;
+       a set CASE selector uses this too.  Big sets compare by range
+       list; same/integer-family bases are compatible. *)
+    IF (ClassOf(l) = ClSet) AND (ClassOf(r) = ClSet) THEN
+      IF IsBigSet(l) OR IsBigSet(r) THEN
+        RETURN IsBigSet(l) AND IsBigSet(r)
+      END;
+      RETURN TRUE
+    END;
     (* ISO: a CHAR (or CHAR subrange) compares with the empty string
        literal (which denotes the terminator). *)
     IF IsEmptyStr(l) AND (ClassOf(r) = ClChar) THEN RETURN TRUE END;
@@ -3675,7 +3710,12 @@ PROCEDURE InCheck (l, set: TypeIndex): BOOLEAN;
     IF (Resolve(l) = InvalidType) OR (Resolve(set) = InvalidType) THEN RETURN TRUE END;
     rs := Resolve(set);
     IF (rs = InvalidType) OR ((tform[rs] # FSet)
-       AND (tform[rs] # FPackSet)) THEN RETURN FALSE END;
+       AND (tform[rs] # FPackSet) AND (tform[rs] # FBigSet)) THEN
+      RETURN FALSE
+    END;
+    IF tform[rs] = FBigSet THEN
+      RETURN SetElemClassOk(ClassOf(l))
+    END;
     b := tref[rs];
     IF SameType(l, b) THEN RETURN TRUE END;
     IF IsIntFamily(l) AND IsIntFamily(b) THEN RETURN TRUE END;

+ 6 - 0
compiler/tests/t_bad_bigconst.mod

@@ -0,0 +1,6 @@
+MODULE TBadBigConst;
+(* Dynamic range-list sets have no static CONST layout yet. *)
+TYPE CS = SET OF CARDINAL;
+CONST c = CS{1, 3};
+BEGIN
+END TBadBigConst.

+ 13 - 0
compiler/tests/t_bad_setbase.mod

@@ -1,4 +1,17 @@
 MODULE TBadSetBase;
+(* SET OF INTEGER is a full-range sorted-range set. *)
 TYPE S = SET OF INTEGER;
+VAR s : S;
+    ExitCode : INTEGER;
 BEGIN
+  s := S{};
+  INCL(s, -7);
+  INCL(s, 2147483647);
+  INCL(s, -2147483648);
+  IF (-7 IN s) AND (2147483647 IN s) AND (-2147483648 IN s)
+     AND NOT (0 IN s) THEN
+    ExitCode := 42
+  ELSE
+    ExitCode := 1
+  END
 END TBadSetBase.

+ 41 - 0
compiler/tests/t_big_ops.mod

@@ -0,0 +1,41 @@
+MODULE TBigOps;
+(* Sorted-range big sets: literals, ranges, copy, equality, operators,
+   EXCL and signed SET OF INTEGER. *)
+TYPE
+  CS = SET OF CARDINAL;
+  IS = SET OF INTEGER;
+VAR
+  a, b, c : CS;
+  i : IS;
+  ExitCode : INTEGER;
+BEGIN
+  a := CS{1, 3, 5..7};
+  b := CS{3, 7};
+  c := a;
+  i := IS{-5..-3, 2, 100000};
+  IF (a = c) AND NOT (a = b) THEN
+    c := a + b;          (* {1,3,5,6,7} *)
+    IF (1 IN c) AND (6 IN c) AND NOT (2 IN c) THEN
+      c := a - b;        (* {1,5,6} *)
+      IF (1 IN c) AND NOT (3 IN c) AND (6 IN c) THEN
+        c := a * b;      (* {3,7} *)
+        IF (3 IN c) AND NOT (5 IN c) THEN
+          c := a / b;    (* {1,5,6} *)
+          EXCL(c, 5);
+          IF (1 IN c) AND NOT (5 IN c) AND (6 IN c) THEN
+            IF (-5 IN i) AND (-3 IN i) AND (2 IN i) AND (100000 IN i)
+               AND NOT (-6 IN i) AND NOT (0 IN i) THEN
+              ExitCode := 42
+            ELSE ExitCode := 1
+            END
+          ELSE ExitCode := 1
+          END
+        ELSE ExitCode := 1
+        END
+      ELSE ExitCode := 1
+      END
+    ELSE ExitCode := 1
+    END
+  ELSE ExitCode := 1
+  END
+END TBigOps.

+ 2 - 0
docs/INDEX.md

@@ -61,6 +61,8 @@ bisecting *why* a decision was made, not for reading end-to-end).
 
 ## Recent session summaries (newest first)
 
+- **`summary_session-2026-10-24-dynamic-sets.md`** — sorted-range
+  `SET OF CARDINAL`/`INTEGER`; tag `v3-dynamic-sets`.
 - **`summary_session-2026-10-24-language-robustness.md`** — ADDRESS/
   pointer equality and the language-robustness pass; tag
   `v3-language-robustness`.

+ 3 - 3
docs/STATUS.md

@@ -4,9 +4,9 @@ This answers "where are we now?".
 
 ## Numbers
 
-- Suite **307/307** (`compiler/run_tests.sh`).
+- Suite **309/309** (`compiler/run_tests.sh`).
 - Self-hosting **FIXPOINT OK** — stage2 and stage3 images are
-  byte-identical (3,809,843 bytes).
+  byte-identical (3,845,439 bytes).
 - Corpus **521 / 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
@@ -14,7 +14,7 @@ This answers "where are we now?".
   imports of gm2-only modules.  Open-array corpus cases `unbounded`,
   `unbounded2`, `unbounded3`, `unbounded4` and `array9` now compile;
   `unbounded`/`unbounded2`/`unbounded3`/`unbounded4` also run.
-- Branch **`master`**, feature state **`v3-wide-sets`**; tree
+- Branch **`master`**, feature state **`v3-dynamic-sets`**; tree
   clean.
 
 ## Architecture (current)

+ 9 - 8
docs/comparison-PIM4-gm2-V3.md

@@ -2,7 +2,7 @@
 
 **Date:** 2026-10-24 · **V3 state:** open arrays, dialect library
 folders and separate compilation with `M2 -I`/incremental objects
-(suite 307/307, self-hosting fixpoint OK 3,809,843 bytes, corpus
+(suite 309/309, self-hosting fixpoint OK 3,845,439 bytes, corpus
 521/612, 0 crashes).
 
 **Scope and sources.**
@@ -72,7 +72,7 @@ change individual cells.
 | Basic integer/real/bool/char types | ✅ | ✅ | ✅ (+ `OCTET`, `LONGCARD`) | ✅ PIM types | ✅ |
 | Subranges | ✅ | ✅ | ✅ (`[lo..hi] OF Base`) | ✅ | ✅ (typed CHAR/enum subranges) |
 | Enumerations | ✅ | ✅ | ✅ extensible | ✅ | ✅ |
-| Sets (`SET OF`) | ✅ any finite ordinal (impl. limit) | ✅ | 🔄 `SET OF <enumType>` only | ✅ range/base/enum bases | 🔄 bool/char/byte/16-bit/enum/small-int; wide spans ≤ 1,000,000 bits; bounded `SET OF CARDINAL` |
+| Sets (`SET OF`) | ✅ any finite ordinal (impl. limit) | ✅ | 🔄 `SET OF <enumType>` only | ✅ range/base/enum bases | 🔄 bounded sets ≤ 1,000,000 bits; `SET OF CARDINAL`/`INTEGER` use a dynamic sorted range list over the full base range |
 | `PACKEDSET` | ❌ (ISO) | ✅ | ❌ | ❌ | ✅ (1/2/4 bytes, span ≤ 32) |
 | Fixed / multi-dimensional arrays | ✅ `[a], [b] OF` | ✅ | ✅ `ARRAY count, count OF` (zero-based) | ✅ | ✅ both `[a], [b]` and `[a, b]` |
 | Open arrays (1-D) | ✅ | ✅ | ✅ (`BARE ARRAY OF`) | ✅ | ✅ |
@@ -162,8 +162,9 @@ change individual cells.
   payload with per-level views; payload `SIZE`/`ADR` and row/subarray
   conversion are implemented.  Its hard gaps are **complex arithmetic,
   finalization/protection, ISO OO** and parts of the **word model**
-  (`SHORTREAL`, `REAL` width), plus a **1,000,000-bit wide `SET`
-  limit** and a **32-bit `PACKEDSET` limit**.  Self-referential pointer records
+  (`SHORTREAL`, `REAL` width), plus a **1,000,000-bit bounded `SET`
+  limit**, dynamic `SET OF CARDINAL`/`INTEGER` range lists and a
+  **32-bit `PACKEDSET` limit**.  Self-referential pointer records
   and chained pointer-field designators (`p^.next^.value`) are
   supported.  **Separate compilation** is now available for normal
   modules: `M2 -c` emits one object per unit, library objects have no
@@ -181,10 +182,10 @@ change individual cells.
   are 32-bit; `LONGINT`/`LONGCARD` are 64-bit; every `REAL`/`SHORTREAL`
   is 8 bytes; `COMPLEX*` is a placeholder.
 - **Sets:** bases limited to bool/char/byte/16-bit/enum/small subranges;
-  wide `SET` spans up to 1,000,000 bits; `SET OF CARDINAL` is bounded
-  to that span; `SET OF INTEGER` remains rejected.  `PACKEDSET` uses
-  1/2/4-byte storage with a 32-bit span limit (wider spans are a loud
-  `230`).
+  bounded `SET` spans up to 1,000,000 bits; `SET OF CARDINAL` and
+  `SET OF INTEGER` use sorted range lists over the full base range.
+  Dynamic `SET CONST` is not supported yet.  `PACKEDSET` uses 1/2/4-byte
+  storage with a 32-bit span limit (wider spans are a loud `230`).
 - **Runtime range checks** are emitted for subrange assignment, value
   actuals, `RETURN` and `INC`/`DEC` (`QbeGen.CheckRange`); they raise a
   catchable `rangeException`, with an unhandled exception aborting at

+ 6 - 2
docs/features.md

@@ -1,4 +1,4 @@
-# m2compiler-V3 — feature status (2026-10-24; suite 307/307 green)
+# m2compiler-V3 — feature status (2026-10-24; suite 309/309 green)
 
 Pipeline: Coco/R `M2.atg` (1748 lines, 73 productions) → `gm2`-built
 `M2` → QBE `.ssa` → `qbe` → `cc` → run. `SymTab.mod` 1623 lines,
@@ -80,7 +80,11 @@ Legend: ✅ done · 🔄 partial · ⏸ not started / deferred.
   as a statement).  Unknown names keep the permissive no-result varargs
   external.  String/CHAR-array actuals pass as `descriptor+8`.
 - ✅ Set builtins `INCL`/`EXCL` and `SHIFT`/`ROTATE`; `SET OF <enum>`
-  and `SET OF <char-range>`; wide sets up to 1,000,000 bits; `SET OF CARDINAL` uses that bounded span. `SET OF INTEGER` remains rejected.
+  and `SET OF <char-range>`; bounded sets up to 1,000,000 bits;
+  `SET OF CARDINAL` and `SET OF INTEGER` use a sorted range-list
+  representation with full base range (empty/range literals,
+  `INCL`/`EXCL`/`IN`, assignment, equality and set operators).
+  Dynamic `SET CONST` is not supported yet (loud `230`).
 - ✅ Module-level forward type references (a global may name a type
   declared further down; emission deferred).
 - ✅ `RECORD` (flat blobs, nested inline, array fields as pointers),

+ 1 - 1
docs/plan-separate-compilation.md

@@ -5,7 +5,7 @@
 one-shot mode remains the default for the suite/corpus; this document
 keeps the design record and the deferred phases.
 
-**Baseline now:** suite 307/307, fixpoint OK 3,809,843 bytes, corpus
+**Baseline now:** suite 309/309, fixpoint OK 3,845,439 bytes, corpus
 521/612, 0 crashes.
 
 ---

+ 67 - 0
docs/summary_session-2026-10-24-dynamic-sets.md

@@ -0,0 +1,67 @@
+# Session 2026-10-24 — dynamic SET OF CARDINAL / INTEGER
+
+**Gates:** suite **309/309**, one-shot fixpoint **OK (3,845,439
+bytes)**, separate-object fixpoint **OK (11 objects)**, corpus
+**521/612 compile-OK, 0 crashes**.
+
+## Representation
+
+`SET OF CARDINAL` and `SET OF INTEGER` now use a **sorted linked list
+of disjoint inclusive ranges** rather than a wide inline bit-vector:
+
+```text
+BigSet = pointer to
+  { lo : CARDINAL; hi : CARDINAL; next : pointer }
+```
+
+- empty set = null pointer
+- adjacent ranges are merged
+- ranges are ordered
+- `INTEGER` values are mapped with the sign-bit flip
+  `VAL(CARDINAL, x) XOR 0x80000000`
+- the full 32-bit base range is available; no 1,000,000-bit window
+
+The runtime helpers live in `runtime/syslib/shim.c`:
+
+```text
+m2bs_incl, m2bs_excl, m2bs_incl_range, m2bs_in
+m2bs_copy_to, m2bs_equal, m2bs_bin
+```
+
+## What works
+
+- empty and range literals: `CS{}`, `CS{1, 3, 5..7}`,
+  `IS{-5..-3, 100000}`
+- `INCL` / `EXCL` / `IN`
+- assignment and value/`VAR` parameters
+- equality
+- union, intersection, difference and symmetric difference
+- record fields (basic use)
+- `SET OF INTEGER` including negative values and the full signed range
+
+Bounded sets such as `SET OF [0..100000]` keep the existing inline
+bit-vector representation and the 1,000,000-bit limit.
+
+## Limits
+
+- **Dynamic `SET CONST` is not supported yet**: a `CONST` of
+  `SET OF CARDINAL`/`INTEGER` is a loud `230` (`t_bad_bigconst`).
+- Record/array assignment containing dynamic sets currently copies the
+  pointer value; deep-copy of such composites is a follow-up.
+- The range-list nodes are allocated with `malloc`; assignment frees the
+  previous list, but there is no GC. Program-exit cleanup is omitted.
+- `PACKEDSET` is unchanged and remains bounded to 32 bits.
+- `SET OF CARDINAL`/`INTEGER` is not used by the compiler itself, so
+  the fixpoint image remains self-hosting.
+
+## Tests
+
+- `t_wide_set` — bounded wide set plus dynamic cardinal set
+- `t_bad_setbase` — now tests full-range `SET OF INTEGER`
+- `t_big_ops` — literals/ranges, copy, equality, all four set operators,
+  `EXCL`, negative integer elements
+- `t_bad_bigconst` — dynamic set constants are rejected for now
+
+## Tag
+
+`v3-dynamic-sets`

+ 1 - 1
docs/summary_session-2026-10-24-language-robustness.md

@@ -1,6 +1,6 @@
 # Session 2026-10-24 — language/robustness pass
 
-**Gates:** suite **307/307**, one-shot fixpoint **OK (3,809,843
+**Gates:** suite **309/309**, one-shot fixpoint **OK (3,845,439
 bytes)**, separate-object fixpoint **OK (11 objects)**, corpus
 **521/612 compile-OK, 0 crashes**.
 

+ 2 - 2
docs/summary_session-2026-10-24-separate-compilation.md

@@ -4,7 +4,7 @@ V3 now builds per-unit objects.  The one-shot mode remains the default
 for the suite/corpus.
 
 **Branch:** `master`.  **Feature state:** `v3-separate-compilation`.
-**Gates:** suite **307/307**, fixpoint **OK (3,809,843 bytes)**,
+**Gates:** suite **309/309**, fixpoint **OK (3,845,439 bytes)**,
 corpus **521/612 compile-OK, 0 crashes**.
 
 ---
@@ -88,7 +88,7 @@ Manual M2make cross-object class check: `CLib.def/.mod` + `CProg.mod`
   `gen_ssa/<Module>.o`, so definition-only data and class vtables link.
 - Added `CDefProg` (class-only `.def` vtable) and M2make checks for the
   ABI stamp and definition-only object.
-- Suite is now 307/307.
+- Suite is now 309/309.
 
 ## Generic-instance objects (2026-10-24)
 

+ 2 - 2
docs/triage-corpus-2026-10-24.md

@@ -72,8 +72,8 @@ No policy relaxation is recommended.
 
 ## Implementation limits
 
-- `settest1`, `settest3`: fixed by the 1,000,000-bit wide-set
-  implementation (corpus 519 → 521).
+- `settest1`, `settest3`: fixed by bounded wide sets plus the dynamic
+  sorted-range representation for `SET OF CARDINAL` (corpus 519 → 521).
 - `largeset5`: set ordering (`<`, `>=`); PIM/ISO only defines
   `=`/`#`/`IN` for sets, so this is a gm2 extension rather than a
   missing standard operation.

+ 188 - 0
runtime/syslib/shim.c

@@ -1225,3 +1225,191 @@ void *m2_exc_new_source(void)
 {
     return (void *)(uintptr_t)(++m2_source_seq);
 }
+
+/* ---------------- sorted range sets (SET OF CARDINAL/INTEGER) ----------------
+ * A value is a pointer to a sorted list of inclusive uint32 ranges.
+ * The list is empty when the pointer is NULL.  Values from INTEGER use a
+ * sign-bit flip so ordinary CARDINAL ordering is preserved.
+ */
+#include <stdint.h>
+
+typedef struct M2BSRange {
+    uint32_t lo;
+    uint32_t hi;
+    struct M2BSRange *next;
+} M2BSRange;
+
+static uint32_t m2bs_key(long v, long sign)
+{
+    uint32_t x = (uint32_t)v;
+    if (sign) x ^= 0x80000000UL;
+    return x;
+}
+
+static void m2bs_free(M2BSRange *p)
+{
+    M2BSRange *q;
+    while (p != NULL) { q = p->next; free(p); p = q; }
+}
+
+static M2BSRange *m2bs_copy(M2BSRange *p)
+{
+    M2BSRange head, *tail = &head, *n;
+    head.next = NULL;
+    while (p != NULL) {
+        n = (M2BSRange *)malloc(sizeof *n);
+        if (n == NULL) break;
+        n->lo = p->lo; n->hi = p->hi; n->next = NULL;
+        tail->next = n; tail = n; p = p->next;
+    }
+    return head.next;
+}
+
+static void m2bs_add(M2BSRange **head, uint32_t lo, uint32_t hi)
+{
+    M2BSRange **pp = head;
+    M2BSRange *n;
+    if (lo > hi) { uint32_t t = lo; lo = hi; hi = t; }
+    while (*pp != NULL && (uint64_t)(*pp)->hi + 1 < (uint64_t)lo) {
+        pp = &(*pp)->next;
+    }
+    if (*pp != NULL && (uint64_t)(*pp)->hi + 1 >= (uint64_t)lo
+                   && (uint64_t)hi + 1 >= (uint64_t)(*pp)->lo) {
+        n = *pp;
+        if (lo < n->lo) n->lo = lo;
+        if (hi > n->hi) n->hi = hi;
+        /* absorb following overlapping/adjacent nodes */
+        while (n->next != NULL
+               && (uint64_t)n->hi + 1 >= (uint64_t)n->next->lo) {
+            M2BSRange *m = n->next;
+            if (m->hi > n->hi) n->hi = m->hi;
+            n->next = m->next;
+            free(m);
+        }
+        return;
+    }
+    n = (M2BSRange *)malloc(sizeof *n);
+    if (n == NULL) return;
+    n->lo = lo; n->hi = hi; n->next = *pp; *pp = n;
+}
+
+static void m2bs_remove(M2BSRange **head, uint32_t x)
+{
+    M2BSRange **pp = head;
+    while (*pp != NULL && (*pp)->hi < x) pp = &(*pp)->next;
+    if (*pp == NULL || (*pp)->lo > x) return;
+    {
+        M2BSRange *n = *pp;
+        if (n->lo == n->hi) {
+            *pp = n->next; free(n);
+        } else if (x == n->lo) {
+            n->lo++;
+        } else if (x == n->hi) {
+            n->hi--;
+        } else {
+            M2BSRange *m = (M2BSRange *)malloc(sizeof *m);
+            if (m == NULL) return;
+            m->lo = x + 1; m->hi = n->hi; m->next = n->next;
+            n->hi = x - 1; n->next = m;
+        }
+    }
+}
+
+long m2bs_incl(long *slot, long v, long sign)
+{
+    if (slot != NULL) m2bs_add((M2BSRange **)slot, m2bs_key(v, sign), m2bs_key(v, sign));
+    return 0;
+}
+
+long m2bs_excl(long *slot, long v, long sign)
+{
+    if (slot != NULL) m2bs_remove((M2BSRange **)slot, m2bs_key(v, sign));
+    return 0;
+}
+
+long m2bs_incl_range(long *slot, long a, long b, long sign)
+{
+    if (slot != NULL) m2bs_add((M2BSRange **)slot, m2bs_key(a, sign), m2bs_key(b, sign));
+    return 0;
+}
+
+long m2bs_in(long *slot, long v, long sign)
+{
+    M2BSRange *p;
+    uint32_t x;
+    if (slot == NULL) return 0;
+    x = m2bs_key(v, sign);
+    p = (M2BSRange *)*slot;
+    while (p != NULL && p->hi < x) p = p->next;
+    return (p != NULL && p->lo <= x);
+}
+
+long m2bs_equal(long *a, long *b)
+{
+    M2BSRange *p = (a == NULL) ? NULL : (M2BSRange *)*a;
+    M2BSRange *q = (b == NULL) ? NULL : (M2BSRange *)*b;
+    while (p != NULL && q != NULL) {
+        if (p->lo != q->lo || p->hi != q->hi) return 0;
+        p = p->next; q = q->next;
+    }
+    return (p == NULL && q == NULL);
+}
+
+long m2bs_copy_to(long *dst, long *src)
+{
+    M2BSRange *p;
+    if (dst == NULL) return 0;
+    p = (M2BSRange *)*dst;
+    if (src != NULL && *src == *dst) return 0;
+    m2bs_free(p);
+    *dst = (long)((src == NULL) ? NULL : m2bs_copy((M2BSRange *)*src));
+    return 0;
+}
+
+static void m2bs_diff_into(M2BSRange **out, M2BSRange *a, M2BSRange *b)
+{
+    M2BSRange *p, *q;
+    for (p = a; p != NULL; p = p->next) {
+        uint32_t lo = p->lo, hi = p->hi;
+        int done = 0;
+        for (q = b; q != NULL && !done; q = q->next) {
+            if (q->hi < lo) continue;
+            if (q->lo > hi) break;
+            if (q->lo > lo) m2bs_add(out, lo, q->lo - 1);
+            if (q->hi >= hi) { done = 1; break; }
+            lo = q->hi + 1;
+            if (lo > hi) done = 1;
+        }
+        if (!done && lo <= hi) m2bs_add(out, lo, hi);
+    }
+}
+
+long m2bs_bin(long *dst, long *la, long *lb, long sel)
+{
+    M2BSRange *a, *b, *p, *q, *res = NULL;
+    if (dst == NULL) return 0;
+    a = (la == NULL) ? NULL : (M2BSRange *)*la;
+    b = (lb == NULL) ? NULL : (M2BSRange *)*lb;
+    if (sel == 0) {                 /* union */
+        res = m2bs_copy(a);
+        for (q = b; q != NULL; q = q->next) m2bs_add(&res, q->lo, q->hi);
+    } else if (sel == 1) {          /* intersection */
+        for (p = a; p != NULL; p = p->next)
+            for (q = b; q != NULL && q->lo <= p->hi; q = q->next)
+                if (q->hi >= p->lo)
+                    m2bs_add(&res,
+                        (q->lo > p->lo) ? q->lo : p->lo,
+                        (q->hi < p->hi) ? q->hi : p->hi);
+    } else if (sel == 2) {          /* left difference */
+        m2bs_diff_into(&res, a, b);
+    } else {                        /* symmetric difference */
+        M2BSRange *t = NULL;
+        m2bs_diff_into(&t, a, b);
+        m2bs_diff_into(&res, b, a);
+        for (q = t; q != NULL; q = q->next) m2bs_add(&res, q->lo, q->hi);
+        m2bs_free(t);
+    }
+    m2bs_free((M2BSRange *)*dst);
+    *dst = (long)res;
+    return 0;
+}