Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ All notable changes to EigenScript are documented here.
## [Unreleased]

### Added
- **Structured runtime errors (#406, BREAKING)** — `catch` now binds a
`{kind, message, line}` dict for built-in runtime errors instead of
the flat `"Error line N: ..."` string: `kind` from a closed,
SPEC-enumerated vocabulary (`undefined_name`, `type_mismatch`,
`value`, `index_range`, `parse`, `io`, `limit`, `sandbox`,
`interrupt`, `assert`, `internal`), `message` without the line frame,
`line` 1-based. Discriminating error classes no longer means string
matching. User-thrown values bind untouched, as before; uncaught
stderr output is unchanged. Also: builtin raise sites now stamp the
live source line (previously `Error line 0:`), `assert` failures are
ordinary catchable errors (kind `assert`; caught asserts no longer
print to stderr), `sandbox_run`'s result carries `"error": {kind,
message, line}` when `ok` is 0, and the embed API gains
`eigs_last_error_kind()` / `eigs_last_error_line()`. The kind table
lives in docs/DIAGNOSTICS.md; SPEC.md Error handling rewritten with
executable examples (suite EM26–EM30).
- **Parse errors print a source excerpt with a caret under the offending
column (#407, increment two)** — for the column-carrying errors
(`expected X, got Y`, one-statement-per-line), in the script runner,
Expand Down
7 changes: 5 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,11 @@ observer/deterministic-replay niche instead of diluting it.**
Fully executed: shipped in 0.27.0 with lint `W017` as the migration
audit; the ouroboros `frontend.eigs` parity leg landed at the
v0.27.0 pin bump (ouroboros PR #68)
- [ ] Structured runtime errors `{kind, message, line}` with a closed
kind set ([#406](https://github.com/InauguralSystems/EigenScript/issues/406))
- [x] Structured runtime errors `{kind, message, line}` with a closed
kind set ([#406](https://github.com/InauguralSystems/EigenScript/issues/406)):
11 kinds (no `arity` — calls pad by design), user throws bind
untouched, uncaught output byte-unchanged; kind-typo lint is the
follow-up ([#469](https://github.com/InauguralSystems/EigenScript/issues/469))
- [~] Column tracking + caret/span diagnostics — parse errors carry
line:col (human + `--lint --json` E002 + LSP range) AND print a
source excerpt + caret (increment 2); per-warning spans and
Expand Down
12 changes: 10 additions & 2 deletions docs/COMPARISON.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,17 +226,25 @@ except Exception as e:
print(f"caught: {e}")
```

EigenScript — errors are strings; `throw of` raises, `catch name:`
binds:
EigenScript — `throw of` raises, `catch name:` binds. A thrown value
binds unchanged; a built-in runtime error binds a `{kind, message,
line}` dict with `kind` from a closed vocabulary (no exception
hierarchy — discriminate on the kind string):

```eigenscript
try:
throw of "boom"
catch e:
print of f"caught: {e}"

try:
x is [1] - 1
catch e:
print of f"caught: {e.kind} at line {e.line}"
```
```output
caught: boom
caught: type_mismatch at line 7
```

## Pipes (vs Lisp threading / shell pipes)
Expand Down
40 changes: 35 additions & 5 deletions docs/DIAGNOSTICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

