feat(errors): recoverable failures as values — try/else over ok/err results (#46)
A fallible function returns a `result` value, built with ok(payload) on success
or err(message) on failure. The caller recovers a value with `try EXPR else {
… }`: on ok the whole expression is the payload; on err the else block runs —
with the failure message bound to `error` — and its trailing expression supplies
the fallback. It is a plain branch on the result's tag: no exceptions, no hidden
control flow, nothing unwinds. is_ok(r) / is_err(r) classify without unwrapping.
Payloads are any i32-width scalar (int/fixed/bool/entity). The feature is
additive and only kicks in when ok/err/try are used, so untouched programs
compile byte-identically (verified) and the C-free bootstrap fixpoint holds.
Complements panic/assert from #8 (the unrecoverable half). The optional
top-level frame `recover` stays deferred (needs a frame-abort mechanism); the
full tagged-union/any generalization is tracked in #1.
Adds the `try` keyword and ok/err/is_ok/is_err builtins across the compiler,
the vocabulary header, JetBrains + TextMate/VSCode grammars, the docs inventory
and pages, examples/library/recover.ludic, and a regression case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
498593311f
commit
0d1b09e4f0
21 changed files with 16590 additions and 15566 deletions
3
changes/recover.md
Normal file
3
changes/recover.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
Recoverable failures as values (#46) — a fallible function returns a `result` (a new value type), built with `ok(payload)` on success or `err(message)` on failure, and the caller recovers a value with `try EXPR else { … }`: on `ok` the whole expression is the payload, on `err` the `else` block runs — with the failure message bound to `error` — and its trailing expression supplies the fallback. It is a plain branch on the result's tag: no exceptions, no hidden control flow, nothing unwinds the stack. `is_ok(r)` / `is_err(r)` classify a result without unwrapping it. Payloads are any i32-width scalar (int/fixed/bool/entity); the whole feature is additive and only kicks in when `ok`/`err`/`try` are used, so programs that don't touch it compile byte-identically. Complements `panic`/`assert` from #8, which handle the unrecoverable programmer-bug half. The optional top-level frame `recover` remains deferred (it needs a frame-abort/unwind mechanism); the full tagged-union/`any` generalization is tracked in #1.
|
||||||
28
docs/language/builtins/fn-err.md
Normal file
28
docs/language/builtins/fn-err.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
id: fn-err
|
||||||
|
name: err
|
||||||
|
category: builtins
|
||||||
|
kind: builtin
|
||||||
|
tokens: err
|
||||||
|
sig: err(msg: string) -> result
|
||||||
|
tip: Wrap a failure message in a result — the sad half, recovered by try/else.
|
||||||
|
order: 13
|
||||||
|
---
|
||||||
|
|
||||||
|
Wraps a failure in a <code>result</code>, carrying a message that says what went wrong. It is the counterpart to <a href="fn-ok"><code>ok</code></a>: a fallible function (declared <code>-> result</code>) returns <code>err("…")</code> on the sad path instead of crashing or returning a magic sentinel. The caller recovers with <a href="kw-try"><code>try</code></a>/<code>else</code> — the <code>else</code> block sees the message as <code>error</code> — or tests it with <a href="fn-is_err"><code>is_err</code></a>. Unlike <a href="fn-panic"><code>panic</code></a>, which aborts, <code>err</code> is a value: the program keeps running and chooses a fallback.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `msg` — a message describing the failure (a string)
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Config {
|
||||||
|
function volume(pct: int) -> result {
|
||||||
|
if pct < 0 or pct > 100 { return err("volume out of range") }
|
||||||
|
return ok(pct)
|
||||||
|
}
|
||||||
|
test "err drives the fallback" {
|
||||||
|
let v = try volume(150) else { 100 } # clamp to a safe default
|
||||||
|
expect_eq(v, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
28
docs/language/builtins/fn-is_err.md
Normal file
28
docs/language/builtins/fn-is_err.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
id: fn-is_err
|
||||||
|
name: is_err
|
||||||
|
category: builtins
|
||||||
|
kind: builtin
|
||||||
|
tokens: is_err
|
||||||
|
sig: is_err(r: result) -> bool
|
||||||
|
tip: True when a result carries a failure.
|
||||||
|
order: 15
|
||||||
|
---
|
||||||
|
|
||||||
|
Tests a <code>result</code>'s tag: <code>true</code> when it was built with <a href="fn-err"><code>err</code></a>, <code>false</code> when it was built with <a href="fn-ok"><code>ok</code></a>. Use it to detect the failure case explicitly — for example to count or log failures — while <a href="kw-try"><code>try</code></a>/<code>else</code> handles recovering a value. It is the exact negation of <a href="fn-is_ok"><code>is_ok</code></a>.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `r` — the result to test
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Retry {
|
||||||
|
function step(n: int) -> result {
|
||||||
|
if n < 3 { return err("not ready") }
|
||||||
|
return ok(n)
|
||||||
|
}
|
||||||
|
test "is_err detects the failure case" {
|
||||||
|
expect(is_err(step(1)))
|
||||||
|
expect(not is_err(step(5)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
28
docs/language/builtins/fn-is_ok.md
Normal file
28
docs/language/builtins/fn-is_ok.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
id: fn-is_ok
|
||||||
|
name: is_ok
|
||||||
|
category: builtins
|
||||||
|
kind: builtin
|
||||||
|
tokens: is_ok
|
||||||
|
sig: is_ok(r: result) -> bool
|
||||||
|
tip: True when a result carries a success payload.
|
||||||
|
order: 14
|
||||||
|
---
|
||||||
|
|
||||||
|
Tests a <code>result</code>'s tag: <code>true</code> when it was built with <a href="fn-ok"><code>ok</code></a>, <code>false</code> when it was built with <a href="fn-err"><code>err</code></a>. Use it when you want to branch on success without unwrapping the payload — the payload itself is recovered with <a href="kw-try"><code>try</code></a>/<code>else</code>. It is the exact negation of <a href="fn-is_err"><code>is_err</code></a>.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `r` — the result to test
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Check {
|
||||||
|
function find(x: int) -> result {
|
||||||
|
if x == 7 { return ok(x) }
|
||||||
|
return err("missing")
|
||||||
|
}
|
||||||
|
test "is_ok classifies without unwrapping" {
|
||||||
|
expect(is_ok(find(7)))
|
||||||
|
expect(not is_ok(find(3)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
30
docs/language/builtins/fn-ok.md
Normal file
30
docs/language/builtins/fn-ok.md
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
id: fn-ok
|
||||||
|
name: ok
|
||||||
|
category: builtins
|
||||||
|
kind: builtin
|
||||||
|
tokens: ok
|
||||||
|
sig: ok(v) -> result
|
||||||
|
tip: Wrap a success payload in a result — the happy half of try/else.
|
||||||
|
order: 12
|
||||||
|
---
|
||||||
|
|
||||||
|
Wraps a success value in a <code>result</code> — the value type a fallible function returns. A <code>result</code> carries either a success payload (this) or a failure message (from <a href="fn-err"><code>err</code></a>), and a caller recovers it with <a href="kw-try"><code>try</code></a>/<code>else</code> or classifies it with <a href="fn-is_ok"><code>is_ok</code></a> / <a href="fn-is_err"><code>is_err</code></a>. A function that can fail declares <code>-> result</code> and returns <code>ok(value)</code> on the happy path.
|
||||||
|
|
||||||
|
The payload is any <code>i32</code>-width scalar — <code>int</code>, <code>fixed</code>, <code>bool</code>, or <code>entity</code>.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `v` — the success payload
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Parse {
|
||||||
|
function to_digit(c: int) -> result {
|
||||||
|
if c >= 48 and c <= 57 { return ok(c - 48) } # '0'..'9'
|
||||||
|
return err("not a digit")
|
||||||
|
}
|
||||||
|
test "ok carries the parsed value" {
|
||||||
|
let d = try to_digit(55) else { 0 } # '7' -> 7
|
||||||
|
expect_eq(d, 7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -11,7 +11,7 @@ order: 10
|
||||||
|
|
||||||
Prints <code>file:line: panic: <msg></code> to standard error and aborts the process with exit code 1. It turns "something went wrong" into a clear, located message a non-expert can act on — the opposite of a raw segfault or a silent wrong result. Use it for the unrecoverable case: a corrupt asset, a broken invariant, a branch that should be impossible. The source location is baked in at compile time, so the message always points at the exact call. For a guarded check that only aborts when a condition fails, use <a href="fn-assert"><code>assert</code></a>.
|
Prints <code>file:line: panic: <msg></code> to standard error and aborts the process with exit code 1. It turns "something went wrong" into a clear, located message a non-expert can act on — the opposite of a raw segfault or a silent wrong result. Use it for the unrecoverable case: a corrupt asset, a broken invariant, a branch that should be impossible. The source location is baked in at compile time, so the message always points at the exact call. For a guarded check that only aborts when a condition fails, use <a href="fn-assert"><code>assert</code></a>.
|
||||||
|
|
||||||
Recoverable failures — a missing save, a bad network packet — are a different story: those should be values a game can fall back from rather than a `panic`. That value-carrying `try`/`else` path is sequenced after the type-system work (tagged unions); `panic` covers the programmer-bug half today.
|
Recoverable failures — a missing save, a bad network packet — are a different story: those should be values a game can fall back from rather than a `panic`. That value-carrying path is <a href="kw-try"><code>try</code></a>/<code>else</code> over a <a href="fn-ok"><code>result</code></a>; `panic` covers the unrecoverable programmer-bug half.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
- `msg` — a message describing what went wrong (a string)
|
- `msg` — a message describing what went wrong (a string)
|
||||||
|
|
|
||||||
41
docs/language/control/kw-try.md
Normal file
41
docs/language/control/kw-try.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
id: kw-try
|
||||||
|
name: try
|
||||||
|
category: control
|
||||||
|
kind: keyword
|
||||||
|
tokens: try
|
||||||
|
sig: try EXPR else { … }
|
||||||
|
tip: Recover a fallible result as a value, with a fallback — no exceptions, no unwinding.
|
||||||
|
order: 12
|
||||||
|
---
|
||||||
|
|
||||||
|
A <code>try EXPR else { … }</code> expression recovers a fallible operation without exceptions or unwinding. <code>EXPR</code> evaluates to a <a href="fn-ok"><code>result</code></a> — a value carrying either a success payload (from <a href="fn-ok"><code>ok</code></a>) or a failure message (from <a href="fn-err"><code>err</code></a>). When it is <code>ok</code>, the whole <code>try</code> expression <em>is</em> that payload and the <code>else</code> block is skipped. When it is <code>err</code>, the <code>else</code> block runs and its trailing expression supplies the fallback value; inside the block the failure message is bound to <code>error</code>, so you can log or branch on it. It is a plain branch on the result's tag — the happy path stays one line, there is no hidden control flow, and nothing unwinds the stack.
|
||||||
|
|
||||||
|
The payload is an <code>i32</code>-width scalar (<code>int</code>, <code>fixed</code>, <code>bool</code>, or <code>entity</code>) — the common case of "a number or nothing". For richer payloads and full pattern-matching over tagged unions, see the type-system work (#1).
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program SaveLoad {
|
||||||
|
# a fallible op returns a result: ok(payload) or err(message)
|
||||||
|
function load_score(slot: int) -> result {
|
||||||
|
if slot == 1 { return ok(4200) }
|
||||||
|
return err("no save in that slot")
|
||||||
|
}
|
||||||
|
|
||||||
|
test "ok yields the payload, err falls back" {
|
||||||
|
let a = try load_score(1) else { 0 } # 4200
|
||||||
|
let b = try load_score(9) else { 0 } # 0 (fallback)
|
||||||
|
expect_eq(a, 4200)
|
||||||
|
expect_eq(b, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "the fallback can read the failure via `error`" {
|
||||||
|
var logged = 0
|
||||||
|
let score = try load_score(3) else {
|
||||||
|
if error == "no save in that slot" { logged = 1 }
|
||||||
|
0 # trailing expression = the fallback
|
||||||
|
}
|
||||||
|
expect_eq(score, 0)
|
||||||
|
expect_eq(logged, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
37
examples/library/recover.ludic
Normal file
37
examples/library/recover.ludic
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# recover.ludic — recoverable failures as values (issue #46). A fallible function
|
||||||
|
# returns a `result`: `ok(payload)` on success, `err(message)` on failure. The
|
||||||
|
# caller recovers a value with `try EXPR else { … }` — on `ok` the expression is
|
||||||
|
# the payload, on `err` the else block runs (with the message bound to `error`)
|
||||||
|
# and its trailing expression is the fallback. No exceptions, no unwinding — just
|
||||||
|
# a branch on the result's tag. `is_ok`/`is_err` classify without unwrapping.
|
||||||
|
#
|
||||||
|
# This is the SUCCESS-style showcase (it always runs to the end), printing:
|
||||||
|
# 2600 100 1 1 42 7
|
||||||
|
program Recover {
|
||||||
|
# loading a possibly-missing save: slot 1 has data, everything else is empty.
|
||||||
|
function load_score(slot: int) -> result {
|
||||||
|
if slot == 1 { return ok(2600) }
|
||||||
|
return err("no save in slot")
|
||||||
|
}
|
||||||
|
|
||||||
|
entry {
|
||||||
|
# ok path: the payload flows straight out of `try`
|
||||||
|
print(try load_score(1) else { 0 }) # 2600
|
||||||
|
|
||||||
|
# err path: the else block supplies a fallback (a fresh game)
|
||||||
|
print(try load_score(4) else { 100 }) # 100
|
||||||
|
|
||||||
|
# the else block can read the failure message via `error`, and log/branch
|
||||||
|
var handled = 0
|
||||||
|
let s = try load_score(9) else {
|
||||||
|
if error == "no save in slot" { handled = 1 }
|
||||||
|
100 # trailing expression = fallback
|
||||||
|
}
|
||||||
|
print(handled) # 1
|
||||||
|
print(s / 100) # 1 (fallback was 100)
|
||||||
|
|
||||||
|
# is_ok / is_err classify a result without unwrapping it
|
||||||
|
if is_ok(load_score(1)) { print(42) } # 42
|
||||||
|
if is_err(load_score(2)) { print(7) } # 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -479,6 +479,50 @@ function emit_call(e: Node) -> Val {
|
||||||
emit(`{lok}:\n`)
|
emit(`{lok}:\n`)
|
||||||
return val("0", "void")
|
return val("0", "void")
|
||||||
}
|
}
|
||||||
|
# --- recoverable failures as values (issue #46) -----------------------------
|
||||||
|
# A `result` is a heap `%Result = { i32 ok, i32 value, ptr err }`. `ok(v)` wraps
|
||||||
|
# a success payload (any i32-width scalar: int/fixed/bool/entity), `err(msg)` a
|
||||||
|
# failure with a message. `is_ok`/`is_err` test the tag; `try E else { … }`
|
||||||
|
# (emit_try) unwraps the value or runs the fallback. Each is guarded by a
|
||||||
|
# find_fn check so a user function of the same name still wins.
|
||||||
|
if (name == "ok") and (find_fn("ok") == null) {
|
||||||
|
g_uses_result = true
|
||||||
|
let v = emit_expr(e.kids[0])
|
||||||
|
let p = emit_bind("call ptr @malloc(i64 16)")
|
||||||
|
let okp = emit_bind(`getelementptr inbounds %Result, ptr {p}, i32 0, i32 0`)
|
||||||
|
emit(` store i32 1, ptr {okp}\n`)
|
||||||
|
let vp = emit_bind(`getelementptr inbounds %Result, ptr {p}, i32 0, i32 1`)
|
||||||
|
emit(` store i32 {coerce_code(v, "int")}, ptr {vp}\n`)
|
||||||
|
let ep = emit_bind(`getelementptr inbounds %Result, ptr {p}, i32 0, i32 2`)
|
||||||
|
emit(` store ptr null, ptr {ep}\n`)
|
||||||
|
return val(p, "result")
|
||||||
|
}
|
||||||
|
if (name == "err") and (find_fn("err") == null) {
|
||||||
|
g_uses_result = true
|
||||||
|
let m = emit_expr(e.kids[0])
|
||||||
|
let p = emit_bind("call ptr @malloc(i64 16)")
|
||||||
|
let okp = emit_bind(`getelementptr inbounds %Result, ptr {p}, i32 0, i32 0`)
|
||||||
|
emit(` store i32 0, ptr {okp}\n`)
|
||||||
|
let vp = emit_bind(`getelementptr inbounds %Result, ptr {p}, i32 0, i32 1`)
|
||||||
|
emit(` store i32 0, ptr {vp}\n`)
|
||||||
|
let ep = emit_bind(`getelementptr inbounds %Result, ptr {p}, i32 0, i32 2`)
|
||||||
|
emit(` store ptr {m.code}, ptr {ep}\n`)
|
||||||
|
return val(p, "result")
|
||||||
|
}
|
||||||
|
if (name == "is_ok") and (find_fn("is_ok") == null) {
|
||||||
|
let r = emit_expr(e.kids[0])
|
||||||
|
let okp = emit_bind(`getelementptr inbounds %Result, ptr {r.code}, i32 0, i32 0`)
|
||||||
|
let okv = emit_bind(`load i32, ptr {okp}`)
|
||||||
|
let c = emit_bind(`icmp ne i32 {okv}, 0`)
|
||||||
|
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
||||||
|
}
|
||||||
|
if (name == "is_err") and (find_fn("is_err") == null) {
|
||||||
|
let r = emit_expr(e.kids[0])
|
||||||
|
let okp = emit_bind(`getelementptr inbounds %Result, ptr {r.code}, i32 0, i32 0`)
|
||||||
|
let okv = emit_bind(`load i32, ptr {okp}`)
|
||||||
|
let c = emit_bind(`icmp eq i32 {okv}, 0`)
|
||||||
|
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
||||||
|
}
|
||||||
# The EV2 reflection ABI (the world table), exposed to Ludic so a Ludic mod can
|
# 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
|
# 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.
|
# only for a modding program (ECS + events), so a plain game is unchanged.
|
||||||
|
|
@ -629,6 +673,7 @@ function emit_expr(e: Node) -> Val {
|
||||||
return emit_load_at(a, g_addr_ty)
|
return emit_load_at(a, g_addr_ty)
|
||||||
}
|
}
|
||||||
if e.kind == S_EMIT { return emit_emit(e) } # emit as an expression -> cancelled flag
|
if e.kind == S_EMIT { return emit_emit(e) } # emit as an expression -> cancelled flag
|
||||||
|
if e.kind == E_TRY { return emit_try(e) } # try E else { … } -> recovered value (issue #46)
|
||||||
if e.kind == E_CALL { return emit_call(e) }
|
if e.kind == E_CALL { return emit_call(e) }
|
||||||
if e.kind == E_BIN { return emit_bin(e) }
|
if e.kind == E_BIN { return emit_bin(e) }
|
||||||
if e.kind == E_UN {
|
if e.kind == E_UN {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ var g_uses_fsrt: bool = false # Fs.*/Path.*/Mime.* was emitted -> emit the file
|
||||||
var g_uses_datert: bool = false # Date.*/DateTime.* was emitted -> emit the civil<->epoch conversions
|
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_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_uses_panic: bool = false # panic/assert was emitted -> declare @fprintf + the panic format
|
||||||
|
var g_uses_result: bool = false # ok()/err()/try was emitted -> define the %Result value type (issue #46)
|
||||||
var g_tests: []Node # test "name" { ... } blocks collected by the parser
|
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_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
|
var g_uses_longstr: bool = false # string(long) / interpolating a long was emitted -> emit fn_long_str
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,7 @@ function emit_program() -> void {
|
||||||
g_uses_loopback = false
|
g_uses_loopback = false
|
||||||
g_uses_expect = false
|
g_uses_expect = false
|
||||||
g_uses_panic = false
|
g_uses_panic = false
|
||||||
|
g_uses_result = false
|
||||||
g_cov_lines = new []int
|
g_cov_lines = new []int
|
||||||
g_cov_active = true
|
g_cov_active = true
|
||||||
loc_name = new []pointer; loc_reg = new []pointer; loc_ty = new []pointer; loc_mut = new []int
|
loc_name = new []pointer; loc_reg = new []pointer; loc_ty = new []pointer; loc_mut = new []int
|
||||||
|
|
@ -198,6 +199,7 @@ function emit_program() -> void {
|
||||||
emith("declare i32 @fprintf(ptr, ptr, ...)\n")
|
emith("declare i32 @fprintf(ptr, ptr, ...)\n")
|
||||||
emith("@.fmt_panic = private unnamed_addr constant [6 x i8] c\"%s%s\\0A\\00\"\n")
|
emith("@.fmt_panic = private unnamed_addr constant [6 x i8] c\"%s%s\\0A\\00\"\n")
|
||||||
}
|
}
|
||||||
|
if g_uses_result { emith("%Result = type { i32, i32, ptr }\n") } # issue #46: ok/err/try value
|
||||||
emit_cov_runtime() # issue #45: --coverage tables + exit dump
|
emit_cov_runtime() # issue #45: --coverage tables + exit dump
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,53 @@
|
||||||
# emit_expr.ludic — lower an expression to IR, returning its register and type.
|
# emit_expr.ludic — lower an expression to IR, returning its register and type.
|
||||||
|
|
||||||
|
# issue #46: `try E else { BODY }` as an expression. Evaluate E (a `result`); on
|
||||||
|
# `ok` the expression is the payload, on `err` BODY runs with the failure message
|
||||||
|
# bound to `error` and its trailing expression supplies the fallback. Pure branch
|
||||||
|
# on the tag — no unwinding, no hidden control flow.
|
||||||
|
function emit_try_body(b: Node, slot: pointer) -> void {
|
||||||
|
var i = 0
|
||||||
|
while i < len(b.kids) {
|
||||||
|
if g_term { return }
|
||||||
|
let st = b.kids[i]
|
||||||
|
if (i == len(b.kids) - 1) and st.kind == S_EXPR { # trailing expression = the fallback value
|
||||||
|
emit_cov_hit(st.line)
|
||||||
|
let v = emit_expr(st.a)
|
||||||
|
emit(` store i32 {coerce_code(v, "int")}, ptr {slot}\n`)
|
||||||
|
} else { emit_stmt(st) }
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emit_try(e: Node) -> Val {
|
||||||
|
g_uses_result = true
|
||||||
|
let r = emit_expr(e.a)
|
||||||
|
if not (r.ty == "result") { perr(`try expects a result value (got {r.ty}) — line {itoa(e.line)}`) }
|
||||||
|
let slot = emit_alloca("i32")
|
||||||
|
emit(` store i32 0, ptr {slot}\n`) # fallback default (an else block with no trailing value)
|
||||||
|
let okp = emit_bind(`getelementptr inbounds %Result, ptr {r.code}, i32 0, i32 0`)
|
||||||
|
let okv = emit_bind(`load i32, ptr {okp}`)
|
||||||
|
let isok = emit_bind(`icmp ne i32 {okv}, 0`)
|
||||||
|
let lok = lbl("tryok"); let lel = lbl("tryelse"); let lend = lbl("tryend")
|
||||||
|
emit(` br i1 {isok}, label %{lok}, label %{lel}\n`)
|
||||||
|
emit(`{lok}:\n`); g_term = false
|
||||||
|
let vp = emit_bind(`getelementptr inbounds %Result, ptr {r.code}, i32 0, i32 1`)
|
||||||
|
let vv = emit_bind(`load i32, ptr {vp}`)
|
||||||
|
emit(` store i32 {vv}, ptr {slot}\n`)
|
||||||
|
emit(` br label %{lend}\n`)
|
||||||
|
emit(`{lel}:\n`); g_term = false
|
||||||
|
let ep = emit_bind(`getelementptr inbounds %Result, ptr {r.code}, i32 0, i32 2`)
|
||||||
|
let ev = emit_bind(`load ptr, ptr {ep}`)
|
||||||
|
let eslot = emit_alloca("ptr")
|
||||||
|
emit(` store ptr {ev}, ptr {eslot}\n`)
|
||||||
|
let save = nloc
|
||||||
|
loc_push("error", eslot, "string") # bind the failure message inside the else block
|
||||||
|
emit_try_body(e.b, slot)
|
||||||
|
nloc = save
|
||||||
|
if not g_term { emit(` br label %{lend}\n`) }
|
||||||
|
emit(`{lend}:\n`); g_term = false
|
||||||
|
return val(emit_bind(`load i32, ptr {slot}`), "int")
|
||||||
|
}
|
||||||
|
|
||||||
function emit_load_at(addr: pointer, ty: pointer) -> Val {
|
function emit_load_at(addr: pointer, ty: pointer) -> Val {
|
||||||
let r = emit_bind(`load {llty(ty)}, ptr {addr}`)
|
let r = emit_bind(`load {llty(ty)}, ptr {addr}`)
|
||||||
return val(r, ty)
|
return val(r, ty)
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ const S_EMIT: int = 49 # emit E(field: v, ...) — fire event E (calls
|
||||||
const S_CANCEL: int = 50 # cancel — inside a listener, veto a `cancellable` event
|
const S_CANCEL: int = 50 # cancel — inside a listener, veto a `cancellable` event
|
||||||
const N_TEST: int = 51 # test "name" { ... } — a named test block (the testing framework)
|
const N_TEST: int = 51 # test "name" { ... } — a named test block (the testing framework)
|
||||||
# s=name a=body block line=source line of the test
|
# s=name a=body block line=source line of the test
|
||||||
|
const E_TRY: int = 52 # try EXPR else { ... } — recover a fallible result as a value (issue #46)
|
||||||
|
# a=the fallible (result-typed) expression b=else block (its
|
||||||
|
# trailing expression is the fallback) line=source line
|
||||||
# statements
|
# statements
|
||||||
const S_LET: int = 10
|
const S_LET: int = 10
|
||||||
const S_ASSIGN: int = 11
|
const S_ASSIGN: int = 11
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,19 @@ function p_primary() -> Node {
|
||||||
if (t.text == "false") { let n = node(E_BOOL); n.ival = 0; pi = pi + 1; return n }
|
if (t.text == "false") { let n = node(E_BOOL); n.ival = 0; pi = pi + 1; return n }
|
||||||
if (t.text == "null") { pi = pi + 1; return node(E_NULL) }
|
if (t.text == "null") { pi = pi + 1; return node(E_NULL) }
|
||||||
if (t.text == "new") { pi = pi + 1; let n = node(E_NEW); n.s = ptype(); if is_op("{") { n.a = record() }; return n }
|
if (t.text == "new") { pi = pi + 1; let n = node(E_NEW); n.s = ptype(); if is_op("{") { n.a = record() }; return n }
|
||||||
|
# try EXPR else { ... } — evaluate a fallible (result-typed) expression; on
|
||||||
|
# `ok` the whole expression is its payload, on `err` the else block runs (with
|
||||||
|
# the message bound to `error`) and its trailing expression is the fallback.
|
||||||
|
if (t.text == "try") {
|
||||||
|
pi = pi + 1
|
||||||
|
let n = node(E_TRY); n.line = t.line
|
||||||
|
n.a = expr()
|
||||||
|
skipnl()
|
||||||
|
if not is_id("else") { perr("try needs an `else { ... }` fallback") }
|
||||||
|
pi = pi + 1; skipnl()
|
||||||
|
n.b = block()
|
||||||
|
return n
|
||||||
|
}
|
||||||
let n = node(E_ID); n.s = t.text; pi = pi + 1; return n
|
let n = node(E_ID); n.s = t.text; pi = pi + 1; return n
|
||||||
}
|
}
|
||||||
if is_op("(") { pi = pi + 1; skipnl(); let e = expr(); skipnl(); eat_op(")"); return e }
|
if is_op("(") { pi = pi + 1; skipnl(); let e = expr(); skipnl(); eat_op(")"); return e }
|
||||||
|
|
|
||||||
31820
selfhost/ludicc.seed.ll
31820
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -48,7 +48,11 @@
|
||||||
"fn-read_char",
|
"fn-read_char",
|
||||||
"fn-file_write",
|
"fn-file_write",
|
||||||
"fn-file_stdout",
|
"fn-file_stdout",
|
||||||
"fn-file_stderr"
|
"fn-file_stderr",
|
||||||
|
"fn-ok",
|
||||||
|
"fn-err",
|
||||||
|
"fn-is_ok",
|
||||||
|
"fn-is_err"
|
||||||
],
|
],
|
||||||
"collision": [
|
"collision": [
|
||||||
"collision-rects",
|
"collision-rects",
|
||||||
|
|
@ -72,7 +76,8 @@
|
||||||
"kw-match",
|
"kw-match",
|
||||||
"kw-machine",
|
"kw-machine",
|
||||||
"kw-state",
|
"kw-state",
|
||||||
"kw-become"
|
"kw-become",
|
||||||
|
"kw-try"
|
||||||
],
|
],
|
||||||
"ease": [
|
"ease": [
|
||||||
"ease-in",
|
"ease-in",
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ object LudicVocabulary {
|
||||||
val STMT = setOf(
|
val STMT = setOf(
|
||||||
"let", "return", "if", "else", "while", "for", "in", "spawn", "despawn",
|
"let", "return", "if", "else", "while", "for", "in", "spawn", "despawn",
|
||||||
"enable", "disable", "match", "machine", "state", "become", "where",
|
"enable", "disable", "match", "machine", "state", "become", "where",
|
||||||
"and", "or", "not", "break", "continue", "new", "emit", "cancel"
|
"and", "or", "not", "break", "continue", "new", "emit", "cancel", "try"
|
||||||
)
|
)
|
||||||
val PRIMITIVES = setOf("int", "long", "fixed", "bool", "entity", "string", "pointer", "byte", "words", "fixeds", "pointers", "Vector", "void")
|
val PRIMITIVES = setOf("int", "long", "fixed", "bool", "entity", "string", "pointer", "byte", "words", "fixeds", "pointers", "Vector", "void")
|
||||||
val PHASES = setOf("Start", "Input", "FixedUpdate", "Update", "LateUpdate", "Render")
|
val PHASES = setOf("Start", "Input", "FixedUpdate", "Update", "LateUpdate", "Render")
|
||||||
|
|
@ -65,6 +65,7 @@ object LudicVocabulary {
|
||||||
"ui_tick", "ui_render", "ui_clicked", "ui_set_text", "ui_set_int", "ui_focus",
|
"ui_tick", "ui_render", "ui_clicked", "ui_set_text", "ui_set_int", "ui_focus",
|
||||||
"ui_focused", "ui_visible", "key", "reg", "set_reg", "self", "save", "load",
|
"ui_focused", "ui_visible", "key", "reg", "set_reg", "self", "save", "load",
|
||||||
"status", "print", "string", "quit", "panic", "assert",
|
"status", "print", "string", "quit", "panic", "assert",
|
||||||
|
"ok", "err", "is_ok", "is_err",
|
||||||
// compiler intrinsics: the floor the Ludic-written runtime stands on
|
// compiler intrinsics: the floor the Ludic-written runtime stands on
|
||||||
"bytes", "words", "resize", "free", "fill", "arg_count", "arg", "file_stderr", "file_stdout",
|
"bytes", "words", "resize", "free", "fill", "arg_count", "arg", "file_stderr", "file_stdout",
|
||||||
"offset",
|
"offset",
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@
|
||||||
},
|
},
|
||||||
"keyword": {
|
"keyword": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{ "name": "keyword.control.ludic", "match": "\\b(if|else|while|for|in|match|machine|become|return|spawn|despawn|enable|disable|where|break|continue|new)\\b" },
|
{ "name": "keyword.control.ludic", "match": "\\b(if|else|while|for|in|match|machine|become|return|spawn|despawn|enable|disable|where|break|continue|new|try)\\b" },
|
||||||
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
|
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
|
||||||
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" },
|
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" },
|
||||||
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
|
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
|
||||||
|
|
@ -204,7 +204,7 @@
|
||||||
{
|
{
|
||||||
"comment": "the runtime surface — every name here resolves to rt_<name> in runtime/native",
|
"comment": "the runtime surface — every name here resolves to rt_<name> in runtime/native",
|
||||||
"name": "support.function.builtin.ludic",
|
"name": "support.function.builtin.ludic",
|
||||||
"match": "\\b(min|max|abs|clamp|seed|rng_range|rng_chance|fixed|floor|map_size|map_row|tile|clear|present|fill_rect|frame_rect|put_px|text|text_int|font_load|text_ttf|text_w|text_h|image_load|draw_image|draw_image_scaled|draw_9slice|png_load|sprites_load|draw_sprite|draw_sprite_scaled|ui_build|ui_open|ui_tick|ui_render|ui_clicked|ui_set_text|ui_set_int|ui_focus|ui_focused|ui_visible|key|reg|set_reg|self|save|load|status|print|string|quit|panic|assert)\\b(?=\\s*\\()"
|
"match": "\\b(min|max|abs|clamp|seed|rng_range|rng_chance|fixed|floor|map_size|map_row|tile|clear|present|fill_rect|frame_rect|put_px|text|text_int|font_load|text_ttf|text_w|text_h|image_load|draw_image|draw_image_scaled|draw_9slice|png_load|sprites_load|draw_sprite|draw_sprite_scaled|ui_build|ui_open|ui_tick|ui_render|ui_clicked|ui_set_text|ui_set_int|ui_focus|ui_focused|ui_visible|key|reg|set_reg|self|save|load|status|print|string|quit|panic|assert|ok|err|is_ok|is_err)\\b(?=\\s*\\()"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"comment": "compiler intrinsics — these lower straight to libc or the OS",
|
"comment": "compiler intrinsics — these lower straight to libc or the OS",
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@
|
||||||
},
|
},
|
||||||
"keyword": {
|
"keyword": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{ "name": "keyword.control.ludic", "match": "\\b(if|else|while|for|in|match|machine|become|return|spawn|despawn|enable|disable|where|break|continue|new)\\b" },
|
{ "name": "keyword.control.ludic", "match": "\\b(if|else|while|for|in|match|machine|become|return|spawn|despawn|enable|disable|where|break|continue|new|try)\\b" },
|
||||||
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
|
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
|
||||||
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" },
|
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" },
|
||||||
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
|
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
|
||||||
|
|
@ -204,7 +204,7 @@
|
||||||
{
|
{
|
||||||
"comment": "the runtime surface — every name here resolves to rt_<name> in runtime/native",
|
"comment": "the runtime surface — every name here resolves to rt_<name> in runtime/native",
|
||||||
"name": "support.function.builtin.ludic",
|
"name": "support.function.builtin.ludic",
|
||||||
"match": "\\b(min|max|abs|clamp|seed|rng_range|rng_chance|fixed|floor|map_size|map_row|tile|clear|present|fill_rect|frame_rect|put_px|text|text_int|font_load|text_ttf|text_w|text_h|image_load|draw_image|draw_image_scaled|draw_9slice|png_load|sprites_load|draw_sprite|draw_sprite_scaled|ui_build|ui_open|ui_tick|ui_render|ui_clicked|ui_set_text|ui_set_int|ui_focus|ui_focused|ui_visible|key|reg|set_reg|self|save|load|status|print|string|quit|panic|assert)\\b(?=\\s*\\()"
|
"match": "\\b(min|max|abs|clamp|seed|rng_range|rng_chance|fixed|floor|map_size|map_row|tile|clear|present|fill_rect|frame_rect|put_px|text|text_int|font_load|text_ttf|text_w|text_h|image_load|draw_image|draw_image_scaled|draw_9slice|png_load|sprites_load|draw_sprite|draw_sprite_scaled|ui_build|ui_open|ui_tick|ui_render|ui_clicked|ui_set_text|ui_set_int|ui_focus|ui_focused|ui_visible|key|reg|set_reg|self|save|load|status|print|string|quit|panic|assert|ok|err|is_ok|is_err)\\b(?=\\s*\\()"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"comment": "compiler intrinsics — these lower straight to libc or the OS",
|
"comment": "compiler intrinsics — these lower straight to libc or the OS",
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ static const char* LUDIC_KW_RESERVED[] = {
|
||||||
static const char* LUDIC_KW_STMT[] = {
|
static const char* LUDIC_KW_STMT[] = {
|
||||||
"let","return","if","else","while","for","in","spawn","despawn",
|
"let","return","if","else","while","for","in","spawn","despawn",
|
||||||
"enable","disable","match","machine","state","become","where",
|
"enable","disable","match","machine","state","become","where",
|
||||||
"and","or","not","break","continue","new","emit","cancel", 0
|
"and","or","not","break","continue","new","emit","cancel","try", 0
|
||||||
};
|
};
|
||||||
static const char* LUDIC_TYPES[] = {
|
static const char* LUDIC_TYPES[] = {
|
||||||
"int","long","fixed","bool","entity","string","pointer","byte","words","fixeds","pointers","Vector","void", 0
|
"int","long","fixed","bool","entity","string","pointer","byte","words","fixeds","pointers","Vector","void", 0
|
||||||
|
|
@ -143,6 +143,10 @@ static const LBuiltin LUDIC_BUILTINS[] = {
|
||||||
{"quit","quit()","Stop the frame loop and exit."},
|
{"quit","quit()","Stop the frame loop and exit."},
|
||||||
{"panic","panic(msg: string)","Print `file:line: panic: msg` to stderr and abort (exit 1) — a located error, not a crash."},
|
{"panic","panic(msg: string)","Print `file:line: panic: msg` to stderr and abort (exit 1) — a located error, not a crash."},
|
||||||
{"assert","assert(cond: bool, msg: string)","Abort with `file:line: assertion failed: msg` when cond is false — for programmer-bug invariants."},
|
{"assert","assert(cond: bool, msg: string)","Abort with `file:line: assertion failed: msg` when cond is false — for programmer-bug invariants."},
|
||||||
|
{"ok","ok(v) -> result","Wrap a success payload (any i32-width scalar) in a result — the happy half of `try`/`else`."},
|
||||||
|
{"err","err(msg: string) -> result","Wrap a failure message in a result — the sad half, recovered by `try`/`else`."},
|
||||||
|
{"is_ok","is_ok(r: result) -> bool","True when a result carries a success payload."},
|
||||||
|
{"is_err","is_err(r: result) -> bool","True when a result carries a failure."},
|
||||||
{0,0,0}
|
{0,0,0}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,7 @@ function cmd_test() -> int {
|
||||||
spec_case("library/testing", "== 6 passed, 0 failed ==")
|
spec_case("library/testing", "== 6 passed, 0 failed ==")
|
||||||
spec_case("library/coverage", "== 3 passed, 0 failed ==")
|
spec_case("library/coverage", "== 3 passed, 0 failed ==")
|
||||||
feat_case("library/errors", "", "5 10 0 7 1", "errors.ludic (assert guards an invariant, holds -> runs to the end; issue #8 success path)")
|
feat_case("library/errors", "", "5 10 0 7 1", "errors.ludic (assert guards an invariant, holds -> runs to the end; issue #8 success path)")
|
||||||
|
feat_case("library/recover", "", "2600 100 1 1 42 7", "recover.ludic (try/else recovers a result value + fallback, error binding, is_ok/is_err; issue #46)")
|
||||||
panic_case()
|
panic_case()
|
||||||
feat_case("library/logging", "", "0 5 2 1", "logging.ludic (Log levels, set_level/level threshold, structured fields)")
|
feat_case("library/logging", "", "0 5 2 1", "logging.ludic (Log levels, set_level/level threshold, structured fields)")
|
||||||
# Os known-folders/arch and Fs.list read the BSD utsname/dirent layout, so
|
# Os known-folders/arch and Fs.list read the BSD utsname/dirent layout, so
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue