feat(types): tagged-union enums — variant payloads + binding match + exhaustiveness (#56)
Extend `enum` from named int constants to a tagged union: a variant may
carry a payload (`enum Tile { Empty, Wall, Door(int), Portal(int, int) }`).
Such enums box to a heap record (an i32 tag at offset 0, then one 8-byte
slot per payload position); an all-bare enum keeps its zero-cost compile-
time-ordinal representation, byte-for-byte unchanged (every golden render
and the bootstrap fixpoint still hold).
- Parser: variant payload declarations, stored as N_PARAM kids on the
variant node.
- Construction: by name — `Door(3)`, `Portal(x, y)`, bare `Empty` — resolved
ahead of the function-call fallback and boxed with the payloads coerced to
their declared types.
- match: destructures a tagged scrutinee, switching on the tag and binding
each arm's payload names in a scoped local frame.
- Checking pass: a tagged `match` must be exhaustive (cover every variant or
end in `_`), and constructor/pattern arities and binding forms are checked
— all reported where the scrutinee's type is known.
Adds selfhost/tests/enums.ludic to the regression suite and documents the
feature in LANGUAGE.md and the enum/match pages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7c91d24595
commit
872f458cb2
11 changed files with 17990 additions and 16481 deletions
31
LANGUAGE.md
31
LANGUAGE.md
|
|
@ -738,12 +738,33 @@ match reg(R_CUR) { Action.Attack => attack() Action.Guard => guard() _ => wait
|
|||
if reg(R_MODE) == Mode.Battle { … }
|
||||
```
|
||||
|
||||
A variant is a **compile-time `int`** accessed as `Enum.Variant` (`Action.Guard`
|
||||
A bare variant is a **compile-time `int`** accessed as `Enum.Variant` (`Action.Guard`
|
||||
is `1`), numbered from `0` by declaration order, so it works anywhere an int does
|
||||
— `match` patterns, comparisons, `set_reg`. Enums are a naming layer over `int`:
|
||||
there is no distinct enum runtime type yet, so an enum value lives in an ordinary
|
||||
`int` or register (and is saved with it). See `examples/games/chronorift/combat.ludic`,
|
||||
whose battle menus dispatch on `KnightAct`/`MageAct` instead of `0..3`.
|
||||
— `match` patterns, comparisons, `set_reg`. A plain (all-bare) enum is a naming
|
||||
layer over `int`: an enum value lives in an ordinary `int` or register (and is
|
||||
saved with it). See `examples/games/chronorift/combat.ludic`, whose battle menus
|
||||
dispatch on `KnightAct`/`MageAct` instead of `0..3`.
|
||||
|
||||
A variant may instead carry a **payload**, which makes the enum a *tagged union*:
|
||||
|
||||
```ludic
|
||||
# doc-check: skip — composite: a declaration plus its uses
|
||||
enum Tile { Empty, Wall, Door(int), Portal(int, int) }
|
||||
|
||||
let t: Tile = Door(3) # constructed by name; bare Empty for no payload
|
||||
match t {
|
||||
Empty => rest()
|
||||
Wall => block()
|
||||
Door(n) => open(n) # payload bound as `n` in this arm
|
||||
Portal(x, y) => teleport(x, y) # both fields bound
|
||||
}
|
||||
```
|
||||
|
||||
A payloaded value is boxed (a tag plus its payload slots) and carries the enum's
|
||||
type, so it flows through `let`, params and returns. A tagged `match` is checked
|
||||
for **exhaustiveness** — every variant must be handled or a `_` arm given — and
|
||||
constructor/pattern arities are checked, so adding a variant flags each match that
|
||||
must learn it. Bare enums are untouched by this and keep their zero-cost form.
|
||||
|
||||
## Expressions
|
||||
|
||||
|
|
|
|||
3
changes/tagged-union-enums.md
Normal file
3
changes/tagged-union-enums.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
Tagged-union enums (#56) — an `enum` variant may now carry a payload (`enum Tile { Empty, Wall, Door(int), Portal(int, int) }`), turning the enum into a sum type. Variants are constructed by name (`Door(3)`, bare `Empty`) and boxed as a tag plus payload slots; `match` destructures them, binding each payload in the arm's scope (`Door(n) => …`, `Portal(x, y) => …`). A tagged `match` is checked for exhaustiveness — it must cover every variant or end in `_`, and constructor/pattern arities are checked too — so adding a variant surfaces every site that must handle it. Plain (all-bare) enums keep their zero-cost compile-time-ordinal `Name.Variant` representation, byte-for-byte unchanged.
|
||||
|
|
@ -11,6 +11,8 @@ order: 4
|
|||
|
||||
A <code>match</code> replaces an `if`/`else` ladder that tests one value against several constants. It evaluates the subject once, then takes the first arm whose pattern matches; an arm lists one or more literal patterns separated by commas and points at a body with `=>`, and a lone `_` arm is the catch-all default. Patterns are compile-time constants — integers, char literals like `'w'`, or `enum` variants such as `Action.Guard` — which makes `match` ideal for dispatching on a key press, a tile code, or a mode. Each arm's body is a single statement or a `{ … }` block; matching lowers to plain branches, so it is as cheap as the `if` chain it replaces.
|
||||
|
||||
When the subject is a **tagged-union enum** (an `enum` whose variants carry payloads), `match` also *destructures* it: an arm names a variant and binds its payload — `Door(n) => …`, `Portal(x, y) => …` — with `n`, `x`, `y` in scope for that arm's body. A tagged `match` must be **exhaustive**: it covers every variant or ends with a `_`, or the compiler rejects it, so a newly added variant flags every match that must handle it. See [enum](../structure/kw-enum) for the full picture.
|
||||
|
||||
```ludic
|
||||
program Steering {
|
||||
property Velocity { delta_x: int = 0, delta_y: int = 0 }
|
||||
|
|
|
|||
|
|
@ -9,7 +9,32 @@ tip: A named set of integer constants — names for a magic-number space.
|
|||
order: 50
|
||||
---
|
||||
|
||||
An <code>enum</code> gives names to a set of related integer values so a magic-number space — a menu selection, a game mode, a machine state — reads as names instead of bare literals. Variants number themselves from `0` in declaration order, and you access one as `Name.Variant`, which is a compile-time `int` usable anywhere an int is: in `match` patterns, comparisons, and assignments. Ludic has no distinct enum runtime type yet — an enum value lives in an ordinary `int` or `var` and is saved with it — so `enum` is best understood as a readable naming layer over `int`.
|
||||
An <code>enum</code> gives names to a set of related integer values so a magic-number space — a menu selection, a game mode, a machine state — reads as names instead of bare literals. Variants number themselves from `0` in declaration order, and you access one as `Name.Variant`, which is a compile-time `int` usable anywhere an int is: in `match` patterns, comparisons, and assignments. When every variant is a bare name, an enum value lives in an ordinary `int` or `var` and is saved with it, so a plain `enum` is best understood as a zero-cost naming layer over `int`.
|
||||
|
||||
## Tagged unions — variants with payloads
|
||||
|
||||
A variant may also carry a **payload**, written as a parenthesized list of types after its name. Doing so turns the whole enum into a *tagged union*: a value is now one of several shapes, each with its own data, and the type remembers which.
|
||||
|
||||
```ludic
|
||||
enum Tile { Empty, Wall, Door(int), Portal(int, int) }
|
||||
```
|
||||
|
||||
You **construct** a variant by name — `Door(3)`, `Portal(x, y)`, or bare `Empty` for a payload-less one — and store or pass it as a value of the enum's type (`let t: Tile = Door(3)`). Each value is a small boxed record: a tag naming the variant, plus its payload.
|
||||
|
||||
You take one apart with <code>match</code>, binding the payload names in each arm:
|
||||
|
||||
```ludic
|
||||
function walk_cost(t: Tile) -> int {
|
||||
match t {
|
||||
Empty => { return 1 }
|
||||
Wall => { return 0 }
|
||||
Door(n) => { return 10 + n } # n is the door's int payload
|
||||
Portal(x, y) => { return x + y } # both fields bound in this arm
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A tagged `match` must be **exhaustive**: it either handles every variant or ends with a `_` catch-all. Leaving a variant out is a compile-time error (`match on Tile is not exhaustive: variant Door is unhandled`), so adding a new variant surfaces every site that must learn about it. Constructor and pattern arities are checked the same way. A plain (all-bare) enum keeps its compile-time-ordinal `Name.Variant` form and is unaffected.
|
||||
|
||||
```ludic
|
||||
program BattleMenu {
|
||||
|
|
|
|||
|
|
@ -539,6 +539,30 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
return emit_call(e)
|
||||
}
|
||||
|
||||
# construct a tagged-enum variant box (issue #56): malloc the box, store the tag
|
||||
# at offset 0, then each payload into its 8-byte slot (slot k at byte 8*(k+1)),
|
||||
# coerced to the variant's declared payload type. `args` are the constructor
|
||||
# argument nodes (empty for a bare nullary variant). Returns the box as an
|
||||
# `en.s`-typed pointer so it flows through lets/params/returns like any handle.
|
||||
function emit_variant_new(en: Node, ord: int, args: []Node) -> Val {
|
||||
let variant = en.kids[ord]
|
||||
let arity = len(variant.kids)
|
||||
if (len(args) != arity) { perr(`enum variant {variant.s} takes {itoa(arity)} payload(s), got {itoa(len(args))}`) }
|
||||
let box = emit_bind(`call ptr @malloc(i64 {itoa(enum_box_size(en))})`)
|
||||
emit(" store i32 "); emit(itoa(ord)); emit(", ptr "); emit(box); emit("\n")
|
||||
var k = 0
|
||||
while k < arity {
|
||||
let pty = variant.kids[k].ty
|
||||
let v = emit_expr(args[k])
|
||||
let cv = coerce_code(v, pty) # emit any widening BEFORE the store line
|
||||
let p = nreg(); emit(" "); emit(p); emit(" = getelementptr inbounds i8, ptr "); emit(box)
|
||||
emit(", i32 "); emit(itoa(8 * (k + 1))); emit("\n")
|
||||
emit(" store "); emit(llty(pty)); emit(" "); emit(cv); emit(", ptr "); emit(p); emit("\n")
|
||||
k = k + 1
|
||||
}
|
||||
return val(box, en.s)
|
||||
}
|
||||
|
||||
function emit_call(e: Node) -> Val {
|
||||
# `Subject.action(...)` — a namespaced builtin (Screen/Random/Input).
|
||||
if e.a.kind == E_MEMBER {
|
||||
|
|
@ -865,6 +889,11 @@ function emit_call(e: Node) -> Val {
|
|||
emit(")\n")
|
||||
return val(erreg, ext.ty)
|
||||
}
|
||||
# a tagged-enum variant constructor with a payload: `Door(3)`, `Portal(x, y)`.
|
||||
# A user function of the same name would have been resolved above; variants are
|
||||
# capitalized by convention, so this rarely competes.
|
||||
let ctor = variant_enum(name)
|
||||
if (ctor != null) { return emit_variant_new(ctor, g_var_ord, e.kids) }
|
||||
var fn2 = find_fn(name)
|
||||
var cname = name
|
||||
if (fn2 == null) {
|
||||
|
|
@ -935,6 +964,10 @@ function emit_expr(e: Node) -> Val {
|
|||
}
|
||||
# a UI_<name> that is not a const/var resolves to its widget index
|
||||
if is_ui_ident(e.s) { return val(itoa(ui_index_of(e.s)), "int") }
|
||||
# a bare payload-less tagged-enum variant: `Empty` boxes a tag with no
|
||||
# payload. Locals/globals were checked first, so a same-named binding wins.
|
||||
let nv = variant_enum(e.s)
|
||||
if (nv != null) { return emit_variant_new(nv, g_var_ord, new []Node) }
|
||||
perr(`unknown identifier {e.s}`)
|
||||
}
|
||||
if e.kind == E_MEMBER {
|
||||
|
|
|
|||
|
|
@ -197,6 +197,67 @@ function find_fn(name: pointer) -> Node {
|
|||
return null
|
||||
}
|
||||
|
||||
# ---- tagged-union enums (issue #56) ----------------------------------------
|
||||
# A tagged enum is one where at least one variant declares a payload. Such enums
|
||||
# use a boxed value ABI (a heap block: an i32 tag at offset 0, then one 8-byte
|
||||
# slot per payload position); a payload-less enum keeps its compile-time-ordinal
|
||||
# int representation untouched, so every existing enum + golden render is byte-
|
||||
# identical. `g_var_ord` carries the ordinal matched by variant_enum().
|
||||
var g_var_ord: int = 0
|
||||
|
||||
function find_enum(name: pointer) -> Node {
|
||||
var i = 0
|
||||
while i < len(prog) { let d = prog[i]; if d.kind == N_ENUM and (d.s == name) { return d }; i = i + 1 }
|
||||
return null
|
||||
}
|
||||
function enum_is_tagged(en: Node) -> bool {
|
||||
var i = 0
|
||||
while i < len(en.kids) { if len(en.kids[i].kids) > 0 { return true }; i = i + 1 }
|
||||
return false
|
||||
}
|
||||
# the tagged enum whose type name is `ty`, or null (a plain enum / non-enum type
|
||||
# yields null, so the scalar match/ordinal paths stay in charge of those).
|
||||
function tagged_enum_of(ty: pointer) -> Node {
|
||||
let en = find_enum(ty)
|
||||
if (en == null) { return null }
|
||||
if enum_is_tagged(en) { return en }
|
||||
return null
|
||||
}
|
||||
# the ordinal of variant `vname` within enum `en`, or -1.
|
||||
function variant_ordinal(en: Node, vname: pointer) -> int {
|
||||
var j = 0
|
||||
while j < len(en.kids) { if (en.kids[j].s == vname) { return j }; j = j + 1 }
|
||||
return 0 - 1
|
||||
}
|
||||
# find the tagged enum that declares a variant named `vname` (used to resolve a
|
||||
# bare constructor like `Door(3)` / `Empty`); sets g_var_ord to its ordinal.
|
||||
# Only tagged enums participate, so a plain enum's `Enum.Variant` member form and
|
||||
# any like-named function are left alone.
|
||||
function variant_enum(vname: pointer) -> Node {
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
if d.kind == N_ENUM {
|
||||
if enum_is_tagged(d) {
|
||||
let ord = variant_ordinal(d, vname)
|
||||
if ord >= 0 { g_var_ord = ord; return d }
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return null
|
||||
}
|
||||
# the widest payload arity across an enum's variants — sizes the box.
|
||||
function enum_max_arity(en: Node) -> int {
|
||||
var m = 0
|
||||
var i = 0
|
||||
while i < len(en.kids) { let a = len(en.kids[i].kids); if a > m { m = a }; i = i + 1 }
|
||||
return m
|
||||
}
|
||||
# byte size of a variant box: an 8-byte tag slot + one 8-byte slot per payload
|
||||
# position (8 bytes holds any i32/i64/ptr payload with natural alignment).
|
||||
function enum_box_size(en: Node) -> int { return 8 * (1 + enum_max_arity(en)) }
|
||||
|
||||
# `extern function name(params) -> T = "sym"` binds a Ludic name to a link symbol. A
|
||||
# call to `name` lowers to a direct `@<sym>` call (no @fn_ prefix — the string is
|
||||
# the exact linked symbol), and emit_extern_decls emits a matching `declare`. This
|
||||
|
|
|
|||
|
|
@ -127,8 +127,90 @@ function arm_is_default(arm: Node) -> bool {
|
|||
return false
|
||||
}
|
||||
|
||||
# bind a tagged variant's payloads as locals for one arm's body. Pattern `pat`
|
||||
# is the E_CALL `Door(n)` / `Portal(x, y)`; each argument E_ID names a payload
|
||||
# slot. Each binding gets its own stack slot (loaded from the box), so it reads
|
||||
# and shadows uniformly with any other local. `sv` is the box pointer.
|
||||
function emit_bind_payload(en: Node, pat: Node, sv: Val) -> void {
|
||||
let ord = variant_ordinal(en, pat.a.s)
|
||||
let variant = en.kids[ord]
|
||||
if (len(pat.kids) != len(variant.kids)) {
|
||||
perr(`match: {pat.a.s} binds {itoa(len(pat.kids))} name(s) but carries {itoa(len(variant.kids))}`)
|
||||
}
|
||||
var k = 0
|
||||
while k < len(pat.kids) {
|
||||
if not (pat.kids[k].kind == E_ID) { perr(`match: {pat.a.s} payload {itoa(k)} must be a binding name`) }
|
||||
let pty = variant.kids[k].ty
|
||||
let lt = llty(pty)
|
||||
let p = nreg(); emit(" "); emit(p); emit(" = getelementptr inbounds i8, ptr "); emit(sv.code)
|
||||
emit(", i32 "); emit(itoa(8 * (k + 1))); emit("\n")
|
||||
let lv = emit_bind(`load {lt}, ptr {p}`)
|
||||
let slot = emit_alloca(lt); store_at(lt, lv, slot)
|
||||
loc_push(pat.kids[k].s, slot, pty)
|
||||
k = k + 1
|
||||
}
|
||||
}
|
||||
|
||||
# `match` over a tagged-union enum (issue #56): switch on the box's tag, bind
|
||||
# each arm's payload names, and require the arms to be exhaustive (cover every
|
||||
# variant) unless a `_` default is present — the checking pass the proposal asks
|
||||
# for, run where the scrutinee's type is known.
|
||||
function emit_match_tagged(st: Node, sv: Val, en: Node) -> void {
|
||||
let tag = emit_bind(`load i32, ptr {sv.code}`)
|
||||
let endl = lbl("mend")
|
||||
var deflt: Node = null
|
||||
var covered = 0 # bitmask of matched ordinals
|
||||
var i = 0
|
||||
while i < len(st.kids) {
|
||||
let arm = st.kids[i]
|
||||
if arm_is_default(arm) { deflt = arm }
|
||||
else {
|
||||
var acc = "0"
|
||||
var first = true
|
||||
var bindPat: Node = null
|
||||
var p = 0
|
||||
while p < len(arm.kids) {
|
||||
let pat = arm.kids[p]
|
||||
var vname = pat.s
|
||||
if pat.kind == E_CALL { vname = pat.a.s; bindPat = pat }
|
||||
let ord = variant_ordinal(en, vname)
|
||||
if (ord < 0) { perr(`match: {vname} is not a variant of enum {en.s}`) }
|
||||
covered = covered | (1 << ord)
|
||||
let c = emit_bind(`icmp eq i32 {tag}, {itoa(ord)}`)
|
||||
if first { acc = c; first = false } else { acc = emit_bind(`or i1 {acc}, {c}`) }
|
||||
p = p + 1
|
||||
}
|
||||
let bodyl = lbl("mbody"); let nextl = lbl("marm")
|
||||
emit(" br i1 "); emit(acc); emit(", label %"); emit(bodyl); emit(", label %"); emit(nextl); emit("\n")
|
||||
emit(bodyl); emit(":\n"); g_term = false
|
||||
let save = nloc
|
||||
if (bindPat != null) { emit_bind_payload(en, bindPat, sv) }
|
||||
emit_block(arm.a)
|
||||
nloc = save # drop the arm's payload bindings
|
||||
if not g_term { emit(" br label %"); emit(endl); emit("\n") }
|
||||
emit(nextl); emit(":\n"); g_term = false
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
if (deflt != null) { emit_block(deflt.a) }
|
||||
else {
|
||||
let full = (1 << len(en.kids)) - 1
|
||||
if not (covered == full) { # find and name the first uncovered variant
|
||||
var m = 0
|
||||
while m < len(en.kids) {
|
||||
if (covered & (1 << m)) == 0 { perr(`match on {en.s} is not exhaustive: variant {en.kids[m].s} is unhandled (add it or a _ arm)`) }
|
||||
m = m + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if not g_term { emit(" br label %"); emit(endl); emit("\n") }
|
||||
emit(endl); emit(":\n"); g_term = false
|
||||
}
|
||||
|
||||
function emit_match(st: Node) -> void {
|
||||
let sv = emit_expr(st.a)
|
||||
let te = tagged_enum_of(sv.ty)
|
||||
if (te != null) { emit_match_tagged(st, sv, te); return }
|
||||
let endl = lbl("mend")
|
||||
var deflt: Node = null
|
||||
var i = 0
|
||||
|
|
|
|||
|
|
@ -210,10 +210,25 @@ function parse_scene() -> void {
|
|||
# enum Name { A, B, C } — named int constants; a variant's value is its index.
|
||||
# Accessed as `Name.A` (a compile-time int), so it names magic-int value spaces
|
||||
# (state ids, menu selections, mode registers) without a runtime cost.
|
||||
#
|
||||
# A variant may also carry a payload — `enum Tile { Empty, Wall, Door(int),
|
||||
# Portal(int, int) }` (issue #56). A payload turns the whole enum into a tagged
|
||||
# union: variants are constructed by name (`Door(3)`, bare `Empty`) and boxed
|
||||
# (a tag + payload slots), and `match` destructures them (`Door(n) => …`). An
|
||||
# all-payload-less enum keeps the zero-cost compile-time-ordinal representation.
|
||||
# Each variant node is an E_ID (s=variant name); its payload types are stored as
|
||||
# N_PARAM kids, one per slot, carrying only `.ty`.
|
||||
function parse_enum() -> Node {
|
||||
pi = pi + 1; let n = node(N_ENUM); n.s = eat_id(); skipnl(); eat_op("{")
|
||||
while true { skipnl(); if is_op("}") { break }
|
||||
let v = node(E_ID); v.s = eat_id(); push(n.kids, v)
|
||||
let v = node(E_ID); v.s = eat_id()
|
||||
if is_op("(") { # variant payload: Door(int), Portal(int, int)
|
||||
pi = pi + 1; skipnl()
|
||||
while not is_op(")") {
|
||||
let p = node(N_PARAM); p.ty = ptype(); push(v.kids, p)
|
||||
if is_op(",") { pi = pi + 1 }; skipnl() }
|
||||
eat_op(")") }
|
||||
push(n.kids, v)
|
||||
if is_op(",") { pi = pi + 1 }; skipnl() }
|
||||
eat_op("}"); return n
|
||||
}
|
||||
|
|
|
|||
34164
selfhost/ludicc.seed.ll
34164
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
50
selfhost/tests/enums.ludic
Normal file
50
selfhost/tests/enums.ludic
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
program T {
|
||||
# a tagged-union enum (issue #56): variants carry payloads, are constructed by
|
||||
# name, and destructure in `match` with the payload names bound in each arm.
|
||||
enum Tile { Empty, Wall, Door(int), Portal(int, int) }
|
||||
|
||||
# match binds each variant's payload; the arms are exhaustive (no `_`).
|
||||
function walk_cost(t: Tile) -> int {
|
||||
match t {
|
||||
Empty => { return 1 }
|
||||
Wall => { return 0 }
|
||||
Door(n) => { return 10 + n }
|
||||
Portal(x, y) => { return x + y }
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
# a nested match and an enum flowing through a return
|
||||
function open(t: Tile) -> Tile {
|
||||
match t {
|
||||
Door(n) => { return Portal(n, n) }
|
||||
_ => { return t }
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
entry {
|
||||
print(walk_cost(Empty)) # 1
|
||||
print(walk_cost(Wall)) # 0
|
||||
print(walk_cost(Door(5))) # 15
|
||||
print(walk_cost(Portal(3, 4))) # 7
|
||||
|
||||
let d = Door(9)
|
||||
print(walk_cost(d)) # 19
|
||||
print(walk_cost(open(d))) # 18 (Door(9) -> Portal(9,9) -> 18)
|
||||
print(walk_cost(open(Wall))) # 0 (default arm returns t unchanged)
|
||||
|
||||
# a match with a default arm, OR-ed nullary patterns, and a bound arm
|
||||
let t = Portal(2, 40)
|
||||
match t {
|
||||
Empty, Wall => { print(100) }
|
||||
Portal(x, y) => { print(x * y) } # 80
|
||||
_ => { print(999) }
|
||||
}
|
||||
|
||||
# payload-less variant boxed and matched
|
||||
var here: Tile = Empty
|
||||
here = Door(7)
|
||||
print(walk_cost(here)) # 17
|
||||
}
|
||||
}
|
||||
|
|
@ -86,6 +86,7 @@ function cmd_selfhost_test() -> int {
|
|||
sh_case("sort", "0 39 20 1 2 3 3 1 30 10 20 40 2 8")
|
||||
sh_case("control", "55 4 15 1")
|
||||
sh_case("match_bits", "1 2 9 16 4 15")
|
||||
sh_case("enums", "1 0 15 7 19 18 0 80 17")
|
||||
sh_case("fixed", "2 3 0 6 1")
|
||||
sh_case("const", "1 0 10 5 20 0 10 7 1 0")
|
||||
sh_case("math", "3 7 5 10 -1 0 1 2 3 3 2 5 25 50")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue