refactor(selfhost): reorganise into concern-based subdirectories
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 12s
ci / build-and-test (push) Successful in 50s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 2s

Split the flat 38-file selfhost/ into concern-based subdirectories:

  frontend/        lex, parse, parse_game, ast
  support/         str, buf, io
  backend/         core IR + expression/statement lowering
  backend/game/    ECS/scene/event/world lowering
  backend/stdlib/  the namespaced Math.*/Text.*/Crypto.*/… intrinsics

and split the three oversized emitters at responsibility boundaries so
no file mixes concerns:

  emit_game.ludic  -> + emit_world.ludic         (reflection world table,
                                                  tick helpers, @main synthesis)
  emit_expr.ludic  -> + emit_call.ludic          (namespaced builtins, call
                                                  lowering, expr dispatch)
  emit_text.ludic  -> + emit_text_prelude.ludic  (emitted string-builder runtime)

FRAGS in tools/x/selfhost.ludic is updated to the new paths with the link
order preserved, and the Python doc/vocabulary tooling is updated to walk
the new layout. Because the build is a plain in-order concatenation and
every split lands on a blank-line boundary, the regenerated seed is
byte-identical: `x reseed` leaves selfhost/ludicc.seed.ll unchanged,
`x bootstrap-cfree` still reaches its fixed point, and both `x test` (56)
and `x selfhost-test` (29, incl. golden renders) stay green.

Closes #29

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-31 00:26:02 +03:00
parent f55216af50
commit 23726afa90
51 changed files with 780 additions and 771 deletions

View file

@ -1,192 +1,4 @@
# emit_expr.ludic — lower an expression to IR, returning its register and type.
function emit_load_at(addr: pointer, ty: pointer) -> Val {
let r = emit_bind(`load {llty(ty)}, ptr {addr}`)
return val(r, ty)
}
# short-circuit `and`/`or`: seed a slot with (left!=0), branch to decide whether
# to overwrite with (right!=0).
function emit_logic(e: Node) -> Val {
let slot = emit_alloca("i32")
let la = emit_expr(e.a)
let lc = emit_bind(`icmp ne i32 {la.code}, 0`)
let lz = emit_bind(`zext i1 {lc} to i32`)
emit(" store i32 "); emit(lz); emit(", ptr "); emit(slot); emit("\n")
let ev = lbl("sc"); let done = lbl("scend")
if (e.s == "and") { emit(" br i1 "); emit(lc); emit(", label %"); emit(ev); emit(", label %"); emit(done); emit("\n") }
else { emit(" br i1 "); emit(lc); emit(", label %"); emit(done); emit(", label %"); emit(ev); emit("\n") }
emit(ev); emit(":\n")
let rb = emit_expr(e.b)
let rc = emit_bind(`icmp ne i32 {rb.code}, 0`)
let rz = emit_bind(`zext i1 {rc} to i32`)
emit(" store i32 "); emit(rz); emit(", ptr "); emit(slot); emit("\n")
emit(" br label %"); emit(done); emit("\n")
emit(done); emit(":\n")
return val(emit_bind(`load i32, ptr {slot}`), "bool")
}
function cmp_code(op: pointer) -> pointer {
if (op == ("<")) { return "slt" }
if (op == ("<=")) { return "sle" }
if (op == (">")) { return "sgt" }
if (op == (">=")) { return "sge" }
if (op == ("==")) { return "eq" }
return "ne"
}
function is_cmp(op: pointer) -> bool {
return (op == ("<")) or (op == ("<=")) or (op == (">")) or (op == (">=")) or (op == ("==")) or (op == ("!="))
}
function arith_code(op: pointer) -> pointer {
if (op == ("+")) { return "add" }
if (op == ("-")) { return "sub" }
if (op == ("*")) { return "mul" }
if (op == ("/")) { return "sdiv" }
if (op == ("&")) { return "and" }
if (op == ("|")) { return "or" }
if (op == ("^")) { return "xor" }
if (op == ("<<")) { return "shl" }
if (op == (">>")) { return "lshr" } # logical shift (unsigned)
return "srem"
}
# widen an int value to Q16.16 by shifting left 16 (a fixed value passes through)
function to_fixed(v: Val) -> pointer {
if (v.ty == "fixed") { return v.code }
return emit_bind(`shl i32 {v.code}, 16`)
}
# coerce a value's code to the LLVM type of `target`, for the only cross-width
# pair the language has: int (i32) <-> long (i64). int widens with sext, long
# narrows with trunc; everything else (same width, or ptr) passes through.
function coerce_code(v: Val, target: pointer) -> pointer {
let lt = llty(target)
let vt = llty(v.ty)
if (lt == vt) { return v.code }
if (lt == "i64") and (vt == "i32") { return emit_bind(`sext i32 {v.code} to i64`) }
if (lt == "i32") and (vt == "i64") { return emit_bind(`trunc i64 {v.code} to i32`) }
return v.code
}
# widen an int value to i64 (a long passes through) — the long analogue of to_fixed
function to_long(v: Val) -> pointer {
if (llty(v.ty) == "i64") { return v.code }
return emit_bind(`sext i32 {v.code} to i64`)
}
# string operators: `a + b` concatenates, `a == b` / `a != b` compare by content.
# Both call the @fn_str_* prelude (emitted once per program that uses them).
function emit_str_op(op: pointer, a: Val, b: Val) -> Val {
g_uses_str = true
if (op == ("+")) {
return val(emit_bind(`call ptr @fn_str_concat(ptr {a.code}, ptr {b.code})`), "string")
}
let r = emit_bind(`call i32 @fn_str_eq(ptr {a.code}, ptr {b.code})`)
if (op == ("!=")) {
let c = emit_bind(`icmp eq i32 {r}, 0`)
return val(emit_bind(`zext i1 {c} to i32`), "bool")
}
return val(r, "bool")
}
function emit_bin(e: Node) -> Val {
if (e.s == "and") or (e.s == "or") { return emit_logic(e) }
let a = emit_expr(e.a)
let b = emit_expr(e.b)
# strings are pointer-typed, so any `+` with a pointer operand is concatenation,
# and `==`/`!=` between pointers is content comparison — except `x == null`,
# which is a pointer-identity test and falls through to the icmp below.
let ptrish = (llty(a.ty) == "ptr") or (llty(b.ty) == "ptr")
let isnull = e.a.kind == E_NULL or e.b.kind == E_NULL
if ptrish {
if (e.s == ("+")) { return emit_str_op("+", a, b) }
if ((e.s == ("==")) or (e.s == ("!="))) and not isnull { return emit_str_op(e.s, a, b) }
}
let fx = (a.ty == "fixed") or (b.ty == "fixed")
# a 64-bit operand (and no fixed/ptr involved) promotes the whole expression to
# i64: the other side widens with sext, and the result stays `long`.
let lng = ((llty(a.ty) == "i64") or (llty(b.ty) == "i64")) and not fx and not ptrish
if is_cmp(e.s) {
var ac = a.code; var bc = b.code
var ct = "i32"
if fx { ac = to_fixed(a); bc = to_fixed(b) }
else { if lng { ct = "i64"; ac = to_long(a); bc = to_long(b) }
else { if (llty(a.ty) == "ptr") or (llty(b.ty) == "ptr") { ct = "ptr" } } } # `p == null`, str/record identity
let c = emit_bind(`icmp {cmp_code(e.s)} {ct} {ac}, {bc}`)
return val(emit_bind(`zext i1 {c} to i32`), "bool")
}
if lng {
let al = to_long(a); let bl = to_long(b)
return val(emit_bind(`{arith_code(e.s)} i64 {al}, {bl}`), "long")
}
if fx {
let af = to_fixed(a); let bf = to_fixed(b)
if (e.s == ("*")) {
let a64 = emit_bind(`sext i32 {af} to i64`)
let b64 = emit_bind(`sext i32 {bf} to i64`)
let m = emit_bind(`mul i64 {a64}, {b64}`)
let sh = emit_bind(`ashr i64 {m}, 16`)
return val(emit_bind(`trunc i64 {sh} to i32`), "fixed")
}
if (e.s == ("/")) {
let a64 = emit_bind(`sext i32 {af} to i64`)
let ash = emit_bind(`shl i64 {a64}, 16`)
let b64 = emit_bind(`sext i32 {bf} to i64`)
let dv = emit_bind(`sdiv i64 {ash}, {b64}`)
return val(emit_bind(`trunc i64 {dv} to i32`), "fixed")
}
let r = emit_bind(`{arith_code(e.s)} i32 {af}, {bf}`)
return val(r, "fixed")
}
let r = emit_bind(`{arith_code(e.s)} i32 {a.code}, {b.code}`)
return val(r, "int")
}
# ---- named arguments -------------------------------------------------------
# An argument list is either all-positional or all-named. When named, each kid
# is an E_FINIT (label -> value); this rewrites e.kids into plain value exprs in
# the order the callee declares its parameters, so the rest of emit_call is
# oblivious to whether the caller used names.
function args_are_named(e: Node) -> bool {
var i = 0
while i < len(e.kids) { if e.kids[i].kind == E_FINIT { return true }; i = i + 1 }
return false
}
function reorder_named(e: Node, labels: []pointer) -> void {
if not args_are_named(e) { return }
var i = 0
while i < len(e.kids) {
if e.kids[i].kind != E_FINIT { perr("named and positional arguments cannot be mixed in one call") }
i = i + 1
}
if len(e.kids) != len(labels) { perr("wrong number of arguments") }
let out = new []Node
var li = 0
while li < len(labels) {
var found: Node = null
var k = 0
while k < len(e.kids) { if (e.kids[k].s == labels[li]) { found = e.kids[k] }; k = k + 1 }
if (found == null) { perr(`no argument named {labels[li]}`) }
push(out, found.a)
li = li + 1
}
e.kids = out
}
# The parameter labels of a resolved fn/extern, in declaration order.
function param_labels(fn: Node) -> []pointer {
let out = new []pointer
var i = 0
while i < len(fn.kids) { if fn.kids[i].kind == N_PARAM { push(out, fn.kids[i].s) }; i = i + 1 }
return out
}
function param_types(fn: Node) -> []pointer {
let out = new []pointer
var i = 0
while i < len(fn.kids) { if fn.kids[i].kind == N_PARAM { push(out, fn.kids[i].ty) }; i = i + 1 }
return out
}
# emit_call.ludic — call lowering: namespaced builtins (Screen.*/Random.*/Input.* …), ordinary/user call emission, and the top-level emit_expr dispatch. Split out of emit_expr.ludic (concern: calls & expression dispatch, vs. emit_expr.ludic's operators/binary/coercion machinery).
# ---- namespaced builtins: Screen.* / Random.* / Input.* --------------------
# The game-facing API reads as `subject.action(...)`. Each method maps to a bare
# runtime builtin plus the parameter labels callers may use as named arguments;

View file

