Implement the rest of NETWORKING-DESIGN.md (N2–N6) and eliminate every
`.c` file from the repo. clang remains only the LLVM-IR assembler; no C
is compiled anywhere.
Networking (selfhost/emit_net.ludic + parser/emit changes):
- N2 @Sync: per-model serialize/apply + by-kind dispatchers; POD-scalar
compile error and empty-participation warning; selective replication.
- N3 @Owned: @L_owner array + owner/set_owner/is_owner; owners snapshot.
- N4 @ToServer/@ToClients remote events: framed net_send + net_pump re-emit.
- N5 @Server/@Predicted role guards + drivable sim (tick_fixed/tick_render,
entry-owns-the-loop).
- Built-in loopback transport so multiplayer runs with zero foreign code;
extern fn net_send/net_poll still overrides it for a real socket.
- N6 blessed runtime (examples/net_rt.ludic) + end-to-end demo (net_demo).
- Fix: llty("entity") is now i32 (entities are i32 handles), so let e = self().
C elimination:
- Networking + foreign-mod-ABI tests rewritten as self-contained pure-Ludic
programs (examples/net_*, world_*, mod_events, scoped); tests/ removed.
- Reflection ABI exposed to Ludic as world_* builtins (Ludic-to-Ludic modding).
- Formatter rewritten C→Ludic: tools/ludic-tools/fmt.ludic.
- Language server rewritten C→Ludic: tools/ludic-tools/lsp.ludic (lexer, index
parser, cross-file workspace resolver, JSON, all LSP handlers).
- Obsolete migrate_*.c codemods deleted; ludic_syntax.h kept as vocabulary data.
Suites: ./test.sh 44/44, ./tools/test-tools.sh 28/28 (LSP 42/42), fixpoint holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
761 lines
40 KiB
Text
761 lines
40 KiB
Text
# 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.
|
|
|
|
fn 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).
|
|
fn 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.
|
|
fn emit_calls_for_phase(phase: ptr) -> 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.
|
|
fn emit_scene_fn(name: ptr, kind: ptr, 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")
|
|
}
|
|
|
|
fn 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.
|
|
fn 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.
|
|
fn 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
|
|
|
|
fn 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")
|
|
}
|
|
|
|
# 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
|
|
# compile-time schema, so it never drifts. Emitted only for an ECS program that
|
|
# also declares events (a modding program), so event-free games stay byte-exact.
|
|
#
|
|
# i32 ludic_prop_id(name) property name -> stable id (-1 = none)
|
|
# i32 ludic_field_id(prop, name) field name within a property -> index
|
|
# i64 ludic_get(entity, prop, field) read a field (sign-extended to i64)
|
|
# void ludic_set(entity, prop, field, i64) write a field (truncated to i32)
|
|
# i32 ludic_has(entity, prop) does the entity have the property?
|
|
#
|
|
# First cut: integer component fields (the common case — hp, x, amount). Property
|
|
# ids are assignment order in the source; field ids are declaration order.
|
|
fn emit_world_table() -> void {
|
|
let me = itoa(MAX_ENT)
|
|
emith("declare i32 @strcmp(ptr, ptr)\n")
|
|
|
|
# EV7 — schema opening: a mod can register a brand-new component at runtime.
|
|
# Compile-time components take prop ids 0..NC-1; mod-defined ones take NC.. and
|
|
# live in these parallel registries (fixed capacity 32). Storage is a flat
|
|
# malloc'd [MAX_ENT x nfields x i32] with a parallel has-flag array. get/set/has
|
|
# and prop_id fall through to this table for a prop id >= NC.
|
|
var ncomp = 0
|
|
var ci0 = 0
|
|
while ci0 < len(prog) { if prog[ci0].kind == N_COMP { ncomp = ncomp + 1 }; ci0 = ci0 + 1 }
|
|
let NC = itoa(ncomp)
|
|
emith("@dyn_count = global i32 0\n")
|
|
emith("@dynS = global [32 x ptr] zeroinitializer\n") # storage base per dyn component
|
|
emith("@dynH = global [32 x ptr] zeroinitializer\n") # has-flag array per dyn component
|
|
emith("@dynF = global [32 x i32] zeroinitializer\n") # field count per dyn component
|
|
emith("@dynName = global [32 x ptr] zeroinitializer\n") # name per dyn component
|
|
|
|
# ludic_prop_id(name): strcmp against each property's name constant
|
|
emit("define i32 @ludic_prop_id(ptr %name) {\nentry:\n")
|
|
var k = 0
|
|
var i = 0
|
|
while i < len(prog) {
|
|
if prog[i].kind == N_COMP {
|
|
let sc = emit_str_const(prog[i].s); let sk = itoa(k)
|
|
emit(" %c"); emit(sk); emit(" = call i32 @strcmp(ptr %name, ptr "); emit(sc); emit(")\n")
|
|
emit(" %e"); emit(sk); emit(" = icmp eq i32 %c"); emit(sk); emit(", 0\n")
|
|
emit(" br i1 %e"); emit(sk); emit(", label %h"); emit(sk); emit(", label %n"); emit(sk); emit("\n")
|
|
emit("h"); emit(sk); emit(":\n ret i32 "); emit(sk); emit("\n")
|
|
emit("n"); emit(sk); emit(":\n")
|
|
k = k + 1
|
|
}
|
|
i = i + 1
|
|
}
|
|
# EV7: not a compile-time component — search the dynamic (mod-registered) names
|
|
emit(" %dpi = alloca i32\n store i32 0, ptr %dpi\n br label %dpl\n")
|
|
emit("dpl:\n %di = load i32, ptr %dpi\n %dn = load i32, ptr @dyn_count\n %dg = icmp slt i32 %di, %dn\n br i1 %dg, label %dpb, label %dpnone\n")
|
|
emit("dpb:\n %dnp = getelementptr inbounds [32 x ptr], ptr @dynName, i32 0, i32 %di\n %dname = load ptr, ptr %dnp\n %dcmp = call i32 @strcmp(ptr %name, ptr %dname)\n %deq = icmp eq i32 %dcmp, 0\n br i1 %deq, label %dphit, label %dpnext\n")
|
|
emit("dphit:\n %drid = add i32 %di, "); emit(NC); emit("\n ret i32 %drid\n")
|
|
emit("dpnext:\n %di1 = add i32 %di, 1\n store i32 %di1, ptr %dpi\n br label %dpl\n")
|
|
emit("dpnone:\n ret i32 -1\n}\n\n")
|
|
|
|
# ludic_field_id(prop, name): within the matched property, strcmp each field name
|
|
emit("define i32 @ludic_field_id(i32 %p, ptr %name) {\nentry:\n")
|
|
k = 0; i = 0
|
|
while i < len(prog) {
|
|
if prog[i].kind == N_COMP {
|
|
let c = prog[i]; let sk = itoa(k)
|
|
emit(" %pm"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n")
|
|
emit(" br i1 %pm"); emit(sk); emit(", label %pk"); emit(sk); emit(", label %pn"); emit(sk); emit("\n")
|
|
emit("pk"); emit(sk); emit(":\n")
|
|
var f = 0
|
|
while f < len(c.kids) {
|
|
let fc = emit_str_const(c.kids[f].s); let fk = `{sk}_{itoa(f)}`
|
|
emit(" %fc"); emit(fk); emit(" = call i32 @strcmp(ptr %name, ptr "); emit(fc); emit(")\n")
|
|
emit(" %fe"); emit(fk); emit(" = icmp eq i32 %fc"); emit(fk); emit(", 0\n")
|
|
emit(" br i1 %fe"); emit(fk); emit(", label %fh"); emit(fk); emit(", label %fn"); emit(fk); emit("\n")
|
|
emit("fh"); emit(fk); emit(":\n ret i32 "); emit(itoa(f)); emit("\n")
|
|
emit("fn"); emit(fk); emit(":\n")
|
|
f = f + 1
|
|
}
|
|
emit(" ret i32 -1\n")
|
|
emit("pn"); emit(sk); emit(":\n")
|
|
k = k + 1
|
|
}
|
|
i = i + 1
|
|
}
|
|
emit(" ret i32 -1\n}\n\n")
|
|
|
|
# ludic_get / ludic_set / ludic_has dispatch prop -> @S_/@H_ storage; the field
|
|
# address is slot + field*4 (integer fields).
|
|
# ludic_get/ludic_set dispatch prop -> component storage, then the field id to a
|
|
# constant struct GEP (so mixed layouts and ptr/byte fields are addressed
|
|
# correctly, not assumed 4-byte). Values cross the ABI as i64: int/bool/fixed
|
|
# sign-extend, byte zero-extends, ptr round-trips through ptrtoint/inttoptr.
|
|
emit("define i64 @ludic_get(i32 %e, i32 %p, i32 %f) {\nentry:\n")
|
|
k = 0; i = 0
|
|
while i < len(prog) {
|
|
if prog[i].kind == N_COMP {
|
|
let c = prog[i]; let cn = c.s; let sk = itoa(k)
|
|
emit(" %m"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n")
|
|
emit(" br i1 %m"); emit(sk); emit(", label %g"); emit(sk); emit(", label %gn"); emit(sk); emit("\n")
|
|
emit("g"); emit(sk); emit(":\n")
|
|
emit(" %s"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(cn); emit("], ptr @S_"); emit(cn); emit(", i32 0, i32 %e\n")
|
|
var fj = 0
|
|
while fj < len(c.kids) {
|
|
let ft = llty(c.kids[fj].ty); let fk = `{sk}_{itoa(fj)}`
|
|
emit(" %gm"); emit(fk); emit(" = icmp eq i32 %f, "); emit(itoa(fj)); emit("\n")
|
|
emit(" br i1 %gm"); emit(fk); emit(", label %gf"); emit(fk); emit(", label %gk"); emit(fk); emit("\n")
|
|
emit("gf"); emit(fk); emit(":\n")
|
|
emit(" %ga"); emit(fk); emit(" = getelementptr inbounds %Cmp_"); emit(cn); emit(", ptr %s"); emit(sk); emit(", i32 0, i32 "); emit(itoa(fj)); emit("\n")
|
|
if (ft == "ptr") {
|
|
emit(" %gl"); emit(fk); emit(" = load ptr, ptr %ga"); emit(fk); emit("\n")
|
|
emit(" %gr"); emit(fk); emit(" = ptrtoint ptr %gl"); emit(fk); emit(" to i64\n ret i64 %gr"); emit(fk); emit("\n")
|
|
} else { if (ft == "i8") {
|
|
emit(" %gl"); emit(fk); emit(" = load i8, ptr %ga"); emit(fk); emit("\n")
|
|
emit(" %gr"); emit(fk); emit(" = zext i8 %gl"); emit(fk); emit(" to i64\n ret i64 %gr"); emit(fk); emit("\n")
|
|
} else {
|
|
emit(" %gl"); emit(fk); emit(" = load i32, ptr %ga"); emit(fk); emit("\n")
|
|
emit(" %gr"); emit(fk); emit(" = sext i32 %gl"); emit(fk); emit(" to i64\n ret i64 %gr"); emit(fk); emit("\n")
|
|
} }
|
|
emit("gk"); emit(fk); emit(":\n")
|
|
fj = fj + 1
|
|
}
|
|
emit(" ret i64 0\n")
|
|
emit("gn"); emit(sk); emit(":\n")
|
|
k = k + 1
|
|
}
|
|
i = i + 1
|
|
}
|
|
# EV7: prop id >= NC -> a mod-registered component; index its flat storage
|
|
emit(" %gdyn = sub i32 %p, "); emit(NC); emit("\n")
|
|
emit(" %gdlo = icmp sge i32 %gdyn, 0\n %gdc = load i32, ptr @dyn_count\n %gdhi = icmp slt i32 %gdyn, %gdc\n %gdok = and i1 %gdlo, %gdhi\n br i1 %gdok, label %gdyng, label %gdnone\n")
|
|
emit("gdyng:\n %gsp = getelementptr inbounds [32 x ptr], ptr @dynS, i32 0, i32 %gdyn\n %gbase = load ptr, ptr %gsp\n")
|
|
emit(" %gfp = getelementptr inbounds [32 x i32], ptr @dynF, i32 0, i32 %gdyn\n %gnf = load i32, ptr %gfp\n %grow = mul i32 %e, %gnf\n %gidx = add i32 %grow, %f\n")
|
|
emit(" %gaddr = getelementptr inbounds i32, ptr %gbase, i32 %gidx\n %gv = load i32, ptr %gaddr\n %gr = sext i32 %gv to i64\n ret i64 %gr\n")
|
|
emit("gdnone:\n ret i64 0\n}\n\n")
|
|
|
|
emit("define void @ludic_set(i32 %e, i32 %p, i32 %f, i64 %val) {\nentry:\n")
|
|
k = 0; i = 0
|
|
while i < len(prog) {
|
|
if prog[i].kind == N_COMP {
|
|
let c = prog[i]; let cn = c.s; let sk = itoa(k)
|
|
emit(" %m"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n")
|
|
emit(" br i1 %m"); emit(sk); emit(", label %g"); emit(sk); emit(", label %gn"); emit(sk); emit("\n")
|
|
emit("g"); emit(sk); emit(":\n")
|
|
emit(" %s"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(cn); emit("], ptr @S_"); emit(cn); emit(", i32 0, i32 %e\n")
|
|
var fj = 0
|
|
while fj < len(c.kids) {
|
|
let ft = llty(c.kids[fj].ty); let fk = `{sk}_{itoa(fj)}`
|
|
emit(" %sm"); emit(fk); emit(" = icmp eq i32 %f, "); emit(itoa(fj)); emit("\n")
|
|
emit(" br i1 %sm"); emit(fk); emit(", label %sf"); emit(fk); emit(", label %sk"); emit(fk); emit("\n")
|
|
emit("sf"); emit(fk); emit(":\n")
|
|
emit(" %sa"); emit(fk); emit(" = getelementptr inbounds %Cmp_"); emit(cn); emit(", ptr %s"); emit(sk); emit(", i32 0, i32 "); emit(itoa(fj)); emit("\n")
|
|
if (ft == "ptr") {
|
|
emit(" %sp"); emit(fk); emit(" = inttoptr i64 %val to ptr\n")
|
|
emit(" store ptr %sp"); emit(fk); emit(", ptr %sa"); emit(fk); emit("\n ret void\n")
|
|
} else { if (ft == "i8") {
|
|
emit(" %sb"); emit(fk); emit(" = trunc i64 %val to i8\n")
|
|
emit(" store i8 %sb"); emit(fk); emit(", ptr %sa"); emit(fk); emit("\n ret void\n")
|
|
} else {
|
|
emit(" %sw"); emit(fk); emit(" = trunc i64 %val to i32\n")
|
|
emit(" store i32 %sw"); emit(fk); emit(", ptr %sa"); emit(fk); emit("\n ret void\n")
|
|
} }
|
|
emit("sk"); emit(fk); emit(":\n")
|
|
fj = fj + 1
|
|
}
|
|
emit(" ret void\n")
|
|
emit("gn"); emit(sk); emit(":\n")
|
|
k = k + 1
|
|
}
|
|
i = i + 1
|
|
}
|
|
# EV7: prop id >= NC -> a mod-registered component
|
|
emit(" %sdyn = sub i32 %p, "); emit(NC); emit("\n")
|
|
emit(" %sdlo = icmp sge i32 %sdyn, 0\n %sdc = load i32, ptr @dyn_count\n %sdhi = icmp slt i32 %sdyn, %sdc\n %sdok = and i1 %sdlo, %sdhi\n br i1 %sdok, label %sdyng, label %sdnone\n")
|
|
emit("sdyng:\n %ssp = getelementptr inbounds [32 x ptr], ptr @dynS, i32 0, i32 %sdyn\n %sbase = load ptr, ptr %ssp\n")
|
|
emit(" %sfp = getelementptr inbounds [32 x i32], ptr @dynF, i32 0, i32 %sdyn\n %snf = load i32, ptr %sfp\n %srow = mul i32 %e, %snf\n %sidx = add i32 %srow, %f\n")
|
|
emit(" %saddr = getelementptr inbounds i32, ptr %sbase, i32 %sidx\n %sv = trunc i64 %val to i32\n store i32 %sv, ptr %saddr\n ret void\n")
|
|
emit("sdnone:\n ret void\n}\n\n")
|
|
|
|
emit("define i32 @ludic_has(i32 %e, i32 %p) {\nentry:\n")
|
|
k = 0; i = 0
|
|
while i < len(prog) {
|
|
if prog[i].kind == N_COMP {
|
|
let cn = prog[i].s; let sk = itoa(k)
|
|
emit(" %m"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n")
|
|
emit(" br i1 %m"); emit(sk); emit(", label %g"); emit(sk); emit(", label %gn"); emit(sk); emit("\n")
|
|
emit("g"); emit(sk); emit(":\n")
|
|
emit(" %hp"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(cn); emit(", i32 0, i32 %e\n")
|
|
emit(" %hv"); emit(sk); emit(" = load i8, ptr %hp"); emit(sk); emit("\n")
|
|
emit(" %hr"); emit(sk); emit(" = zext i8 %hv"); emit(sk); emit(" to i32\n ret i32 %hr"); emit(sk); emit("\n")
|
|
emit("gn"); emit(sk); emit(":\n")
|
|
k = k + 1
|
|
}
|
|
i = i + 1
|
|
}
|
|
# EV7: prop id >= NC -> a mod-registered component's has-flag array
|
|
emit(" %hdyn = sub i32 %p, "); emit(NC); emit("\n")
|
|
emit(" %hdlo = icmp sge i32 %hdyn, 0\n %hdc = load i32, ptr @dyn_count\n %hdhi = icmp slt i32 %hdyn, %hdc\n %hdok = and i1 %hdlo, %hdhi\n br i1 %hdok, label %hdyng, label %hdnone\n")
|
|
emit("hdyng:\n %hhp = getelementptr inbounds [32 x ptr], ptr @dynH, i32 0, i32 %hdyn\n %hh = load ptr, ptr %hhp\n %hslot = getelementptr inbounds i8, ptr %hh, i32 %e\n %hval = load i8, ptr %hslot\n %hres = zext i8 %hval to i32\n ret i32 %hres\n")
|
|
emit("hdnone:\n ret i32 0\n}\n\n")
|
|
|
|
# ludic_register_prop(name, nfields) -> prop id — a mod declares a NEW component.
|
|
# Allocates flat [MAX_ENT x nfields x i32] storage + a MAX_ENT has-flag array,
|
|
# zeroed. The returned id works with get/set/has/attach exactly like a built-in.
|
|
emit("define i32 @ludic_register_prop(ptr %name, i32 %nfields) {\nentry:\n")
|
|
emit(" %dc = load i32, ptr @dyn_count\n %full = icmp slt i32 %dc, 32\n br i1 %full, label %do, label %rej\n")
|
|
emit("do:\n %nf4 = mul i32 %nfields, 4\n %sz = mul i32 %nf4, "); emit(me); emit("\n %szl = sext i32 %sz to i64\n")
|
|
emit(" %buf = call ptr @malloc(i64 %szl)\n call ptr @memset(ptr %buf, i32 0, i64 %szl)\n")
|
|
emit(" %sp = getelementptr inbounds [32 x ptr], ptr @dynS, i32 0, i32 %dc\n store ptr %buf, ptr %sp\n")
|
|
emit(" %hbuf = call ptr @malloc(i64 "); emit(me); emit(")\n call ptr @memset(ptr %hbuf, i32 0, i64 "); emit(me); emit(")\n")
|
|
emit(" %hp = getelementptr inbounds [32 x ptr], ptr @dynH, i32 0, i32 %dc\n store ptr %hbuf, ptr %hp\n")
|
|
emit(" %fp = getelementptr inbounds [32 x i32], ptr @dynF, i32 0, i32 %dc\n store i32 %nfields, ptr %fp\n")
|
|
emit(" %np = getelementptr inbounds [32 x ptr], ptr @dynName, i32 0, i32 %dc\n store ptr %name, ptr %np\n")
|
|
emit(" %id = add i32 %dc, "); emit(NC); emit("\n %dc1 = add i32 %dc, 1\n store i32 %dc1, ptr @dyn_count\n ret i32 %id\n")
|
|
emit("rej:\n ret i32 -1\n}\n\n")
|
|
|
|
# ludic_attach_dyn / ludic_detach_dyn(entity, prop) — set/clear a mod-registered
|
|
# component's has-flag on an entity (the dynamic analogue of attach/detach).
|
|
emit("define void @ludic_attach_dyn(i32 %e, i32 %p) {\nentry:\n")
|
|
emit(" %dyn = sub i32 %p, "); emit(NC); emit("\n %hp = getelementptr inbounds [32 x ptr], ptr @dynH, i32 0, i32 %dyn\n %h = load ptr, ptr %hp\n %slot = getelementptr inbounds i8, ptr %h, i32 %e\n store i8 1, ptr %slot\n ret void\n}\n\n")
|
|
emit("define void @ludic_detach_dyn(i32 %e, i32 %p) {\nentry:\n")
|
|
emit(" %dyn = sub i32 %p, "); emit(NC); emit("\n %hp = getelementptr inbounds [32 x ptr], ptr @dynH, i32 0, i32 %dyn\n %h = load ptr, ptr %hp\n %slot = getelementptr inbounds i8, ptr %h, i32 %e\n store i8 0, ptr %slot\n ret void\n}\n\n")
|
|
|
|
# ludic_entity_count / ludic_kind / ludic_model_id — a mod scans the world and
|
|
# identifies each entity's model, then reads/writes it with get/set/has above.
|
|
emit("define i32 @ludic_entity_count() {\nentry:\n %n = load i32, ptr @L_entc\n ret i32 %n\n}\n\n")
|
|
emit("define i32 @ludic_kind(i32 %e) {\nentry:\n")
|
|
emit(" %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n")
|
|
emit(" %k = load i32, ptr %kp\n ret i32 %k\n}\n\n")
|
|
emit("define i32 @ludic_model_id(ptr %name) {\nentry:\n")
|
|
k = 0; i = 0
|
|
while i < len(prog) {
|
|
if prog[i].kind == N_ARCH {
|
|
let sc = emit_str_const(prog[i].s); let sk = itoa(k)
|
|
emit(" %mdc"); emit(sk); emit(" = call i32 @strcmp(ptr %name, ptr "); emit(sc); emit(")\n")
|
|
emit(" %mde"); emit(sk); emit(" = icmp eq i32 %mdc"); emit(sk); emit(", 0\n")
|
|
emit(" br i1 %mde"); emit(sk); emit(", label %mdh"); emit(sk); emit(", label %mdn"); emit(sk); emit("\n")
|
|
emit("mdh"); emit(sk); emit(":\n ret i32 "); emit(itoa(find_arch_id(prog[i].s))); emit("\n")
|
|
emit("mdn"); emit(sk); emit(":\n")
|
|
k = k + 1
|
|
}
|
|
i = i + 1
|
|
}
|
|
emit(" ret i32 -1\n}\n\n")
|
|
|
|
# ludic_spawn(model_id) -> entity — a mod creates a new entity. Each model gets a
|
|
# @ludic_spawn_<M> that reuses the compiler's own spawn lowering (alloc, kind,
|
|
# component defaults, @OnSpawn, and the model_<M>_spawn event), so a mod-spawned
|
|
# entity is indistinguishable from one born in source. A dispatcher routes the id.
|
|
i = 0
|
|
while i < len(prog) {
|
|
if prog[i].kind == N_ARCH {
|
|
let m = prog[i].s
|
|
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
|
|
ret_ty = "int"
|
|
let fbody = buf_new()
|
|
falloc = buf_new()
|
|
let saved = code
|
|
code = fbody
|
|
let syn = node(S_SPAWN); syn.s = m # a defaults-only spawn of model m
|
|
let se = emit_spawn(syn)
|
|
emit(" ret i32 "); emit(se); emit("\n")
|
|
code = saved
|
|
emit("define i32 @ludic_spawn_"); emit(m); emit("() {\nentry:\n")
|
|
emit(buf_str(falloc))
|
|
emit(buf_str(fbody))
|
|
emit("}\n\n")
|
|
}
|
|
i = i + 1
|
|
}
|
|
emit("define i32 @ludic_spawn(i32 %m) {\nentry:\n")
|
|
k = 0; i = 0
|
|
while i < len(prog) {
|
|
if prog[i].kind == N_ARCH {
|
|
let m = prog[i].s; let sk = itoa(k)
|
|
emit(" %sm"); emit(sk); emit(" = icmp eq i32 %m, "); emit(itoa(find_arch_id(m))); emit("\n")
|
|
emit(" br i1 %sm"); emit(sk); emit(", label %sh"); emit(sk); emit(", label %sn"); emit(sk); emit("\n")
|
|
emit("sh"); emit(sk); emit(":\n %sr"); emit(sk); emit(" = call i32 @ludic_spawn_"); emit(m); emit("()\n ret i32 %sr"); emit(sk); emit("\n")
|
|
emit("sn"); emit(sk); emit(":\n")
|
|
k = k + 1
|
|
}
|
|
i = i + 1
|
|
}
|
|
emit(" ret i32 -1\n}\n\n")
|
|
|
|
# ludic_query_next(prop_id, from) -> the next live entity (>= from) that has the
|
|
# property, or -1. A mod iterates: for (e = query_next(p, 0); e >= 0; e =
|
|
# query_next(p, e+1)). Reuses ludic_has for the membership test.
|
|
emit("define i32 @ludic_query_next(i32 %p, i32 %from) {\nentry:\n")
|
|
emit(" %n = load i32, ptr @L_entc\n br label %loop\n")
|
|
emit("loop:\n %e = phi i32 [ %from, %entry ], [ %e1, %cont ]\n")
|
|
emit(" %go = icmp slt i32 %e, %n\n br i1 %go, label %body, label %none\n")
|
|
emit("body:\n %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %e\n")
|
|
emit(" %al = load i32, ptr %ap\n %isa = icmp ne i32 %al, 0\n br i1 %isa, label %chk, label %cont\n")
|
|
emit("chk:\n %h = call i32 @ludic_has(i32 %e, i32 %p)\n %hit = icmp ne i32 %h, 0\n br i1 %hit, label %hitb, label %cont\n")
|
|
emit("hitb:\n ret i32 %e\n")
|
|
emit("cont:\n %e1 = add i32 %e, 1\n br label %loop\n")
|
|
emit("none:\n ret i32 -1\n}\n\n")
|
|
}
|
|
|
|
# N5 — the drivable sim (NETWORKING-DESIGN §5). The per-frame phases the auto-loop
|
|
# runs are also exposed as callables, so a game that owns its `entry` loop can
|
|
# drive the simulation itself (for prediction/rollback, replay, headless tests, or
|
|
# AI). tick_fixed() runs the sim phases; tick_render() runs Render.
|
|
fn emit_tick_helpers() -> void {
|
|
emit("define void @L_tick_fixed() {\nentry:\n")
|
|
ll_t = 0; ll_lbl = 0
|
|
emit_calls_for_phase("Input")
|
|
emit_calls_for_phase("FixedUpdate")
|
|
emit_calls_for_phase("Update")
|
|
emit_calls_for_phase("LateUpdate")
|
|
emit(" ret void\n}\n\n")
|
|
emit("define void @L_tick_render() {\nentry:\n")
|
|
ll_t = 0; ll_lbl = 0
|
|
emit_calls_for_phase("Render")
|
|
emit(" ret void\n}\n\n")
|
|
}
|
|
|
|
# system functions + lifecycle hooks + the drivable tick helpers — shared by the
|
|
# auto-loop game (emit_game_main) and an entry-driven game that owns its own loop.
|
|
fn emit_game_defs() -> void {
|
|
var i = 0
|
|
while i < len(prog) { if prog[i].kind == N_SYS { emit_system_fn(prog[i]) }; i = i + 1 }
|
|
emit_despawn_hooks()
|
|
emit_scene_hooks()
|
|
emit_tick_helpers()
|
|
}
|
|
|
|
fn emit_game_main() -> void {
|
|
emit_game_defs()
|
|
|
|
emit("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n")
|
|
emit(" store i32 %argc, ptr @L_argc\n")
|
|
emit(" store ptr %argv, ptr @L_argv\n")
|
|
if (find_fn("rt_init") != null) { emit(" call void @fn_rt_init()\n") }
|
|
emit_calls_for_phase("Start")
|
|
# enter the start scene once, after boot: store its id and run its on-enter.
|
|
if len(g_scenes) > 0 {
|
|
emit(" store i32 "); emit(itoa(g_start_scene)); emit(", ptr @L_scene\n")
|
|
var si = 0
|
|
while si < len(g_scenes) {
|
|
if (g_scenes[si].ival == g_start_scene) { emit(" call void @scene_enter_"); emit(g_scenes[si].s); emit("()\n") }
|
|
si = si + 1
|
|
}
|
|
}
|
|
if (find_event("program_start") != null) { emit(" call void @ev_program_start()\n") } # EV1: @Public @OnStart
|
|
emit(" br label %loop\n")
|
|
emit("loop:\n")
|
|
let r = emit_bind("load i32, ptr @L_running")
|
|
let rc = emit_bind(`icmp ne i32 {r}, 0`)
|
|
if (find_fn("rt_running") != null) {
|
|
let pr = emit_bind("call i32 @fn_rt_running()")
|
|
let pc = emit_bind(`icmp ne i32 {pr}, 0`)
|
|
let go = emit_bind(`and i1 {rc}, {pc}`)
|
|
emit(" br i1 "); emit(go); emit(", label %body, label %done\n")
|
|
} else {
|
|
emit(" br i1 "); emit(rc); emit(", label %body, label %done\n")
|
|
}
|
|
emit("body:\n")
|
|
if (find_fn("rt_poll") != null) {
|
|
let k = emit_bind("call i32 @fn_rt_poll()")
|
|
emit(" store i32 "); emit(k); emit(", ptr @L_key\n")
|
|
}
|
|
emit_calls_for_phase("Input")
|
|
emit_calls_for_phase("FixedUpdate")
|
|
emit_calls_for_phase("Update")
|
|
emit_calls_for_phase("LateUpdate")
|
|
emit_calls_for_phase("Render")
|
|
emit(" br label %loop\n")
|
|
emit("done:\n")
|
|
if len(g_ondespawn) > 0 { emit(" call void @L_despawn_all(i32 2)\n") } # LC1: every survivor's @OnDespawn fires with reason Quit
|
|
emit_calls_for_phase("OnQuit") # @OnQuit shutdown hooks run once, before teardown
|
|
if (find_event("program_quit") != null) { emit(" call void @ev_program_quit()\n") } # EV1: @Public @OnQuit
|
|
if (find_fn("rt_shutdown") != null) { emit(" call void @fn_rt_shutdown()\n") }
|
|
emit(" ret i32 0\n}\n")
|
|
}
|