Skip to content

feat(errors): structured runtime errors — catch binds {kind, message, line} (#406)#468

Merged
InauguralPhysicist merged 2 commits into
mainfrom
feat-406-structured-errors
Jul 7, 2026
Merged

feat(errors): structured runtime errors — catch binds {kind, message, line} (#406)#468
InauguralPhysicist merged 2 commits into
mainfrom
feat-406-structured-errors

Conversation

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator

Closes #406

What

catch binds a small dict {kind, message, line} for built-in runtime errors; kind comes from a closed, SPEC-enumerated vocabulary. User-thrown values bind untouched. No finally, no exception hierarchy — the closed kind vocabulary is the same design instinct as the closed trajectory vocabulary. BREAKING (v0.28.0 train), no backcompat shims.

The closed kind set

undefined_name · type_mismatch · value · index_range · parse · io · limit · sandbox · interrupt · assert · internal — table with semantics in docs/DIAGNOSTICS.md. Notably no arity kind: user fn calls pad with null by design, so builtin argument-contract violations are type_mismatch.

How

  • runtime_error(line, fmt)rt_error(kind, line, fmt) — the rename forces every raise site through classification (an added-first-enum-param would have silently compiled rt_error(5, "msg") with the int line as kind). All 173 sites bucketed.
  • Catch dict built lazily in vm_take_error_value; the uncaught path never allocates. Uncaught stderr output is byte-unchanged (EM1–EM25 untouched).
  • Builtin raises (line 0) now stamp the VM's live line — kills the Error line 0: wart.
  • assert routes through rt_error (kind assert): catchable, and caught asserts no longer print to stderr.
  • sandbox_run result gains "error": {kind, message, line} on ok: 0 — the iLambdaAi graded ladder can discriminate sandbox denial vs type error vs bad descriptor without re-running.
  • Embed API (additive): eigs_last_error_kind(), eigs_last_error_line().

Verification

  • Release suite 2603/2603, incl. new EM26–EM30 (kind binding, bare message, line field, thrown-string untouched, catchable assert)
  • ASan+UBSan suite 2603/2603 with detect_leaks=1, leak tally 0
  • make http / make lsp / make jit-smoke / make freestanding-check / amalgamation + embed-smoke / tools/embed_stack_soak.sh all green
  • Executable docs: 63/63 SPEC+COMPARISON+CONCURRENCY examples byte-exact
  • make full (db) not buildable locally (no libpq on this box — pre-existing); the ext_db.c diff is two mechanical call rewrites, release CI builds that variant

Consumer audit (pre-landing, all 10 + EigenOS)

Zero breaking catch sites. No consumer string-matches builtin error text or discriminates via type of e. Bump-time ledger for v0.28.0:

  • ouroboros frontend.eigs:1896 (+ iLambdaAi vendored ouro_frontend.eigs:1584) — the self-hosted interpreter must mirror the dict binding for its own raised errors to stay byte-exact vs the VM oracle
  • Tidepool tidepool.eigs:1067 — print of f"Render error: {err}" output becomes a dict repr (cosmetic)
  • in-repo suite migration pattern (string-match → .message/.kind) is the template for any consumer that grows one later

Follow-up (separate, per the issue)

Lint warning on comparing e.kind against a string outside the closed set — filing next.

🤖 Generated with Claude Code

InauguralPhysicist and others added 2 commits July 7, 2026 04:45
… line} (#406)

BREAKING: catch now binds a {kind, message, line} dict for BUILT-IN
runtime errors instead of the flat "Error line N: ..." string. kind is
drawn from a closed, SPEC-enumerated vocabulary (undefined_name,
type_mismatch, value, index_range, parse, io, limit, sandbox,
interrupt, assert, internal) — discriminating error classes no longer
means string matching (the silent-tolerance bug class). User-thrown
values bind untouched, exactly as before. Uncaught stderr output is
unchanged, so the EM corpus survives byte-for-byte.

Mechanics: runtime_error(line, fmt) is renamed rt_error(kind, line,
fmt) — the rename forces every raise site through classification; all
173 sites across vm.c/builtins.c/ext_store.c/ext_db.c/ext_http.c are
bucketed. The catch dict is built lazily in vm_take_error_value (heap
dict, owned children — no allocation on the uncaught path). Builtin
raise sites (line 0) now stamp the VM's live line, fixing the
"Error line 0:" wart.

Also:
- assert failures are ordinary catchable errors (kind assert); caught
  asserts no longer leak an "ASSERT FAIL" line to stderr
- sandbox_run's result dict gains "error": {kind, message, line} when
  ok is 0 (the graded ladder can discriminate denial vs type vs parse)
- embed API: eigs_last_error_kind() / eigs_last_error_line(); throw
  stamps kind "user" for host introspection only
- SPEC.md Error handling rewritten with executable examples;
  DIAGNOSTICS.md gains the closed kind table; COMPARISON/EMBEDDING
  synced; examples/catching_by_kind.eigs added
- suite: EM26–EM30 assert the new binding; ~75 test string-matches
  migrated to .message/.kind (user-throw sites deliberately left)

Consumer audit (all 10 + EigenOS): zero breaking catch sites. Parity
note for the v0.28.0 bump: ouroboros frontend.eigs (+ the iLambdaAi
vendored copy) must mirror the dict binding for interpreter-raised
errors; Tidepool tidepool.eigs:1067 render-error print becomes a dict
repr (cosmetic).

Closes #406

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@InauguralPhysicist InauguralPhysicist merged commit e8e4860 into main Jul 7, 2026
17 checks passed
@InauguralPhysicist InauguralPhysicist deleted the feat-406-structured-errors branch July 7, 2026 09:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Structured runtime errors: catch binds {kind, message, line} with a closed kind set

1 participant