Просмотр исходного кода

language: catchable language traps (M4)

Add TrapCode, split CheckIndex from CheckRange, trap CASE no-match as caseSelectException, and route internal mismatches through sysException. Existing unhandled trap tests still abort with rc 134. Add t_except_trap, t_except_case and t_except_unhandled. Gates: suite 275/275, fixpoint OK 4,141,580 bytes, corpus 512/612 compile-OK.
Eric Streit 8 часов назад
Родитель
Сommit
02b3763e5c

+ 2 - 2
README.md

@@ -47,8 +47,8 @@ docs/              STATUS.md, plan.md, plan-*.md, analysis_*.md, summary_*.md
 
 ## Status (2026-10-23)
 
-- Suite **272/272**; self-hosting **fixpoint OK** (byte-identical
-  stage2/stage3, 4,108,488 bytes); corpus **512/612 compile-OK,
+- Suite **275/275**; self-hosting **fixpoint OK** (byte-identical
+  stage2/stage3, 4,141,580 bytes); corpus **512/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`**.

+ 11 - 0
compiler/run_tests.sh

@@ -257,6 +257,7 @@ expect_run t_strtrunc.mod 42
 expect_trap t_trap_setspan.mod
 expect_run t_strempty.mod 42
 expect_trap t_trap_chr8.mod
+expect_trap t_except_unhandled.mod
 expect_fail t_defunit.mod ""
 expect_fail t_implunit.mod ""
 expect_run t_nested.mod 42
@@ -328,6 +329,16 @@ expect_run_files TExceptLib 42 \
   ../stdlib/m2exception.def ../stdlib/m2exception.mod \
   ../stdlib/exceptions.def ../stdlib/exceptions.mod \
   t_except_lib.mod
+expect_run_files TExceptTrap 42 \
+  ../runtime/syslib/SysExceptions.def \
+  ../runtime/syslib/SysExceptions.mod \
+  ../stdlib/m2exception.def ../stdlib/m2exception.mod \
+  t_except_trap.mod
+expect_run_files TExceptCase 42 \
+  ../runtime/syslib/SysExceptions.def \
+  ../runtime/syslib/SysExceptions.mod \
+  ../stdlib/m2exception.def ../stdlib/m2exception.mod \
+  t_except_case.mod
 expect_run_files TAst 42 \
   ../runtime/syslib/SysShim.def ../runtime/syslib/SysShim.mod \
   ../runtime/syslib/FileIO.def ../runtime/syslib/FileIO.mod \

+ 7 - 1
compiler/src/Lower.mod

@@ -1151,7 +1151,7 @@ PROCEDURE LowerDesign (n: AST.Node; VAR t: SymTab.TypeIndex; VAR k: INTEGER;
             END;
             QbeGen.IntStr(lo, qlo); QbeGen.IntStr(hi, qhi)
           END;
-          QbeGen.CheckRange(ql, qlo, qhi);
+          QbeGen.CheckIndex(ql, qlo, qhi);
           eT := SymTab.ArrayElem(t);
           QbeGen.ElemAddr(q, ql, qlo, t, qe);
           IF SymTab.ClassOf(eT) = SymTab.ClArray THEN
@@ -1846,8 +1846,10 @@ PROCEDURE LowerCase (n: AST.Node);
   VAR i, j: CARDINAL; arm, lab, seq, next: AST.Node;
       tsel, t2, t3: SymTab.TypeIndex;
       qsel, q2, q3, qc, qd, qe, lEnd, lBody, lAfter, lNext: QbeGen.QVal;
+      hasElse: BOOLEAN;
   BEGIN
     LowerExpr(AST.Child(n, 0), tsel, qsel);
+    hasElse := FALSE;
     QbeGen.NewLabel(lEnd);
     (* pass 1: alternatives (ELSE arms come after) *)
     seq := AST.Child(n, 1);
@@ -1859,6 +1861,7 @@ PROCEDURE LowerCase (n: AST.Node);
           (* continuation chunk *)
         ELSIF AST.Op(arm) = 1 THEN
           (* ELSE arm: emitted in pass 2 *)
+          hasElse := TRUE
         ELSE
           QbeGen.NewLabel(lBody); QbeGen.NewLabel(lAfter);
           j := 0;
@@ -1920,6 +1923,9 @@ PROCEDURE LowerCase (n: AST.Node);
       END;
       seq := next
     END;
+    IF NOT hasElse THEN
+      QbeGen.TrapCode(2)   (* caseSelectException *)
+    END;
     (* pass 2: ELSE arms *)
     seq := AST.Child(n, 1);
     WHILE seq # AST.NoNode DO

+ 8 - 3
compiler/src/QbeGen.def

@@ -351,7 +351,10 @@ PROCEDURE LoadCount (base: ARRAY OF CHAR; VAR q: QVal);
 (* q := loadl(base) in l (open-array element count). *)
 
 PROCEDURE CheckRange (idx, lo, hi: ARRAY OF CHAR);
-(* Trap unless lo <= idx <= hi (l-domain operands). *)
+(* Trap with rangeException unless lo <= idx <= hi (l-domain operands). *)
+
+PROCEDURE CheckIndex (idx, lo, hi: ARRAY OF CHAR);
+(* Trap with indexException unless lo <= idx <= hi. *)
 
 PROCEDURE ElemAddr (base, idx, lo: ARRAY OF CHAR; elemT: INTEGER;
                     VAR q: QVal);
@@ -373,9 +376,11 @@ PROCEDURE CopyArray (dst: ARRAY OF CHAR; src: ARRAY OF CHAR;
 PROCEDURE HaltQ;
 (* HALT: terminate the process via exit(1) (noreturn). *)
 
+PROCEDURE TrapCode (code: CARDINAL);
+(* Raise language exception `code`; unhandled aborts with rc 134. *)
+
 PROCEDURE Trap;
-(* call $abort(); hlt. Interim trap until runtime/syslib/Trap;
-   sets the dead flag (terminator). *)
+(* Internal mismatch trap: TrapCode(sysException). *)
 
 (* ---------------- sets (step 3.3: multi-word masks, no header) ---------------- *)
 (* Set values are memory addresses (globals or alloc8 temps holding

+ 21 - 4
compiler/src/QbeGen.mod

@@ -2126,12 +2126,19 @@ PROCEDURE HaltQ;
     dead := TRUE
   END HaltQ;
 
-PROCEDURE Trap;
+PROCEDURE TrapCode (code: CARDINAL);
+  VAR q: QVal;
   BEGIN
     Revive;
-    WL("  call $abort()");
+    IntStr(VAL(INTEGER, code), q);
+    W("  call $m2_exc_raise_lang(w "); W(q); WL(")");
     WL("  hlt");
     dead := TRUE
+  END TrapCode;
+
+PROCEDURE Trap;
+  BEGIN
+    TrapCode(12)   (* sysException *)
   END Trap;
 
 (* ---------------- records: flat blobs, pointer fields ---------------- *)
@@ -3193,7 +3200,7 @@ PROCEDURE LoadCount (base: ARRAY OF CHAR; VAR q: QVal);
     W("  "); W(q); W(" =l loadl "); WL(base)
   END LoadCount;
 
-PROCEDURE CheckRange (idx, lo, hi: ARRAY OF CHAR);
+PROCEDURE CheckCode (idx, lo, hi: ARRAY OF CHAR; code: CARDINAL);
 (* l-domain operands (widened index, immediates, or open hi temp);
    comparisons are long (result w). *)
   VAR c1, c2, c: QVal;
@@ -3208,10 +3215,20 @@ PROCEDURE CheckRange (idx, lo, hi: ARRAY OF CHAR);
     NewLabel(lok); NewLabel(lbad);
     Jnz(c, lok, lbad);
     EmitLabel(lbad);
-    Trap;
+    TrapCode(code);
     EmitLabel(lok)
+  END CheckCode;
+
+PROCEDURE CheckRange (idx, lo, hi: ARRAY OF CHAR);
+  BEGIN
+    CheckCode(idx, lo, hi, 1)   (* rangeException *)
   END CheckRange;
 
+PROCEDURE CheckIndex (idx, lo, hi: ARRAY OF CHAR);
+  BEGIN
+    CheckCode(idx, lo, hi, 0)   (* indexException *)
+  END CheckIndex;
+
 PROCEDURE ElemAddr (base, idx, lo: ARRAY OF CHAR; elemT: INTEGER;
                     VAR q: QVal);
 (* q := base + 8 + (idx - lo) * elemSize in l, fresh temp.

+ 31 - 0
compiler/tests/t_except_case.mod

@@ -0,0 +1,31 @@
+MODULE TExceptCase;
+(* M4: a CASE selector with no matching label and no ELSE raises
+   caseSelectException. *)
+FROM M2EXCEPTION IMPORT
+  M2Exception, IsM2Exception, caseSelectException;
+
+TYPE
+  Colour = (red, green, blue);
+VAR ExitCode : INTEGER;
+VAR c : Colour;
+
+PROCEDURE Select;
+BEGIN
+  c := blue;
+  CASE c OF
+    red:   ExitCode := 1
+  | green: ExitCode := 2
+  END;
+  ExitCode := 3
+EXCEPT
+  IF IsM2Exception() AND (M2Exception() = caseSelectException) THEN
+    ExitCode := 42
+  ELSE
+    ExitCode := 4
+  END
+END Select;
+
+BEGIN
+  ExitCode := 0;
+  Select
+END TExceptCase.

+ 44 - 0
compiler/tests/t_except_trap.mod

@@ -0,0 +1,44 @@
+MODULE TExceptTrap;
+(* M4: compiler-emitted index and subrange traps are catchable language
+   exceptions with the right M2EXCEPTION values. *)
+FROM M2EXCEPTION IMPORT
+  M2Exception, IsM2Exception, indexException, rangeException;
+
+TYPE
+  Small = [0..7];
+VAR ExitCode : INTEGER;
+VAR a : ARRAY [0..2] OF INTEGER;
+VAR s : Small;
+VAR i : INTEGER;
+
+PROCEDURE IndexTrap;
+BEGIN
+  i := 3;
+  a[i] := 1;
+  ExitCode := 1
+EXCEPT
+  IF IsM2Exception() AND (M2Exception() = indexException) THEN
+    ExitCode := 2
+  ELSE
+    ExitCode := 3
+  END
+END IndexTrap;
+
+PROCEDURE RangeTrap;
+BEGIN
+  i := 9;
+  s := i;
+  ExitCode := 4
+EXCEPT
+  IF IsM2Exception() AND (M2Exception() = rangeException) THEN
+    ExitCode := 42
+  ELSE
+    ExitCode := 5
+  END
+END RangeTrap;
+
+BEGIN
+  ExitCode := 0;
+  IndexTrap;
+  IF ExitCode = 2 THEN RangeTrap END
+END TExceptTrap.

+ 7 - 0
compiler/tests/t_except_unhandled.mod

@@ -0,0 +1,7 @@
+MODULE TExceptUnhandled;
+(* M4: an unhandled language exception is a termination event (abort,
+   rc 134), preserving the old trap contract. *)
+FROM SYSTEM IMPORT THROW;
+BEGIN
+  THROW(1)
+END TExceptUnhandled.

+ 3 - 3
docs/INDEX.md

@@ -39,9 +39,9 @@ bisecting *why* a decision was made, not for reading end-to-end).
   layout (deferred until the compiler is finished).
 - **`design-exceptions.md`** — ISO exceptions design: grammar/AST,
   handler stack, `sigsetjmp`/`siglongjmp` codegen, `EXCEPTIONS` /
-  `M2EXCEPTION`, trap mapping, tests.  M1 parser/AST, M2
-  runtime/codegen and M3 libraries are implemented; compiler traps
-  remain (M4).
+  `M2EXCEPTION`, trap mapping, tests.  M1–M4 are implemented
+  (parser/AST, runtime/codegen, libraries, index/range/CASE traps);
+  follow-ups are `functionException`, `FINALLY`, coroutine/protection.
 - `blaise-phases-map.md`, `language-report.md`, `grammar.md` — the
   grammar/phases reference.
 - `summary_l3_selfhost_fix.md` — the self-host root cause; also where

+ 6 - 5
docs/STATUS.md

@@ -4,9 +4,9 @@ This answers "where are we now?".
 
 ## Numbers
 
-- Suite **272/272** (`compiler/run_tests.sh`).
+- Suite **275/275** (`compiler/run_tests.sh`).
 - Self-hosting **FIXPOINT OK** — stage2 and stage3 images are
-  byte-identical (4,108,488 bytes).
+  byte-identical (4,141,580 bytes).
 - Corpus **512 / 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
@@ -173,8 +173,9 @@ declarations still work (`t_fwdproc`, `t_fwdok`).
    `INC`/`DEC`, leading CASE `|`, `__FUNCTION__`).  Corpus is **511/612**
    at the stop point.  The remaining `LANG` entries are mostly:
    **declaration order** cases V3 rejects by policy (`docs/gaps/`); ISO
-   exceptions (M3 libraries/runtime; M4 compiler traps pending — see
-   `docs/design-exceptions.md`); multi-dimensional
+   exceptions (M4 done; `functionException`/`FINALLY`/coroutine/
+   protection follow-ups — see `docs/design-exceptions.md`);
+   multi-dimensional
    open arrays (representation, see `docs/handoff-2026-10-23-next.md`);
    and gm2-only extensions.  See `docs/analysis_gaps.md`.
 2. ~~**The size-sensitive V3 miscompile**~~ — **root-caused and fixed**
@@ -217,7 +218,7 @@ declarations still work (`t_fwdproc`, `t_fwdok`).
 ```sh
 cd compiler
 ./build.sh          # M2.atg -> scanner/parser/driver -> ./M2
