Proposal: error handling for games — try/else values + panic/recover (no crashes for non-experts) #8
Labels
No labels
area:ci
area:docs
area:input
area:net
area:rendering
area:repo
area:stdlib
area:tooling
area:types
cleanup
dx
priority:high
priority:low
priority:medium
proposal
status:in-progress
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: workshopsoft/ludic#8
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Give Ludic a first-class error story so that "something went wrong"
(a missing save file, a bad network packet, a divide-by-zero, an out-of-range
list index) is a value a game can recover from — not a crash and not a silent
wrong result.
This is a language-level feature (new keywords + lowering), so it belongs
before the stdlib libraries that will lean on it (Filesystem, Net, Time parsing,
Regex, JSON).
Why it matters for game devs (who aren't systems programmers)
Non-expert developers should never see a raw segfault. When a save is corrupt or
a mod file is malformed, the game should be able to say "couldn't load that,
here's the fallback" in one or two obvious lines — no boilerplate, no undefined
behavior.
Design direction (needs an RFC before coding)
Two models are on the table; we should pick one and keep it small:
Result-style values (Go/Rust flavored, no unwinding) — a function that can
fail returns a value carrying either an
okpayload or anerr. Ergonomicsugar makes the happy path short:
panic / recover for programmer mistakes (index out of range, assertion
failed) — always logs file:line, and by default aborts the frame cleanly
rather than corrupting the ECS world. A top-level
recoverlets a game loopsurvive a bad frame in dev builds.
Recommended split: recoverable failures = values (
try/else, no hiddencontrol flow), bugs = panic (loud, with location). This keeps determinism
and avoids exceptions-as-goto, which non-experts (and the ECS scheduler) find
hard to reason about.
Considerations
file:lineand a human sentence.errtype / tagged union).Scope / acceptance
.lloutput.panic(msg)builtin with file:line; documented abort behavior.Related: #1 (type system / tagged unions), #2 (stdlib), and every I/O library below.
Shipped in
c7c8e27— the first half of the error story: programmer bugs abort loud and located instead of crashing.RFC decision. Per the issue's own recommendation, the model is the split: bugs → panic (loud, with
file:line), recoverable failures → values (try/else, no hidden control flow). This keeps determinism and avoids exceptions-as-goto. Because value-carryingtry/elseneeds a canonicalerr/tagged-union type — the issue notes the interop with #1 — that half is sequenced after #1. This change ships the panic half, which stands alone.What landed:
panic(msg)— printsfile:line: panic: <msg>to stderr and aborts with exit code 1. A clear, located message a non-expert can act on, never a raw segfault or a silent wrong result.assert(cond, msg)— the guarded form: aborts withfile:line: assertion failed: <msg>only whencondis false; otherwise execution continues. For the invariants the language should catch loudly (index in range, value non-negative, impossible branch).How it's built: the call node now carries its source line and
g_src_name(set from the input path) carries the file, so the location is baked in at compile time; both builtins lower inemit_callto anfprintf-to-stderr +exit(1)+unreachabletail (assertbranches on the condition first).@fprintfand the format constant are declared on demand (g_uses_panic), so a program that never panics is byte-identical to before — and the compiler's own source uses neither, so the C-free bootstrap fixpoint still holds (verified).panic/assertare registered as builtins across the vocabulary (ludic_syntax.h, JetBrains lexer, TextMate grammar —x test-toolsconfirms sync) and documented atdocs/language/builtins/.Acceptance: ✅ RFC picks the model (the split above), ✅ lowering in the self-host compiler +
.ll, ✅panic(msg)withfile:line+ documented abort (exit 1, stderr), ✅ docs page + example (examples/library/errors.ludic), ✅ tests covering both paths — the success path (asserts hold, program runs to the end) as a suite example, and the failure path (panic→ non-zero exit + the located stderr line) as a dedicatedpanic_case;x testis now 69 checks. Deferred: value-carryingtry/else(rides #1's tagged unions) and a top-levelrecoverfor the dev game loop. Closing the panic half.