Преглед изворни кода

language: runtime subrange range checks

Assigning a value outside a subrange now traps (ISO range exception):
Lower.CheckSub emits QbeGen.CheckRange on

* assignment to a subrange (scalar, field or array element),
* value actuals whose formal is a subrange,
* RETURN whose function result is a subrange,
* INC/DEC on a subrange.

The post-increment of a FOR control variable is deliberately exempt: the
final successor of a full-subrange loop may legitimately exceed the
upper bound.  LONGINT subranges are not checked yet.

Regression: t_trap_range (assignment), t_trap_range_arg (actual),
t_trap_range_ret (RETURN); t_subrange remains the in-range check.

Also inspected PACKEDSET for the planned next step: onebyte* assert
SIZE(PACKEDSET)=SIZE(BYTE)=1 and ORD(set), so aliasing it to SET would
be semantically wrong.  It is parked pending a packed representation
(see docs/handoff-2026-10-23-next.md).

Suite 262/262, fixpoint OK (4,032,793 bytes), corpus 511/612, 0 crashes.
Eric Streit пре 17 часа
родитељ
комит
54d84b98e1

+ 2 - 2
README.md

@@ -47,8 +47,8 @@ docs/              STATUS.md, plan.md, plan-*.md, analysis_*.md, summary_*.md
 
 ## Status (2026-10-22)
 
-- Suite **259/259**; self-hosting **fixpoint OK** (byte-identical
-  stage2/stage3, 4,028,447 bytes); corpus **511/612 compile-OK,
+- Suite **262/262**; self-hosting **fixpoint OK** (byte-identical
+  stage2/stage3, 4,032,793 bytes); corpus **511/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 **`master`**.

+ 3 - 0
compiler/run_tests.sh

@@ -241,6 +241,9 @@ expect_trap() {
 }
 
 expect_trap t_trap_index.mod
+expect_trap t_trap_range.mod
+expect_trap t_trap_range_arg.mod
+expect_trap t_trap_range_ret.mod
 expect_run t_strtrunc.mod 42
 expect_trap t_trap_setspan.mod
 expect_run t_strempty.mod 42

+ 25 - 0
compiler/src/Lower.mod

@@ -1200,6 +1200,23 @@ PROCEDURE UserLabel (name: ARRAY OF CHAR; VAR q: QbeGen.QVal);
     END
   END UserLabel;
 
+PROCEDURE CheckSub (t: SymTab.TypeIndex; q: ARRAY OF CHAR);
+(* ISO range check: trap when a value is assigned to a subrange outside
+   its bounds.  Only 32-bit-or-smaller ordinal bases; LONGINT subranges
+   are left unchecked for now. *)
+  VAR lo, hi, c: INTEGER; ql, qlo, qhi: QbeGen.QVal;
+  BEGIN
+    IF (t # SymTab.InvalidType) AND SymTab.SubBounds(t, lo, hi) THEN
+      c := SymTab.ClassOf(t);
+      IF (c # SymTab.ClLong) AND (c # SymTab.ClInvalid) THEN
+        QbeGen.WidenIndex(q, ql);
+        QbeGen.IntStr(lo, qlo);
+        QbeGen.IntStr(hi, qhi);
+        QbeGen.CheckRange(ql, qlo, qhi)
+      END
+    END
+  END CheckSub;
+
 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;
@@ -1308,6 +1325,7 @@ PROCEDURE LowerCall (n: AST.Node; VAR t: SymTab.TypeIndex;
             (* ISO: empty string actual for a CHAR formal is 0 *)
             QbeGen.CopyOp("0", qe); at := SymTab.CharType()
           END;
+          IF (NOT va) AND (NOT isV) THEN CheckSub(ft, qe) END;
           IF va THEN
             (* permissive C variadic argument *)
             QbeGen.CArgAdd(qe, at, qa, cl)
@@ -1607,6 +1625,7 @@ PROCEDURE LowerBuiltinStmt (n: AST.Node; nm: SymTab.Name);
         IF isInc THEN QbeGen.Op3("add", qn2, qv, qstep, FALSE)
         ELSE QbeGen.Op3("sub", qn2, qv, qstep, FALSE)
         END;
+        CheckSub(dt, qn2);
         IF sfx OR (dk = SymTab.KindField) THEN QbeGen.ElemStore(qd, qn2, dt)
         ELSIF SymTab.IsSmallInt(dt) THEN
           QbeGen.StoreShortVar(qn, SymTab.SmallIntBytes(dt), qn2)
@@ -2295,6 +2314,7 @@ PROCEDURE LowerStmt (seq: AST.Node);
         (* ISO: the empty string assigned to CHAR is the terminator *)
         QbeGen.CopyOp("0", qe); et := SymTab.CharType()
       END;
+      CheckSub(dt, qe);
       isR := (dt # SymTab.InvalidType) AND (SymTab.ClassOf(dt) = SymTab.ClReal);
       conv := isR AND SymTab.IsIntFamily(et);
       wconv := (dt # SymTab.InvalidType) AND SymTab.IsLongFamily(dt)
@@ -2392,6 +2412,8 @@ PROCEDURE LowerStmt (seq: AST.Node);
       IF SymTab.IsEmptyStr(t2) THEN
         QbeGen.CopyOp("0", qhi); t2 := SymTab.CharType()
       END;
+      CheckSub(SymTab.SymType(lv), qlo);
+      CheckSub(SymTab.SymType(lv), qhi);
       (* optional BY step: a constant, default +1; its sign also
          selects the termination comparison *)
       step := 1;
@@ -2453,6 +2475,9 @@ PROCEDURE LowerStmt (seq: AST.Node);
         QbeGen.EmitRet(q, FALSE)
       ELSE
         LowerExpr(AST.Child(seq, 0), t, q);
+        IF (emitProc >= 0) AND (emitProc < VAL(INTEGER, nProcs)) THEN
+          CheckSub(procRes[emitProc], q)
+        END;
         IF SymTab.IsEmptyStr(t)
            AND (emitProc >= 0) AND (emitProc < VAL(INTEGER, nProcs))
            AND (SymTab.ClassOf(procRes[emitProc]) = SymTab.ClChar) THEN

+ 10 - 0
compiler/tests/t_trap_range.mod

@@ -0,0 +1,10 @@
+MODULE TTrapRange;
+// ISO range check: assigning an out-of-range value to a subrange must
+// trap.  The harness expects SIGABRT (134) after parse/qbe/cc.
+TYPE I = [0..10];
+VAR x : I;
+VAR i : INTEGER;
+BEGIN
+  i := 11;
+  x := i
+END TTrapRange.

+ 9 - 0
compiler/tests/t_trap_range_arg.mod

@@ -0,0 +1,9 @@
+MODULE TTrapRangeArg;
+// A value actual outside the formal subrange must trap at the call.
+TYPE I = [0..10];
+PROCEDURE P (x: I);
+BEGIN
+END P;
+BEGIN
+  P(11)
+END TTrapRangeArg.

+ 11 - 0
compiler/tests/t_trap_range_ret.mod

@@ -0,0 +1,11 @@
+MODULE TTrapRangeRet;
+// A returned value outside the function's subrange result must trap.
+TYPE I = [0..10];
+PROCEDURE F () : I;
+BEGIN
+  RETURN 11
+END F;
+VAR x : I;
+BEGIN
+  x := F()
+END TTrapRangeRet.

+ 8 - 4
docs/STATUS.md

@@ -4,9 +4,9 @@ This answers "where are we now?".
 
 ## Numbers
 
-- Suite **259/259** (`compiler/run_tests.sh`).
+- Suite **262/262** (`compiler/run_tests.sh`).
 - Self-hosting **FIXPOINT OK** — stage2 and stage3 images are
-  byte-identical (4,028,447 bytes).
+  byte-identical (4,032,793 bytes).
 - Corpus **511 / 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
@@ -57,7 +57,11 @@ control variable (`FOR c := red TO blue`).  The ISO empty string
 literal (`""`/`''`) is compatible with `CHAR` (value 0) in assignment,
 comparison, `CASE` labels, `FOR` bounds, actuals and `RETURN`, while
 remaining a string for `ARRAY OF CHAR` (whole-array assignment and
-content comparison).  Adjacent one-character string literals concatenate
+content comparison).  Runtime **subrange range checks** now trap on
+assignment, value actuals, `RETURN` and `INC`/`DEC`
+(`QbeGen.CheckRange`); the post-increment of a `FOR` control variable is
+not checked (the final successor may legitimately exceed a full
+subrange).  Adjacent one-character string literals concatenate
 in `CONST` (`"W" + "o"`), relations of ordinal constants fold (so
 `ORD(a > b)` works in a `CONST`), `HIGH`/`LEN` of a string `CONST` use
 the pooled literal's length, `INC`/`DEC` accept any ordinal, a leading
@@ -206,7 +210,7 @@ declarations still work (`t_fwdproc`, `t_fwdok`).
 ```sh
 cd compiler
 ./build.sh          # M2.atg -> scanner/parser/driver -> ./M2
-./run_tests.sh      # 259/259
+./run_tests.sh      # 262/262
 cd ..
 ./bootstrap/fixpoint.sh   # FIXPOINT OK (byte-identical stage2/stage3)
 ```

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

@@ -1,8 +1,8 @@
 # PIM4 vs GNU Modula-2 vs M2R10 vs TopSpeed V3 vs V3 — feature comparison
 
-**Date:** 2026-10-23 · **V3 state:** `v3-smallfix` (gates run
-2026-10-22: suite 259/259, self-hosting fixpoint OK 4,028,447 bytes,
-corpus 511/612, 0 crashes).
+**Date:** 2026-10-23 · **V3 state:** `v3-rangechecks` (suite
+262/262, self-hosting fixpoint OK 4,032,793 bytes, corpus 511/612,
+0 crashes).
 
 **Scope and sources.**
 
@@ -100,6 +100,7 @@ change individual cells.
 | `LENGTH` | ❌ (ISO adds it) | ✅ | 🔄 library/pervasive, not in the base grammar | ❌ | ✅ |
 | `HIGH` | ✅ | ✅ | ❌ (not in the R10 grammar) | ✅ | ✅ (including string `CONST`) |
 | `TSIZE`/`SIZE`/`ADR` | ✅ | ✅ | 🔄 SYSTEM/library forms | ✅ | ✅ |
+| Runtime subrange range checks | ⚠️ error per language; checks implementation-defined | ✅ | ✅ (R10 safety) | 🔄 | ✅ assignment/value actual/`RETURN`/`INC`/`DEC`; `FOR` final increment exempt |
 | `SHIFT`/`ROTATE` | ⚠️ implementation SYSTEM extension | ✅ | ❌ | ✅ (`<<`/`>>` operators) | ✅ |
 | `TBITSIZE` | ❌ | ✅ | ❌ | ❌ | ❌ |
 | ISO exceptions (`EXCEPT`, `RETRY`, `EXCEPTIONS`) | ❌ | ✅ | ❌ (not in the base R10 grammar) | ❌ | ❌ |
@@ -168,8 +169,10 @@ change individual cells.
   is 8 bytes; `COMPLEX*` is a placeholder.
 - **Sets:** bases limited to bool/char/byte/16-bit/enum/small subranges;
   total span ≤ 65536; no `SET OF CARDINAL`.
-- **No runtime range checks** on subrange assignment (V3 is already
-  lenient for the integer family).
+- **Runtime range checks** are emitted for subrange assignment, value
+  actuals, `RETURN` and `INC`/`DEC` (`QbeGen.CheckRange`, abort trap).
+  The post-increment of a `FOR` control variable is not checked: the
+  final successor may legitimately exceed a full subrange.
 - **Multi-dimensional open arrays** are not supported: V3 represents an
   array level as its own descriptor, whereas gm2 passes a flat block —
   accepting the corpus files would compile but miscompile.

+ 3 - 1
docs/features.md

@@ -22,7 +22,9 @@ Legend: ✅ done · 🔄 partial · ⏸ not started / deferred.
   `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.
+  compare with it; **runtime range checks** trap on assignment, value
+  actuals, `RETURN` and `INC`/`DEC` (the `FOR` final increment is
+  exempt).
 - ✅ `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.

+ 12 - 3
docs/gaps/README.md

@@ -138,9 +138,11 @@ gap22_file_line.mod             invalid Fact            (__LINE__)
   `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).
+  **+3**: `subrange16`, `subrange17`, `enum3`.  A **runtime range
+  check** is now emitted on assignment, value actuals, `RETURN` and
+  `INC`/`DEC`; the post-increment of a `FOR` control variable is exempt
+  (its final successor may exceed a full subrange).  Regressions
+  `t_trap_range`, `t_trap_range_arg`, `t_trap_range_ret`.
 - **`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),
@@ -229,6 +231,13 @@ compatibility (they do not relax any conforming rule).
   so the sized family is now 8/16/32 at its nominal widths.  `WORD`/
   `WORD32` remain 32-bit.  Regression `t_word16` (SIZE, wrapping, signed
   fields, arrays, `SET OF WORD16`/`INTEGER16`, conversions).
+- **`PACKEDSET` — inspected, not implemented** (2026-10-23): ISO
+  `PACKEDSET OF <ordinal>` is `SIZE`- and `ORD`-sensitive (`onebyte*`
+  assert `SIZE(PACKEDSET) = SIZE(BYTE) = 1` and `ORD(set)` values), so
+  aliasing it to `SET` would compile but be semantically wrong.  A
+  faithful implementation needs a 1/2/4-byte packed representation and
+  packed set operations; parked until that is scheduled.  Corpus:
+  `onebyte`, `onebyte2`, `onebyte3`.
 - **`gap10` scalar `VAR` actual, same size — accepted** (2026-10-19).
   `VarParamOk` accepts a scalar VAR actual of a *different* type when
   `ObjectSize` matches (e.g. `INTEGER` for `VAR WORD`).  `testparam3`