@ -0,0 +1,189 @@
# emit_expr.ludic — lower an expression to IR, returning its register and type.
function emit_load_at(addr: pointer, ty: pointer) -> Val {
let r = emit_bind(`load {llty(ty)}, ptr {addr}`)
return val(r, ty)
}
# short-circuit `and`/`or`: seed a slot with (left!=0), branch to decide whether
# to overwrite with (right!=0).
function emit_logic(e: Node) -> Val {
let slot = emit_alloca("i32")
let la = emit_expr(e.a)
let lc = emit_bind(`icmp ne i32 {la.code}, 0`)
let lz = emit_bind(`zext i1 {lc} to i32`)
emit(" store i32 "); emit(lz); emit(", ptr "); emit(slot); emit("\n")
let ev = lbl("sc"); let done = lbl("scend")
if (e.s == "and") { emit(" br i1 "); emit(lc); emit(", label %"); emit(ev); emit(", label %"); emit(done); emit("\n") }
else { emit(" br i1 "); emit(lc); emit(", label %"); emit(done); emit(", label %"); emit(ev); emit("\n") }
emit(ev); emit(":\n")
let rb = emit_expr(e.b)
let rc = emit_bind(`icmp ne i32 {rb.code}, 0`)
let rz = emit_bind(`zext i1 {rc} to i32`)
emit(" store i32 "); emit(rz); emit(", ptr "); emit(slot); emit("\n")
emit(" br label %"); emit(done); emit("\n")
emit(done); emit(":\n")
return val(emit_bind(`load i32, ptr {slot}`), "bool")
}
function cmp_code(op: pointer) -> pointer {
if (op == ("<")) { return "slt" }
if (op == ("<=")) { return "sle" }
if (op == (">")) { return "sgt" }
if (op == (">=")) { return "sge" }
if (op == ("==")) { return "eq" }
return "ne"
}
function is_cmp(op: pointer) -> bool {
return (op == ("<")) or (op == ("<=")) or (op == (">")) or (op == (">=")) or (op == ("==")) or (op == ("!="))
}
function arith_code(op: pointer) -> pointer {
if (op == ("+")) { return "add" }
if (op == ("-")) { return "sub" }
if (op == ("*")) { return "mul" }
if (op == ("/")) { return "sdiv" }
if (op == ("&")) { return "and" }
if (op == ("|")) { return "or" }
if (op == ("^")) { return "xor" }
if (op == ("<<")) { return "shl" }
if (op == (">>")) { return "lshr" } # logical shift (unsigned)
return "srem"
}
# widen an int value to Q16.16 by shifting left 16 (a fixed value passes through)
function to_fixed(v: Val) -> pointer {
if (v.ty == "fixed") { return v.code }
return emit_bind(`shl i32 {v.code}, 16`)
}
# coerce a value's code to the LLVM type of `target`, for the only cross-width
# pair the language has: int (i32) <-> long (i64). int widens with sext, long
# narrows with trunc; everything else (same width, or ptr) passes through.
function coerce_code(v: Val, target: pointer) -> pointer {
let lt = llty(target)
let vt = llty(v.ty)
if (lt == vt) { return v.code }
if (lt == "i64") and (vt == "i32") { return emit_bind(`sext i32 {v.code} to i64`) }
if (lt == "i32") and (vt == "i64") { return emit_bind(`trunc i64 {v.code} to i32`) }
return v.code
}
# widen an int value to i64 (a long passes through) — the long analogue of to_fixed
function to_long(v: Val) -> pointer {
if (llty(v.ty) == "i64") { return v.code }
return emit_bind(`sext i32 {v.code} to i64`)
}
# string operators: `a + b` concatenates, `a == b` / `a != b` compare by content.
# Both call the @fn_str_* prelude (emitted once per program that uses them).
function emit_str_op(op: pointer, a: Val, b: Val) -> Val {
g_uses_str = true
if (op == ("+")) {
return val(emit_bind(`call ptr @fn_str_concat(ptr {a.code}, ptr {b.code})`), "string")
}
let r = emit_bind(`call i32 @fn_str_eq(ptr {a.code}, ptr {b.code})`)
if (op == ("!=")) {
let c = emit_bind(`icmp eq i32 {r}, 0`)
return val(emit_bind(`zext i1 {c} to i32`), "bool")
}
return val(r, "bool")
}
function emit_bin(e: Node) -> Val {
if (e.s == "and") or (e.s == "or") { return emit_logic(e) }
let a = emit_expr(e.a)
let b = emit_expr(e.b)
# strings are pointer-typed, so any `+` with a pointer operand is concatenation,
# and `==`/`!=` between pointers is content comparison — except `x == null`,
# which is a pointer-identity test and falls through to the icmp below.
let ptrish = (llty(a.ty) == "ptr") or (llty(b.ty) == "ptr")
let isnull = e.a.kind == E_NULL or e.b.kind == E_NULL
if ptrish {
if (e.s == ("+")) { return emit_str_op("+", a, b) }
if ((e.s == ("==")) or (e.s == ("!="))) and not isnull { return emit_str_op(e.s, a, b) }
}
let fx = (a.ty == "fixed") or (b.ty == "fixed")
# a 64-bit operand (and no fixed/ptr involved) promotes the whole expression to
# i64: the other side widens with sext, and the result stays `long`.
let lng = ((llty(a.ty) == "i64") or (llty(b.ty) == "i64")) and not fx and not ptrish
if is_cmp(e.s) {
var ac = a.code; var bc = b.code
var ct = "i32"
if fx { ac = to_fixed(a); bc = to_fixed(b) }
else { if lng { ct = "i64"; ac = to_long(a); bc = to_long(b) }
else { if (llty(a.ty) == "ptr") or (llty(b.ty) == "ptr") { ct = "ptr" } } } # `p == null`, str/record identity
let c = emit_bind(`icmp {cmp_code(e.s)} {ct} {ac}, {bc}`)
return val(emit_bind(`zext i1 {c} to i32`), "bool")
}
if lng {
let al = to_long(a); let bl = to_long(b)
return val(emit_bind(`{arith_code(e.s)} i64 {al}, {bl}`), "long")
}
if fx {
let af = to_fixed(a); let bf = to_fixed(b)
if (e.s == ("*")) {
let a64 = emit_bind(`sext i32 {af} to i64`)
let b64 = emit_bind(`sext i32 {bf} to i64`)
let m = emit_bind(`mul i64 {a64}, {b64}`)
let sh = emit_bind(`ashr i64 {m}, 16`)
return val(emit_bind(`trunc i64 {sh} to i32`), "fixed")
}
if (e.s == ("/")) {
let a64 = emit_bind(`sext i32 {af} to i64`)
let ash = emit_bind(`shl i64 {a64}, 16`)
let b64 = emit_bind(`sext i32 {bf} to i64`)
let dv = emit_bind(`sdiv i64 {ash}, {b64}`)
return val(emit_bind(`trunc i64 {dv} to i32`), "fixed")
}
let r = emit_bind(`{arith_code(e.s)} i32 {af}, {bf}`)
return val(r, "fixed")
}
let r = emit_bind(`{arith_code(e.s)} i32 {a.code}, {b.code}`)
return val(r, "int")
}
# ---- named arguments -------------------------------------------------------
# An argument list is either all-positional or all-named. When named, each kid
# is an E_FINIT (label -> value); this rewrites e.kids into plain value exprs in
# the order the callee declares its parameters, so the rest of emit_call is
# oblivious to whether the caller used names.
function args_are_named(e: Node) -> bool {
var i = 0
while i < len(e.kids) { if e.kids[i].kind == E_FINIT { return true }; i = i + 1 }
return false
}
function reorder_named(e: Node, labels: []pointer) -> void {
if not args_are_named(e) { return }
var i = 0
while i < len(e.kids) {
if e.kids[i].kind != E_FINIT { perr("named and positional arguments cannot be mixed in one call") }
i = i + 1
}
if len(e.kids) != len(labels) { perr("wrong number of arguments") }
let out = new []Node
var li = 0
while li < len(labels) {
var found: Node = null
var k = 0
while k < len(e.kids) { if (e.kids[k].s == labels[li]) { found = e.kids[k] }; k = k + 1 }
if (found == null) { perr(`no argument named {labels[li]}`) }
push(out, found.a)
li = li + 1
}
e.kids = out
}
# The parameter labels of a resolved fn/extern, in declaration order.
function param_labels(fn: Node) -> []pointer {
let out = new []pointer
var i = 0
while i < len(fn.kids) { if fn.kids[i].kind == N_PARAM { push(out, fn.kids[i].s) }; i = i + 1 }
return out
}
function param_types(fn: Node) -> []pointer {
let out = new []pointer
var i = 0
while i < len(fn.kids) { if fn.kids[i].kind == N_PARAM { push(out, fn.kids[i].ty) }; i = i + 1 }
return out
}

View file

