feat(types): tagged-union enums — variant payloads + binding match + exhaustiveness (#56)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 25s
ci / build-and-test (push) Successful in 1m26s
commit-lint / conventional-commits (push) Successful in 2s
docs / build-and-deploy (push) Successful in 23s

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:
Orkun ÇAKILKAYA 2026-09-01 03:07:46 +03:00
parent 7c91d24595
commit 872f458cb2
11 changed files with 17990 additions and 16481 deletions

View file

@ -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 {