aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AGENTS/ARCH.md14
-rw-r--r--AGENTS/CONVENTIONS.md15
-rw-r--r--AGENTS/EXTENDING.md19
-rw-r--r--AGENTS/PITFALLS.md20
-rw-r--r--AGENTS/SUBSYSTEMS.md28
-rw-r--r--README.md3
-rw-r--r--doc/cpsc.1.md12
-rw-r--r--doc/language/cpsc-language-overview.7.md5
-rw-r--r--doc/language/function/cpsc-function-eof.7.md4
-rw-r--r--doc/language/keyword/cpsc-keyword-closefile.7.md4
-rw-r--r--doc/language/keyword/cpsc-keyword-declare.7.md4
-rw-r--r--doc/language/keyword/cpsc-keyword-div.7.md4
-rw-r--r--doc/language/keyword/cpsc-keyword-mod.7.md4
-rw-r--r--doc/language/keyword/cpsc-keyword-openfile.7.md4
-rw-r--r--doc/language/keyword/cpsc-keyword-readfile.7.md4
-rw-r--r--doc/language/keyword/cpsc-keyword-try.7.md48
-rw-r--r--doc/language/keyword/cpsc-keyword-type.7.md6
-rw-r--r--doc/language/keyword/cpsc-keyword-writefile.7.md4
18 files changed, 154 insertions, 48 deletions
diff --git a/AGENTS/ARCH.md b/AGENTS/ARCH.md
index d60fcbf..2551161 100644
--- a/AGENTS/ARCH.md
+++ b/AGENTS/ARCH.md
@@ -33,7 +33,7 @@ cmake --build build
build/cpsc prog.cps -o prog.ll # default without -o: prog.ir beside the source
build/cpsc prog.cps -r # execute using the selected build mode
build/cpsc prog.cps -r -o prog.ir # save IR, then run it
-clang-18 prog.ll -o prog && ./prog
+clang++-18 prog.ll -o prog && ./prog
```
`doc/build.md` recommends which packages to install and which to download;
@@ -54,6 +54,12 @@ managed root. Downloads belong to the package-manager commands in `doc/build.md`
not to CMake. The Windows Pandoc ZIP is downloaded directly from its official
release because MSYS2 has no Pandoc package. Only the Linux build is verified.
+Generated IR uses the C++ exception ABI; native binaries must link its runtime
+(use `clang++-18`). TRY/CATCH/ENDTRY and bare THROW are compiler extensions.
+Runtime checks throw anonymous exceptions; the final handler in generated main
+prints `Unhandled exception. Program terminated.` to stdout and returns 1.
+Exception execution is verified on Linux with LLVM 18; Windows/macOS are unverified.
+
Driver behavior (`tools/driver/main.cc`):
- One source file, with optional `-o path` and `-r`; options may precede or follow the source.
@@ -79,7 +85,9 @@ Driver behavior (`tools/driver/main.cc`):
alone uses temporary IR removed on return, including execution failure.
- In EMBEDDED mode, `-r` uses MCJIT in-process (`tools/driver/EmbeddedRunner.cc`).
The engine owns a clone of CodeGen's module; CodeGen retains the context and
- must outlive the engine. The host target is initialized only for execution.
+ must outlive the engine. Static C++ exception ABI symbols are explicitly
+ registered for JIT lookup; MCJIT registers the generated unwind frames.
+ The host target is initialized only for execution.
No external runner or PATH lookup is used; `-r` alone runs in memory.
- In NONE mode, `-r` returns 1 before compiling or writing output; ordinary IR
compilation remains available. Only EMBEDDED links execution-engine components.
@@ -159,7 +167,7 @@ untested.)
| `lib/Parser/ParserType.cc` | TYPE/ENDTYPE and CLASS/ENDCLASS declarations, shared field parsing, SET/DEFINE reserved stubs |
| `lib/CodeGen/CodeGen.cc` | Hub: dispatch, coercion, control flow, INPUT/OUTPUT, main() |
| `lib/CodeGen/Designator.cc` | CodeGen methods for the TYPE system: type pre-pass, `emitLValue`/`loadFromLValue`, `emitCoercedExpr` gate, user-kind binary-op interception |
-| `lib/CodeGen/*Handler.cc`, `FunctionGen.cc`, `RuntimeCheck.cc`, `TypeSystem.cc` | Helpers owned by CodeGen |
+| `lib/CodeGen/*Handler.cc`, `FunctionGen.cc`, `RuntimeCheck.cc`, `TypeSystem.cc` | Helpers owned by CodeGen (including exception emission) |
| `CMakeLists.txt` | 3 static libs (CPSLexer, CPSParser, CPSCodeGen) + `cpsc`. **New .cc files must be added to the source lists here** |
| `tools/driver/EmbeddedRunner.cc`, `cmake/EmbeddedLLVM.cmake` | EMBEDDED-only MCJIT runner and static support-library selection |
| `cmake/LocalDependencies.cmake`, `cmake/*Packages.cmake` | Local package extraction and platform relocation |
diff --git a/AGENTS/CONVENTIONS.md b/AGENTS/CONVENTIONS.md
index 638013a..b38b73c 100644
--- a/AGENTS/CONVENTIONS.md
+++ b/AGENTS/CONVENTIONS.md
@@ -122,7 +122,7 @@ no registered signature is a compile error. Function names get no mangling, so
BYVAL (§8.1). A BYREF parameter in a FUNCTION is a parse error. **Array
parameters**: `ARRAY OF T` (rank 1, bounds travel with the argument) or
`ARRAY[l:u,...] OF T` (literal bounds, any rank, a contract checked at compile
-time when the caller's bounds are constants, else at run time with `[Fatal]`).
+time when the caller's bounds are constants, else at run time by throwing an exception).
The LLVM ABI flattens each rank-R array parameter into a data `ptr` plus
`kBoundArgsPerDim` (=2) i64 bounds per dimension; the callee rebuilds its
`ArrayMetadata` from those (`ArrayHandler::bindArrayParameter` via the binder
@@ -137,4 +137,15 @@ the single source of truth: `emitExpr`'s `CallExprAST` branch checks arity again
before dispatching to the per-builtin emit branches (still an if-chain, ahead of the
user-function fallback), and `getExprTypeInfo` reads return types from it. EOF is the
odd one out: its intercept consumes `CallExprAST::getLine()` (runtime diagnostics) and
-it is the only builtin that touches mutable runtime state and can exit(1) at run time.
+it is the only builtin that touches mutable runtime state and can throw at run time.
+
+**Program exceptions use the C++ ABI; compiler diagnostics do not.**
+`ExceptionHandler` emits anonymous integer exceptions (an internal placeholder),
+`invoke`/`landingpad`/`resume`, and paired `__cxa_begin_catch`/`__cxa_end_catch`.
+Its handler stack and catch depth are keyed by LLVM function, so synthesizing file
+helpers never inherits the caller's catches. User calls, file runtime calls and
+THROW must use `ExceptionHandler::emitCall`; ordinary nonthrowing libc calls stay
+plain calls. RETURN must finish all active catches after evaluating its value.
+`RuntimeCheck::emitCheck` throws without printing. Generated main catches anything
+unhandled, prints the single generic message to stdout, and returns 1. Arrays in
+TRY and CATCH use separate block scopes; scalars retain existing function scope.
diff --git a/AGENTS/EXTENDING.md b/AGENTS/EXTENDING.md
index 7a44281..43af5e0 100644
--- a/AGENTS/EXTENDING.md
+++ b/AGENTS/EXTENDING.md
@@ -51,14 +51,17 @@ walk in `getExprTypeInfo`'s designator branch (they must agree) — plus the
degrade rule in `ParseIdentifierExpr`/`ParseAssignStmt` if the new form can
collapse to a legacy node.
-**New runtime check:** message global + `emitXxxCheck` in `RuntimeCheck` (build an i1
-fail-condition, call `emitErrorAndExit` — it splits the block and leaves the builder at
-the continuation), then call it from the emit site with the AST node's `getLine()`.
-Precedents: `emitIndexCheck` (per-dimension, from ArrayHandler), `emitEnumRangeCheck`
-(after enum ± INTEGER — the only site that can manufacture an invalid ordinal),
-`emitNullDerefCheck` (before every `^` deref), `emitArrayArgBoundsCheck` (bounded
-array-parameter contract). Note that failure messages print to **stdout**
-(`[Fatal] line %d: ...`) and `exit(1)`.
+**New runtime check:** add `emitXxxCheck` in `RuntimeCheck` (build an i1
+fail-condition and call `emitCheck` — it splits the block and leaves the builder at
+the continuation), then call it from the emit site. Failure throws an anonymous
+exception without printing; generated main supplies the uncaught message and
+status 1. Precedents: `emitIndexCheck`, `emitEnumRangeCheck`, `emitNullDerefCheck`,
+`emitArrayArgBoundsCheck` and FileHandler's direct `emitCheck` calls.
+
+**New potentially throwing call:** route it through `ExceptionHandler::emitCall`
+so it invokes the current handler or catch cleanup when needed. Give generated
+functions unwind tables via `prepareFunction`. Exception ABI dependencies must
+also be registered in EmbeddedRunner for static-runtime JIT symbol lookup.
**New libc dependency:** declare via `getOrInsertFunction` in the owning component's
`setupExternalFunctions()` (re-declaring the same symbol in two handlers is fine — it
diff --git a/AGENTS/PITFALLS.md b/AGENTS/PITFALLS.md
index b70718c..ce72de8 100644
--- a/AGENTS/PITFALLS.md
+++ b/AGENTS/PITFALLS.md
@@ -18,7 +18,7 @@ test programs.
RETURN arrays. A whole-array variable is also rejected as an operand or as a
value of its element type (it used to leak its raw data pointer).
- **Arrays are additionally block-scoped.** An array DECLAREd inside an
- IF/WHILE/REPEAT/FOR body is not visible after the block ("Undeclared array") —
+ IF/WHILE/REPEAT/FOR/TRY/CATCH body is not visible after the block ("Undeclared array") —
its buffer setup and bound SSA values live inside the branch, so letting it
escape meant wild-pointer writes or invalid IR. Scalars remain function-scoped
(their alloca is hoisted to the entry block); declare at the top of the
@@ -36,7 +36,7 @@ test programs.
at any new call site) and internally defaults to INTEGER for arithmetic binaries and
unknown-function calls; OUTPUT reports an error for a nullptr result.
- The lexer emits no newline tokens; the grammar is purely keyword-delimited. Bare
- `RETURN` is only recognized right before ENDIF/ELSE/ENDFUNCTION/ENDPROCEDURE.
+ `RETURN` is recognized before block terminators, including CATCH and ENDTRY.
- `verifyModule` runs at the end of `compile()`; invalid modules make the driver
exit non-zero without writing IR.
- The FOR loop variable must be pre-DECLAREd as INTEGER; FOR reuses that alloca. The
@@ -49,11 +49,11 @@ test programs.
READFILE's target must be a plain STRING variable — no CHAR, no `arr[i]` (mirrors
INPUT). The CIE guide's empty-line test `IF LineOfText = ""` compares contents.
At most 16 files open at once. All runtime misuse (not open, wrong mode, reopen,
- fopen failure, READFILE past EOF) is a `[Fatal] line %d:` exit(1), by design — guard reads with
+ fopen failure, READFILE past EOF) throws an anonymous exception — guard reads with
`WHILE NOT EOF(...)`. Caveat: the open-file table is keyed by the filename *string*
(per CIE semantics), so two spellings of one OS path (`"a.txt"` vs `"./a.txt"`) evade
the already-open check and get two independent buffered streams — reads through the
- alias can see stale/empty data instead of a Fatal.
+ alias can see stale/empty data instead of an exception.
- **TYPE-system sharp edges (§4).** `TYPE`/`ENDTYPE`/`SET`/`DEFINE` are reserved
keywords now. Typing is *nominal*: two pointer types with the same pointee, or two
@@ -61,8 +61,8 @@ test programs.
official guide's own worked example (`^INTEGER` pointed at a `Season` variable) —
deliberate deviation. Enum OUTPUT prints the 0-based ordinal; a DECLAREd-but-unset
enum *is* the first enumerator (no uninitialized state). `E ± INTEGER` needs the
- enum on the left; out-of-range results die `[Fatal]` at run time. There is no NULL
- literal: pointers zero-init to null, dereferencing one dies `[Fatal]`, and unset-ness
+ enum on the left; out-of-range results throw at run time. There is no NULL
+ literal: pointers zero-init to null, dereferencing one throws, and unset-ness
is deliberately untestable. `RETURN ^localVar` dangles — documented, unchecked.
Record STRING fields stay null until read (normalized to `""` per-load, not at copy
time). Enum value names are globally reserved (they block DECLARE/parameter/function
@@ -78,6 +78,14 @@ test programs.
cannot be accessed externally; whole-value initialization/copying is allowed.
CLASS inherits record limitations, including no array fields or empty bodies.
+- **Exception extensions.** TRY requires exactly one parameterless CATCH and
+ ENDTRY. Bare THROW raises a new anonymous exception even inside CATCH; there is
+ no rethrow syntax, error object or what(). Runtime checks are catchable;
+ compiler errors are not. Uncaught exceptions print only the generic message
+ and return 1. Native binaries must link the C++ runtime with `clang++-18`.
+ Unwinding does not free heap strings/arrays or close files. Linux LLVM 18 is
+ verified; Windows and macOS exception execution is not.
+
## Not implemented (9618 features)
CASE OF, CONSTANT, SET types (`SET`/`DEFINE` are reserved with dedicated errors),
diff --git a/AGENTS/SUBSYSTEMS.md b/AGENTS/SUBSYSTEMS.md
index 0dafd12..95e1abb 100644
--- a/AGENTS/SUBSYSTEMS.md
+++ b/AGENTS/SUBSYSTEMS.md
@@ -64,17 +64,29 @@ context or language-level inheritance yet.
**Runtime checks** (`RuntimeCheck`): array bounds, file misuse, enum ranges, null
dereferences and array-parameter bounds contracts are checked at their emit sites.
`ArithmeticHandler` calls `emitDivZeroCheck` before `/`, DIV and MOD; the check accepts
-i64 and double divisors, including both floating-point signed zeros, and exits with
-a `[Fatal]` diagnostic and status 1. The public `emitErrorAndExit(Cond, Msg, ArrayRef<Value*>)`
-overload exists for failure sites whose printf arguments (line, filename) are runtime
-values — FileHandler's helpers use it; the private compile-time-line form delegates to
-it.
+i64 and double divisors, including both floating-point signed zeros.
+`emitCheck(Cond)` splits the block and calls `ExceptionHandler::emitThrow` on failure.
+No check prints an error; source-line parameters remain reserved for future diagnostics.
+
+**Exceptions** (`ExceptionHandler`): holds Context/Builder/Module refs and a map of
+per-function handler stacks and active catch depths. TRY pushes a catch target and
+an entry-block alloca for its exception pointer. A potentially throwing call with
+a handler or active catch emits an `invoke` and a landing pad that finishes catches being left and transfers
+to the nearest handler, or resumes unwinding when only cleanup is needed. CATCH
+uses `__cxa_begin_catch` and removes its own TRY target before emitting its body;
+normal exit and RETURN call `__cxa_end_catch`. Bare THROW always creates a new
+anonymous integer exception, using `_ZTIi` internally without exposing a value.
+Generated main owns the final catch target, prints the generic uncaught message,
+and returns 1. Native binaries require the C++ runtime; EmbeddedRunner explicitly
+registers its statically linked ABI symbols, while MCJIT manages unwind frames.
+This does not change heap-string/array lifetimes or automatically close files.
**Files** (`FileHandler`): text-file runtime for §9.1. StringHandler-shaped leaf —
-holds Context/Builder/Module/RuntimeCheck refs only, never sees CodeGen, AST nodes or
-the symbol table; `CodeGen::emitStmt`'s four file branches (and the EOF builtin
+holds Context/Builder/Module/RuntimeCheck/ExceptionHandler refs only, never sees
+CodeGen, AST nodes or the symbol table; `CodeGen::emitStmt`'s four file branches (and the EOF builtin
intercept) evaluate/coerce operands and hand over `llvm::Value*`s plus the statement
-line. On first file use, `ensureRuntime()` synthesizes the whole runtime once — the
+line. Calls into throwing helpers go through ExceptionHandler. On first file use,
+`ensureRuntime()` synthesizes the whole runtime once — the
only module-level helper functions in the project: internal-linkage `__cps_file_find/
open/read/write/close/eof` plus a 16-slot open-file table (`__cps_file_names/handles/
modes` parallel globals; mode byte 0 = free). Files are keyed by filename *content*
diff --git a/README.md b/README.md
index 7eb5737..5895679 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,8 @@ Details can be found here: [Pseudocode Guide for Teachers](https://www.cambridge
## Features
- **LLVM compilation**: Generate LLVM IR and run programs when execution is enabled in the build.
-- **Runtime checks**: Report array bounds errors, division by zero, file misuse, enum range errors and null pointer dereferences.
+- **Runtime checks**: Throw exceptions for array bounds errors, division by zero, file misuse, enum range errors and null pointer dereferences.
+- **Exception handling**: Catch exceptions with stack unwinding using the compiler extensions [TRY/CATCH/ENDTRY and THROW](doc/language/keyword/cpsc-keyword-try.7.md).
- **Arrays**: Support multiple dimensions and custom lower and upper bounds.
- **Strings**: Support dynamic strings and built-in string functions.
- **Functions and procedures**: Pass values, arrays and records by value; procedures also support `BYREF` parameters.
diff --git a/doc/cpsc.1.md b/doc/cpsc.1.md
index 228fd66..b6b7ad0 100644
--- a/doc/cpsc.1.md
+++ b/doc/cpsc.1.md
@@ -66,6 +66,14 @@ Save IR and run it:
cpsc test.cps -r -o result.ir
```
+Build a native program with a C++ linker so the exception runtime is linked:
+
+```sh
+cpsc test.cps -o test.ll
+clang++-18 test.ll -o test
+./test
+```
+
# EXIT STATUS
**0**
@@ -76,12 +84,16 @@ cpsc test.cps -r -o result.ir
With **-r**, return the executed program's exit status. In PATH mode, failure to launch `lli` or a child-process crash returns 1.
+An uncaught program exception returns 1 after printing `Unhandled exception. Program terminated.` to standard output. Runtime check failures are exceptions and can be handled with TRY/CATCH/ENDTRY.
+
# NOTES
Compiler diagnostics go to stderr. Failed compilation does not write IR. Successful compilation overwrites an existing output file, but an output path that refers to the source file (including a symbolic or hard link) is rejected.
Missing input, including invoking `cpsc` without arguments, reports `cpsc: fatal error: no input files`. Ordinary lexical, syntax and semantic errors use `error:`; failures that prevent compilation, such as file I/O errors, use `fatal error:`. Compilation and argument failures end with one `compilation terminated.` line. Execution-stage failures do not print that line.
+Generated IR uses the C++ exception ABI. Native binaries need the C++ runtime; EMBEDDED includes it, while PATH uses the runtime available to `lli`. Exception execution is verified on Linux with LLVM 18; Windows and macOS remain unverified.
+
# SEE ALSO
[cpsc-language-overview(7)](language/cpsc-language-overview.7.md) describes supported syntax and links to the keyword and built-in function manuals.
diff --git a/doc/language/cpsc-language-overview.7.md b/doc/language/cpsc-language-overview.7.md
index a471854..5c327b0 100644
--- a/doc/language/cpsc-language-overview.7.md
+++ b/doc/language/cpsc-language-overview.7.md
@@ -16,12 +16,15 @@ Literals include integers, decimal real numbers, TRUE/FALSE, strings in double q
Declare variables before use. See DECLARE for built-in types and arrays, TYPE for records/enumerations/pointers, and CLASS for public/private fields.
+The compiler extensions TRY/CATCH/ENDTRY and THROW support anonymous exceptions with stack unwinding. Runtime check failures can be caught; an uncaught exception prints a generic message and returns status 1. There is no error object or what() support.
+
# KEYWORDS
- [cpsc-keyword-declare(7)](keyword/cpsc-keyword-declare.7.md): declare variables and arrays.
- [cpsc-keyword-input(7)](keyword/cpsc-keyword-input.7.md): read a value from standard input.
- [cpsc-keyword-output(7)](keyword/cpsc-keyword-output.7.md): print values to standard output.
- [cpsc-keyword-if(7)](keyword/cpsc-keyword-if.7.md): choose between branches.
+- [cpsc-keyword-try(7)](keyword/cpsc-keyword-try.7.md): catch or throw exceptions.
- [cpsc-keyword-while(7)](keyword/cpsc-keyword-while.7.md): repeat while a condition is true.
- [cpsc-keyword-repeat(7)](keyword/cpsc-keyword-repeat.7.md): repeat until a condition is true.
- [cpsc-keyword-for(7)](keyword/cpsc-keyword-for.7.md): iterate over an integer range.
@@ -40,7 +43,7 @@ Declare variables before use. See DECLARE for built-in types and arrays, TYPE fo
- [cpsc-keyword-div(7)](keyword/cpsc-keyword-div.7.md): perform integer division.
- [cpsc-keyword-mod(7)](keyword/cpsc-keyword-mod.7.md): find the integer division remainder.
-Closing and auxiliary keywords are documented with their main constructs: THEN/ELSE/ENDIF with IF; DO/ENDWHILE with WHILE; UNTIL with REPEAT; TO/STEP/NEXT with FOR; RETURN/RETURNS/BYVAL/BYREF and function/procedure closing keywords with FUNCTION/PROCEDURE; READ/WRITE/APPEND with OPENFILE; ARRAY/OF and INTEGER/REAL/BOOLEAN/CHAR/STRING with DECLARE; ENDTYPE with TYPE; PUBLIC/PRIVATE/ENDCLASS with CLASS. TRUE/FALSE are BOOLEAN literals.
+Closing and auxiliary keywords are documented with their main constructs: THEN/ELSE/ENDIF with IF; CATCH/ENDTRY/THROW with TRY; DO/ENDWHILE with WHILE; UNTIL with REPEAT; TO/STEP/NEXT with FOR; RETURN/RETURNS/BYVAL/BYREF and function/procedure closing keywords with FUNCTION/PROCEDURE; READ/WRITE/APPEND with OPENFILE; ARRAY/OF and INTEGER/REAL/BOOLEAN/CHAR/STRING with DECLARE; ENDTYPE with TYPE; PUBLIC/PRIVATE/ENDCLASS with CLASS. TRUE/FALSE are BOOLEAN literals.
# BUILT-IN FUNCTIONS
diff --git a/doc/language/function/cpsc-function-eof.7.md b/doc/language/function/cpsc-function-eof.7.md
index 397c713..2f7e271 100644
--- a/doc/language/function/cpsc-function-eof.7.md
+++ b/doc/language/function/cpsc-function-eof.7.md
@@ -12,7 +12,7 @@ EOF(filename : STRING) RETURNS BOOLEAN
# DESCRIPTION
-Return true when no further characters remain in a file opened FOR READ. The test does not consume the next character. Testing an unopened file or a file in the wrong mode terminates execution.
+Return true when no further characters remain in a file opened FOR READ. The test does not consume the next character. Testing an unopened file or a file in the wrong mode throws an exception that TRY/CATCH can handle.
# EXAMPLE
@@ -28,4 +28,4 @@ CLOSEFILE "notes.txt"
# SEE ALSO
-[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-openfile(7)](../keyword/cpsc-keyword-openfile.7.md), [cpsc-keyword-readfile(7)](../keyword/cpsc-keyword-readfile.7.md), [cpsc-keyword-closefile(7)](../keyword/cpsc-keyword-closefile.7.md).
+[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-openfile(7)](../keyword/cpsc-keyword-openfile.7.md), [cpsc-keyword-readfile(7)](../keyword/cpsc-keyword-readfile.7.md), [cpsc-keyword-closefile(7)](../keyword/cpsc-keyword-closefile.7.md), [cpsc-keyword-try(7)](../keyword/cpsc-keyword-try.7.md).
diff --git a/doc/language/keyword/cpsc-keyword-closefile.7.md b/doc/language/keyword/cpsc-keyword-closefile.7.md
index 2359b82..45d321e 100644
--- a/doc/language/keyword/cpsc-keyword-closefile.7.md
+++ b/doc/language/keyword/cpsc-keyword-closefile.7.md
@@ -12,7 +12,7 @@ CLOSEFILE filename
# DESCRIPTION
-Close the file associated with this filename string and release its open-file slot. Closing a filename that is not open terminates execution.
+Close the file associated with this filename string and release its open-file slot. Closing a filename that is not open throws an exception that TRY/CATCH can handle.
# EXAMPLE
@@ -24,4 +24,4 @@ CLOSEFILE "notes.txt"
# SEE ALSO
-[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-openfile(7)](cpsc-keyword-openfile.7.md), [cpsc-keyword-readfile(7)](cpsc-keyword-readfile.7.md), [cpsc-keyword-writefile(7)](cpsc-keyword-writefile.7.md).
+[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-openfile(7)](cpsc-keyword-openfile.7.md), [cpsc-keyword-readfile(7)](cpsc-keyword-readfile.7.md), [cpsc-keyword-writefile(7)](cpsc-keyword-writefile.7.md), [cpsc-keyword-try(7)](cpsc-keyword-try.7.md).
diff --git a/doc/language/keyword/cpsc-keyword-declare.7.md b/doc/language/keyword/cpsc-keyword-declare.7.md
index 0c4ea28..dcaa8ce 100644
--- a/doc/language/keyword/cpsc-keyword-declare.7.md
+++ b/doc/language/keyword/cpsc-keyword-declare.7.md
@@ -15,7 +15,7 @@ DECLARE name : ARRAY[lower:upper, ...] OF type
Declare one variable per statement. Built-in types are INTEGER (signed 64-bit), REAL, BOOLEAN, CHAR (one byte), and STRING. User-defined TYPE and CLASS names are also accepted. Variables start with zero, FALSE, an empty string, or a zero-initialized user-defined value.
-ARRAY bounds are inclusive and may be expressions; multiple ranges create multiple dimensions. Access elements with `name[index, ...]`. Bounds are checked at run time. Whole-array assignment is unsupported. Declare arrays before using them; arrays declared inside a control-flow block stay in that block.
+ARRAY bounds are inclusive and may be expressions; multiple ranges create multiple dimensions. Access elements with `name[index, ...]`. Bounds are checked at run time; an invalid index throws an exception that TRY/CATCH can handle. Whole-array assignment is unsupported. Declare arrays before using them; arrays declared inside a control-flow block stay in that block.
# EXAMPLE
@@ -29,4 +29,4 @@ OUTPUT Names[Count]
# SEE ALSO
-[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-type(7)](cpsc-keyword-type.7.md), [cpsc-keyword-class(7)](cpsc-keyword-class.7.md), [cpsc-keyword-output(7)](cpsc-keyword-output.7.md).
+[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-type(7)](cpsc-keyword-type.7.md), [cpsc-keyword-class(7)](cpsc-keyword-class.7.md), [cpsc-keyword-output(7)](cpsc-keyword-output.7.md), [cpsc-keyword-try(7)](cpsc-keyword-try.7.md).
diff --git a/doc/language/keyword/cpsc-keyword-div.7.md b/doc/language/keyword/cpsc-keyword-div.7.md
index f033633..af2443d 100644
--- a/doc/language/keyword/cpsc-keyword-div.7.md
+++ b/doc/language/keyword/cpsc-keyword-div.7.md
@@ -12,7 +12,7 @@ left DIV right
# DESCRIPTION
-Both operands must be INTEGER. Return the quotient truncated toward zero. A zero divisor terminates execution. DIV has the same precedence as multiplication. Use `/` for division returning REAL.
+Both operands must be INTEGER. Return the quotient truncated toward zero. A zero divisor throws an exception that TRY/CATCH can handle. DIV has the same precedence as multiplication. Use `/` for division returning REAL.
# EXAMPLE
@@ -22,4 +22,4 @@ OUTPUT 7 DIV 2
# SEE ALSO
-[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-mod(7)](cpsc-keyword-mod.7.md).
+[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-mod(7)](cpsc-keyword-mod.7.md), [cpsc-keyword-try(7)](cpsc-keyword-try.7.md).
diff --git a/doc/language/keyword/cpsc-keyword-mod.7.md b/doc/language/keyword/cpsc-keyword-mod.7.md
index ea1b431..45267e2 100644
--- a/doc/language/keyword/cpsc-keyword-mod.7.md
+++ b/doc/language/keyword/cpsc-keyword-mod.7.md
@@ -12,7 +12,7 @@ left MOD right
# DESCRIPTION
-Both operands must be INTEGER. Return the remainder after division truncated toward zero; a nonzero remainder has the dividend's sign. A zero divisor terminates execution. MOD has the same precedence as multiplication.
+Both operands must be INTEGER. Return the remainder after division truncated toward zero; a nonzero remainder has the dividend's sign. A zero divisor throws an exception that TRY/CATCH can handle. MOD has the same precedence as multiplication.
# EXAMPLE
@@ -22,4 +22,4 @@ OUTPUT 7 MOD 2
# SEE ALSO
-[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-div(7)](cpsc-keyword-div.7.md).
+[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-div(7)](cpsc-keyword-div.7.md), [cpsc-keyword-try(7)](cpsc-keyword-try.7.md).
diff --git a/doc/language/keyword/cpsc-keyword-openfile.7.md b/doc/language/keyword/cpsc-keyword-openfile.7.md
index c2c3743..6df521f 100644
--- a/doc/language/keyword/cpsc-keyword-openfile.7.md
+++ b/doc/language/keyword/cpsc-keyword-openfile.7.md
@@ -16,7 +16,7 @@ OPENFILE filename FOR APPEND
The filename is a STRING expression. READ opens an existing file for reading; WRITE creates or truncates a file; APPEND writes at the end, creating the file if needed. READ, WRITE, APPEND, and this use of FOR belong here.
-At most 16 files may be open at once. Use the same filename string for subsequent operations. Reopening an open filename, failed opens, and wrong-mode operations terminate execution with a fatal diagnostic. RANDOM is reserved but unsupported.
+At most 16 files may be open at once. Use the same filename string for subsequent operations. Reopening an open filename, failed opens, exceeding the open-file limit, and wrong-mode operations throw exceptions that TRY/CATCH can handle. RANDOM is reserved but unsupported.
# EXAMPLE
@@ -28,4 +28,4 @@ CLOSEFILE "notes.txt"
# SEE ALSO
-[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-readfile(7)](cpsc-keyword-readfile.7.md), [cpsc-keyword-writefile(7)](cpsc-keyword-writefile.7.md), [cpsc-keyword-closefile(7)](cpsc-keyword-closefile.7.md).
+[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-readfile(7)](cpsc-keyword-readfile.7.md), [cpsc-keyword-writefile(7)](cpsc-keyword-writefile.7.md), [cpsc-keyword-closefile(7)](cpsc-keyword-closefile.7.md), [cpsc-keyword-try(7)](cpsc-keyword-try.7.md).
diff --git a/doc/language/keyword/cpsc-keyword-readfile.7.md b/doc/language/keyword/cpsc-keyword-readfile.7.md
index de0e849..200fc68 100644
--- a/doc/language/keyword/cpsc-keyword-readfile.7.md
+++ b/doc/language/keyword/cpsc-keyword-readfile.7.md
@@ -12,7 +12,7 @@ READFILE filename, variable
# DESCRIPTION
-Read one line from a file opened FOR READ, removing the line ending. The target must be a plain, previously declared STRING variable. Guard reads with EOF; reading past the end terminates execution.
+Read one line from a file opened FOR READ, removing the line ending. The target must be a plain, previously declared STRING variable. Guard reads with EOF; reading past the end, using an unopened file, or using the wrong mode throws an exception that TRY/CATCH can handle.
# EXAMPLE
@@ -28,4 +28,4 @@ CLOSEFILE "notes.txt"
# SEE ALSO
-[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-openfile(7)](cpsc-keyword-openfile.7.md), [cpsc-keyword-closefile(7)](cpsc-keyword-closefile.7.md), [cpsc-function-eof(7)](../function/cpsc-function-eof.7.md).
+[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-openfile(7)](cpsc-keyword-openfile.7.md), [cpsc-keyword-closefile(7)](cpsc-keyword-closefile.7.md), [cpsc-function-eof(7)](../function/cpsc-function-eof.7.md), [cpsc-keyword-try(7)](cpsc-keyword-try.7.md).
diff --git a/doc/language/keyword/cpsc-keyword-try.7.md b/doc/language/keyword/cpsc-keyword-try.7.md
new file mode 100644
index 0000000..3b48dc4
--- /dev/null
+++ b/doc/language/keyword/cpsc-keyword-try.7.md
@@ -0,0 +1,48 @@
+% CPSC-KEYWORD-TRY(7)
+
+# NAME
+
+cpsc-keyword-try - catch exceptions with stack unwinding
+
+# SYNOPSIS
+
+```text
+TRY
+ statements
+CATCH
+ handler statements
+ENDTRY
+
+THROW
+```
+
+# DESCRIPTION
+
+TRY, CATCH, ENDTRY, and THROW are compiler extensions to Cambridge pseudocode. Each TRY requires exactly one CATCH and an ENDTRY. CATCH takes no type or error variable; there is no error object or what() support.
+
+Normal execution skips CATCH. An exception stops the current operation, unwinds function and procedure calls, and enters the nearest active CATCH. After the handler completes, execution continues after ENDTRY. Blocks may nest and may appear inside functions, procedures, and loops.
+
+THROW takes no argument and always raises a new anonymous exception. An exception raised inside CATCH propagates to an enclosing TRY or a caller's handler; the same CATCH cannot catch it again. RETURN may leave either body, and leaving a handler finishes its catch state.
+
+Runtime checks throw the same anonymous exceptions for division by zero, array index errors, null pointer dereferences, invalid enumeration values, array-parameter bounds mismatches, and file misuse. Compiler errors cannot be caught. A caught exception prints nothing automatically. If no handler catches an exception, the program prints `Unhandled exception. Program terminated.` to standard output and returns status 1.
+
+Arrays declared inside either body remain local to that block. Stack unwinding does not automatically free heap strings or arrays, close files, or run user-defined destructors. Exception execution is verified on Linux with LLVM 18; Windows and macOS remain unverified.
+
+# EXAMPLE
+
+```text
+PROCEDURE Fail()
+ THROW
+ENDPROCEDURE
+
+TRY
+ CALL Fail()
+CATCH
+ OUTPUT "Caught an exception"
+ENDTRY
+OUTPUT "Continuing"
+```
+
+# SEE ALSO
+
+[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-function(7)](cpsc-keyword-function.7.md), [cpsc-keyword-procedure(7)](cpsc-keyword-procedure.7.md), [cpsc(1)](../../cpsc.1.md).
diff --git a/doc/language/keyword/cpsc-keyword-type.7.md b/doc/language/keyword/cpsc-keyword-type.7.md
index 5159dd5..cdc41ff 100644
--- a/doc/language/keyword/cpsc-keyword-type.7.md
+++ b/doc/language/keyword/cpsc-keyword-type.7.md
@@ -16,9 +16,9 @@ ENDTYPE
# DESCRIPTION
-Define types at top level. Enumeration values have globally reserved names; OUTPUT prints their zero-based ordinals. Enumerations support comparisons with the same type and addition/subtraction of INTEGER offsets; invalid ordinals terminate execution.
+Define types at top level. Enumeration values have globally reserved names; OUTPUT prints their zero-based ordinals. Enumerations support comparisons with the same type and addition/subtraction of INTEGER offsets; invalid ordinals throw exceptions that TRY/CATCH can handle.
-A prefix caret takes an address (`^variable`); a suffix caret dereferences a pointer (`pointer^`). Pointee types must match exactly, even where guide section 4.2's example mixes types. Pointers start null; dereferencing null terminates execution. There is no NULL literal. Avoid pointers to local variables after the function returns.
+A prefix caret takes an address (`^variable`); a suffix caret dereferences a pointer (`pointer^`). Pointee types must match exactly, even where guide section 4.2's example mixes types. Pointers start null; dereferencing null throws an exception. There is no NULL literal. Avoid pointers to local variables after the function returns.
Records contain one or more DECLARE fields and end with ENDTYPE. Access fields with a dot. Whole records copy by value; array fields are unsupported. Distinct user-defined type names remain distinct types. SET/DEFINE are reserved but unsupported.
@@ -39,4 +39,4 @@ OUTPUT Position.X
# SEE ALSO
-[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-declare(7)](cpsc-keyword-declare.7.md), [cpsc-keyword-class(7)](cpsc-keyword-class.7.md).
+[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-declare(7)](cpsc-keyword-declare.7.md), [cpsc-keyword-class(7)](cpsc-keyword-class.7.md), [cpsc-keyword-try(7)](cpsc-keyword-try.7.md).
diff --git a/doc/language/keyword/cpsc-keyword-writefile.7.md b/doc/language/keyword/cpsc-keyword-writefile.7.md
index d3ce2c4..ae0f7e6 100644
--- a/doc/language/keyword/cpsc-keyword-writefile.7.md
+++ b/doc/language/keyword/cpsc-keyword-writefile.7.md
@@ -12,7 +12,7 @@ WRITEFILE filename, expression
# DESCRIPTION
-Write one value as text followed by a newline. Open the file FOR WRITE or APPEND first. Exactly one data expression is accepted; join strings with `&` when needed. Records, classes, and pointers are unsupported as data.
+Write one value as text followed by a newline. Open the file FOR WRITE or APPEND first. Exactly one data expression is accepted; join strings with `&` when needed. Records, classes, and pointers are unsupported as data. An unopened file or a file opened FOR READ throws an exception that TRY/CATCH can handle.
# EXAMPLE
@@ -24,4 +24,4 @@ CLOSEFILE "notes.txt"
# SEE ALSO
-[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-openfile(7)](cpsc-keyword-openfile.7.md), [cpsc-keyword-closefile(7)](cpsc-keyword-closefile.7.md), [cpsc-function-num_to_str(7)](../function/cpsc-function-num_to_str.7.md).
+[cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-openfile(7)](cpsc-keyword-openfile.7.md), [cpsc-keyword-closefile(7)](cpsc-keyword-closefile.7.md), [cpsc-function-num_to_str(7)](../function/cpsc-function-num_to_str.7.md), [cpsc-keyword-try(7)](cpsc-keyword-try.7.md).