@ -0,0 +1,388 @@
# emit_game.ludic — system functions and the frame loop. A system compiles to a
# void function; main() boots (Start systems), then runs the per-frame phases in
# order until the game stops. Runtime hooks (rt_init/poll/running/shutdown) are
# called only when the runtime defines them.
function emit_system_fn(sys: Node) -> void {
g_cur_scene = sys.c # scene owning this handler (null if global) — for `become`
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
ret_ty = "void"
let fbody = buf_new()
falloc = buf_new()
let saved = code
code = fbody
emit_block(sys.a)
if not g_term { emit(" br label %ret\n") }
emit("ret:\n ret void\n")
code = saved
emit("define void @sys_"); emit(sys.s); emit("() {\nentry:\n")
emit(buf_str(falloc))
emit(buf_str(fbody))
emit("}\n\n")
}
# one enable-gated call to @sys_<d.s> (skipped while the handler is disabled).
function emit_call_one(d: Node) -> void {
let he = emit_bind(`load i32, ptr @HE_{d.s}`)
var hc = emit_bind(`icmp ne i32 {he}, 0`)
# a handler in a toggled layer also gates on its layer's @LE_ flag (byte-identical
# for layers that are never enabled/disabled, since d.b is only read when managed)
if (d.b != null) and is_toggled_layer(d.b.s) {
let le = emit_bind(`load i32, ptr @LE_{d.b.s}`)
let lc = emit_bind(`icmp ne i32 {le}, 0`)
hc = emit_bind(`and i1 {hc}, {lc}`)
}
# N5: an @Server handler (d.ival==1) runs only on the authority (@L_role==1).
# Unmarked and @Predicted handlers run on every peer. Offline @L_role defaults to
# 1, so the guard collapses to "run here" and a non-networked build is unchanged.
if d.ival == 1 {
let rv = emit_bind("load i32, ptr @L_role")
let rc = emit_bind(`icmp eq i32 {rv}, 1`)
hc = emit_bind(`and i1 {hc}, {rc}`)
}
let run = lbl("hrun"); let skip = lbl("hskip")
emit(" br i1 "); emit(hc); emit(", label %"); emit(run); emit(", label %"); emit(skip); emit("\n")
emit(run); emit(":\n call void @sys_"); emit(d.s); emit("()\n")
emit(" br label %"); emit(skip); emit("\n"); emit(skip); emit(":\n")
}
# Global handlers run first, then the active scene's layer handlers in
# declaration (draw) order. The active scene is snapshotted once per phase, so a
# `become` mid-phase takes effect at the next phase boundary — exactly one scene
# is active within any single phase.
function emit_calls_for_phase(phase: pointer) -> void {
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_SYS and (d.ty == phase) and (d.c == null) { emit_call_one(d) }
i = i + 1
}
# any scene-owned handlers in this phase? gate them on one @L_scene snapshot.
var has_sc = false
i = 0
while i < len(prog) { let d = prog[i]; if d.kind == N_SYS and (d.ty == phase) and (d.c != null) { has_sc = true }; i = i + 1 }
if not has_sc { return }
let cs = emit_bind("load i32, ptr @L_scene")
i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_SYS and (d.ty == phase) and (d.c != null) {
let ce = emit_bind(`icmp eq i32 {cs}, {itoa(d.c.ival)}`)
let run = lbl("scrun"); let skip = lbl("scskip")
emit(" br i1 "); emit(ce); emit(", label %"); emit(run); emit(", label %"); emit(skip); emit("\n")
emit(run); emit(":\n")
emit_call_one(d)
emit(" br label %"); emit(skip); emit("\n"); emit(skip); emit(":\n")
}
i = i + 1
}
}
# on enter / on exit compile to void functions @scene_enter_<Name> /
# @scene_exit_<Name>, called at the transition point (and enter at boot for the
# start scene). Emitted for every scene, empty body when the hook is absent.
function emit_scene_fn(name: pointer, kind: pointer, body: Node) -> void {
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
ret_ty = "void"
let fbody = buf_new()
falloc = buf_new()
let saved = code
code = fbody
if (body != null) { emit_block(body) }
# EV1: a `public` scene fires scene_<S>_enter / scene_<S>_exit after its block
let sev = `scene_{name}_{kind}`
if (not g_term) and (find_event(sev) != null) { emit(" call void @ev_"); emit(sev); emit("()\n") }
if not g_term { emit(" br label %ret\n") }
emit("ret:\n ret void\n")
code = saved
emit("define void @scene_"); emit(kind); emit("_"); emit(name); emit("() {\nentry:\n")
emit(buf_str(falloc))
emit(buf_str(fbody))
emit("}\n\n")
}
function emit_scene_hooks() -> void {
var i = 0
while i < len(g_scenes) {
let sc = g_scenes[i]
g_cur_scene = sc
emit_scene_fn(sc.s, "enter", sc.a)
emit_scene_fn(sc.s, "exit", sc.b)
i = i + 1
}
}
# @OnDespawn(Model) hooks compile to `@on_despawn_<Model>(entity, reason)`
# functions that bind the model's properties and run the body — dispatched by
# kind at `despawn`. LC1: the `i32 %reason` parameter carries an EndReason; when
# the hook declared `reason: r`, `r` is bound as an int local reading it.
function emit_despawn_hooks() -> void {
var i = 0
while i < len(g_ondespawn) {
let hk = g_ondespawn[i]
let model = find_arch(hk.s)
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
ret_ty = "void"
let fbody = buf_new()
falloc = buf_new()
let saved = code
code = fbody
if (hk.ty != null) { # bind the reason: r name to %reason
let rslot = emit_alloca("i32")
emit(" store i32 %reason, ptr "); emit(rslot); emit("\n")
loc_push(hk.ty, rslot, "int")
}
emit_bind_props(model, "%e")
emit_block(hk.a)
if not g_term { emit(" br label %ret\n") }
emit("ret:\n ret void\n")
code = saved
emit("define void @on_despawn_"); emit(hk.s); emit("(i32 %e, i32 %reason) {\nentry:\n")
emit(buf_str(falloc))
emit(buf_str(fbody))
emit("}\n\n")
i = i + 1
}
emit_despawn_all_fn()
}
# LC1 "no silent deaths": at program shutdown every still-live entity's despawn
# hook fires with reason Quit, so teardown that must run on exit is not skipped.
# @L_despawn_all(reason) walks the live set and dispatches each entity by kind —
# the same per-model dispatch as `despawn`, but without freeing (the process is
# ending). Emitted only when the program has @OnDespawn hooks, so despawn-free
# programs are byte-for-byte unchanged.
function emit_despawn_all_fn() -> void {
if len(g_ondespawn) == 0 { return }
let me = itoa(MAX_ENT)
emit("define void @L_despawn_all(i32 %reason) {\nentry:\n br label %loop\n")
emit("loop:\n %i = phi i32 [ 0, %entry ], [ %i1, %cont ]\n")
emit(" %n = load i32, ptr @L_entc\n %go = icmp slt i32 %i, %n\n")
emit(" br i1 %go, label %body, label %fin\n")
emit("body:\n %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %i\n")
emit(" %al = load i32, ptr %ap\n %isa = icmp ne i32 %al, 0\n")
emit(" br i1 %isa, label %do, label %cont\n")
emit("do:\n %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %i\n")
emit(" %k = load i32, ptr %kp\n")
var i = 0
while i < len(g_ondespawn) {
let mname = g_ondespawn[i].s
let si = itoa(i)
emit(" %c"); emit(si); emit(" = icmp eq i32 %k, "); emit(itoa(find_arch_id(mname))); emit("\n")
emit(" br i1 %c"); emit(si); emit(", label %hit"); emit(si); emit(", label %next"); emit(si); emit("\n")
emit("hit"); emit(si); emit(":\n call void @on_despawn_"); emit(mname); emit("(i32 %i, i32 %reason)\n")
let dev = `model_{mname}_despawn` # EV1: @Public despawn event at shutdown
if (find_event(dev) != null) { emit(" call void @ev_"); emit(dev); emit("(i32 %i, i32 %reason)\n") }
emit(" br label %next"); emit(si); emit("\n")
emit("next"); emit(si); emit(":\n")
i = i + 1
}
emit(" br label %cont\n")
emit("cont:\n %i1 = add i32 %i, 1\n br label %loop\n")
emit("fin:\n ret void\n}\n\n")
}
# EV0: each declared `event E` compiles to a `@ev_<E>(payload…)` function whose
# body is (1) its `@On(E)` listeners concatenated in declaration order — the
# closed, compile-time half — then (2) a loop over a runtime listener array, the
# open half a mod in another language joins through the C ABI. The payload fields
# arrive as params (%p0, %p1, …), bound by name so a listener body reads them bare
# (like a query/hook binding). Emitted only when g_events is non-empty, so an
# event-free program is byte-for-byte unchanged.
#
# The runtime half is the deliberate opt-in exception to "no dispatch tables":
# %Ev_<E> — the POD payload struct passed by pointer to foreign listeners
# @evL_<E> — a fixed-capacity [16 x ptr] array of foreign callbacks
# @evN_<E> — how many are registered (registration order = dispatch order)
# @ludic_on_<E>(ptr cb) -> i32 — the C ABI: a mod appends its callback
# A native Ludic listener costs a direct call; a foreign one costs one indirect
# call. With no foreign listeners registered the loop runs zero times (one branch).
const EV_CAP: int = 16
# EV6 — re-entrant emit is bounded: a listener may `emit` another event, but the
# nesting is capped so an event cycle traps as an early return instead of hanging
# the frame. @ev_depth counts the live dispatch nesting; past the cap a dispatch
# returns immediately (a cancellable event returns "not cancelled").
const EV_DEPTH_CAP: int = 32
function emit_event_fns() -> void {
emith("@ev_depth = global i32 0\n") # EV6 re-entrancy counter
var e = 0
while e < len(g_events) {
let ev = g_events[e]
let en = ev.s
let cap = itoa(EV_CAP)
# --- module-level: payload struct + the foreign listener registry (into head)
# A cancellable event's payload carries a trailing i32 `cancelled` flag that a
# listener (native or foreign) can set; the caller reads it back.
emith("%Ev_"); emith(en); emith(" = type { ")
var t = 0
while t < len(ev.kids) {
if t > 0 { emith(", ") }
emith(llty(ev.kids[t].ty))
t = t + 1
}
if ev.ival == 1 { if len(ev.kids) > 0 { emith(", ") }; emith("i32") }
emith(" }\n")
emith("@evL_"); emith(en); emith(" = global ["); emith(cap); emith(" x ptr] zeroinitializer\n")
emith("@evN_"); emith(en); emith(" = global i32 0\n")
# EV5: a parallel owner array — -1 = program-scoped (never swept), >=0 = the
# entity that owns the listener (swept when that entity despawns).
emith("@evO_"); emith(en); emith(" = global ["); emith(cap); emith(" x i32] zeroinitializer\n")
# --- @ludic_on_<E>(cb): append a program-scoped callback, return a token
emit("define i32 @ludic_on_"); emit(en); emit("(ptr %cb) {\nentry:\n")
emit(" %n = load i32, ptr @evN_"); emit(en); emit("\n")
emit(" %full = icmp slt i32 %n, "); emit(cap); emit("\n")
emit(" br i1 %full, label %add, label %drop\n")
emit("add:\n %slot = getelementptr inbounds ["); emit(cap); emit(" x ptr], ptr @evL_"); emit(en); emit(", i32 0, i32 %n\n")
emit(" store ptr %cb, ptr %slot\n")
emit(" %oslot = getelementptr inbounds ["); emit(cap); emit(" x i32], ptr @evO_"); emit(en); emit(", i32 0, i32 %n\n")
emit(" store i32 -1, ptr %oslot\n")
emit(" %n1 = add i32 %n, 1\n store i32 %n1, ptr @evN_"); emit(en); emit("\n ret i32 %n\n")
emit("drop:\n ret i32 -1\n}\n\n") # registry full: reject (token -1)
# --- @ludic_on_entity_<E>(owner, cb): append an entity-scoped callback
emit("define i32 @ludic_on_entity_"); emit(en); emit("(i32 %owner, ptr %cb) {\nentry:\n")
emit(" %n = load i32, ptr @evN_"); emit(en); emit("\n")
emit(" %full = icmp slt i32 %n, "); emit(cap); emit("\n")
emit(" br i1 %full, label %add, label %drop\n")
emit("add:\n %slot = getelementptr inbounds ["); emit(cap); emit(" x ptr], ptr @evL_"); emit(en); emit(", i32 0, i32 %n\n")
emit(" store ptr %cb, ptr %slot\n")
emit(" %oslot = getelementptr inbounds ["); emit(cap); emit(" x i32], ptr @evO_"); emit(en); emit(", i32 0, i32 %n\n")
emit(" store i32 %owner, ptr %oslot\n")
emit(" %n1 = add i32 %n, 1\n store i32 %n1, ptr @evN_"); emit(en); emit("\n ret i32 %n\n")
emit("drop:\n ret i32 -1\n}\n\n")
# --- @ludic_off_<E>(token): remove a listener (tombstone the slot to null)
emit("define void @ludic_off_"); emit(en); emit("(i32 %tok) {\nentry:\n")
emit(" %n = load i32, ptr @evN_"); emit(en); emit("\n")
emit(" %lo = icmp sge i32 %tok, 0\n %hi = icmp slt i32 %tok, %n\n %ok = and i1 %lo, %hi\n")
emit(" br i1 %ok, label %do, label %skip\n")
emit("do:\n %slot = getelementptr inbounds ["); emit(cap); emit(" x ptr], ptr @evL_"); emit(en); emit(", i32 0, i32 %tok\n")
emit(" store ptr null, ptr %slot\n br label %skip\n")
emit("skip:\n ret void\n}\n\n")
# --- @ev_<E>(payload): fire compile-time listeners, then foreign ones
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
ret_ty = "void"
let fbody = buf_new()
falloc = buf_new()
let saved = code
code = fbody
# EV6: bound re-entrant emit — past EV_DEPTH_CAP, return without dispatching
emit(" %evd = load i32, ptr @ev_depth\n")
emit(" %evover = icmp sge i32 %evd, "); emit(itoa(EV_DEPTH_CAP)); emit("\n")
emit(" br i1 %evover, label %evcap, label %evgo\n")
emit("evcap:\n")
if ev.ival == 1 { emit(" ret i32 0\n") } else { emit(" ret void\n") }
emit("evgo:\n")
emit(" %evd1 = add i32 %evd, 1\n store i32 %evd1, ptr @ev_depth\n")
# a stack copy of the payload, passed by pointer to every foreign listener
let pl = emit_alloca(`%Ev_{en}`)
# bind each field: store the param into the payload struct AND a name slot the
# compile-time listener bodies read bare.
var f = 0
while f < len(ev.kids) {
let fd = ev.kids[f]
let lt = llty(fd.ty)
let pa = nreg(); emit(" "); emit(pa); emit(" = getelementptr inbounds %Ev_"); emit(en); emit(", ptr "); emit(pl); emit(", i32 0, i32 "); emit(itoa(f)); emit("\n")
emit(" store "); emit(lt); emit(" %p"); emit(itoa(f)); emit(", ptr "); emit(pa); emit("\n")
let slot = emit_alloca(lt)
emit(" store "); emit(lt); emit(" %p"); emit(itoa(f)); emit(", ptr "); emit(slot); emit("\n")
loc_push(fd.s, slot, fd.ty)
f = f + 1
}
# cancellable: zero the flag and expose its address to `cancel` in the listeners
var caddr = null
if ev.ival == 1 {
caddr = nreg(); emit(" "); emit(caddr); emit(" = getelementptr inbounds %Ev_"); emit(en); emit(", ptr "); emit(pl); emit(", i32 0, i32 "); emit(itoa(len(ev.kids))); emit("\n")
emit(" store i32 0, ptr "); emit(caddr); emit("\n")
g_cancel_addr = caddr
}
let base = nloc # listeners share the params but not each other's locals
var i = 0
while i < len(g_onlisten) {
if (g_onlisten[i].s == en) { nloc = base; g_term = false; emit_block(g_onlisten[i].a) }
i = i + 1
}
# the open half: walk the foreign callback array in registration order
if not g_term {
let ci = emit_alloca("i32"); emit(" store i32 0, ptr "); emit(ci); emit("\n")
let L = lbl("evl"); let B = lbl("evb"); let D = lbl("evd")
emit(" br label %"); emit(L); emit("\n")
emit(L); emit(":\n")
let iv = emit_bind(`load i32, ptr {ci}`)
let nn = emit_bind(`load i32, ptr @evN_{en}`)
let go = emit_bind(`icmp slt i32 {iv}, {nn}`)
emit(" br i1 "); emit(go); emit(", label %"); emit(B); emit(", label %"); emit(D); emit("\n")
emit(B); emit(":\n")
let sp = nreg(); emit(" "); emit(sp); emit(" = getelementptr inbounds ["); emit(cap); emit(" x ptr], ptr @evL_"); emit(en); emit(", i32 0, i32 "); emit(iv); emit("\n")
let cb = emit_bind(`load ptr, ptr {sp}`)
let cbn = emit_bind(`icmp eq ptr {cb}, null`) # EV5: a removed (off) listener is null — skip it
let doc = lbl("evdo"); let skp = lbl("evsk")
emit(" br i1 "); emit(cbn); emit(", label %"); emit(skp); emit(", label %"); emit(doc); emit("\n")
emit(doc); emit(":\n")
emit(" call void "); emit(cb); emit("(ptr "); emit(pl); emit(")\n")
emit(" br label %"); emit(skp); emit("\n")
emit(skp); emit(":\n")
let i2 = emit_bind(`add i32 {iv}, 1`)
emit(" store i32 "); emit(i2); emit(", ptr "); emit(ci); emit("\n")
emit(" br label %"); emit(L); emit("\n")
emit(D); emit(":\n")
emit(" br label %ret\n")
}
emit("ret:\n")
emit(" %evdd = load i32, ptr @ev_depth\n %evdd1 = sub i32 %evdd, 1\n store i32 %evdd1, ptr @ev_depth\n") # EV6: leave one nesting level
if ev.ival == 1 { # return the (possibly set) cancelled flag
let cv = emit_bind(`load i32, ptr {caddr}`)
emit(" ret i32 "); emit(cv); emit("\n")
} else { emit(" ret void\n") }
g_cancel_addr = null # leaves listener scope
code = saved
var rt = "void"; if ev.ival == 1 { rt = "i32" }
emit("define "); emit(rt); emit(" @ev_"); emit(en); emit("(")
var g = 0
while g < len(ev.kids) {
if g > 0 { emit(", ") }
emit(llty(ev.kids[g].ty)); emit(" %p"); emit(itoa(g))
g = g + 1
}
emit(") {\nentry:\n")
emit(buf_str(falloc))
emit(buf_str(fbody))
emit("}\n\n")
e = e + 1
}
# EV5: @ludic_sweep_entity(owner) — remove every entity-scoped listener owned by
# a despawning entity, across all events. Called from `despawn`, so a listener
# bound to an entity cannot outlive it (the Node listener-leak footgun, gone).
let capS = itoa(EV_CAP)
emit("define void @ludic_sweep_entity(i32 %owner) {\nentry:\n %ci = alloca i32\n store i32 0, ptr %ci\n br label %sw0\n")
var e2 = 0
while e2 < len(g_events) {
let en2 = g_events[e2].s; let sk = itoa(e2)
emit("sw"); emit(sk); emit(":\n")
emit(" %swi"); emit(sk); emit(" = load i32, ptr %ci\n")
emit(" %swn"); emit(sk); emit(" = load i32, ptr @evN_"); emit(en2); emit("\n")
emit(" %swg"); emit(sk); emit(" = icmp slt i32 %swi"); emit(sk); emit(", %swn"); emit(sk); emit("\n")
emit(" br i1 %swg"); emit(sk); emit(", label %swb"); emit(sk); emit(", label %swd"); emit(sk); emit("\n")
emit("swb"); emit(sk); emit(":\n")
emit(" %swop"); emit(sk); emit(" = getelementptr inbounds ["); emit(capS); emit(" x i32], ptr @evO_"); emit(en2); emit(", i32 0, i32 %swi"); emit(sk); emit("\n")
emit(" %swov"); emit(sk); emit(" = load i32, ptr %swop"); emit(sk); emit("\n")
emit(" %swm"); emit(sk); emit(" = icmp eq i32 %swov"); emit(sk); emit(", %owner\n")
emit(" br i1 %swm"); emit(sk); emit(", label %swh"); emit(sk); emit(", label %swx"); emit(sk); emit("\n")
emit("swh"); emit(sk); emit(":\n")
emit(" %swlp"); emit(sk); emit(" = getelementptr inbounds ["); emit(capS); emit(" x ptr], ptr @evL_"); emit(en2); emit(", i32 0, i32 %swi"); emit(sk); emit("\n")
emit(" store ptr null, ptr %swlp"); emit(sk); emit("\n br label %swx"); emit(sk); emit("\n")
emit("swx"); emit(sk); emit(":\n")
emit(" %swi1"); emit(sk); emit(" = add i32 %swi"); emit(sk); emit(", 1\n store i32 %swi1"); emit(sk); emit(", ptr %ci\n br label %sw"); emit(sk); emit("\n")
emit("swd"); emit(sk); emit(":\n store i32 0, ptr %ci\n")
if (e2 + 1) < len(g_events) { emit(" br label %sw"); emit(itoa(e2 + 1)); emit("\n") }
else { emit(" ret void\n") }
e2 = e2 + 1
}
emit("}\n\n")
}