EigenScript reports errors with source line numbers and consistent
formatting. This document defines the error contract. The behaviors
here are enforced by the suite (`run_all_tests.sh` EM1–EM23 and
here are enforced by the suite (`run_all_tests.sh` EM1–EM30 and
`examples/errors/`).

## Error Categories
Expand All @@ -12,9 +12,9 @@ here are enforced by the suite (`run_all_tests.sh` EM1–EM23 and
| Syntax error | `Syntax error line N: ...` | 1 | Tokenizer problem. Accumulates all errors, aborts before execution. |
| Parse error | `Parse error line N: ...` | 1 | Parser problem. Accumulates all errors, aborts before execution. |
| Runtime error (uncaught) | `Error line N: ...` + stack trace | 1 | Type mismatch, undefined variable, index out of bounds, non-callable, uncaught `throw`. Prints to stderr and **halts** — no statement after the error runs. |
| Runtime error (caught) | bound to the `catch` variable | — | Recoverable with `try`/`catch`; execution continues in the handler. |
| Runtime error (caught) | `{kind, message, line}` dict bound to the `catch` variable | — | Recoverable with `try`/`catch`; execution continues in the handler. `kind` is from the closed set below. |
| Warning | `Warning line N: ...` | 0 | Recoverable issue (division by zero yields `0`). Prints to stderr, continues execution. |
| Assertion | `ASSERT FAIL: ...` | 1 | Intentional termination via `assert`. |
| Assertion | `Error line N: ASSERT FAIL: ...` | 1 | `assert` failure — an ordinary runtime error with kind `assert` (catchable). |

Parse errors prevent execution entirely — the program never runs if
parsing fails. Warnings allow execution to continue; **uncaught runtime
Expand All @@ -38,12 +38,42 @@ Caught errors print nothing; the handler decides.

## What `catch` binds

- A **runtime error** binds its message string
(`"Error line 2: cannot apply '-' to list and num"`).
- A **built-in runtime error** binds a `{kind, message, line}` dict
(#406): `kind` from the closed vocabulary below, `message` without
the `Error line N:` frame (`"cannot apply '-' to list and num"`),
`line` 1-based. Discriminate on `kind`, never on message text —
messages are wording, kinds are contract.
- A **thrown value** binds unchanged: `throw of {"kind": "validation"}`
gives the catch variable that dict — match on fields instead of
substring-searching a message. Thrown strings bind as strings.

## Runtime error kinds (closed set)

Every built-in runtime error carries exactly one kind. The set is
CLOSED by design — the same instinct as the closed trajectory
vocabulary. Extending it is a SPEC + DIAGNOSTICS change, not a
patch-release tweak. (`ErrKind` in `src/eigenscript.h`; the strings
below are the contract.)

| kind | meaning | examples |
|------|---------|----------|
| `undefined_name` | no binding for a name | `undefined variable 'x'` |
| `type_mismatch` | operation/argument of the wrong type | `cannot apply '-' to list and num`, `bit_not expects a number` |
| `value` | right type, unacceptable value | `index must be an integer, got 1.5`, `chr of 0`, invalid channel |
| `index_range` | index/slice outside bounds | `index 10 out of range (list length 3)` |
| `parse` | runtime-surfaced parse/compile failure | `eval: parse error in code string`, `import: parse errors in 'm'` |
| `io` | the outside world failed | `import: cannot read 'm'`, `store_open: cannot create`, thread-create failure |
| `limit` | engine resource cap hit | `call stack overflow`, `store_put: record too large`, route table full |
| `sandbox` | sandbox policy denial or budget | `blocked in sandbox`, `sandbox memory budget exceeded` |
| `interrupt` | host-requested abort (`eigs_abort`) | `aborted` |
| `assert` | `assert` builtin failure | `ASSERT FAIL: ...` |
| `internal` | VM invariant broke — report it | `unknown opcode`, `SET_LOCAL slot out of range` |

`throw` stamps kind `user` for host/embed introspection
(`eigs_last_error_kind`), but a catch never sees it — the thrown value
itself binds. `sandbox_run` reports failures with the same shape: its
result dict gains `"error": {kind, message, line}` when `ok` is 0.

## Exit Codes

| Situation | Exit Code |
Expand Down
8 changes: 7 additions & 1 deletion docs/EMBEDDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,16 @@ if (!r) {
const char *eigs_last_error_message(void); /* NULL when no error */
int eigs_has_error(void);
void eigs_clear_error(void);
/* #406 structured errors: kind from the closed vocabulary in
* docs/DIAGNOSTICS.md ("user" for `throw`), NULL when no error;
* 1-based line, 0 when unknown. */
const char *eigs_last_error_kind(void);
int eigs_last_error_line(void);
```

The returned message pointer is owned by the thread state — copy it if
you need to keep it past the next API call.
you need to keep it past the next API call. (The kind string itself is
static.)

## Aborting a running eval

Expand Down
69 changes: 59 additions & 10 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -520,19 +520,21 @@ print of (a + b + c)
6
```

Out-of-range indexing is a runtime error (catchable with `try`):
Out-of-range indexing is a runtime error (catchable with `try`; the
caught value is a `{kind, message, line}` dict — see
[Error handling](#error-handling)):

```eigenscript
xs is [1, 2]
try:
v is xs[10]
catch e:
print of "caught:"
print of e
print of e.kind
print of e.message
```
```output
caught:
Error line 3: index 10 out of range (list length 2)
index_range
index 10 out of range (list length 2)
```

## Dictionaries
Expand Down Expand Up @@ -802,8 +804,12 @@ expressions match too

## Error handling

`try:` / `catch name:` captures runtime errors; the caught message is
bound as a string. `throw of value` raises a user error. An *uncaught*
`try:` / `catch name:` captures runtime errors. A **built-in** runtime
error binds a small dict `{kind, message, line}`: `kind` is drawn from
a closed vocabulary (below), `message` is the error text without the
`Error line N:` frame, `line` is the 1-based source line. `throw of
value` raises a user error and the catch variable binds the thrown
value itself, unchanged — a thrown string stays a string. An *uncaught*
runtime error stops the program with a nonzero exit; warnings (like
division by zero) do not.

Expand All @@ -816,19 +822,62 @@ catch e:
try:
x is undefined_name
catch e:
print of e
print of e.kind
print of e.message
print of e.line

print of "execution continues"
```
```output
caught: custom failure
Error line 7: undefined variable 'undefined_name'
undefined_name
undefined variable 'undefined_name'
7
execution continues
```

The kind set is **closed** — the same design instinct as the closed
trajectory vocabulary. Every built-in runtime error carries exactly one
of:

| kind | raised by |
|------|-----------|
| `undefined_name` | reading a name with no binding |
| `type_mismatch` | an operation or builtin argument of the wrong type |
| `value` | right type, unacceptable value (fractional index, `chr of 0`) |
| `index_range` | index or slice outside the target's bounds |
| `parse` | runtime-surfaced parse/compile failure (`eval`, `import`, `load_file`) |
| `io` | the outside world failed: files, stores, sockets, threads |
| `limit` | an engine resource cap: stack overflow, size caps |
| `sandbox` | sandbox policy denial or budget exhaustion |
| `interrupt` | host-requested abort |
| `assert` | `assert` builtin failure |
| `internal` | a VM invariant broke (report it) |

Discriminate on `kind`, not on message text — messages are wording,
kinds are contract:

```eigenscript
xs is [1, 2, 3]
define get(i) as:
try:
return xs[i]
catch e:
if e.kind == "index_range":
return null
throw of e

print of (get of 1)
print of (get of 99)
```
```output
2
null
```

`throw` preserves the thrown *value*: throw a dict (or list) and the
catch variable binds it unchanged, so errors can carry data and be
matched on fields. Runtime errors and thrown strings bind as strings.
matched on fields. Thrown strings bind as strings.

```eigenscript
define validate(age) as:
Expand Down
33 changes: 33 additions & 0 deletions examples/catching_by_kind.eigs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Structured runtime errors (#406): a built-in runtime error binds a
# {kind, message, line} dict to the catch variable. `kind` is drawn
# from a closed vocabulary (docs/DIAGNOSTICS.md) — discriminate on it
# instead of substring-matching message text. Thrown values still bind
# untouched. This example runs clean (exit 0): every error is caught.

xs is [10, 20, 30]

define get_or_null(i) as:
try:
return xs[i]
catch e:
if e.kind == "index_range":
return null
# anything else is unexpected here — re-raise it
throw of e

print of (get_or_null of 1)
print of (get_or_null of 99)

# The dict carries the bare message and the failing line too:
try:
print of ([1] - 1)
catch e:
print of e.kind
print of e.message
print of e.line

# User throws are untouched — a thrown dict binds as that dict:
try:
throw of {"kind": "validation", "field": "age"}
catch e:
print of e.field
Loading
Loading