feat(errors): panic(msg) + assert(cond, msg) with file:line — no raw crashes (#8)
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:
parent
ea2c6ab246
commit
c7c8e2779c
13 changed files with 5102 additions and 4778 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ var g_uses_unicodert: bool = false # Unicode.* was emitted -> emit the UTF-8 ru
|
|||
var g_uses_fsrt: bool = false # Fs.*/Path.*/Mime.* was emitted -> emit the filesystem runtime
|
||||
var g_uses_datert: bool = false # Date.*/DateTime.* was emitted -> emit the civil<->epoch conversions
|
||||
var g_uses_expect: bool = false # expect/expect_eq/expect_near was emitted -> emit the test-assert globals
|
||||
var g_uses_panic: bool = false # panic/assert was emitted -> declare @fprintf + the panic format
|
||||
var g_tests: []Node # test "name" { ... } blocks collected by the parser
|
||||
var g_src_name: pointer = "?" # base name of the source file, for panic/expect file:line messages
|
||||
var g_uses_longstr: bool = false # string(long) / interpolating a long was emitted -> emit fn_long_str
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ function emit_program() -> void {
|
|||
g_uses_strslice = false
|
||||
g_uses_loopback = false
|
||||
g_uses_expect = false
|
||||
g_uses_panic = false
|
||||
loc_name = new []pointer; loc_reg = new []pointer; loc_ty = new []pointer; loc_mut = new []int
|
||||
brk_lbl = new []pointer; cnt_lbl = new []pointer
|
||||
self_stk = new []pointer
|
||||
|
|
@ -184,6 +185,10 @@ function emit_program() -> void {
|
|||
if g_uses_unicodert { emit_unicode_prelude() } # @fn_uni_len/valid/decode/case/truncate/grapheme (UTF-8)
|
||||
if g_uses_fsrt { emit_fs_prelude() } # @fn_fs_*/fn_path_*/fn_mime_* (libc + string ops)
|
||||
if g_uses_datert { emit_datetime_prelude() } # @fn_days_from_civil / @fn_civil_from_days conversions
|
||||
if g_uses_panic { # panic/assert: located abort to stderr
|
||||
emith("declare i32 @fprintf(ptr, ptr, ...)\n")
|
||||
emith("@.fmt_panic = private unnamed_addr constant [6 x i8] c\"%s%s\\0A\\00\"\n")
|
||||
}
|
||||
}
|
||||
|
||||
# Flush the emitted IR. With a null path it goes to stdout (the pipe the shell
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue