ludic/selfhost/backend/emit_stmt.ludic
Orkuncakilkaya 872f458cb2
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
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>
2026-09-01 03:07:46 +03:00

352 lines
14 KiB
Text

# emit_stmt.ludic — lower statements. Terminators set g_term so the rest of a
# block is skipped until a new basic block opens.
function emit_block(b: Node) -> void {
var i = 0
while i < len(b.kids) {
if g_term { return }
emit_stmt(b.kids[i])
i = i + 1
}
}
# store `val` (llvm type `lt`) into address `addr`
function store_at(lt: pointer, v: pointer, addr: pointer) -> void {
emit(" store "); emit(lt); emit(" "); emit(v); emit(", ptr "); emit(addr); emit("\n")
}
function emit_assign(st: Node) -> void {
# resolve the target's address and type
let t = st.a
var addr = "0"
var ty = "int"
if t.kind == E_ID {
let li = loc_find(t.s)
if li >= 0 {
# a `let` binding is immutable: rebinding it is an error (mutating THROUGH
# it — `x.field = …`, `x[i] = …` — is fine, and lands in the branches below).
if loc_mut[li] == 0 { perr(`cannot assign to immutable '{t.s}' (declared with let; use var) — line {itoa(st.line)}`) }
addr = loc_reg[li]; ty = loc_ty[li]
}
else {
let g = find_global(t.s)
if (g == null) { perr(`assign to unknown {t.s}`) }
addr = `@g_{t.s}`; ty = g.ty
}
} else {
if t.kind == E_MEMBER { addr = emit_member_addr(t); ty = g_addr_ty }
else { if t.kind == E_INDEX { addr = emit_index_addr(t); ty = g_addr_ty }
else { perr("bad assignment target") } }
}
let lt = llty(ty)
let rv = emit_expr(st.b)
var v = coerce_code(rv, ty)
if (lt == "i8") { v = emit_bind(`trunc i32 {v} to i8`) } # narrow to a byte for p[i] = v
if not (st.s == "=") {
let cur = emit_bind(`load {lt}, ptr {addr}`)
var opc = "add"
if (st.s == ("-=")) { opc = "sub" }
if (st.s == ("*=")) { opc = "mul" }
if (st.s == ("/=")) { opc = "sdiv" }
v = emit_bind(`{opc} {lt} {cur}, {v}`)
}
store_at(lt, v, addr)
}
function emit_if(st: Node) -> void {
let c = emit_expr(st.a)
let cc = emit_bind(`icmp ne i32 {c.code}, 0`)
let has_else = (st.c != null)
let tl = lbl("then"); let el = lbl("else"); let en = lbl("ifend")
if has_else { emit(" br i1 "); emit(cc); emit(", label %"); emit(tl); emit(", label %"); emit(el); emit("\n") }
else { emit(" br i1 "); emit(cc); emit(", label %"); emit(tl); emit(", label %"); emit(en); emit("\n") }
emit(tl); emit(":\n"); g_term = false
emit_block(st.b)
if not g_term { emit(" br label %"); emit(en); emit("\n") }
if has_else {
emit(el); emit(":\n"); g_term = false
# else is either a block or a nested if-statement
if st.c.kind == S_IF { emit_stmt(st.c) } else { emit_block(st.c) }
if not g_term { emit(" br label %"); emit(en); emit("\n") }
}
emit(en); emit(":\n"); g_term = false
}
function emit_while(st: Node) -> void {
let cl = lbl("wcond"); let bl = lbl("wbody"); let en = lbl("wend")
emit(" br label %"); emit(cl); emit("\n")
emit(cl); emit(":\n")
let c = emit_expr(st.a)
let cc = emit_bind(`icmp ne i32 {c.code}, 0`)
emit(" br i1 "); emit(cc); emit(", label %"); emit(bl); emit(", label %"); emit(en); emit("\n")
emit(bl); emit(":\n"); g_term = false
loop_push(cl, en)
emit_block(st.b)
loop_pop()
if not g_term { emit(" br label %"); emit(cl); emit("\n") }
emit(en); emit(":\n"); g_term = false
}
function emit_for(st: Node) -> void {
let slot = emit_alloca("i32")
let lo = emit_expr(st.a)
store_at("i32", lo.code, slot)
loc_push(st.s, slot, "int")
let cl = lbl("fcond"); let bl = lbl("fbody"); let ct = lbl("fcont"); let en = lbl("fend")
emit(" br label %"); emit(cl); emit("\n")
emit(cl); emit(":\n")
let iv = emit_bind(`load i32, ptr {slot}`)
let hi = emit_expr(st.b)
let cc = emit_bind(`icmp slt i32 {iv}, {hi.code}`)
emit(" br i1 "); emit(cc); emit(", label %"); emit(bl); emit(", label %"); emit(en); emit("\n")
emit(bl); emit(":\n"); g_term = false
loop_push(ct, en)
emit_block(st.c)
loop_pop()
if not g_term { emit(" br label %"); emit(ct); emit("\n") }
emit(ct); emit(":\n")
let i2 = emit_bind(`load i32, ptr {slot}`)
let i3 = emit_bind(`add i32 {i2}, 1`)
store_at("i32", i3, slot)
emit(" br label %"); emit(cl); emit("\n")
emit(en); emit(":\n"); g_term = false
}
function emit_return(st: Node) -> void {
if (st.a != null) {
let v = emit_expr(st.a)
store_at(llty(ret_ty), coerce_code(v, ret_ty), "%retval")
}
emit(" br label %ret\n")
g_term = true
}
function arm_is_default(arm: Node) -> bool {
var p = 0
while p < len(arm.kids) { if arm.kids[p].kind == E_ID and (arm.kids[p].s == "_") { return true }; p = p + 1 }
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
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 p = 0
while p < len(arm.kids) {
let pv = emit_expr(arm.kids[p])
let c = emit_bind(`icmp eq i32 {sv.code}, {pv.code}`)
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
emit_block(arm.a)
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) }
if not g_term { emit(" br label %"); emit(endl); emit("\n") }
emit(endl); emit(":\n"); g_term = false
}
# emit E(field: v, ...) — evaluate the payload args in the event's declared field
# order (so call args line up with the `@ev_<E>` signature), then a direct call.
# A missing arg falls back to the field's default; an unset scalar/ptr to 0/null.
function emit_emit(st: Node) -> Val {
let ev = find_event(st.s)
if (ev == null) { perr(`emit: unknown event {st.s}`) }
# evaluate each payload field in declared order (default for a missing arg)
let fcodes = new []pointer
let ftys = new []pointer
var f = 0
while f < len(ev.kids) {
let fd = ev.kids[f]
var av: Node = null # the caller's value for this field, if given
var j = 0
while j < len(st.a.kids) { if (st.a.kids[j].s == fd.s) { av = st.a.kids[j].a }; j = j + 1 }
let lt = llty(fd.ty)
var code = "0"
if (lt == "ptr") { code = "null" }
if (av != null) { let v = emit_expr(av); code = v.code }
else { if (fd.a != null) { let dv = emit_expr(fd.a); code = dv.code } } # declared default
push(fcodes, code); push(ftys, lt)
f = f + 1
}
# N4: a remote event (@ToServer/@ToClients) serializes its payload as
# [i32 event_id][packed fields] and net_send in its direction — the far side's
# net_pump() re-emits it. It is a remote call, so there is no local dispatch.
if (ev.ty != null) {
let idp = nreg(); emit(" "); emit(idp); emit(" = getelementptr inbounds i8, ptr @L_sendbuf, i32 0\n")
emit(" store i32 "); emit(itoa(net_event_id(st.s))); emit(", ptr "); emit(idp); emit("\n")
var off = 4
var k = 0
while k < len(ev.kids) {
let dp = nreg(); emit(" "); emit(dp); emit(" = getelementptr inbounds i8, ptr @L_sendbuf, i32 "); emit(itoa(off)); emit("\n")
emit(" store "); emit(ftys[k]); emit(" "); emit(fcodes[k]); emit(", ptr "); emit(dp); emit("\n")
off = off + net_field_ibytes(ev.kids[k].ty)
k = k + 1
}
var peer = "0"
if (ev.ty == "toclients") { peer = "-1" } # broadcast (loopback ignores the peer id)
if (find_extern("net_send") == null) { g_uses_loopback = true }
emit(" call void @"); emit(net_send_sym()); emit("(i32 "); emit(peer); emit(", ptr @L_sendbuf, i32 "); emit(itoa(off)); emit(")\n")
return val("0", "int")
}
# a local event: build the call args and dispatch. A cancellable event returns
# its cancelled flag (i32); a plain event is void.
let args = buf_new()
var g = 0
while g < len(fcodes) {
if g > 0 { buf_puts(args, ", ") }
buf_puts(args, ftys[g]); buf_puts(args, " "); buf_puts(args, fcodes[g])
g = g + 1
}
if ev.ival == 1 {
let r = nreg()
emit(" "); emit(r); emit(" = call i32 @ev_"); emit(st.s); emit("("); emit(buf_str(args)); emit(")\n")
return val(r, "int")
}
emit(" call void @ev_"); emit(st.s); emit("("); emit(buf_str(args)); emit(")\n")
return val("0", "int")
}
function emit_stmt(st: Node) -> void {
emit_cov_hit(st.line) # --coverage: bump this line's hit counter (no-op otherwise)
if st.kind == S_LET {
var ty = st.ty
if (ty == null) { let v0 = emit_expr(st.a); ty = v0.ty
let slot = emit_alloca(llty(ty)); store_at(llty(ty), v0.code, slot); loc_push(st.s, slot, ty); loc_set_mut(st.ival); return }
let slot = emit_alloca(llty(ty))
if (st.a == null) {
var z = "0"
if (llty(ty) == "ptr") { z = "null" }
store_at(llty(ty), z, slot)
} else { let v = emit_expr(st.a); store_at(llty(ty), coerce_code(v, ty), slot) }
loc_push(st.s, slot, ty)
loc_set_mut(st.ival)
return
}
if st.kind == S_ASSIGN { emit_assign(st); return }
if st.kind == S_IF { emit_if(st); return }
if st.kind == S_WHILE { emit_while(st); return }
if st.kind == S_FOR { emit_for(st); return }
if st.kind == S_RETURN { emit_return(st); return }
if st.kind == S_BREAK { emit(" br label %"); emit(brk_lbl[nloop - 1]); emit("\n"); g_term = true; return }
if st.kind == S_CONTINUE { emit(" br label %"); emit(cnt_lbl[nloop - 1]); emit("\n"); g_term = true; return }
if st.kind == S_MATCH { emit_match(st); return }
if st.kind == S_QUERY { emit_query(st); return }
if st.kind == S_SPAWN { let se = emit_spawn(st); return }
if st.kind == S_DESPAWN { emit_despawn(st); return }
if st.kind == S_TOGGLE { emit_toggle(st); return }
if st.kind == S_ATTACH { emit_attach(st); return }
if st.kind == S_DETACH { emit_detach(st); return }
if st.kind == S_EMIT { let v = emit_emit(st); return } # statement form: discard the flag
if st.kind == S_CANCEL { # veto the enclosing cancellable event
if (g_cancel_addr == null) { perr("cancel outside a cancellable event listener") }
emit(" store i32 1, ptr "); emit(g_cancel_addr); emit("\n"); return
}
if st.kind == S_MACHINE { emit_machine(st); return }
if st.kind == S_BECOME { emit_become(st); return }
if st.kind == S_EXPR { let v = emit_expr(st.a); return }
perr("cannot emit statement")
}
function loop_push(cont: pointer, brk: pointer) -> void {
if nloop < len(cnt_lbl) { cnt_lbl[nloop] = cont; brk_lbl[nloop] = brk }
else { push(cnt_lbl, cont); push(brk_lbl, brk) }
nloop = nloop + 1
}
function loop_pop() -> void { nloop = nloop - 1 }