feat(errors): panic(msg) + assert(cond, msg) with file:line — no raw crashes (#8)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 17s
ci / build-and-test (push) Successful in 1m13s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 19s

RFC decision (the split the issue recommended): programmer bugs abort loud and
located; recoverable failures become values. This ships the first half.

panic(msg) prints `file:line: panic: <msg>` to stderr and aborts the process with
exit code 1 — a clear, located error instead of a segfault or a silent wrong
result. assert(cond, msg) is the guarded form: it aborts with `file:line:
assertion failed: <msg>` only when cond is false, otherwise execution continues.
The location is baked in at compile time (the call node carries its source line,
g_src_name carries the file); the message is any string.

Both lower in emit_call to an fprintf-to-stderr + exit(1) + unreachable tail
(assert branches on the condition first). @fprintf and the format constant are
declared on demand (g_uses_panic), so a program that never panics is unchanged —
and the compiler's own source uses neither, so the C-free bootstrap fixpoint holds.

- panic/assert registered as builtins across the vocabulary (ludic_syntax.h, the
  JetBrains lexer, the TextMate grammar) and documented (docs/language/builtins/)
- examples/library/errors.ludic covers the success path (asserts hold, program
  runs to the end); a panic_case in the suite covers the failure path (non-zero
  exit + the located stderr message). x test is now 69 checks.

Deferred: recoverable failures as `try`/`else` values (needs the tagged-union
type system, #1) and a top-level `recover` for the dev game loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-31 14:33:37 +03:00
parent ea2c6ab246
commit c7c8e2779c
13 changed files with 5102 additions and 4778 deletions

View file

@ -448,6 +448,37 @@ function emit_call(e: Node) -> Val {
emit_expect_fail(c, msg, a.code, b.code)
return val("0", "void")
}
# --- error handling: panic / assert (issue #8) ------------------------------
# panic(msg) prints `file:line: panic: <msg>` to stderr and aborts the process
# cleanly (exit 1) — a located, human error instead of a raw crash. assert(cond,
# msg) is the same, guarded: it aborts only when `cond` is false (a programmer
# bug — an index out of range, an invariant broken). The location is baked in at
# compile time; the message is any string.
if (name == "panic") {
g_uses_panic = true
let m = emit_expr(e.kids[0])
let prefix = emit_str_const(`{g_src_name}:{itoa(e.line)}: panic: `)
let se = emit_bind("load ptr, ptr @__stderrp")
emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {se}, ptr @.fmt_panic, ptr {prefix}, ptr {m.code})\n`)
emit(" call void @exit(i32 1)\n unreachable\n")
g_term = true
return val("0", "void")
}
if (name == "assert") {
g_uses_panic = true
let c = emit_expr(e.kids[0])
let cond = emit_bind(`icmp ne i32 {c.code}, 0`)
let lok = lbl("asok"); let lbad = lbl("asbad")
emit(` br i1 {cond}, label %{lok}, label %{lbad}\n`)
emit(`{lbad}:\n`)
let m = emit_expr(e.kids[1])
let prefix = emit_str_const(`{g_src_name}:{itoa(e.line)}: assertion failed: `)
let se = emit_bind("load ptr, ptr @__stderrp")
emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {se}, ptr @.fmt_panic, ptr {prefix}, ptr {m.code})\n`)
emit(" call void @exit(i32 1)\n unreachable\n")
emit(`{lok}:\n`)
return val("0", "void")
}
# The EV2 reflection ABI (the world table), exposed to Ludic so a Ludic mod can
# introspect the world by name — the same functions a foreign mod binds. Emitted
# only for a modding program (ECS + events), so a plain game is unchanged.