# 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_ (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_ / # @scene_exit_, 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__enter / scene__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_(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_(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_ — the POD payload struct passed by pointer to foreign listeners # @evL_ — a fixed-capacity [16 x ptr] array of foreign callbacks # @evN_ — how many are registered (registration order = dispatch order) # @ludic_on_(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_(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_(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_(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_(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. function 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_ that reuses the compiler's own spawn lowering (alloc, kind, # component defaults, @OnSpawn, and the model__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. function 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. function 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() } function 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") let fcur = emit_bind("load i32, ptr @L_frame") # Time.frame(): count completed frames let fnext = emit_bind(`add i32 {fcur}, 1`) emit(" store i32 "); emit(fnext); emit(", ptr @L_frame\n") 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") }