feat(types): option (some/none) safety type (#53)
Types phase 3 — the option/result safety pair. result/ok/err/try shipped in #46; this adds its companion option: - some(v) -> option (present value; any i32-width scalar) - none() -> option (empty; no magic -1 sentinel) - is_some / is_none -> bool - unwrap_or(o, fallback) -> int A heap %Option = { i32 present, i32 value }, bare builtins guarded by find_fn (a user fn of the same name still wins), gated by g_uses_option so unused programs compile byte-identically — same idiom as result. Wired: emit_call codegen + %Option decl (emit_decl) + g_uses_option (emit_core), reseeded seed, vocabulary sync (header/JetBrains/TextMate), builtin docs + inventory, and a self-asserting example (examples/library/optionresult.ludic + feat_case). All suites green incl. golden renders byte-identical and the bootstrap fixpoint. Tagged-union enums (variant payloads + binding match + exhaustiveness) are the deep type-system feature, split out to #56. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5dc8394f22
commit
790eda6f73
17 changed files with 5526 additions and 4905 deletions
11
changes/types-option.md
Normal file
11
changes/types-option.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**`option` — a maybe-a-value type (#53, types phase 3).** The companion to
|
||||
`result` (#46): `some(v)` wraps a present value (any i32-width scalar), `none()`
|
||||
is the empty case — a missing value with no magic `-1` sentinel — and
|
||||
`is_some`/`is_none`/`unwrap_or(o, fallback)` test and read it. A heap
|
||||
`%Option = { i32 present, i32 value }`, additive and gated, so a program that
|
||||
does not use it compiles byte-identically. With `result`/`ok`/`err`/`try`
|
||||
already shipped, the concrete option/result safety types of phase 3 are done;
|
||||
general tagged-union enums (variant payloads + binding `match` + exhaustiveness)
|
||||
are tracked separately in #56.
|
||||
24
docs/language/builtins/fn-is_none.md
Normal file
24
docs/language/builtins/fn-is_none.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: fn-is_none
|
||||
name: is_none
|
||||
category: builtins
|
||||
kind: builtin
|
||||
tokens: is_none
|
||||
sig: is_none(o: option) -> bool
|
||||
tip: True when an option is empty.
|
||||
order: 19
|
||||
---
|
||||
|
||||
Returns true when an <code>option</code> is empty (it is <a href="fn-none"><code>none</code></a>). The complement of <a href="fn-is_some"><code>is_some</code></a>.
|
||||
|
||||
Parameters:
|
||||
- `o` — the option to test
|
||||
|
||||
```ludic
|
||||
program AI {
|
||||
handler Step phase Update {
|
||||
let target = none()
|
||||
if is_none(target) { wander() }
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/builtins/fn-is_some.md
Normal file
24
docs/language/builtins/fn-is_some.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: fn-is_some
|
||||
name: is_some
|
||||
category: builtins
|
||||
kind: builtin
|
||||
tokens: is_some
|
||||
sig: is_some(o: option) -> bool
|
||||
tip: True when an option holds a value.
|
||||
order: 18
|
||||
---
|
||||
|
||||
Returns true when an <code>option</code> holds a value (it is <a href="fn-some"><code>some</code></a>, not <a href="fn-none"><code>none</code></a>) — the guard before acting on an optional target or slot.
|
||||
|
||||
Parameters:
|
||||
- `o` — the option to test
|
||||
|
||||
```ludic
|
||||
program AI {
|
||||
handler Step phase Update {
|
||||
let target = none()
|
||||
if is_some(target) { chase(unwrap_or(target, 0)) }
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/builtins/fn-none.md
Normal file
24
docs/language/builtins/fn-none.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: fn-none
|
||||
name: none
|
||||
category: builtins
|
||||
kind: builtin
|
||||
tokens: none
|
||||
sig: none() -> option
|
||||
tip: The empty option — a missing value.
|
||||
order: 17
|
||||
---
|
||||
|
||||
The empty <code>option</code> — a missing value with no sentinel. Return it when there is nothing to give: no target, an empty slot, a lookup that missed. The caller distinguishes it from <a href="fn-some"><code>some</code></a> with <a href="fn-is_none"><code>is_none</code></a> / <a href="fn-is_some"><code>is_some</code></a>, or supplies a default with <a href="fn-unwrap_or"><code>unwrap_or</code></a>.
|
||||
|
||||
```ludic
|
||||
program Slots {
|
||||
function held(slot: int) -> option {
|
||||
if slot < 0 { return none() }
|
||||
return some(slot)
|
||||
}
|
||||
test "none is empty" {
|
||||
expect_eq(is_none(held(0 - 1)), true)
|
||||
}
|
||||
}
|
||||
```
|
||||
28
docs/language/builtins/fn-some.md
Normal file
28
docs/language/builtins/fn-some.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
id: fn-some
|
||||
name: some
|
||||
category: builtins
|
||||
kind: builtin
|
||||
tokens: some
|
||||
sig: some(v) -> option
|
||||
tip: Wrap a present value in an option.
|
||||
order: 16
|
||||
---
|
||||
|
||||
Wraps a present value in an <code>option</code> — the value type that models <em>maybe a value</em> without a magic <code>-1</code> sentinel. An <code>option</code> is either <code>some(value)</code> (this) or <a href="fn-none"><code>none</code></a> (empty); test it with <a href="fn-is_some"><code>is_some</code></a> / <a href="fn-is_none"><code>is_none</code></a> and read it with <a href="fn-unwrap_or"><code>unwrap_or</code></a>. 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 present value
|
||||
|
||||
```ludic
|
||||
program Targeting {
|
||||
function nearest(count: int) -> option {
|
||||
if count == 0 { return none() }
|
||||
return some(count - 1) # the nearest target index
|
||||
}
|
||||
test "some carries the value" {
|
||||
let t = unwrap_or(nearest(3), 0 - 1)
|
||||
expect_eq(t, 2)
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/builtins/fn-unwrap_or.md
Normal file
24
docs/language/builtins/fn-unwrap_or.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: fn-unwrap_or
|
||||
name: unwrap_or
|
||||
category: builtins
|
||||
kind: builtin
|
||||
tokens: unwrap_or
|
||||
sig: unwrap_or(o: option, fallback) -> int
|
||||
tip: The option value, or a fallback if empty.
|
||||
order: 20
|
||||
---
|
||||
|
||||
Reads an <code>option</code>: returns its value when it is <a href="fn-some"><code>some</code></a>, or <code>fallback</code> when it is <a href="fn-none"><code>none</code></a>. The safe, one-line way to get a usable value out of an option without a separate branch.
|
||||
|
||||
Parameters:
|
||||
- `o` — the option to read
|
||||
- `fallback` — the value to use when the option is empty
|
||||
|
||||
```ludic
|
||||
program Config {
|
||||
handler Step phase Update {
|
||||
let volume = unwrap_or(none(), 100) # default to 100
|
||||
}
|
||||
}
|
||||
```
|
||||
39
examples/library/optionresult.ludic
Normal file
39
examples/library/optionresult.ludic
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# optionresult.ludic — the safety value types: `option` (some/none, "maybe a
|
||||
# value") and `result` (ok/err/try, "a value or a failure"). Each assertion that
|
||||
# holds prints its number, so a full run prints:
|
||||
# 1 2 3 4 5 6 7 8 9 10 11 12
|
||||
# option is issue #53; result shipped in #46. Both are additive — a program that
|
||||
# does not use them compiles byte-identically.
|
||||
program OptionResult {
|
||||
# a lookup that may find nothing — no magic -1 sentinel
|
||||
function slot_of(id: int) -> option {
|
||||
if id < 0 { return none() }
|
||||
return some(id * 10)
|
||||
}
|
||||
# a fallible parse — a value or a message
|
||||
function to_digit(c: int) -> result {
|
||||
if c >= 48 and c <= 57 { return ok(c - 48) } # '0'..'9'
|
||||
return err("not a digit")
|
||||
}
|
||||
entry {
|
||||
# --- option ---
|
||||
let a = some(42)
|
||||
if is_some(a) { print(1) }
|
||||
if not is_none(a) { print(2) }
|
||||
if unwrap_or(a, 0 - 1) == 42 { print(3) }
|
||||
let b = none()
|
||||
if is_none(b) { print(4) }
|
||||
if unwrap_or(b, 0 - 1) == 0 - 1 { print(5) }
|
||||
if unwrap_or(slot_of(5), 0) == 50 { print(6) }
|
||||
if is_none(slot_of(0 - 1)) { print(7) }
|
||||
|
||||
# --- result + try/else ---
|
||||
if is_ok(to_digit(55)) { print(8) }
|
||||
if is_err(to_digit(120)) { print(9) }
|
||||
let d = try to_digit(55) else { 0 } # '7' -> 7
|
||||
if d == 7 { print(10) }
|
||||
let bad = try to_digit(120) else { 0 - 1 } # not a digit -> fallback
|
||||
if bad == 0 - 1 { print(11) }
|
||||
let msg = try to_digit(120) else { print(12); 0 } # else binds `error`
|
||||
}
|
||||
}
|
||||
|
|
@ -760,6 +760,55 @@ function emit_call(e: Node) -> Val {
|
|||
let c = emit_bind(`icmp eq i32 {okv}, 0`)
|
||||
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
||||
}
|
||||
# An `option` is a heap `%Option = { i32 present, i32 value }` — the "maybe a
|
||||
# value" companion to `result` (issue #53). `some(v)` wraps a present payload
|
||||
# (any i32-width scalar: int/fixed/bool/entity), `none()` is the absent case
|
||||
# (no magic -1 sentinel); `is_some`/`is_none` test presence and `unwrap_or`
|
||||
# reads the payload with a fallback. Each is guarded by a find_fn check so a
|
||||
# user function of the same name still wins.
|
||||
if (name == "some") and (find_fn("some") == null) {
|
||||
g_uses_option = true
|
||||
let v = emit_expr(e.kids[0])
|
||||
let p = emit_bind("call ptr @malloc(i64 8)")
|
||||
let pp = emit_bind(`getelementptr inbounds %Option, ptr {p}, i32 0, i32 0`)
|
||||
emit(` store i32 1, ptr {pp}\n`)
|
||||
let vp = emit_bind(`getelementptr inbounds %Option, ptr {p}, i32 0, i32 1`)
|
||||
emit(` store i32 {coerce_code(v, "int")}, ptr {vp}\n`)
|
||||
return val(p, "option")
|
||||
}
|
||||
if (name == "none") and (find_fn("none") == null) {
|
||||
g_uses_option = true
|
||||
let p = emit_bind("call ptr @malloc(i64 8)")
|
||||
let pp = emit_bind(`getelementptr inbounds %Option, ptr {p}, i32 0, i32 0`)
|
||||
emit(` store i32 0, ptr {pp}\n`)
|
||||
let vp = emit_bind(`getelementptr inbounds %Option, ptr {p}, i32 0, i32 1`)
|
||||
emit(` store i32 0, ptr {vp}\n`)
|
||||
return val(p, "option")
|
||||
}
|
||||
if (name == "is_some") and (find_fn("is_some") == null) {
|
||||
let o = emit_expr(e.kids[0])
|
||||
let pp = emit_bind(`getelementptr inbounds %Option, ptr {o.code}, i32 0, i32 0`)
|
||||
let pv = emit_bind(`load i32, ptr {pp}`)
|
||||
let c = emit_bind(`icmp ne i32 {pv}, 0`)
|
||||
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
||||
}
|
||||
if (name == "is_none") and (find_fn("is_none") == null) {
|
||||
let o = emit_expr(e.kids[0])
|
||||
let pp = emit_bind(`getelementptr inbounds %Option, ptr {o.code}, i32 0, i32 0`)
|
||||
let pv = emit_bind(`load i32, ptr {pp}`)
|
||||
let c = emit_bind(`icmp eq i32 {pv}, 0`)
|
||||
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
||||
}
|
||||
if (name == "unwrap_or") and (find_fn("unwrap_or") == null) {
|
||||
let o = emit_expr(e.kids[0])
|
||||
let fb = emit_expr(e.kids[1])
|
||||
let pp = emit_bind(`getelementptr inbounds %Option, ptr {o.code}, i32 0, i32 0`)
|
||||
let pv = emit_bind(`load i32, ptr {pp}`)
|
||||
let vp = emit_bind(`getelementptr inbounds %Option, ptr {o.code}, i32 0, i32 1`)
|
||||
let vv = emit_bind(`load i32, ptr {vp}`)
|
||||
let present = emit_bind(`icmp ne i32 {pv}, 0`)
|
||||
return val(emit_bind(`select i1 {present}, i32 {vv}, i32 {coerce_code(fb, "int")}`), "int")
|
||||
}
|
||||
# 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.
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ var g_uses_datert: bool = false # Date.*/DateTime.* was emitted -> emit the civ
|
|||
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_result: bool = false # ok()/err()/try was emitted -> define the %Result value type (issue #46)
|
||||
var g_uses_option: bool = false # some()/none() was emitted -> define the %Option value type (issue #53)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ function emit_program() -> void {
|
|||
g_uses_expect = false
|
||||
g_uses_panic = false
|
||||
g_uses_result = false
|
||||
g_uses_option = false
|
||||
g_cov_lines = new []int
|
||||
g_cov_active = true
|
||||
loc_name = new []pointer; loc_reg = new []pointer; loc_ty = new []pointer; loc_mut = new []int
|
||||
|
|
@ -205,6 +206,7 @@ function emit_program() -> void {
|
|||
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
|
||||
if g_uses_option { emith("%Option = type { i32, i32 }\n") } # issue #53: some/none value
|
||||
emit_cov_runtime() # issue #45: --coverage tables + exit dump
|
||||
}
|
||||
|
||||
|
|
|
|||
10187
selfhost/ludicc.seed.ll
10187
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -52,7 +52,12 @@
|
|||
"fn-ok",
|
||||
"fn-err",
|
||||
"fn-is_ok",
|
||||
"fn-is_err"
|
||||
"fn-is_err",
|
||||
"fn-some",
|
||||
"fn-none",
|
||||
"fn-is_some",
|
||||
"fn-is_none",
|
||||
"fn-unwrap_or"
|
||||
],
|
||||
"collision": [
|
||||
"collision-rects",
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ object LudicVocabulary {
|
|||
"ui_focused", "ui_visible", "key", "reg", "set_reg", "self", "save", "load",
|
||||
"status", "print", "string", "quit", "panic", "assert",
|
||||
"ok", "err", "is_ok", "is_err",
|
||||
"some", "none", "is_some", "is_none", "unwrap_or",
|
||||
// compiler intrinsics: the floor the Ludic-written runtime stands on
|
||||
"bytes", "words", "resize", "free", "fill", "arg_count", "arg", "file_stderr", "file_stdout",
|
||||
"offset",
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@
|
|||
{
|
||||
"comment": "the runtime surface — every name here resolves to rt_<name> in runtime/native",
|
||||
"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|ok|err|is_ok|is_err)\\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|some|none|is_some|is_none|unwrap_or)\\b(?=\\s*\\()"
|
||||
},
|
||||
{
|
||||
"comment": "compiler intrinsics — these lower straight to libc or the OS",
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@
|
|||
{
|
||||
"comment": "the runtime surface — every name here resolves to rt_<name> in runtime/native",
|
||||
"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|ok|err|is_ok|is_err)\\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|some|none|is_some|is_none|unwrap_or)\\b(?=\\s*\\()"
|
||||
},
|
||||
{
|
||||
"comment": "compiler intrinsics — these lower straight to libc or the OS",
|
||||
|
|
|
|||
|
|
@ -147,6 +147,11 @@ static const LBuiltin LUDIC_BUILTINS[] = {
|
|||
{"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."},
|
||||
{"some","some(v) -> option","Wrap a present value (any i32-width scalar) in an option — the `has a value` case."},
|
||||
{"none","none() -> option","The empty option — a missing value with no magic -1 sentinel."},
|
||||
{"is_some","is_some(o: option) -> bool","True when an option holds a value."},
|
||||
{"is_none","is_none(o: option) -> bool","True when an option is empty."},
|
||||
{"unwrap_or","unwrap_or(o: option, fallback) -> int","The option's value, or fallback when it is empty."},
|
||||
{0,0,0}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -196,6 +196,7 @@ function cmd_test() -> int {
|
|||
feat_case("library/bignum", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16", "bignum.ludic (BigInt arbitrary-precision + Decimal exact base-10 money)")
|
||||
feat_case("library/containers", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "containers.ludic (Dict string-keyed hash map + Set string set)")
|
||||
feat_case("library/numeric", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20", "numeric.ludic (Huge idle big-numbers + Angle wrapping radians + Percent clamped [0,1])")
|
||||
feat_case("library/optionresult", "", "1 2 3 4 5 6 7 8 9 10 11 12", "optionresult.ludic (option some/none + result ok/err/try safety types)")
|
||||
feat_case("library/regex", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "regex.ludic (Regex match/find/groups/classes/quantifiers/replace + linear-time safety)")
|
||||
feat_case("library/grid", "", "1 2 3 4 5 6 7 8 9 10 11 12 13", "grid.ludic (Grid line/flood/line_of_sight + A* pathfinding over the tilemap)")
|
||||
feat_case("library/anim", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34", "anim.ludic (Anim frame/once/pingpong/cell + Tween progress/loop/yoyo/ease/number/round/point/tint)")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue