|
|
@@ -0,0 +1,675 @@
|
|
|
+# Design — ISO exceptions for V3
|
|
|
+
|
|
|
+**Status: design only (2026-10-23).** Nothing in this document is
|
|
|
+implemented yet. Baseline: `v3-packedset`; suite **263/263**, fixpoint
|
|
|
+OK **4,075,853 bytes**, corpus **511/612**, 0 crashes.
|
|
|
+
|
|
|
+The handoff calls this the largest remaining language feature. This
|
|
|
+document fixes the syntax, AST, runtime model, code-generation contract,
|
|
|
+library modules, trap mapping and test strategy before code is written.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. Scope
|
|
|
+
|
|
|
+ISO 10514-1 exceptions include:
|
|
|
+
|
|
|
+- `EXCEPT` after a procedure or module body, with `RETRY` in the handler;
|
|
|
+- the system modules `EXCEPTIONS` and `M2EXCEPTION`;
|
|
|
+- the language exceptions (`indexException`, `rangeException`, …,
|
|
|
+ `exException`);
|
|
|
+- detection of implementation traps (array bounds, subranges, `CASE`,
|
|
|
+ `NIL` dereference, division by zero, …) as language exceptions.
|
|
|
+
|
|
|
+Out of scope for the first implementation:
|
|
|
+
|
|
|
+- ISO `FINALLY` / module termination (V3 has no finalization yet);
|
|
|
+- exception handling in coroutines (V3 coroutines are PIM-style);
|
|
|
+- protection exceptions;
|
|
|
+- complex-number exceptions (complex arithmetic is a placeholder);
|
|
|
+- debugger integration for unhandled exceptions.
|
|
|
+
|
|
|
+The design does include the gm2 `SYSTEM.THROW` extension because the
|
|
|
+corpus uses it.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. ISO surface
|
|
|
+
|
|
|
+### 2.1 Syntax
|
|
|
+
|
|
|
+Add two reserved words: `EXCEPT` and `RETRY`.
|
|
|
+
|
|
|
+The block grammar becomes (subject to a final check against the ISO
|
|
|
+grammar):
|
|
|
+
|
|
|
+```text
|
|
|
+Block = DeclSeq
|
|
|
+ [ "BEGIN" [ StatSeq ] ]
|
|
|
+ [ "EXCEPT" [ StatSeq ] ]
|
|
|
+ "END" ident .
|
|
|
+```
|
|
|
+
|
|
|
+`EXCEPT` is part of the block, not a statement. A handler is a
|
|
|
+`StatSeq` and may be empty. `RETRY` is a statement only valid inside a
|
|
|
+handler.
|
|
|
+
|
|
|
+The same shape applies to program, implementation and local-module
|
|
|
+bodies. Definitions have no body.
|
|
|
+
|
|
|
+### 2.2 Dynamic semantics
|
|
|
+
|
|
|
+- If the body completes normally, the handler is skipped.
|
|
|
+- If an exception is raised in the body, control passes to the nearest
|
|
|
+ active handler on the call stack.
|
|
|
+- An exception raised **inside a handler** is not caught by that same
|
|
|
+ handler; it propagates to an outer handler.
|
|
|
+- `RETRY` resumes the body from its beginning. It does not re-push the
|
|
|
+ handler, does not re-run parameter initialisation, and does not reset
|
|
|
+ locals.
|
|
|
+- Falling out of a handler normally means the exception is handled and
|
|
|
+ the block completes normally.
|
|
|
+- `RETURN` inside the body or handler returns from the procedure.
|
|
|
+- If no handler exists, raising an exception is a termination event.
|
|
|
+- **Exception state is block-scoped.** A procedure (or module body)
|
|
|
+ with an `EXCEPT` part executes its body in the normal state and
|
|
|
+ restores the caller's state when it leaves, even when it was called
|
|
|
+ from a handler. A block without an `EXCEPT` part simply inherits the
|
|
|
+ caller's state. This is why the runtime needs a saved-state stack,
|
|
|
+ not just one global flag.
|
|
|
+
|
|
|
+### 2.3 `EXCEPTIONS`
|
|
|
+
|
|
|
+```text
|
|
|
+TYPE ExceptionSource; (* opaque *)
|
|
|
+ ExceptionNumber = CARDINAL;
|
|
|
+
|
|
|
+PROCEDURE AllocateSource(VAR newSource: ExceptionSource);
|
|
|
+PROCEDURE RAISE(source: ExceptionSource; number: ExceptionNumber;
|
|
|
+ message: ARRAY OF CHAR);
|
|
|
+PROCEDURE CurrentNumber(source: ExceptionSource): ExceptionNumber;
|
|
|
+PROCEDURE GetMessage(VAR text: ARRAY OF CHAR);
|
|
|
+PROCEDURE IsCurrentSource(source: ExceptionSource): BOOLEAN;
|
|
|
+PROCEDURE IsExceptionalExecution(): BOOLEAN;
|
|
|
+```
|
|
|
+
|
|
|
+Semantics:
|
|
|
+
|
|
|
+- `CurrentNumber` returns the number only when the current state is
|
|
|
+ exceptional **and** the source matches; otherwise it raises
|
|
|
+ `exException`.
|
|
|
+- `GetMessage` returns the current message, or the empty string in
|
|
|
+ normal state.
|
|
|
+- `IsCurrentSource` is true iff exceptional and source matches.
|
|
|
+- `AllocateSource` allocates a unique opaque source token; allocation
|
|
|
+ failure would raise `exException` (the token allocator is practically
|
|
|
+ inexhaustible).
|
|
|
+- `RAISE` associates source, number and message with the current
|
|
|
+ context, then raises. It does not return.
|
|
|
+
|
|
|
+### 2.4 `M2EXCEPTION`
|
|
|
+
|
|
|
+```text
|
|
|
+TYPE M2Exceptions =
|
|
|
+ (indexException, rangeException, caseSelectException, invalidLocation,
|
|
|
+ functionException, wholeValueException, wholeDivException,
|
|
|
+ realValueException, realDivException, complexValueException,
|
|
|
+ complexDivException, protException, sysException, coException,
|
|
|
+ exException);
|
|
|
+
|
|
|
+PROCEDURE M2Exception(): M2Exceptions;
|
|
|
+PROCEDURE IsM2Exception(): BOOLEAN;
|
|
|
+```
|
|
|
+
|
|
|
+A **language** exception has a distinguished source that users cannot
|
|
|
+allocate. `IsM2Exception` is true only for that source.
|
|
|
+`M2Exception` returns the corresponding enumeration value and otherwise
|
|
|
+raises `exException`.
|
|
|
+
|
|
|
+### 2.5 `SYSTEM.THROW`
|
|
|
+
|
|
|
+gm2 extension used by `except5` and `except7`:
|
|
|
+
|
|
|
+```text
|
|
|
+PROCEDURE THROW(i: INTEGER); (* noreturn *)
|
|
|
+```
|
|
|
+
|
|
|
+Map `i` to `M2Exceptions(i)`; out-of-range values map to `exException`.
|
|
|
+The exception is a language exception, so `M2EXCEPTION` can identify it.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. Current V3 baseline
|
|
|
+
|
|
|
+- `QbeGen.Trap` emits `call $abort()`; it does not raise a catchable
|
|
|
+ exception.
|
|
|
+- `EXCEPT` / `RETRY` are still identifiers/keywords absent from the
|
|
|
+ grammar.
|
|
|
+- `FileIO.mod` has the ISO `EXCEPT` clauses commented out.
|
|
|
+- `SYSTEM` is hard-coded in `SymTab`; it has no `THROW`.
|
|
|
+- There is no `EXCEPTIONS` or `M2EXCEPTION` module.
|
|
|
+- Traps currently used:
|
|
|
+ - `QbeGen.CheckRange`: array index and subrange checks;
|
|
|
+ - `QbeGen.CopySet` / `CopyRecord`: malformed descriptor/copy checks;
|
|
|
+ - explicit `$abort` elsewhere.
|
|
|
+- Existing trap tests expect **rc=134** (SIGABRT). Unhandled
|
|
|
+ exceptions must keep that.
|
|
|
+
|
|
|
+### Corpus reality
|
|
|
+
|
|
|
+The handoff lists seven exception files, but six of them still hit V3's
|
|
|
+strict declaration-order rule before reaching `EXCEPT`:
|
|
|
+
|
|
|
+- `except2`, `except4`, `except5`, `except7`, `except8`, `except9` use
|
|
|
+ the module variable `ip` inside procedures declared before
|
|
|
+ `VAR ip: POINTER TO INTEGER` at the bottom.
|
|
|
+- `lazyunique` declares `head` before its procedures and is a genuine
|
|
|
+ exception test.
|
|
|
+
|
|
|
+So the realistic corpus gain is **+1 (`lazyunique`)** unless the
|
|
|
+declaration order policy changes. We should keep test copies with `ip`
|
|
|
+moved above the procedures to prove that the exception implementation
|
|
|
+itself handles `except2/4/5/7/8/9`. This mirrors the PACKEDSET
|
|
|
+`onebyte*` finding; do not relax declaration order.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. Runtime model
|
|
|
+
|
|
|
+### 4.1 Handler stack and saved state
|
|
|
+
|
|
|
+Use a fixed-size stack in the C shim. No C++ exceptions, no unwind
|
|
|
+library, no `malloc` on the raise path.
|
|
|
+
|
|
|
+```c
|
|
|
+typedef struct {
|
|
|
+ int active;
|
|
|
+ int language; /* language exception vs EXCEPTIONS.RAISE */
|
|
|
+ void *source; /* opaque source token, or language token */
|
|
|
+ unsigned number;
|
|
|
+ char message[1024];
|
|
|
+} ExcState;
|
|
|
+
|
|
|
+typedef struct {
|
|
|
+ void *env; /* pointer to the sigjmp_buf in the frame */
|
|
|
+ int in_handler;
|
|
|
+ ExcState saved; /* caller state to restore on leave */
|
|
|
+} ExcHandler;
|
|
|
+
|
|
|
+static ExcState current;
|
|
|
+static ExcHandler handlers[1024];
|
|
|
+static int top;
|
|
|
+```
|
|
|
+
|
|
|
+Operations:
|
|
|
+
|
|
|
+- `push(env)`:
|
|
|
+ - copy `current` into `handlers[top].saved`;
|
|
|
+ - set `current` to the normal state;
|
|
|
+ - store `env`, `in_handler = 0`;
|
|
|
+ - `top++`.
|
|
|
+- `leave()`:
|
|
|
+ - `top--`;
|
|
|
+ - `current = handlers[top].saved`.
|
|
|
+- `retry()`:
|
|
|
+ - `current` becomes normal (not the caller state);
|
|
|
+ - `handlers[top-1].in_handler = 0`;
|
|
|
+ - `siglongjmp(env, 2)`.
|
|
|
+- `raise(new_state)`:
|
|
|
+ - search from `top-1` downwards for the first entry with
|
|
|
+ `in_handler == 0`;
|
|
|
+ - discard entries above the target (`top = target + 1`);
|
|
|
+ - mark the target `in_handler = 1`;
|
|
|
+ - `current = new_state`;
|
|
|
+ - `siglongjmp(target.env, 1)`.
|
|
|
+- If no target: write the message to stderr and `abort()` (termination
|
|
|
+ event; preserves rc=134).
|
|
|
+
|
|
|
+The `saved` copy is what makes the block-scoped rule in §2.2 work:
|
|
|
+a procedure with `EXCEPT` sees normal state in its body even when its
|
|
|
+caller is already handling an exception, and restores the caller's
|
|
|
+state when it leaves.
|
|
|
+
|
|
|
+### 4.2 C ABI
|
|
|
+
|
|
|
+Add to `runtime/syslib/shim.c` (or a separate `exceptions.c` linked by
|
|
|
+the same commands):
|
|
|
+
|
|
|
+```c
|
|
|
+void m2_exc_init(void);
|
|
|
+void m2_exc_push(void *env);
|
|
|
+void m2_exc_leave(void);
|
|
|
+void m2_exc_retry(void) __attribute__((noreturn));
|
|
|
+void m2_exc_raise_lang(int code) __attribute__((noreturn));
|
|
|
+void m2_exc_raise_user(void *source, unsigned number,
|
|
|
+ const long *msg_desc) __attribute__((noreturn));
|
|
|
+void m2_exc_throw(long code) __attribute__((noreturn));
|
|
|
+
|
|
|
+int m2_exc_active(void);
|
|
|
+int m2_exc_language(void);
|
|
|
+int m2_exc_is_source(void *source);
|
|
|
+unsigned m2_exc_number(void);
|
|
|
+void m2_exc_copy_message(long *text_desc);
|
|
|
+void *m2_exc_new_source(void);
|
|
|
+```
|
|
|
+
|
|
|
+- `msg_desc` / `text_desc` are V3 string descriptors:
|
|
|
+ `[0] = length`, then NUL-terminated bytes.
|
|
|
+- `m2_exc_new_source` returns a unique non-NULL token; it is never
|
|
|
+ dereferenced. This keeps `ExceptionSource` opaque and avoids
|
|
|
+ allocation.
|
|
|
+- `m2_exc_init` is idempotent and installs the signal handlers.
|
|
|
+- `m2_exc_throw` is the C link for `SYSTEM.THROW`.
|
|
|
+
|
|
|
+### 4.3 Signals and traps
|
|
|
+
|
|
|
+Install handlers for `SIGSEGV`, `SIGBUS`, `SIGFPE` (and optionally
|
|
|
+`SIGILL`) with `SA_SIGINFO`. The handler must only classify the signal
|
|
|
+and call `m2_exc_raise_lang`; it must not allocate or print on the
|
|
|
+caught path.
|
|
|
+
|
|
|
+Mapping:
|
|
|
+
|
|
|
+| Signal / source | Exception |
|
|
|
+| --- | --- |
|
|
|
+| `SIGSEGV`, `SIGBUS` | `invalidLocation` |
|
|
|
+| `SIGFPE` / `FPE_INTDIV` | `wholeDivException` |
|
|
|
+| `SIGFPE` / `FPE_INTOVF` | `wholeValueException` |
|
|
|
+| `SIGFPE` / `FPE_FLTDIV` | `realDivException` |
|
|
|
+| `SIGFPE` / `FPE_FLTOVF`, `FLTUND`, `FLTRES`, `FLTINV` | `realValueException` |
|
|
|
+| `SIGFPE` / `FPE_FLTSUB` | `indexException` |
|
|
|
+| `SIGILL` (optional) | `sysException` |
|
|
|
+
|
|
|
+Compiler-emitted checks use the same runtime:
|
|
|
+
|
|
|
+- array index -> `indexException`;
|
|
|
+- subrange assignment/actual/`RETURN`/`INC`/`DEC`, set element -> `rangeException`;
|
|
|
+- `CASE` with no matching label and no `ELSE` -> `caseSelectException`;
|
|
|
+- descriptor/copy mismatch, invalid `SYSTEM` operation -> `sysException`.
|
|
|
+
|
|
|
+`QbeGen.CheckRange` gains a code parameter (or split into
|
|
|
+`CheckIndex`/`CheckRange`) so the same compare-and-branch machinery can
|
|
|
+trap with the right number. `QbeGen.Trap` becomes
|
|
|
+`TrapCode(code)` and calls `$m2_exc_raise_lang(w code)` instead of
|
|
|
+`$abort()`.
|
|
|
+
|
|
|
+`m2_exc_init` must run before user code. Emit a call at the start of
|
|
|
+generated `main` (`QbeGen.BeginBody`). If no handler exists, the
|
|
|
+signal handler still calls the runtime, which reports and aborts;
|
|
|
+existing trap tests remain rc=134.
|
|
|
+
|
|
|
+**Signal-mask note:** generated code must use a setjmp that saves the
|
|
|
+signal mask (`sigsetjmp` / glibc `__sigsetjmp`), otherwise a fault
|
|
|
+handler can leave `SIGSEGV` blocked after `longjmp`. V3 targets
|
|
|
+Linux/amd64 today; keep this binding behind a small target macro.
|
|
|
+
|
|
|
+### 4.4 `setjmp` in generated code
|
|
|
+
|
|
|
+`setjmp` cannot be hidden behind a normal C helper: the helper's frame
|
|
|
+would be gone. Generated code must call the real symbol directly.
|
|
|
+
|
|
|
+Use a stack `sigjmp_buf` per handler-bearing block. The current target
|
|
|
+is glibc/amd64; allocate 512 bytes and statically assert
|
|
|
+`sizeof(sigjmp_buf) <= 512` in the shim. The environment lives in the
|
|
|
+QBE frame and remains valid until the block returns or a `RETRY`
|
|
|
+re-enters it.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. Code generation
|
|
|
+
|
|
|
+### 5.1 Procedure with `EXCEPT`
|
|
|
+
|
|
|
+Pseudo-QBE for a procedure whose body has a handler:
|
|
|
+
|
|
|
+```qbe
|
|
|
+export function w $P(l %sl) {
|
|
|
+@start
|
|
|
+ ...
|
|
|
+ %env =l alloc8 512
|
|
|
+ call $m2_exc_push(l %env)
|
|
|
+@dispatch
|
|
|
+ %r =w call $__sigsetjmp(l %env, w 1)
|
|
|
+ %zero =w ceqw %r, 0
|
|
|
+ jnz %zero, @body, @check
|
|
|
+@check
|
|
|
+ %two =w ceqw %r, 2
|
|
|
+ jnz %two, @body, @handler
|
|
|
+@body
|
|
|
+ ... body statements ...
|
|
|
+ call $m2_exc_leave()
|
|
|
+ jmp @after
|
|
|
+@handler
|
|
|
+ ... handler statements ...
|
|
|
+ call $m2_exc_leave()
|
|
|
+@after
|
|
|
+ ret ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- `RETRY` emits `call $m2_exc_retry()` followed by `hlt`; the runtime
|
|
|
+ longjmps back to the saved environment, `sigsetjmp` returns 2, and
|
|
|
+ control goes to `@body` without re-pushing the handler.
|
|
|
+- `RETURN` in the body or handler emits `call $m2_exc_leave()` before
|
|
|
+ the QBE `ret`. QbeGen can track `funcHasExc` and make `EmitRet`
|
|
|
+ emit the pop.
|
|
|
+- Normal fall-through of the body goes to `@after` after popping.
|
|
|
+- Normal fall-through of the handler also goes to `@after` after
|
|
|
+ popping.
|
|
|
+- If there is no `EXCEPT`, none of this is emitted; behaviour is
|
|
|
+ unchanged.
|
|
|
+
|
|
|
+### 5.2 QbeGen API
|
|
|
+
|
|
|
+Add (names illustrative):
|
|
|
+
|
|
|
+```text
|
|
|
+ExcBegin -- allocate env, push, sigsetjmp, @body/@handler dispatch
|
|
|
+ExcBodyDone -- leave + jump @after
|
|
|
+ExcHandlerBegin -- emit @handler
|
|
|
+ExcEnd -- leave + @after
|
|
|
+ExcRetry -- call m2_exc_retry, dead
|
|
|
+```
|
|
|
+
|
|
|
+`EndFunc`, `EndInit` and the program `main` path must not emit an extra
|
|
|
+pop; the body/handler paths already balance the stack. `EmitRet`
|
|
|
+handles early returns. `funcHasExc` is reset in `AbortFunc`.
|
|
|
+
|
|
|
+Module-body handling:
|
|
|
+
|
|
|
+- Implementation/local module `BEGIN ... END`: `BeginInit` →
|
|
|
+ `ExcBegin` → body → `EndInit` (with the same handler shape).
|
|
|
+- Program module body: `BeginBody` emits `main` and the imported
|
|
|
+ module-init calls; `ExcBegin` follows that, so the program handler
|
|
|
+ covers its own body, not imported module initialisation.
|
|
|
+- No handler: no change.
|
|
|
+
|
|
|
+`QbeGen.BeginBody` also emits `call $m2_exc_init()` before the init
|
|
|
+calls, so hardware faults outside a handler still terminate through the
|
|
|
+runtime and existing trap tests keep rc=134.
|
|
|
+
|
|
|
+### 5.3 State restoration at returns
|
|
|
+
|
|
|
+Every return path from a block with a handler must call
|
|
|
+`m2_exc_leave` exactly once. This includes:
|
|
|
+
|
|
|
+- normal fall-through of the body;
|
|
|
+- normal fall-through of the handler;
|
|
|
+- `RETURN` in the body;
|
|
|
+- `RETURN` in the handler.
|
|
|
+
|
|
|
+`RETRY` must not call `leave`; it stays inside the same block and
|
|
|
+restarts the body.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. Front end
|
|
|
+
|
|
|
+### 6.1 Scanner and grammar
|
|
|
+
|
|
|
+- `EXCEPT` and `RETRY` become reserved words in the Coco/R grammar.
|
|
|
+- Add `"EXCEPT" [ StatSeq ]` to `Block`, `ImplUnit`, `NestedModule` and
|
|
|
+ the class-body path that reuses `Block`.
|
|
|
+- Add `RETRY` to `Statement`.
|
|
|
+- `RETRY` outside a handler is a semantic error. Add a specific
|
|
|
+ message (proposal: code 237, `RETRY outside EXCEPT`).
|
|
|
+- Two-phase defer/reparse: the handler lives inside the deferred body.
|
|
|
+ The token skipper must treat `EXCEPT`/`RETRY` as ordinary tokens
|
|
|
+ while looking for the matching `END`, and the phase-2 reparse must
|
|
|
+ rebuild `NkExcept`/`NkRetry` like any other body node. Do not add
|
|
|
+ handler state to the skipper.
|
|
|
+
|
|
|
+### 6.2 AST
|
|
|
+
|
|
|
+Preferred shape: a wrapper node so Lower has one place to handle the
|
|
|
+pair and ordinary statement sequences stay unchanged.
|
|
|
+
|
|
|
+```text
|
|
|
+NkExcept = 56; (* [0] = body statements, [1] = handler statements *)
|
|
|
+NkRetry = 57;
|
|
|
+```
|
|
|
+
|
|
|
+- A block with no `EXCEPT` keeps its old body node.
|
|
|
+- A block with `EXCEPT` sets its body to an `NkExcept` node.
|
|
|
+- `NkRetry` is a leaf statement.
|
|
|
+- `NkProcDecl` / `NkUnit` / local-module bodies keep their current
|
|
|
+ child positions; only the body node's kind changes.
|
|
|
+
|
|
|
+### 6.3 Static checks
|
|
|
+
|
|
|
+- `RETRY` only in the handler of the current block, including nested
|
|
|
+ control statements inside that handler.
|
|
|
+- `RETRY` in a nested procedure is invalid for the outer handler; each
|
|
|
+ procedure has its own handler context.
|
|
|
+- `RETURN` is allowed in a handler.
|
|
|
+- `EXCEPT` cannot appear as a statement or in a definition module.
|
|
|
+- No new type rules; `EXCEPTIONS`/`M2EXCEPTION` are ordinary library
|
|
|
+ modules.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. Library implementation
|
|
|
+
|
|
|
+### 7.1 `runtime/syslib/SysExceptions.def/.mod`
|
|
|
+
|
|
|
+A V3 binding module over the C runtime, in the style of `SysShim`:
|
|
|
+
|
|
|
+```text
|
|
|
+PROCEDURE RaiseLang(code: CARDINAL);
|
|
|
+PROCEDURE RaiseUser(source: ADDRESS; number: CARDINAL;
|
|
|
+ VAR message: ARRAY OF CHAR);
|
|
|
+PROCEDURE Retry;
|
|
|
+PROCEDURE Leave;
|
|
|
+PROCEDURE Active(): BOOLEAN;
|
|
|
+PROCEDURE IsLanguage(): BOOLEAN;
|
|
|
+PROCEDURE IsSource(source: ADDRESS): BOOLEAN;
|
|
|
+PROCEDURE Number(): CARDINAL;
|
|
|
+PROCEDURE CopyMessage(VAR text: ARRAY OF CHAR);
|
|
|
+PROCEDURE NewSource(): ADDRESS;
|
|
|
+```
|
|
|
+
|
|
|
+`RaiseLang`/`RaiseUser`/`Retry` map to `noreturn` C functions. The
|
|
|
+Modula declarations do not need pragmas.
|
|
|
+
|
|
|
+### 7.2 `stdlib/exceptions.def/.mod`
|
|
|
+
|
|
|
+- Copy the ISO definition without the `<* noreturn *>` pragma (V3 does
|
|
|
+ not parse declaration pragmas yet).
|
|
|
+- Implementation:
|
|
|
+
|
|
|
+```text
|
|
|
+TYPE ExceptionSource = ADDRESS; (* opaque in the definition *)
|
|
|
+```
|
|
|
+
|
|
|
+`AllocateSource` calls `SysExceptions.NewSource`. Since the source is
|
|
|
+an opaque token and is never dereferenced, no heap allocation is
|
|
|
+needed. The implementation imports `M2EXCEPTION` for `exException`;
|
|
|
+`M2EXCEPTION` does not import `EXCEPTIONS`, so there is no module
|
|
|
+cycle.
|
|
|
+
|
|
|
+`CurrentNumber`:
|
|
|
+
|
|
|
+```text
|
|
|
+IF SysExceptions.IsSource(source) THEN
|
|
|
+ RETURN SysExceptions.Number()
|
|
|
+ELSE
|
|
|
+ SysExceptions.RaiseLang(ORD(M2EXCEPTION.exException));
|
|
|
+ RETURN 0
|
|
|
+END
|
|
|
+```
|
|
|
+
|
|
|
+`GetMessage` calls `CopyMessage`; `IsExceptionalExecution` calls
|
|
|
+`Active`; `IsCurrentSource` calls `IsSource`; `RAISE` calls
|
|
|
+`RaiseUser`.
|
|
|
+
|
|
|
+### 7.3 `stdlib/m2exception.def/.mod`
|
|
|
+
|
|
|
+`M2Exception` returns `VAL(M2Exceptions, SysExceptions.Number())` when
|
|
|
+`IsLanguage` is true; otherwise it raises `exException`.
|
|
|
+`IsM2Exception` returns `SysExceptions.IsLanguage()`.
|
|
|
+
|
|
|
+### 7.4 `SYSTEM.THROW`
|
|
|
+
|
|
|
+Add to the hard-coded `SYSTEM` setup in `SymTab.Init`:
|
|
|
+
|
|
|
+```text
|
|
|
+pr := PredefCProc("THROW", "m2_exc_throw", InvalidType);
|
|
|
+AddFormal(pr, dInt, FALSE)
|
|
|
+```
|
|
|
+
|
|
|
+`m2_exc_throw` raises language code `i` (or `exException` if out of
|
|
|
+range) and never returns. `FROM SYSTEM IMPORT THROW` then works
|
|
|
+through the existing external-procedure path.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 8. Language-exception mapping
|
|
|
+
|
|
|
+| Value | Trigger in V3 |
|
|
|
+| --- | --- |
|
|
|
+| `indexException` | array index check; `FPE_FLTSUB` |
|
|
|
+| `rangeException` | subrange assignment/actual/`RETURN`/`INC`/`DEC`; set element; set span |
|
|
|
+| `caseSelectException` | `CASE` no match, no `ELSE` (add explicit trap) |
|
|
|
+| `invalidLocation` | `SIGSEGV`/`SIGBUS`, NIL/uninitialised pointer |
|
|
|
+| `functionException` | function body ends without `RETURN` (follow-up; current V3 returns 0) |
|
|
|
+| `wholeValueException` | `SIGFPE` integer overflow |
|
|
|
+| `wholeDivException` | `SIGFPE` integer divide-by-zero; optional negative divisor check |
|
|
|
+| `realValueException` | `SIGFPE` real overflow/underflow/invalid |
|
|
|
+| `realDivException` | `SIGFPE` real divide-by-zero |
|
|
|
+| `complexValueException` | not detected (no complex arithmetic) |
|
|
|
+| `complexDivException` | not detected |
|
|
|
+| `protException` | not detected (no protection model) |
|
|
|
+| `sysException` | descriptor/copy mismatch, invalid `SYSTEM` operation |
|
|
|
+| `coException` | not detected (coroutine contexts deferred) |
|
|
|
+| `exException` | wrong source/state in `EXCEPTIONS`/`M2EXCEPTION`; bad `THROW` |
|
|
|
+
|
|
|
+Messages are implementation-defined; use a short table, e.g.
|
|
|
+`"array index out of range"`, `"range check failed"`, `"invalid memory
|
|
|
+location"`, `"divide by zero"`.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 9. Testing
|
|
|
+
|
|
|
+### 9.1 Unit / regression tests
|
|
|
+
|
|
|
+Add to `compiler/run_tests.sh`:
|
|
|
+
|
|
|
+- `t_except_basic`: `RAISE` from a nested procedure, handler checks
|
|
|
+ `CurrentNumber`, `GetMessage`, `IsCurrentSource`,
|
|
|
+ `IsExceptionalExecution`; exit 42.
|
|
|
+- `t_except_retry`: two-level retry like `except4`/`except8`; exit 42.
|
|
|
+- `t_except_throw`: `SYSTEM.THROW` caught, then `M2Exception` checked.
|
|
|
+- `t_except_nested`: handler calls a procedure with its own handler;
|
|
|
+ verifies state restoration (outer `CurrentNumber` after inner returns).
|
|
|
+- `t_except_return`: `RETURN` from body and from handler; outer handler
|
|
|
+ still catches a later exception.
|
|
|
+- `t_except_module`: module-body `EXCEPT`/`RETRY`.
|
|
|
+- `t_except_trap`: integer divide-by-zero and NIL dereference caught;
|
|
|
+ `M2Exception` returns `wholeDivException` / `invalidLocation`.
|
|
|
+- `t_except_unhandled`: expect_trap (rc=134).
|
|
|
+- `t_except_retry_bad`: `RETRY` outside `EXCEPT` rejected with the new
|
|
|
+ message.
|
|
|
+- `t_except_lib`: `EXCEPTIONS` source allocation and wrong-source
|
|
|
+ `CurrentNumber` raises `exException`.
|
|
|
+
|
|
|
+Use `expect_run_files` with:
|
|
|
+
|
|
|
+```text
|
|
|
+stdlib/exceptions.def stdlib/exceptions.mod
|
|
|
+stdlib/m2exception.def stdlib/m2exception.mod
|
|
|
+runtime/syslib/SysExceptions.def runtime/syslib/SysExceptions.mod
|
|
|
+```
|
|
|
+
|
|
|
+### 9.2 Corpus
|
|
|
+
|
|
|
+- `lazyunique` should become a genuine +1 (corpus 511 -> 512).
|
|
|
+- `except2/4/5/7/8/9` still fail declaration order because `ip` is
|
|
|
+ declared after the procedures. Keep temporary copies with `ip`
|
|
|
+ moved above the procedures and run them outside the corpus count.
|
|
|
+- Do not claim the handoff's "+7" in the status until the declaration
|
|
|
+ order cases are re-counted.
|
|
|
+
|
|
|
+### 9.3 Gates
|
|
|
+
|
|
|
+For every step:
|
|
|
+
|
|
|
+```sh
|
|
|
+cd compiler && ./build.sh && ./run_tests.sh
|
|
|
+cd .. && TMP="$PWD/.scratch/fixpoint" ./bootstrap/fixpoint.sh
|
|
|
+tools/v3-corpus/corpus.sh
|
|
|
+```
|
|
|
+
|
|
|
+Expected after the first full implementation: suite 263 + new tests,
|
|
|
+fixpoint OK (size will change), corpus **512/612** (only
|
|
|
+`lazyunique`), 0 crashes. Existing trap tests must stay rc=134.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 10. Milestones
|
|
|
+
|
|
|
+1. **M1 — parser/AST**
|
|
|
+ - `EXCEPT`, `RETRY`; `NkExcept`/`NkRetry`; `RETRY` placement check;
|
|
|
+ `EXCEPT` accepted in procedure, implementation, program and local
|
|
|
+ module bodies. No codegen yet.
|
|
|
+2. **M2 — runtime + codegen for user `RAISE`**
|
|
|
+ - `m2_exc_*` handler stack, `ExcBegin`/`ExcBodyDone`/`ExcHandlerBegin`/
|
|
|
+ `ExcEnd`/`ExcRetry`, `EmitRet` pop, `m2_exc_init` in `main`.
|
|
|
+ - Positive tests for `RAISE`, `RETRY`, `RETURN`, nested handlers.
|
|
|
+3. **M3 — libraries**
|
|
|
+ - `SysExceptions`, `EXCEPTIONS`, `M2EXCEPTION`, `SYSTEM.THROW`.
|
|
|
+ - Library tests; `except5/7` variants.
|
|
|
+4. **M4 — language traps**
|
|
|
+ - `TrapCode`; split index/range; CASE trap; signal handlers and
|
|
|
+ mapping; `lazyunique`; `t_except_trap`; keep rc=134 for unhandled.
|
|
|
+5. **M5 — docs and corpus**
|
|
|
+ - Update `features.md`, `STATUS.md`, comparison table, gaps, handoff,
|
|
|
+ summary; commit with `v3-exceptions`; record corpus reality.
|
|
|
+
|
|
|
+`functionException`, real/complex/protection/coroutine detection can
|
|
|
+follow separately; they should not block M1–M4.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 11. Risks and deferred work
|
|
|
+
|
|
|
+- **`setjmp` ABI:** generated code must call `__sigsetjmp` directly;
|
|
|
+ the environment size and signal-mask behaviour are glibc/amd64
|
|
|
+ specific. Keep a target macro and a static assert.
|
|
|
+- **QBE and returns-twice:** test early. All V3 locals are memory
|
|
|
+ objects, which helps, but a small standalone test should verify that
|
|
|
+ SSA values used after the handler branch are still valid.
|
|
|
+- **Signal safety:** no allocation/printf on the caught path; use
|
|
|
+ `siglongjmp`; do not catch signals while the runtime is already
|
|
|
+ unwinding. A stack-overflow `SIGSEGV` cannot be handled on the
|
|
|
+ overflowed stack; an optional `sigaltstack` is a later hardening
|
|
|
+ item.
|
|
|
+- **Handler depth:** fixed 1024 entries; overflow should report
|
|
|
+ `sysException` or abort, not corrupt memory.
|
|
|
+- **No finalization:** longjmp bypasses any future cleanup; `FINALLY`
|
|
|
+ needs its own design.
|
|
|
+- **Coroutines:** the state/handler stack is global, not per-coroutine.
|
|
|
+ `coException` remains undetected.
|
|
|
+- **Function result:** current V3 returns 0 when a function falls off
|
|
|
+ the end; raising `functionException` is a follow-up because it can
|
|
|
+ change existing behaviour.
|
|
|
+- **Separate compilation:** the runtime ABI (`m2_exc_*`) is stable, but
|
|
|
+ per-unit exception tables/handler metadata would be needed once
|
|
|
+ objects are separate. Not part of this milestone.
|
|
|
+- **Pragmas:** local `EXCEPTIONS.def` omits `<* noreturn *>`; adding
|
|
|
+ ISO declaration pragmas is independent.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 12. Open questions
|
|
|
+
|
|
|
+1. Confirm the ISO block grammar allows `EXCEPT` without a preceding
|
|
|
+ `BEGIN`; accept both forms in V3 unless the spec forbids it.
|
|
|
+2. Decide whether `RETRY` in a program-module body is accepted (gm2
|
|
|
+ allows module and procedure blocks; ISO mentions module
|
|
|
+ initialization/finalization). Proposed: yes.
|
|
|
+3. Decide the exact `THROW(i)` mapping when `i` is outside
|
|
|
+ `0 .. 14` (proposed: `exException`).
|
|
|
+4. Confirm whether `functionException` should be raised now for
|
|
|
+ functions with a handler whose body/handler falls through without
|
|
|
+ `RETURN`; proposed: follow-up, not M1.
|