View file

@ -1,391 +1,4 @@
# emit_game.ludic — system functions and the frame loop. A system compiles to a
# void function; main() boots (Start systems), then runs the per-frame phases in
# order until the game stops. Runtime hooks (rt_init/poll/running/shutdown) are
# called only when the runtime defines them.
function emit_system_fn(sys: Node) -> void {
g_cur_scene = sys.c # scene owning this handler (null if global) — for `become`
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
ret_ty = "void"
let fbody = buf_new()
falloc = buf_new()
let saved = code
code = fbody
emit_block(sys.a)
if not g_term { emit(" br label %ret\n") }
emit("ret:\n ret void\n")
code = saved
emit("define void @sys_"); emit(sys.s); emit("() {\nentry:\n")
emit(buf_str(falloc))
emit(buf_str(fbody))
emit("}\n\n")
}
# one enable-gated call to @sys_<d.s> (skipped while the handler is disabled).
function emit_call_one(d: Node) -> void {
let he = emit_bind(`load i32, ptr @HE_{d.s}`)
var hc = emit_bind(`icmp ne i32 {he}, 0`)
# a handler in a toggled layer also gates on its layer's @LE_ flag (byte-identical
# for layers that are never enabled/disabled, since d.b is only read when managed)
if (d.b != null) and is_toggled_layer(d.b.s) {
let le = emit_bind(`load i32, ptr @LE_{d.b.s}`)
let lc = emit_bind(`icmp ne i32 {le}, 0`)
hc = emit_bind(`and i1 {hc}, {lc}`)
}
# N5: an @Server handler (d.ival==1) runs only on the authority (@L_role==1).
# Unmarked and @Predicted handlers run on every peer. Offline @L_role defaults to
# 1, so the guard collapses to "run here" and a non-networked build is unchanged.
if d.ival == 1 {
let rv = emit_bind("load i32, ptr @L_role")
let rc = emit_bind(`icmp eq i32 {rv}, 1`)
hc = emit_bind(`and i1 {hc}, {rc}`)
}
let run = lbl("hrun"); let skip = lbl("hskip")
emit(" br i1 "); emit(hc); emit(", label %"); emit(run); emit(", label %"); emit(skip); emit("\n")
emit(run); emit(":\n call void @sys_"); emit(d.s); emit("()\n")
emit(" br label %"); emit(skip); emit("\n"); emit(skip); emit(":\n")
}
# Global handlers run first, then the active scene's layer handlers in
# declaration (draw) order. The active scene is snapshotted once per phase, so a
# `become` mid-phase takes effect at the next phase boundary — exactly one scene
# is active within any single phase.
function emit_calls_for_phase(phase: pointer) -> void {
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_SYS and (d.ty == phase) and (d.c == null) { emit_call_one(d) }
i = i + 1
}
# any scene-owned handlers in this phase? gate them on one @L_scene snapshot.
var has_sc = false
i = 0
while i < len(prog) { let d = prog[i]; if d.kind == N_SYS and (d.ty == phase) and (d.c != null) { has_sc = true }; i = i + 1 }
if not has_sc { return }
let cs = emit_bind("load i32, ptr @L_scene")
i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_SYS and (d.ty == phase) and (d.c != null) {
let ce = emit_bind(`icmp eq i32 {cs}, {itoa(d.c.ival)}`)
let run = lbl("scrun"); let skip = lbl("scskip")
emit(" br i1 "); emit(ce); emit(", label %"); emit(run); emit(", label %"); emit(skip); emit("\n")
emit(run); emit(":\n")
emit_call_one(d)
emit(" br label %"); emit(skip); emit("\n"); emit(skip); emit(":\n")
}
i = i + 1
}
}
# on enter / on exit compile to void functions @scene_enter_<Name> /
# @scene_exit_<Name>, called at the transition point (and enter at boot for the
# start scene). Emitted for every scene, empty body when the hook is absent.
function emit_scene_fn(name: pointer, kind: pointer, body: Node) -> void {
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
ret_ty = "void"
let fbody = buf_new()
falloc = buf_new()
let saved = code
code = fbody
if (body != null) { emit_block(body) }
# EV1: a `public` scene fires scene_<S>_enter / scene_<S>_exit after its block
let sev = `scene_{name}_{kind}`
if (not g_term) and (find_event(sev) != null) { emit(" call void @ev_"); emit(sev); emit("()\n") }
if not g_term { emit(" br label %ret\n") }
emit("ret:\n ret void\n")
code = saved
emit("define void @scene_"); emit(kind); emit("_"); emit(name); emit("() {\nentry:\n")
emit(buf_str(falloc))
emit(buf_str(fbody))
emit("}\n\n")
}
function emit_scene_hooks() -> void {
var i = 0
while i < len(g_scenes) {
let sc = g_scenes[i]
g_cur_scene = sc
emit_scene_fn(sc.s, "enter", sc.a)
emit_scene_fn(sc.s, "exit", sc.b)
i = i + 1
}
}
# @OnDespawn(Model) hooks compile to `@on_despawn_<Model>(entity, reason)`
# functions that bind the model's properties and run the body — dispatched by
# kind at `despawn`. LC1: the `i32 %reason` parameter carries an EndReason; when
# the hook declared `reason: r`, `r` is bound as an int local reading it.
function emit_despawn_hooks() -> void {
var i = 0
while i < len(g_ondespawn) {
let hk = g_ondespawn[i]
let model = find_arch(hk.s)
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
ret_ty = "void"
let fbody = buf_new()
falloc = buf_new()
let saved = code
code = fbody
if (hk.ty != null) { # bind the reason: r name to %reason
let rslot = emit_alloca("i32")
emit(" store i32 %reason, ptr "); emit(rslot); emit("\n")
loc_push(hk.ty, rslot, "int")
}
emit_bind_props(model, "%e")
emit_block(hk.a)
if not g_term { emit(" br label %ret\n") }
emit("ret:\n ret void\n")
code = saved
emit("define void @on_despawn_"); emit(hk.s); emit("(i32 %e, i32 %reason) {\nentry:\n")
emit(buf_str(falloc))
emit(buf_str(fbody))
emit("}\n\n")
i = i + 1
}
emit_despawn_all_fn()
}
# LC1 "no silent deaths": at program shutdown every still-live entity's despawn
# hook fires with reason Quit, so teardown that must run on exit is not skipped.
# @L_despawn_all(reason) walks the live set and dispatches each entity by kind —
# the same per-model dispatch as `despawn`, but without freeing (the process is
# ending). Emitted only when the program has @OnDespawn hooks, so despawn-free
# programs are byte-for-byte unchanged.
function emit_despawn_all_fn() -> void {
if len(g_ondespawn) == 0 { return }
let me = itoa(MAX_ENT)
emit("define void @L_despawn_all(i32 %reason) {\nentry:\n br label %loop\n")
emit("loop:\n %i = phi i32 [ 0, %entry ], [ %i1, %cont ]\n")
emit(" %n = load i32, ptr @L_entc\n %go = icmp slt i32 %i, %n\n")
emit(" br i1 %go, label %body, label %fin\n")
emit("body:\n %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %i\n")
emit(" %al = load i32, ptr %ap\n %isa = icmp ne i32 %al, 0\n")
emit(" br i1 %isa, label %do, label %cont\n")
emit("do:\n %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %i\n")
emit(" %k = load i32, ptr %kp\n")
var i = 0
while i < len(g_ondespawn) {
let mname = g_ondespawn[i].s
let si = itoa(i)
emit(" %c"); emit(si); emit(" = icmp eq i32 %k, "); emit(itoa(find_arch_id(mname))); emit("\n")
emit(" br i1 %c"); emit(si); emit(", label %hit"); emit(si); emit(", label %next"); emit(si); emit("\n")
emit("hit"); emit(si); emit(":\n call void @on_despawn_"); emit(mname); emit("(i32 %i, i32 %reason)\n")
let dev = `model_{mname}_despawn` # EV1: @Public despawn event at shutdown
if (find_event(dev) != null) { emit(" call void @ev_"); emit(dev); emit("(i32 %i, i32 %reason)\n") }
emit(" br label %next"); emit(si); emit("\n")
emit("next"); emit(si); emit(":\n")
i = i + 1
}
emit(" br label %cont\n")
emit("cont:\n %i1 = add i32 %i, 1\n br label %loop\n")
emit("fin:\n ret void\n}\n\n")
}
# EV0: each declared `event E` compiles to a `@ev_<E>(payload…)` function whose
# body is (1) its `@On(E)` listeners concatenated in declaration order — the
# closed, compile-time half — then (2) a loop over a runtime listener array, the
# open half a mod in another language joins through the C ABI. The payload fields
# arrive as params (%p0, %p1, …), bound by name so a listener body reads them bare
# (like a query/hook binding). Emitted only when g_events is non-empty, so an
# event-free program is byte-for-byte unchanged.
#
# The runtime half is the deliberate opt-in exception to "no dispatch tables":
# %Ev_<E> — the POD payload struct passed by pointer to foreign listeners
# @evL_<E> — a fixed-capacity [16 x ptr] array of foreign callbacks
# @evN_<E> — how many are registered (registration order = dispatch order)
# @ludic_on_<E>(ptr cb) -> i32 — the C ABI: a mod appends its callback
# A native Ludic listener costs a direct call; a foreign one costs one indirect
# call. With no foreign listeners registered the loop runs zero times (one branch).
const EV_CAP: int = 16
# EV6 — re-entrant emit is bounded: a listener may `emit` another event, but the
# nesting is capped so an event cycle traps as an early return instead of hanging
# the frame. @ev_depth counts the live dispatch nesting; past the cap a dispatch
# returns immediately (a cancellable event returns "not cancelled").
const EV_DEPTH_CAP: int = 32
function emit_event_fns() -> void {
emith("@ev_depth = global i32 0\n") # EV6 re-entrancy counter
var e = 0
while e < len(g_events) {
let ev = g_events[e]
let en = ev.s
let cap = itoa(EV_CAP)
# --- module-level: payload struct + the foreign listener registry (into head)
# A cancellable event's payload carries a trailing i32 `cancelled` flag that a
# listener (native or foreign) can set; the caller reads it back.
emith("%Ev_"); emith(en); emith(" = type { ")
var t = 0
while t < len(ev.kids) {
if t > 0 { emith(", ") }
emith(llty(ev.kids[t].ty))
t = t + 1
}
if ev.ival == 1 { if len(ev.kids) > 0 { emith(", ") }; emith("i32") }
emith(" }\n")
emith("@evL_"); emith(en); emith(" = global ["); emith(cap); emith(" x ptr] zeroinitializer\n")
emith("@evN_"); emith(en); emith(" = global i32 0\n")
# EV5: a parallel owner array — -1 = program-scoped (never swept), >=0 = the
# entity that owns the listener (swept when that entity despawns).
emith("@evO_"); emith(en); emith(" = global ["); emith(cap); emith(" x i32] zeroinitializer\n")
# --- @ludic_on_<E>(cb): append a program-scoped callback, return a token
emit("define i32 @ludic_on_"); emit(en); emit("(ptr %cb) {\nentry:\n")
emit(" %n = load i32, ptr @evN_"); emit(en); emit("\n")
emit(" %full = icmp slt i32 %n, "); emit(cap); emit("\n")
emit(" br i1 %full, label %add, label %drop\n")
emit("add:\n %slot = getelementptr inbounds ["); emit(cap); emit(" x ptr], ptr @evL_"); emit(en); emit(", i32 0, i32 %n\n")
emit(" store ptr %cb, ptr %slot\n")
emit(" %oslot = getelementptr inbounds ["); emit(cap); emit(" x i32], ptr @evO_"); emit(en); emit(", i32 0, i32 %n\n")
emit(" store i32 -1, ptr %oslot\n")
emit(" %n1 = add i32 %n, 1\n store i32 %n1, ptr @evN_"); emit(en); emit("\n ret i32 %n\n")
emit("drop:\n ret i32 -1\n}\n\n") # registry full: reject (token -1)
# --- @ludic_on_entity_<E>(owner, cb): append an entity-scoped callback
emit("define i32 @ludic_on_entity_"); emit(en); emit("(i32 %owner, ptr %cb) {\nentry:\n")
emit(" %n = load i32, ptr @evN_"); emit(en); emit("\n")
emit(" %full = icmp slt i32 %n, "); emit(cap); emit("\n")
emit(" br i1 %full, label %add, label %drop\n")
emit("add:\n %slot = getelementptr inbounds ["); emit(cap); emit(" x ptr], ptr @evL_"); emit(en); emit(", i32 0, i32 %n\n")
emit(" store ptr %cb, ptr %slot\n")
emit(" %oslot = getelementptr inbounds ["); emit(cap); emit(" x i32], ptr @evO_"); emit(en); emit(", i32 0, i32 %n\n")
emit(" store i32 %owner, ptr %oslot\n")
emit(" %n1 = add i32 %n, 1\n store i32 %n1, ptr @evN_"); emit(en); emit("\n ret i32 %n\n")
emit("drop:\n ret i32 -1\n}\n\n")
# --- @ludic_off_<E>(token): remove a listener (tombstone the slot to null)
emit("define void @ludic_off_"); emit(en); emit("(i32 %tok) {\nentry:\n")
emit(" %n = load i32, ptr @evN_"); emit(en); emit("\n")
emit(" %lo = icmp sge i32 %tok, 0\n %hi = icmp slt i32 %tok, %n\n %ok = and i1 %lo, %hi\n")
emit(" br i1 %ok, label %do, label %skip\n")
emit("do:\n %slot = getelementptr inbounds ["); emit(cap); emit(" x ptr], ptr @evL_"); emit(en); emit(", i32 0, i32 %tok\n")
emit(" store ptr null, ptr %slot\n br label %skip\n")
emit("skip:\n ret void\n}\n\n")
# --- @ev_<E>(payload): fire compile-time listeners, then foreign ones
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
ret_ty = "void"
let fbody = buf_new()
falloc = buf_new()
let saved = code
code = fbody
# EV6: bound re-entrant emit — past EV_DEPTH_CAP, return without dispatching
emit(" %evd = load i32, ptr @ev_depth\n")
emit(" %evover = icmp sge i32 %evd, "); emit(itoa(EV_DEPTH_CAP)); emit("\n")
emit(" br i1 %evover, label %evcap, label %evgo\n")
emit("evcap:\n")
if ev.ival == 1 { emit(" ret i32 0\n") } else { emit(" ret void\n") }
emit("evgo:\n")
emit(" %evd1 = add i32 %evd, 1\n store i32 %evd1, ptr @ev_depth\n")
# a stack copy of the payload, passed by pointer to every foreign listener
let pl = emit_alloca(`%Ev_{en}`)
# bind each field: store the param into the payload struct AND a name slot the
# compile-time listener bodies read bare.
var f = 0
while f < len(ev.kids) {
let fd = ev.kids[f]
let lt = llty(fd.ty)
let pa = nreg(); emit(" "); emit(pa); emit(" = getelementptr inbounds %Ev_"); emit(en); emit(", ptr "); emit(pl); emit(", i32 0, i32 "); emit(itoa(f)); emit("\n")
emit(" store "); emit(lt); emit(" %p"); emit(itoa(f)); emit(", ptr "); emit(pa); emit("\n")
let slot = emit_alloca(lt)
emit(" store "); emit(lt); emit(" %p"); emit(itoa(f)); emit(", ptr "); emit(slot); emit("\n")
loc_push(fd.s, slot, fd.ty)
f = f + 1
}
# cancellable: zero the flag and expose its address to `cancel` in the listeners
var caddr = null
if ev.ival == 1 {
caddr = nreg(); emit(" "); emit(caddr); emit(" = getelementptr inbounds %Ev_"); emit(en); emit(", ptr "); emit(pl); emit(", i32 0, i32 "); emit(itoa(len(ev.kids))); emit("\n")
emit(" store i32 0, ptr "); emit(caddr); emit("\n")
g_cancel_addr = caddr
}
let base = nloc # listeners share the params but not each other's locals
var i = 0
while i < len(g_onlisten) {
if (g_onlisten[i].s == en) { nloc = base; g_term = false; emit_block(g_onlisten[i].a) }
i = i + 1
}
# the open half: walk the foreign callback array in registration order
if not g_term {
let ci = emit_alloca("i32"); emit(" store i32 0, ptr "); emit(ci); emit("\n")
let L = lbl("evl"); let B = lbl("evb"); let D = lbl("evd")
emit(" br label %"); emit(L); emit("\n")
emit(L); emit(":\n")
let iv = emit_bind(`load i32, ptr {ci}`)
let nn = emit_bind(`load i32, ptr @evN_{en}`)
let go = emit_bind(`icmp slt i32 {iv}, {nn}`)
emit(" br i1 "); emit(go); emit(", label %"); emit(B); emit(", label %"); emit(D); emit("\n")
emit(B); emit(":\n")
let sp = nreg(); emit(" "); emit(sp); emit(" = getelementptr inbounds ["); emit(cap); emit(" x ptr], ptr @evL_"); emit(en); emit(", i32 0, i32 "); emit(iv); emit("\n")
let cb = emit_bind(`load ptr, ptr {sp}`)
let cbn = emit_bind(`icmp eq ptr {cb}, null`) # EV5: a removed (off) listener is null — skip it
let doc = lbl("evdo"); let skp = lbl("evsk")
emit(" br i1 "); emit(cbn); emit(", label %"); emit(skp); emit(", label %"); emit(doc); emit("\n")
emit(doc); emit(":\n")
emit(" call void "); emit(cb); emit("(ptr "); emit(pl); emit(")\n")
emit(" br label %"); emit(skp); emit("\n")
emit(skp); emit(":\n")
let i2 = emit_bind(`add i32 {iv}, 1`)
emit(" store i32 "); emit(i2); emit(", ptr "); emit(ci); emit("\n")
emit(" br label %"); emit(L); emit("\n")
emit(D); emit(":\n")
emit(" br label %ret\n")
}
emit("ret:\n")
emit(" %evdd = load i32, ptr @ev_depth\n %evdd1 = sub i32 %evdd, 1\n store i32 %evdd1, ptr @ev_depth\n") # EV6: leave one nesting level
if ev.ival == 1 { # return the (possibly set) cancelled flag
let cv = emit_bind(`load i32, ptr {caddr}`)
emit(" ret i32 "); emit(cv); emit("\n")
} else { emit(" ret void\n") }
g_cancel_addr = null # leaves listener scope
code = saved
var rt = "void"; if ev.ival == 1 { rt = "i32" }
emit("define "); emit(rt); emit(" @ev_"); emit(en); emit("(")
var g = 0
while g < len(ev.kids) {
if g > 0 { emit(", ") }
emit(llty(ev.kids[g].ty)); emit(" %p"); emit(itoa(g))
g = g + 1
}
emit(") {\nentry:\n")
emit(buf_str(falloc))
emit(buf_str(fbody))
emit("}\n\n")
e = e + 1
}
# EV5: @ludic_sweep_entity(owner) — remove every entity-scoped listener owned by
# a despawning entity, across all events. Called from `despawn`, so a listener
# bound to an entity cannot outlive it (the Node listener-leak footgun, gone).
let capS = itoa(EV_CAP)
emit("define void @ludic_sweep_entity(i32 %owner) {\nentry:\n %ci = alloca i32\n store i32 0, ptr %ci\n br label %sw0\n")
var e2 = 0
while e2 < len(g_events) {
let en2 = g_events[e2].s; let sk = itoa(e2)
emit("sw"); emit(sk); emit(":\n")
emit(" %swi"); emit(sk); emit(" = load i32, ptr %ci\n")
emit(" %swn"); emit(sk); emit(" = load i32, ptr @evN_"); emit(en2); emit("\n")
emit(" %swg"); emit(sk); emit(" = icmp slt i32 %swi"); emit(sk); emit(", %swn"); emit(sk); emit("\n")
emit(" br i1 %swg"); emit(sk); emit(", label %swb"); emit(sk); emit(", label %swd"); emit(sk); emit("\n")
emit("swb"); emit(sk); emit(":\n")
emit(" %swop"); emit(sk); emit(" = getelementptr inbounds ["); emit(capS); emit(" x i32], ptr @evO_"); emit(en2); emit(", i32 0, i32 %swi"); emit(sk); emit("\n")
emit(" %swov"); emit(sk); emit(" = load i32, ptr %swop"); emit(sk); emit("\n")
emit(" %swm"); emit(sk); emit(" = icmp eq i32 %swov"); emit(sk); emit(", %owner\n")
emit(" br i1 %swm"); emit(sk); emit(", label %swh"); emit(sk); emit(", label %swx"); emit(sk); emit("\n")
emit("swh"); emit(sk); emit(":\n")
emit(" %swlp"); emit(sk); emit(" = getelementptr inbounds ["); emit(capS); emit(" x ptr], ptr @evL_"); emit(en2); emit(", i32 0, i32 %swi"); emit(sk); emit("\n")
emit(" store ptr null, ptr %swlp"); emit(sk); emit("\n br label %swx"); emit(sk); emit("\n")
emit("swx"); emit(sk); emit(":\n")
emit(" %swi1"); emit(sk); emit(" = add i32 %swi"); emit(sk); emit(", 1\n store i32 %swi1"); emit(sk); emit(", ptr %ci\n br label %sw"); emit(sk); emit("\n")
emit("swd"); emit(sk); emit(":\n store i32 0, ptr %ci\n")
if (e2 + 1) < len(g_events) { emit(" br label %sw"); emit(itoa(e2 + 1)); emit("\n") }
else { emit(" ret void\n") }
e2 = e2 + 1
}
emit("}\n\n")
}
# emit_world.ludic — the generated reflection ABI (the "world table" a mod reads/writes entity state by name through), the per-frame tick helpers, and the synthesized @main. Split out of emit_game.ludic (concern: runtime world/entry synthesis, vs. emit_game.ludic's system/scene/event lowering).
# EV2 — the world table: a generated reflection ABI so a mod reads and writes
# entity state *by name*, without having compiled against the game. This is the
# "game table" that lets a modding layer be ported in. Generated from the

View file

@ -0,0 +1,137 @@
# emit_text.ludic — the Text.* namespace over `str` (null-terminated byte
# strings). The libc-backed queries (length/char_at/starts_with/ends_with/
# contains/index_of/to_int) allocate nothing; slice/from_int/equals/concat reuse
# the string preludes that the `+`, `s[a..b]` and string(int) operators emit.
function is_text_ns(meth: pointer) -> bool {
if (meth == "length") or (meth == "char_at") or (meth == "slice") { return true }
if (meth == "equals") or (meth == "concat") or (meth == "to_int") or (meth == "from_int") { return true }
if (meth == "starts_with") or (meth == "ends_with") { return true }
if (meth == "contains") or (meth == "index_of") { return true }
if (meth == "upper") or (meth == "lower") or (meth == "trim") or (meth == "repeat") { return true }
if (meth == "pad_left") or (meth == "pad_right") { return true }
if (meth == "split") or (meth == "join") or (meth == "replace") { return true }
return false
}
function emit_text_ns(meth: pointer, e: Node) -> Val {
if (meth == "from_int") { # int -> string, same as string(n)
let n = emit_expr(e.kids[0])
g_uses_intstr = true
return val(emit_bind(`call ptr @fn_int_str(i32 {n.code})`), "string")
}
if (meth == "slice") { # s[a..b], same substring helper
let s0 = emit_expr(e.kids[0]); let a = emit_expr(e.kids[1]); let b = emit_expr(e.kids[2])
g_uses_strslice = true
return val(emit_bind(`call ptr @fn_str_slice(ptr {s0.code}, i32 {a.code}, i32 {b.code})`), "string")
}
if (meth == "equals") { # byte-wise equality, same as ==
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
g_uses_str = true
return val(emit_bind(`call i32 @fn_str_eq(ptr {a.code}, ptr {b.code})`), "bool")
}
if (meth == "concat") { # a + b, same as the + operator
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
g_uses_str = true
return val(emit_bind(`call ptr @fn_str_concat(ptr {a.code}, ptr {b.code})`), "string")
}
let s = emit_expr(e.kids[0])
if (meth == "length") { # byte length
let r = emit_bind(`call i64 @strlen(ptr {s.code})`)
return val(emit_bind(`trunc i64 {r} to i32`), "int")
}
if (meth == "char_at") { # the byte at index i, 0..255
let i = emit_expr(e.kids[1])
let a = emit_bind(`getelementptr inbounds i8, ptr {s.code}, i32 {i.code}`)
let c = emit_bind(`load i8, ptr {a}`)
return val(emit_bind(`zext i8 {c} to i32`), "int")
}
if (meth == "to_int") { # parse a leading integer, 0 if none
return val(emit_bind(`call i32 @atoi(ptr {s.code})`), "int")
}
if (meth == "upper") { # ASCII a-z -> A-Z, fresh string
g_uses_textrt = true
return val(emit_bind(`call ptr @fn_str_upper(ptr {s.code})`), "string")
}
if (meth == "lower") { # ASCII A-Z -> a-z, fresh string
g_uses_textrt = true
return val(emit_bind(`call ptr @fn_str_lower(ptr {s.code})`), "string")
}
if (meth == "trim") { # drop leading/trailing whitespace
g_uses_textrt = true
return val(emit_bind(`call ptr @fn_str_trim(ptr {s.code})`), "string")
}
if (meth == "repeat") { # s repeated n times
g_uses_textrt = true
let n = emit_expr(e.kids[1])
return val(emit_bind(`call ptr @fn_str_repeat(ptr {s.code}, i32 {n.code})`), "string")
}
if (meth == "pad_left") { # pad with spaces to width, on the left
g_uses_textrt = true
let w = emit_expr(e.kids[1])
return val(emit_bind(`call ptr @fn_str_pad(ptr {s.code}, i32 {w.code}, i1 1)`), "string")
}
if (meth == "pad_right") { # pad with spaces to width, on the right
g_uses_textrt = true
let w = emit_expr(e.kids[1])
return val(emit_bind(`call ptr @fn_str_pad(ptr {s.code}, i32 {w.code}, i1 0)`), "string")
}
if (meth == "replace") { # replace every `from` with `to`
g_uses_textrt2 = true
let from = emit_expr(e.kids[1]); let to = emit_expr(e.kids[2])
return val(emit_bind(`call ptr @fn_str_replace(ptr {s.code}, ptr {from.code}, ptr {to.code})`), "string")
}
if (meth == "split") { # split on a separator -> []string
g_uses_textrt2 = true
let sep = emit_expr(e.kids[1])
return val(emit_bind(`call ptr @fn_str_split(ptr {s.code}, ptr {sep.code})`), "[]string")
}
if (meth == "join") { # join a []string with a separator (s is the slice)
g_uses_textrt2 = true
let sep = emit_expr(e.kids[1])
return val(emit_bind(`call ptr @fn_str_join(ptr {s.code}, ptr {sep.code})`), "string")
}
if (meth == "contains") or (meth == "index_of") { # substring search
let sub = emit_expr(e.kids[1])
let p = emit_bind(`call ptr @strstr(ptr {s.code}, ptr {sub.code})`)
if (meth == "contains") {
let nn = emit_bind(`icmp ne ptr {p}, null`)
return val(emit_bind(`zext i1 {nn} to i32`), "bool")
}
let isnull = emit_bind(`icmp eq ptr {p}, null`) # index_of -> byte offset or -1
let pi = emit_bind(`ptrtoint ptr {p} to i64`)
let si = emit_bind(`ptrtoint ptr {s.code} to i64`)
let d = emit_bind(`sub i64 {pi}, {si}`)
let d32 = emit_bind(`trunc i64 {d} to i32`)
return val(emit_bind(`select i1 {isnull}, i32 -1, i32 {d32}`), "int")
}
# starts_with / ends_with: compare against the affix over its own length
let affix = emit_expr(e.kids[1])
let la = emit_bind(`call i64 @strlen(ptr {affix.code})`)
if (meth == "starts_with") { # strncmp of the head is null-safe
let cmp = emit_bind(`call i32 @strncmp(ptr {s.code}, ptr {affix.code}, i64 {la})`)
let eqz = emit_bind(`icmp eq i32 {cmp}, 0`)
return val(emit_bind(`zext i1 {eqz} to i32`), "bool")
}
# ends_with: compare the tail, but only when the affix fits (else a negative
# offset would read before the string) — branch so the strncmp never underflows
let ls = emit_bind(`call i64 @strlen(ptr {s.code})`)
let off = emit_bind(`sub i64 {ls}, {la}`)
let res = emit_alloca("i32")
store_at("i32", "0", res)
let neg = emit_bind(`icmp slt i64 {off}, 0`)
let cmpl = lbl("ew_cmp"); let en = lbl("ew_end")
emit(" br i1 "); emit(neg); emit(", label %"); emit(en); emit(", label %"); emit(cmpl); emit("\n")
emit(cmpl); emit(":\n")
let tail = emit_bind(`getelementptr inbounds i8, ptr {s.code}, i64 {off}`)
let cmp = emit_bind(`call i32 @strncmp(ptr {tail}, ptr {affix.code}, i64 {la})`)
let eqz = emit_bind(`icmp eq i32 {cmp}, 0`)
let z = emit_bind(`zext i1 {eqz} to i32`)
store_at("i32", z, res)
emit(" br label %"); emit(en); emit("\n")
emit(en); emit(":\n")
return val(emit_bind(`load i32, ptr {res}`), "bool")
}

View file

@ -1,140 +1,4 @@
# emit_text.ludic — the Text.* namespace over `str` (null-terminated byte
# strings). The libc-backed queries (length/char_at/starts_with/ends_with/
# contains/index_of/to_int) allocate nothing; slice/from_int/equals/concat reuse
# the string preludes that the `+`, `s[a..b]` and string(int) operators emit.
function is_text_ns(meth: pointer) -> bool {
if (meth == "length") or (meth == "char_at") or (meth == "slice") { return true }
if (meth == "equals") or (meth == "concat") or (meth == "to_int") or (meth == "from_int") { return true }
if (meth == "starts_with") or (meth == "ends_with") { return true }
if (meth == "contains") or (meth == "index_of") { return true }
if (meth == "upper") or (meth == "lower") or (meth == "trim") or (meth == "repeat") { return true }
if (meth == "pad_left") or (meth == "pad_right") { return true }
if (meth == "split") or (meth == "join") or (meth == "replace") { return true }
return false
}
function emit_text_ns(meth: pointer, e: Node) -> Val {
if (meth == "from_int") { # int -> string, same as string(n)
let n = emit_expr(e.kids[0])
g_uses_intstr = true
return val(emit_bind(`call ptr @fn_int_str(i32 {n.code})`), "string")
}
if (meth == "slice") { # s[a..b], same substring helper
let s0 = emit_expr(e.kids[0]); let a = emit_expr(e.kids[1]); let b = emit_expr(e.kids[2])
g_uses_strslice = true
return val(emit_bind(`call ptr @fn_str_slice(ptr {s0.code}, i32 {a.code}, i32 {b.code})`), "string")
}
if (meth == "equals") { # byte-wise equality, same as ==
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
g_uses_str = true
return val(emit_bind(`call i32 @fn_str_eq(ptr {a.code}, ptr {b.code})`), "bool")
}
if (meth == "concat") { # a + b, same as the + operator
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
g_uses_str = true
return val(emit_bind(`call ptr @fn_str_concat(ptr {a.code}, ptr {b.code})`), "string")
}
let s = emit_expr(e.kids[0])
if (meth == "length") { # byte length
let r = emit_bind(`call i64 @strlen(ptr {s.code})`)
return val(emit_bind(`trunc i64 {r} to i32`), "int")
}
if (meth == "char_at") { # the byte at index i, 0..255
let i = emit_expr(e.kids[1])
let a = emit_bind(`getelementptr inbounds i8, ptr {s.code}, i32 {i.code}`)
let c = emit_bind(`load i8, ptr {a}`)
return val(emit_bind(`zext i8 {c} to i32`), "int")
}
if (meth == "to_int") { # parse a leading integer, 0 if none
return val(emit_bind(`call i32 @atoi(ptr {s.code})`), "int")
}
if (meth == "upper") { # ASCII a-z -> A-Z, fresh string
g_uses_textrt = true
return val(emit_bind(`call ptr @fn_str_upper(ptr {s.code})`), "string")
}
if (meth == "lower") { # ASCII A-Z -> a-z, fresh string
g_uses_textrt = true
return val(emit_bind(`call ptr @fn_str_lower(ptr {s.code})`), "string")
}
if (meth == "trim") { # drop leading/trailing whitespace
g_uses_textrt = true
return val(emit_bind(`call ptr @fn_str_trim(ptr {s.code})`), "string")
}
if (meth == "repeat") { # s repeated n times
g_uses_textrt = true
let n = emit_expr(e.kids[1])
return val(emit_bind(`call ptr @fn_str_repeat(ptr {s.code}, i32 {n.code})`), "string")
}
if (meth == "pad_left") { # pad with spaces to width, on the left
g_uses_textrt = true
let w = emit_expr(e.kids[1])
return val(emit_bind(`call ptr @fn_str_pad(ptr {s.code}, i32 {w.code}, i1 1)`), "string")
}
if (meth == "pad_right") { # pad with spaces to width, on the right
g_uses_textrt = true
let w = emit_expr(e.kids[1])
return val(emit_bind(`call ptr @fn_str_pad(ptr {s.code}, i32 {w.code}, i1 0)`), "string")
}
if (meth == "replace") { # replace every `from` with `to`
g_uses_textrt2 = true
let from = emit_expr(e.kids[1]); let to = emit_expr(e.kids[2])
return val(emit_bind(`call ptr @fn_str_replace(ptr {s.code}, ptr {from.code}, ptr {to.code})`), "string")
}
if (meth == "split") { # split on a separator -> []string
g_uses_textrt2 = true
let sep = emit_expr(e.kids[1])
return val(emit_bind(`call ptr @fn_str_split(ptr {s.code}, ptr {sep.code})`), "[]string")
}
if (meth == "join") { # join a []string with a separator (s is the slice)
g_uses_textrt2 = true
let sep = emit_expr(e.kids[1])
return val(emit_bind(`call ptr @fn_str_join(ptr {s.code}, ptr {sep.code})`), "string")
}
if (meth == "contains") or (meth == "index_of") { # substring search
let sub = emit_expr(e.kids[1])
let p = emit_bind(`call ptr @strstr(ptr {s.code}, ptr {sub.code})`)
if (meth == "contains") {
let nn = emit_bind(`icmp ne ptr {p}, null`)
return val(emit_bind(`zext i1 {nn} to i32`), "bool")
}
let isnull = emit_bind(`icmp eq ptr {p}, null`) # index_of -> byte offset or -1
let pi = emit_bind(`ptrtoint ptr {p} to i64`)
let si = emit_bind(`ptrtoint ptr {s.code} to i64`)
let d = emit_bind(`sub i64 {pi}, {si}`)
let d32 = emit_bind(`trunc i64 {d} to i32`)
return val(emit_bind(`select i1 {isnull}, i32 -1, i32 {d32}`), "int")
}
# starts_with / ends_with: compare against the affix over its own length
let affix = emit_expr(e.kids[1])
let la = emit_bind(`call i64 @strlen(ptr {affix.code})`)
if (meth == "starts_with") { # strncmp of the head is null-safe
let cmp = emit_bind(`call i32 @strncmp(ptr {s.code}, ptr {affix.code}, i64 {la})`)
let eqz = emit_bind(`icmp eq i32 {cmp}, 0`)
return val(emit_bind(`zext i1 {eqz} to i32`), "bool")
}
# ends_with: compare the tail, but only when the affix fits (else a negative
# offset would read before the string) — branch so the strncmp never underflows
let ls = emit_bind(`call i64 @strlen(ptr {s.code})`)
let off = emit_bind(`sub i64 {ls}, {la}`)
let res = emit_alloca("i32")
store_at("i32", "0", res)
let neg = emit_bind(`icmp slt i64 {off}, 0`)
let cmpl = lbl("ew_cmp"); let en = lbl("ew_end")
emit(" br i1 "); emit(neg); emit(", label %"); emit(en); emit(", label %"); emit(cmpl); emit("\n")
emit(cmpl); emit(":\n")
let tail = emit_bind(`getelementptr inbounds i8, ptr {s.code}, i64 {off}`)
let cmp = emit_bind(`call i32 @strncmp(ptr {tail}, ptr {affix.code}, i64 {la})`)
let eqz = emit_bind(`icmp eq i32 {cmp}, 0`)
let z = emit_bind(`zext i1 {eqz} to i32`)
store_at("i32", z, res)
emit(" br label %"); emit(en); emit("\n")
emit(en); emit(":\n")
return val(emit_bind(`load i32, ptr {res}`), "bool")
}
# emit_text_prelude.ludic — the emitted Text runtime prelude: the string-builder helpers (upper/lower/trim/repeat/pad and the split/join machinery) written into each program that uses Text.*. Split out of emit_text.ludic (concern: emitted runtime code, vs. emit_text.ludic's Text.* namespace dispatch).
# emit_text_prelude — string builders that allocate: upper/lower/trim/
# repeat/pad. Emitted once per program that uses them (g_uses_textrt). Plain
# libc (strlen/malloc/memcpy), deterministic, C-string in and out.

View file

@ -153,7 +153,7 @@ compare("primitive types", h_types, grammar_alternation(grammar, kpath, "fixed")
def parser_keywords():
kws = set()
for name in ("parse.ludic", "parse_game.ludic"):
text = open(os.path.join(ROOT, "selfhost", name), encoding="utf-8").read()
text = open(os.path.join(ROOT, "selfhost", "frontend", name), encoding="utf-8").read()
kws |= set(re.findall(r'is_id\("([a-z]+)"\)', text))
kws |= set(re.findall(r'streq\(t\.text,\s*"([a-z]+)"\)', text))
return kws
@ -161,7 +161,7 @@ def parser_keywords():
pkw = parser_keywords()
h_reserved = c_string_list(syntax_h, "LUDIC_KW_RESERVED")
if not pkw:
problems.append("could not extract any keywords from selfhost/parse*.ludic")
problems.append("could not extract any keywords from selfhost/frontend/parse*.ludic")
else:
for label, kws in (("declaration keywords", h_decl), ("clause keywords", h_clause)):
unparsed = (kws - pkw) - h_reserved

View file

@ -7,7 +7,7 @@ never written. So the set of things that MUST have a doc page is read straight
from the implementation, and every one is required to have a page (and every
namespace-method page is required to correspond to something real):
* namespace methods selfhost/emit_expr.ludic `emit_ns_call` + the
* namespace methods selfhost/backend/emit_call.ludic `emit_ns_call` + the
`is_<ns>_ns` predicates it delegates to (Math/Text/List)
* keywords tools/ludic-tools/ludic_syntax.h LUDIC_KW_* (minus
LUDIC_KW_RESERVED, which is not-yet-implemented)
@ -45,20 +45,23 @@ def fn_body(text, name):
def all_selfhost_source():
src = ""
d = os.path.join(ROOT, "selfhost")
for fn in sorted(os.listdir(d)):
if fn.endswith(".ludic"):
src += read("selfhost", fn) + "\n"
files = []
for dirpath, _dirs, names in os.walk(d):
for fn in names:
if fn.endswith(".ludic"):
files.append(os.path.join(dirpath, fn))
for path in sorted(files):
src += open(path, encoding="utf-8").read() + "\n"
return src
def compiler_ns_methods():
"""{Namespace: set(method)} the compiler actually dispatches on."""
expr = read("selfhost", "emit_expr.ludic")
body = fn_body(expr, "emit_ns_call")
if not body:
problems.append("emit_ns_call not found in selfhost/emit_expr.ludic")
return {}
allsrc = all_selfhost_source()
body = fn_body(allsrc, "emit_ns_call")
if not body:
problems.append("emit_ns_call not found in selfhost/ sources")
return {}
result = {}
# each `if (ns == "X")` opens a block that runs to the next such marker
chunks = re.split(r'if \(ns == "', body)

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# Single source of truth for Ludic's named color palette.
# Emits: selfhost/emit_color.ludic (compiler lookup) and palette.json (docs).
# Emits: selfhost/backend/stdlib/emit_color.ludic (compiler lookup) and palette.json (docs).
import json, sys, os
# (Name, 0xRRGGBB, group). Names are PascalCase, unique. >200 entries.
@ -293,6 +293,6 @@ if __name__ == "__main__":
names = guard_check()
root = os.path.dirname(os.path.abspath(__file__))
repo = "/Users/orkuncakilkaya/workspace/gpp"
emit_ludic(os.path.join(repo, "selfhost", "emit_color.ludic"))
emit_ludic(os.path.join(repo, "selfhost", "backend", "stdlib", "emit_color.ludic"))
emit_json(os.path.join(root, "palette.json"))
print(f"OK {len(PALETTE)} colors ({len(set(names))} unique names)")

View file

@ -1190,7 +1190,7 @@ program LudicLsp {
function builtin_sig(name: pointer) -> pointer { var i = 0; while i < len(g_builtins) { if (g_builtins[i] == name) { return g_bsigs[i] }; i = i + 1 }; return null }
# The namespaced standard-library surface: `Namespace.method`, mirroring the
# compiler's emit_ns_call (selfhost/emit_expr.ludic). Screen/Random/Input/Map
# compiler's emit_ns_call (selfhost/backend/emit_call.ludic). Screen/Random/Input/Map
# methods resolve to the underlying bare builtin's signature; Math/Text/List
# are pure fixed-point / string / slice ops with their own signatures. Returns
# the signature label for signature help, or null if it is not a known method.

View file

@ -12,49 +12,52 @@ function cc() -> pointer { return getenv_or("LUDIC_CC", "clang") }
# the old scripts each carried their own copy.
function selfhost_frags() -> []pointer {
let f = new []pointer
push(f, "selfhost/str.ludic")
push(f, "selfhost/buf.ludic")
push(f, "selfhost/io.ludic")
push(f, "selfhost/ast.ludic")
push(f, "selfhost/lex.ludic")
push(f, "selfhost/parse.ludic")
push(f, "selfhost/parse_game.ludic")
push(f, "selfhost/emit_core.ludic")
push(f, "selfhost/emit_head.ludic")
push(f, "selfhost/emit_addr.ludic")
push(f, "selfhost/emit_intrin.ludic")
push(f, "selfhost/emit_intrin2.ludic")
push(f, "selfhost/emit_math.ludic")
push(f, "selfhost/emit_vector.ludic")
push(f, "selfhost/emit_text.ludic")
push(f, "selfhost/emit_hash.ludic")
push(f, "selfhost/emit_crypto.ludic")
push(f, "selfhost/emit_uuid.ludic")
push(f, "selfhost/emit_noise.ludic")
push(f, "selfhost/emit_log.ludic")
push(f, "selfhost/emit_os.ludic")
push(f, "selfhost/emit_unicode.ludic")
push(f, "selfhost/emit_fs.ludic")
push(f, "selfhost/emit_list.ludic")
push(f, "selfhost/emit_ease.ludic")
push(f, "selfhost/emit_collide.ludic")
push(f, "selfhost/emit_mem.ludic")
push(f, "selfhost/emit_time.ludic")
push(f, "selfhost/emit_datetime.ludic")
push(f, "selfhost/emit_colorfn.ludic")
push(f, "selfhost/emit_color.ludic")
push(f, "selfhost/emit_new.ludic")
push(f, "selfhost/emit_expr.ludic")
push(f, "selfhost/emit_stmt.ludic")
push(f, "selfhost/emit_ecs.ludic")
push(f, "selfhost/emit_query.ludic")
push(f, "selfhost/emit_spawn.ludic")
push(f, "selfhost/emit_game.ludic")
push(f, "selfhost/emit_machine.ludic")
push(f, "selfhost/emit_save.ludic")
push(f, "selfhost/emit_net.ludic")
push(f, "selfhost/emit_ui.ludic")
push(f, "selfhost/emit_decl.ludic")
push(f, "selfhost/support/str.ludic")
push(f, "selfhost/support/buf.ludic")
push(f, "selfhost/support/io.ludic")
push(f, "selfhost/frontend/ast.ludic")
push(f, "selfhost/frontend/lex.ludic")
push(f, "selfhost/frontend/parse.ludic")
push(f, "selfhost/frontend/parse_game.ludic")
push(f, "selfhost/backend/emit_core.ludic")
push(f, "selfhost/backend/emit_head.ludic")
push(f, "selfhost/backend/emit_addr.ludic")
push(f, "selfhost/backend/emit_intrin.ludic")
push(f, "selfhost/backend/emit_intrin2.ludic")
push(f, "selfhost/backend/stdlib/emit_math.ludic")
push(f, "selfhost/backend/stdlib/emit_vector.ludic")
push(f, "selfhost/backend/stdlib/emit_text.ludic")
push(f, "selfhost/backend/stdlib/emit_text_prelude.ludic")
push(f, "selfhost/backend/stdlib/emit_hash.ludic")
push(f, "selfhost/backend/stdlib/emit_crypto.ludic")
push(f, "selfhost/backend/stdlib/emit_uuid.ludic")
push(f, "selfhost/backend/stdlib/emit_noise.ludic")
push(f, "selfhost/backend/stdlib/emit_log.ludic")
push(f, "selfhost/backend/stdlib/emit_os.ludic")
push(f, "selfhost/backend/stdlib/emit_unicode.ludic")
push(f, "selfhost/backend/stdlib/emit_fs.ludic")
push(f, "selfhost/backend/stdlib/emit_list.ludic")
push(f, "selfhost/backend/stdlib/emit_ease.ludic")
push(f, "selfhost/backend/game/emit_collide.ludic")
push(f, "selfhost/backend/emit_mem.ludic")
push(f, "selfhost/backend/stdlib/emit_time.ludic")
push(f, "selfhost/backend/stdlib/emit_datetime.ludic")
push(f, "selfhost/backend/stdlib/emit_colorfn.ludic")
push(f, "selfhost/backend/stdlib/emit_color.ludic")
push(f, "selfhost/backend/emit_new.ludic")
push(f, "selfhost/backend/emit_expr.ludic")
push(f, "selfhost/backend/emit_call.ludic")
push(f, "selfhost/backend/emit_stmt.ludic")
push(f, "selfhost/backend/game/emit_ecs.ludic")
push(f, "selfhost/backend/game/emit_query.ludic")
push(f, "selfhost/backend/game/emit_spawn.ludic")
push(f, "selfhost/backend/game/emit_game.ludic")
push(f, "selfhost/backend/game/emit_world.ludic")
push(f, "selfhost/backend/game/emit_machine.ludic")
push(f, "selfhost/backend/game/emit_save.ludic")
push(f, "selfhost/backend/stdlib/emit_net.ludic")
push(f, "selfhost/backend/game/emit_ui.ludic")
push(f, "selfhost/backend/emit_decl.ludic")
push(f, "selfhost/main.ludic")
return f
}