ludic/selfhost/backend/emit_stmt.ludic
Orkuncakilkaya e738741521 feat(cli): ludic deps - the module graph as the compiler resolved it
With LUDIC_DEPS=<file> the compiler records every reference the
visibility pass resolves (from module, to module) and every assignment
to another module's global. ludic deps prints the modules,
dependencies, largest cycle, cross writes and globals written from
outside, with --graph, --dot, --writes, --uses MOD, --check FILE and
--baseline FILE. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:16:34 +03:00

405 lines
16 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 {
# each block is its own place to declare a name once (loc_blk), though a local declared in a
# block still stays visible after it
let outer = g_blk_cur
g_blk_next += 1
g_blk_cur = g_blk_next
var i = 0
while i < len(b.kids) and not g_term {
emit_stmt(b.kids[i])
i += 1
}
g_blk_cur = outer
}
# 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")
}
# the binary operator behind a compound assignment token: "+=" -> "+"
function compound_op(tok: pointer) -> pointer {
return tok[0..1]
}
function emit_assign(st: Node) -> void {
# resolve the target's address and type
let t = st.a
var addr: pointer = "0"
var ty: pointer = "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}`) }
vis_check(g, t.s)
deps_write(g, t.s)
addr = `@g_{t.s}`; ty = g.ty
}
} else {
if deps_on() {
let root = deps_target_root(t)
if root != null { deps_write(root, root.s) }
}
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)
var rv = emit_expr(st.b)
if not (st.s == "=") {
# `x op= y` is `x = x op y`: reload the target and lower through the same
# path as a binary expression, so fixed `*=`, string `+=` and long promotion
# all behave exactly like their spelled-out forms.
let cur = val(emit_bind(`load {lt}, ptr {addr}`), ty)
rv = emit_bin_vals(compound_op(st.s), cur, rv, false)
}
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
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) and (ret_ty == "void") { perr("a function that returns nothing cannot return a value") }
if (st.a != null) {
let v = emit_expr(st.a)
store_at(llty(ret_ty), coerce_code(v, ret_ty), "%retval")
}
emit(` br label %{g_ret_label}\n`)
g_term = true
}
# Does this block always end - a return on every path through it? A return ends it; an `if` ends
# it when it has an `else` and both branches end; a `match` when it has a default arm and every
# arm ends. Asked of a function's body, so one with a result cannot run off its end.
function block_ends(b: Node) -> bool {
if b == null { return false }
var i = 0
while i < len(b.kids) {
if stmt_ends(b.kids[i]) { return true }
i += 1
}
return false
}
function stmt_ends(st: Node) -> bool {
if st.kind == S_RETURN { return true }
if st.kind == S_UNSAFE { return block_ends(st.a) }
if st.kind == S_IF {
if st.c == null or not block_ends(st.b) { return false }
if st.c.kind == S_IF { return stmt_ends(st.c) }
return block_ends(st.c)
}
if st.kind == S_MATCH {
var has_default = false
var i = 0
while i < len(st.kids) {
let arm = st.kids[i]
if arm_is_default(arm) { has_default = true }
if not block_ends(arm.a) { return false }
i += 1
}
return has_default
}
return false
}
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 += 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 += 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: pointer = "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 += 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 += 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 += 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: pointer = "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 += 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 += 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}`) }
vis_check(ev, 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 += 1 }
let lt = llty(fd.ty)
var code: pointer = "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 += 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 += net_field_ibytes(ev.kids[k].ty)
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 += 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 {
g_err_file = st.file; g_err_line = st.line # so a lowering error names its statement
emit_cov_hit(st.line) # --coverage: bump this line's hit counter (no-op otherwise)
if st.kind == S_LET {
loc_check_redeclared(st.s)
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_mark_declared(); loc_set_mut(st.ival); return }
let slot = emit_alloca(llty(ty))
if (st.a == null) {
store_at(llty(ty), zero_of(llty(ty)), slot)
} else { let v = emit_expr(st.a); store_at(llty(ty), coerce_code(v, ty), slot) }
loc_push(st.s, slot, ty)
loc_mark_declared()
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_UNSAFE { emit_block(st.a); 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 += 1
}
function loop_pop() -> void { nloop -= 1 }