-./run_tests.sh      # 272/272
+./run_tests.sh      # 275/275
 cd ..
 ./bootstrap/fixpoint.sh   # FIXPOINT OK (byte-identical stage2/stage3)
 ```

+ 6 - 6
docs/comparison-PIM4-gm2-V3.md

@@ -1,7 +1,7 @@
 # PIM4 vs GNU Modula-2 vs M2R10 vs TopSpeed V3 vs V3 — feature comparison
 
 **Date:** 2026-10-23 · **V3 state:** `v3-packedset` (suite
-272/272, self-hosting fixpoint OK 4,108,488 bytes, corpus 512/612,
+275/275, self-hosting fixpoint OK 4,141,580 bytes, corpus 512/612,
 0 crashes).
 
 **Scope and sources.**
@@ -103,7 +103,7 @@ change individual cells.
 | 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) | ❌ | 🔄 `EXCEPT`/`RETRY`/`SYSTEM.THROW`, `EXCEPTIONS`/`M2EXCEPTION`; range/index/CASE traps pending |
+| ISO exceptions (`EXCEPT`, `RETRY`, `EXCEPTIONS`) | ❌ | ✅ | ❌ (not in the base R10 grammar) | ❌ | 🔄 `EXCEPT`/`RETRY`/`SYSTEM.THROW`, `EXCEPTIONS`/`M2EXCEPTION`, index/range/CASE traps; `functionException`/`FINALLY` pending |
 | Module finalization (`FINALLY`) | ❌ | ✅ | ❌ | ❌ | ❌ |
 | `__FILE__` / `__LINE__` / `__FUNCTION__` | ❌ | ✅ | 🔄 COMPILER pseudo-module introspection | ❌ | ✅ / ✅ / ✅ |
 | `__BUILTIN__` / `__ATTRIBUTE__` pragmas | ❌ | ✅ | ✅ language pragmas `<* … *>` | ⚠️ compiler directives | ✅ (parsed, ignored) |
@@ -120,7 +120,7 @@ change individual cells.
 | Object orientation | ❌ | ❌ | ❌ (extensible records only) | ✅ Clarion-form `CLASS`, `VIRTUAL`, inheritance | 🔄 Clarion-form `CLASS` (single inheritance, virtual dispatch), not ISO 10514-3 |
 | PIM coroutines (`PROCESS`, `NEWPROCESS`, `TRANSFER`, `IOTRANSFER`) | ✅ | ✅ | 🔄 `COROUTINE` type/`YIELD` (R10 phase II) | ✅ PIM-style | ✅ (`IOTRANSFER` degrades to a transfer) |
 | ISO `COROUTINES` module | ❌ | ✅ | ❌ | ❌ | ❌ (PIM interface only) |
-| Standard library | ❌ (none defined) | ✅ PIM + ISO libraries | ✅ redesigned R10 library | ✅ TopSpeed libraries (historical) | 🔄 subset: ISO/PIM I/O, strings, storage, SysClock; no ISO exceptions/termination libs |
+| Standard library | ❌ (none defined) | ✅ PIM + ISO libraries | ✅ redesigned R10 library | ✅ TopSpeed libraries (historical) | 🔄 subset: ISO/PIM I/O, strings, storage, SysClock, EXCEPTIONS; no termination/coroutine libs |
 | Self-hosting | n/a | ✅ (part of GCC) | n/a (reference compiler under development) | n/a (historical commercial compiler) | ✅ (byte-identical 3-stage fixpoint) |
 | Backend / target | n/a | GCC (many targets) | reference compiler | x86/DOS/Windows (historical) | QBE → C (amd64 SysV today) |
 
@@ -152,9 +152,9 @@ change individual cells.
   its chosen gm2 compatibility (`__FILE__`/`__LINE__`/`__FUNCTION__`,
   scalar→`ARRAY OF BYTE`, same-size `VAR`, bare proc values, variant
   extensions, set `CASE` labels, libc prototypes).  ISO exceptions are
-  **partial** (`EXCEPT`/`RETRY`/`SYSTEM.THROW`, `EXCEPTIONS`/
-  `M2EXCEPTION`; compiler range/index/CASE traps pending).  Its hard
-  gaps are **complex arithmetic,
+  **mostly complete** (`EXCEPT`/`RETRY`/`SYSTEM.THROW`, `EXCEPTIONS`/
+  `M2EXCEPTION`, index/range/CASE traps) with `functionException` and
+  `FINALLY` pending.  Its hard gaps are **complex arithmetic,
   finalization/protection, multi-dimensional open arrays, ISO OO** and
   parts of the **word model** (`SHORTREAL`, `REAL` width), plus a
   **65536-bit `SET` span limit** and a **32-bit `PACKEDSET` limit**.

+ 21 - 18
docs/design-exceptions.md

@@ -1,11 +1,12 @@
 # Design — ISO exceptions for V3
 
-**Status: design + M1 parser/AST + M2 runtime/codegen + M3 libraries
-(2026-10-23).**  `EXCEPT`/`RETRY`, the handler stack, `SYSTEM.THROW`,
-`EXCEPTIONS`, `M2EXCEPTION`, `RETURN` unwinding and basic signal
-mapping are implemented.  Compiler-emitted range/index/CASE traps (M4)
-remain.  Baseline: `v3-packedset`; suite **272/272**, fixpoint OK
-**4,108,488 bytes**, corpus **512/612** compile-OK, 0 crashes.
+**Status: design + M1–M4 implemented (2026-10-23).**  `EXCEPT`/`RETRY`,
+the handler stack, `SYSTEM.THROW`, `EXCEPTIONS`, `M2EXCEPTION`, `RETURN`
+unwinding, signal mapping, and compiler-emitted index/range/CASE traps
+are implemented.  `functionException`, `FINALLY`, coroutine/protection
+exceptions and complex arithmetic remain follow-ups.  Baseline:
+`v3-packedset`; suite **275/275**, fixpoint OK **4,141,580 bytes**,
+corpus **512/612** compile-OK, 0 crashes.
 
 The handoff calls this the largest remaining language feature.  This
 document fixes the syntax, AST, runtime model, code-generation contract,
@@ -572,14 +573,15 @@ Implemented in M1–M3 (`compiler/run_tests.sh`):
 - `t_except_lib`: `EXCEPTIONS`/`M2EXCEPTION` `RAISE`, `CurrentNumber`,
   `GetMessage`, `IsCurrentSource`, `IsExceptionalExecution`,
   wrong-source `exException`, and `M2Exception` via `SYSTEM.THROW`.
+- `t_except_trap`: array index and subrange traps with
+  `indexException`/`rangeException`.
+- `t_except_case`: no matching `CASE` label gives
+  `caseSelectException`.
+- `t_except_unhandled`: expect_trap (rc=134).
 - `t_retry_bad`: `RETRY` outside `EXCEPT` rejected with `237`.
 
-Still planned:
-
-- `t_except_basic`: a dedicated nested-handler state-restoration test
-  (the current `t_except_lib` covers the public queries).
-- `t_except_trap`: integer divide-by-zero and `CASE`/range traps.
-- `t_except_unhandled`: expect_trap (rc=134).
+`functionException` and real/complex/protection/coroutine detection are
+still follow-ups.
 
 Use `expect_run_files` with:
 
@@ -611,8 +613,8 @@ cd .. && TMP="$PWD/.scratch/fixpoint" ./bootstrap/fixpoint.sh
 tools/v3-corpus/corpus.sh
 ```
 
-Current M3 gates: suite **272/272**, fixpoint OK
-**4,108,488 bytes**, corpus **512/612 compile-OK**, 0 crashes.
+Current M4 gates: suite **275/275**, fixpoint OK
+**4,141,580 bytes**, corpus **512/612 compile-OK**, 0 crashes.
 Existing trap tests must stay rc=134.
 
 ---
@@ -622,7 +624,7 @@ Existing trap tests must stay rc=134.
 1. **M1 — parser/AST** — **done (2026-10-23)**
    - `EXCEPT`, `RETRY`; `NkExcept`/`NkRetry`; `RETRY` placement check;
      `EXCEPT` accepted in procedure, implementation, program, local
-     module and class bodies.  `EXCEPT` reports `230` until M2.
+     module and class bodies.
 2. **M2 — runtime + codegen** — **done (2026-10-23)**
    - `m2_exc_*` handler stack, `ExcBegin`/`ExcBodyDone`/`ExcHandlerBegin`/
      `ExcEnd`/`ExcRetry`, `EmitRet` pop, `m2_exc_init` in `main`.
@@ -634,9 +636,10 @@ Existing trap tests must stay rc=134.
    - `t_except_lib`: `RAISE`, `CurrentNumber`, `GetMessage`,
      `IsCurrentSource`, `IsExceptionalExecution`, wrong-source
      `exException`, and `M2Exception` via `SYSTEM.THROW`.
-4. **M4 — language traps**
-   - `TrapCode`; split index/range; CASE trap; range-check traps; keep
-     rc=134 for unhandled; finish `t_except_trap`.
+4. **M4 — language traps** — **done (2026-10-23)**
+   - `TrapCode`; `CheckIndex` vs `CheckRange`; CASE trap; unhandled
+     exceptions keep rc=134; `t_except_trap`, `t_except_case`,
+     `t_except_unhandled`.
 5. **M5 — docs and corpus**
    - Update `features.md`, `STATUS.md`, comparison table, gaps, handoff,
      summary; commit with `v3-exceptions`; record corpus reality.

+ 5 - 4
docs/features.md

@@ -154,10 +154,11 @@ Legend: ✅ done · 🔄 partial · ⏸ not started / deferred.
   composite function results (record/array/set/class, sret) and chained
   post-call method calls (`F().M()`); ⏸ per-file object emission, import
   cycles.
-- 🔄 ISO exceptions: **M3** — `EXCEPT`/`RETRY`, handler stack,
-  `SYSTEM.THROW`, `EXCEPTIONS`, `M2EXCEPTION`, NIL/divide-by-zero signal
-  mapping, `RETURN`/nested handlers.  Still pending: compiler-emitted
-  range/index/CASE traps (M4).  Design in `docs/design-exceptions.md`.
+- 🔄 ISO exceptions: **M4** — `EXCEPT`/`RETRY`, handler stack,
+  `SYSTEM.THROW`, `EXCEPTIONS`, `M2EXCEPTION`, signal mapping,
+  `RETURN`/nested handlers, and catchable index/range/CASE traps.
+  Remaining follow-ups: `functionException`, `FINALLY`, coroutine/
+  protection exceptions.  Design in `docs/design-exceptions.md`.
 
 ## Backend (QBE)
 - ✅ Length-prefixed arrays (count + elements), deep record copy,

+ 4 - 4
docs/gaps/README.md

@@ -94,11 +94,11 @@ gap22_file_line.mod             invalid Fact            (__LINE__)
 
 ## Genuine gaps (worth fixing, standard Modula-2)
 
-- **ISO exceptions — M3 done, M4 traps pending** (2026-10-23):
+- **ISO exceptions — M4 done, follow-ups remain** (2026-10-23):
   `EXCEPT`/`RETRY`, handler stack + saved state, `SYSTEM.THROW`,
-  `EXCEPTIONS`, `M2EXCEPTION`, `RETURN` unwinding, nested handlers, and
-  basic `SIGSEGV`/`SIGFPE` mapping work.  Compiler-emitted
-  range/index/CASE traps remain (M4).
+  `EXCEPTIONS`, `M2EXCEPTION`, `RETURN` unwinding, nested handlers,
+  signal mapping, and catchable index/range/CASE traps work.  Remaining:
+  `functionException`, `FINALLY`, coroutine/protection exceptions.
   `lazyunique` now compiles (corpus 512) but its QBE hits a
   pre-existing pointer-chain (`p^.next^.value`) codegen gap, so it is
   not yet a runtime gain.  `except2/4/5/7/8/9` still fail declaration

+ 15 - 16
docs/handoff-2026-10-23-next.md

@@ -1,7 +1,7 @@
 # Next session — plan (from 2026-10-23)
 
 **Branch:** `master`.  **Feature state:** `v3-packedset`.
-**Gates:** suite **272/272**, fixpoint **OK (4,108,488 bytes)**,
+**Gates:** suite **275/275**, fixpoint **OK (4,141,580 bytes)**,
 corpus **512/612, 0 crashes**.
 
 Read first: `docs/STATUS.md`, `docs/features.md`,
@@ -26,7 +26,7 @@ inside `assert` before its declaration (declaration order, rejected by
 policy).  With `r` moved before the procedure they compile and run.
 Do not relax declaration order for this corpus case.
 
-## 2. ISO exceptions — M3 done, M4 next (`docs/design-exceptions.md`)
+## 2. ISO exceptions — M4 done, M5 finalization next (`docs/design-exceptions.md`)
 
 A dedicated feature session, not a small fix.  The design is in
 `docs/design-exceptions.md` (grammar/AST, handler stack, `sigsetjmp`/
@@ -51,14 +51,14 @@ state, `m2_exc_push`/`leave`/`retry`/`raise`, `sigsetjmp` dispatch,
 `IsCurrentSource`, `IsExceptionalExecution`, wrong-source `exException`,
 and `M2Exception` after `SYSTEM.THROW`.
 
-Next (**M4**): compiler-emitted language traps
-(`indexException`/`rangeException`/`caseSelectException`), unhandled
-exception policy, and trap tests.  M5 remains docs/corpus/final tag.
+**M4 language traps — done (2026-10-23).**  `TrapCode`, `CheckIndex`
+vs `CheckRange`, CASE `caseSelectException`, and unhandled-exception
+abort (rc 134).  Tests: `t_except_trap`, `t_except_case`,
+`t_except_unhandled`.
 
-Still to implement:
-
-- M4 compiler-emitted language traps and trap tests;
-- M5 docs/corpus/final tag.
+Next (**M5**): docs/corpus summary and the final `v3-exceptions` tag.
+Optional follow-ups: `functionException`, `FINALLY`, coroutine/
+protection exceptions, and the `lazyunique` pointer-chain codegen gap.
 
 **Corpus reality:** `lazyunique` now **compiles** (corpus **512**) but
 is not a verified runtime +1: its QBE hits a pre-existing pointer-chain
@@ -106,7 +106,7 @@ this feature.
 ## Gates for every step
 
 ```sh
-cd compiler && ./build.sh && ./run_tests.sh          # 272/272
+cd compiler && ./build.sh && ./run_tests.sh          # 275/275
 cd .. && TMP="$PWD/.scratch/fixpoint" ./bootstrap/fixpoint.sh
 tools/v3-corpus/corpus.sh                            # 512 OK, 0 crash
 ```
@@ -119,13 +119,12 @@ session summary; commit with a `v3-*` tag.  Do not push without asking.
 ```sh
 cd .../m2compiler-V3
 git checkout master
-cd compiler && ./build.sh && ./run_tests.sh          # 272/272
+cd compiler && ./build.sh && ./run_tests.sh          # 275/275
 cd .. && TMP="$PWD/.scratch/fixpoint" ./bootstrap/fixpoint.sh
 tools/v3-corpus/corpus.sh                            # 512 OK, 0 crash
 ```
 
-**Recommendation:** continue with **M4 of ISO exceptions**
-(compiler-emitted range/index/CASE traps and unhandled policy), or take
-the multi-dimensional-open-array ABI decision if you prefer an ABI task.
-Library folders and separate compilation remain deferred
-(`docs/plan-separate-compilation.md`).
+**Recommendation:** finish **M5** (session summary, final docs, tag),
+or fix the pre-existing pointer-chain codegen gap that keeps
+`lazyunique` compile-only.  Library folders and separate compilation
+remain deferred (`docs/plan-separate-compilation.md`).