# 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 { det_enter(sys.s) 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") g_det_ctx = "" } # 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") } # Engine-owned systems (#43/#47): systems the compiler injects into the frame # loop over a component the game merely declares and carries — the ECS hook the # animation and lighting follow-ups both stand on. Each entry is (component, # runtime fn, phase); the call is emitted only when the game declares that # component (so systems.ludic was spliced and the fn exists). They run *after* # the game's own handlers for the phase, so gameplay this frame is already # settled when the engine advances animation / accumulates light. Keep the # component list in sync with uses_engine_systems (emit_ecs.ludic). function emit_one_engine_system(comp: pointer, fn: pointer) -> void { if is_system_disabled(fn) { return } # lever 5 (#57): `disable system ` drops the tick if (find_comp(comp) != null) and (find_fn(fn) != null) { emit(" call void @fn_"); emit(fn); emit("()\n") } } function emit_engine_systems_for_phase(phase: pointer) -> void { # #62: registry-driven — core entries (seeded in parse_program) keep their # historical per-phase order, and packages that appended via @EngineSystem run # after them in the same phase. Each call is component-gated (no-op when the # component is absent), so an unused registration is byte-identical. var i = 0 while i < len(g_esys_comp) { if (g_esys_phase[i] == phase) { emit_one_engine_system(g_esys_comp[i], g_esys_fn[i]) } i += 1 } if (phase == "Update") { # Tween.* fluent handles (#48): advanced each Update tick when the game uses # them (gated on g_uses_tween_rt rather than a declared component). if g_uses_tween_rt and (find_fn("esys_tween") != null) { emit(" call void @fn_esys_tween()\n") } if has_countdowns() { emit(" call void @L_countdowns()\n") } # `countdown` fields tick toward 0 if g_uses_fx and (find_fn("fx_tick") != null) { emit(" call void @fn_fx_tick()\n") } # Fx.* age and move } if (phase == "Render") { if g_uses_fx and (find_fn("fx_draw") != null) { emit(" call void @fn_fx_draw()\n") } # Fx.* draw after the sprites } } # @L_countdowns(): every `countdown` field of every live entity that carries the # component steps toward 0 once per Update (never below it). The timer idiom — # "frames left" fields counted down by hand in a handler — becomes a field type. function emit_countdown_system() -> void { let me = itoa(MAX_ENT) emit("define void @L_countdowns() {\nentry:\n") emit(" %n = load i32, ptr @L_entc\n br label %loop\nloop:\n") emit(" %i = phi i32 [ 0, %entry ], [ %i1, %next ]\n") emit(" %go = icmp slt i32 %i, %n\n br i1 %go, label %body, label %done\nbody:\n") emit(" %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %i\n") emit(" %al = load i32, ptr %ap\n %alive = icmp ne i32 %al, 0\n br i1 %alive, label %c0, label %next\n") var k = 0 var i = 0 while i < len(prog) { let d = prog[i] if d.kind == N_COMP { var any = false var f = 0 while f < len(d.kids) { if (d.kids[f].ty == "countdown") { any = true }; f += 1 } if any { let sk = itoa(k); let nk = itoa(k + 1) emit("c"); emit(sk); emit(":\n") emit(" %h"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(d.s); emit(", i32 0, i32 %i\n") emit(" %hv"); emit(sk); emit(" = load i8, ptr %h"); emit(sk); emit("\n") emit(" %has"); emit(sk); emit(" = icmp ne i8 %hv"); emit(sk); emit(", 0\n") emit(" br i1 %has"); emit(sk); emit(", label %t"); emit(sk); emit(", label %c"); emit(nk); emit("\n") emit("t"); emit(sk); emit(":\n") emit(" %s"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(d.s); emit("], ptr @S_"); emit(d.s); emit(", i32 0, i32 %i\n") f = 0 while f < len(d.kids) { if (d.kids[f].ty == "countdown") { let fk = sk + ("_") + itoa(f) emit(" %fa"); emit(fk); emit(" = getelementptr inbounds %Cmp_"); emit(d.s); emit(", ptr %s"); emit(sk); emit(", i32 0, i32 "); emit(itoa(f)); emit("\n") emit(" %fv"); emit(fk); emit(" = load i32, ptr %fa"); emit(fk); emit("\n") emit(" %fp"); emit(fk); emit(" = icmp sgt i32 %fv"); emit(fk); emit(", 0\n") emit(" %fd"); emit(fk); emit(" = sub i32 %fv"); emit(fk); emit(", 1\n") emit(" %fn"); emit(fk); emit(" = select i1 %fp"); emit(fk); emit(", i32 %fd"); emit(fk); emit(", i32 %fv"); emit(fk); emit("\n") emit(" store i32 %fn"); emit(fk); emit(", ptr %fa"); emit(fk); emit("\n") } f += 1 } emit(" br label %c"); emit(nk); emit("\n") k += 1 } } i += 1 } emit("c"); emit(itoa(k)); emit(":\n br label %next\n") emit("next:\n %i1 = add i32 %i, 1\n br label %loop\ndone:\n ret void\n}\n\n") } # @L_prefab_(): spawn one prefab with its presets; @L_spawn_prefab(name): # the runtime dispatch by name that Prefab.spawn(name:) calls (-1 for no such prefab). function emit_prefab_fn(pre: Node) -> void { ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0 g_cur_scene = null ret_ty = "int" let fbody = buf_new() falloc = buf_new() let saved = code code = fbody let sp = node(S_SPAWN); sp.s = pre.s let e = emit_spawn(sp) emit(" ret i32 "); emit(e); emit("\n") code = saved emit("define i32 @L_prefab_"); emit(pre.s); emit("() {\nentry:\n") emit(buf_str(falloc)) emit(buf_str(fbody)) emit("}\n\n") } function emit_prefab_fns() -> void { var i = 0 while i < len(prog) { if prog[i].kind == N_PREFAB { emit_prefab_fn(prog[i]) }; i += 1 } g_uses_str = true ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0 let fbody = buf_new() falloc = buf_new() let saved = code code = fbody i = 0 while i < len(prog) { let d = prog[i] if d.kind == N_PREFAB { let lit = node(E_STR); lit.s = d.s let sv = emit_expr(lit) let same = emit_bind(`call i32 @lp_str_eq(ptr %name, ptr {sv.code})`) let hit = emit_bind(`icmp ne i32 {same}, 0`) let yes = lbl("pf"); let no = lbl("pfn") emit(" br i1 "); emit(hit); emit(", label %"); emit(yes); emit(", label %"); emit(no); emit("\n") emit(yes); emit(":\n") let r = emit_bind(`call i32 @L_prefab_{d.s}()`) emit(" ret i32 "); emit(r); emit("\n") emit(no); emit(":\n") } i += 1 } emit(" ret i32 -1\n") code = saved emit("define i32 @L_spawn_prefab(ptr %name) {\nentry:\n") emit(buf_str(falloc)) emit(buf_str(fbody)) emit("}\n\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. Engine-owned systems for the phase run last. # --- dynamic (mod-registered) systems, issue #64 ----------------------------- # A prebuilt binary module registers a `void()` system into a phase through the C # ABI @ludic_register_system(fn, phase); the host's frame loop dispatches every # registered system for a phase after its own handlers. This mirrors the EV6 # foreign event-listener array, and is the systems analogue of @ludic_register_prop # (dynamic components). Emitted for every ECS program (the moddability baseline); # with nothing registered the dispatch loop runs zero times, so a game that hosts # no modules produces byte-identical output. const SYS_CAP: int = 64 # stable phase ids shared with modules (see docs — a module passes these to # @ludic_register_system). Keep in sync with the frame loop's phase order. function phase_id(phase: pointer) -> int { if phase == "Input" { return 0 } if phase == "FixedUpdate" { return 1 } if phase == "Update" { return 2 } if phase == "LateUpdate" { return 3 } if phase == "Render" { return 4 } if phase == "Start" { return 5 } if phase == "OnQuit" { return 6 } # the @OnQuit hook's internal slot, never written as `phase OnQuit` if phase == "Overlay" { return 7 } # HUD/UI pass after the engine drew sprites perr(`unknown phase '{phase}'`) # the parser rejects these; a desugaring bug if we get here return 7 } # the phases a handler may declare — the frame loop's buckets, in the order the # frame runs them (Start once, then Input … Overlay every tick) function is_phase_name(name: pointer) -> bool { if name == "Start" or name == "Input" or name == "FixedUpdate" or name == "Update" { return true } if name == "LateUpdate" or name == "Render" or name == "Overlay" { return true } return false } # --- binary-module glue (issue #64) ------------------------------------------ # A module compiled with --emit-module carries no main and no world table (the # consumer owns them). It references the host's reflection ABI, so we (1) declare # every host-ABI symbol it may call — resolved from the host image at dylib load # via -undefined dynamic_lookup — and (2) emit a load-time constructor that # registers the module's @System functions and runs its `module_init` (where it # registers its dynamic components via world_register_prop). Ludic source cannot # take a function's address, so the compiler supplies @fn_ here. function emit_module_glue() -> void { emith("declare i32 @ludic_register_prop(ptr, i32)\n") emith("declare i32 @ludic_register_system(ptr, i32)\n") emith("declare i64 @ludic_get(i32, i32, i32)\n") emith("declare void @ludic_set(i32, i32, i32, i64)\n") emith("declare i32 @ludic_has(i32, i32)\n") emith("declare void @ludic_attach_dyn(i32, i32)\n") emith("declare void @ludic_detach_dyn(i32, i32)\n") emith("declare i32 @ludic_prop_id(ptr)\n") emith("declare i32 @ludic_field_id(i32, ptr)\n") emith("declare i32 @ludic_entity_count()\n") emith("declare i32 @ludic_kind(i32)\n") emith("declare i32 @ludic_model_id(ptr)\n") emith("declare i32 @ludic_spawn(i32)\n") emith("declare i32 @ludic_query_next(i32, i32)\n") emith("declare i32 @ludic_prop_count()\n") emith("declare ptr @ludic_prop_name(i32)\n") emith("declare i32 @ludic_field_count(i32)\n") emith("declare ptr @ludic_field_name(i32, i32)\n") emith("declare ptr @ludic_field_type(i32, i32)\n") emit("define void @__ludic_mod_init() {\nentry:\n") if (find_fn("module_init") != null) { emit(" call void @fn_module_init()\n") } # component registration + setup var i = 0 while i < len(g_mod_sys_fn) { emit(" call i32 @ludic_register_system(ptr @fn_"); emit(g_mod_sys_fn[i]); emit(", i32 "); emit(itoa(phase_id(g_mod_sys_phase[i]))); emit(")\n") i += 1 } emit(" ret void\n}\n\n") # run @__ludic_mod_init at image load (dyld runs constructors before main) emith("@llvm.global_ctors = appending global [1 x { i32, ptr, ptr }] [{ i32, ptr, ptr } { i32 65535, ptr @__ludic_mod_init, ptr null }]\n") } # the registry globals + the @ludic_register_system C-ABI entry (append fn+phase) function emit_system_registry() -> void { let cap = itoa(SYS_CAP) emith("@sysreg_fn = global ["); emith(cap); emith(" x ptr] zeroinitializer\n") emith("@sysreg_phase = global ["); emith(cap); emith(" x i32] zeroinitializer\n") emith("@sysreg_count = global i32 0\n") emit("define i32 @ludic_register_system(ptr %fn, i32 %phase) {\nentry:\n") emit(" %n = load i32, ptr @sysreg_count\n") emit(" %ok = icmp slt i32 %n, "); emit(cap); emit("\n") emit(" br i1 %ok, label %add, label %drop\n") emit("add:\n") emit(" %fp = getelementptr inbounds ["); emit(cap); emit(" x ptr], ptr @sysreg_fn, i32 0, i32 %n\n") emit(" store ptr %fn, ptr %fp\n") emit(" %pp = getelementptr inbounds ["); emit(cap); emit(" x i32], ptr @sysreg_phase, i32 0, i32 %n\n") emit(" store i32 %phase, ptr %pp\n") emit(" %n1 = add i32 %n, 1\n store i32 %n1, ptr @sysreg_count\n ret i32 %n\n") emit("drop:\n ret i32 -1\n}\n\n") } # the per-phase dispatch loop: call every registered system whose phase matches. # phi-based (no alloca — this is emitted inline into @main / the tick helpers, # which have no falloc entry block). Names/labels come from nreg()/lbl() so the # five per-frame calls in one function never collide. function emit_dyn_systems_for_phase(phase: pointer) -> void { let cap = itoa(SYS_CAP) let pid = itoa(phase_id(phase)) let iv = nreg(); let inext = nreg() let pre = lbl("dsp"); let head = lbl("dsh"); let body = lbl("dsb"); let doo = lbl("dsdo"); let cont = lbl("dsc"); let done = lbl("dsn") emit(" br label %"); emit(pre); emit("\n") emit(pre); emit(":\n br label %"); emit(head); emit("\n") emit(head); emit(":\n") emit(" "); emit(iv); emit(" = phi i32 [ 0, %"); emit(pre); emit(" ], [ "); emit(inext); emit(", %"); emit(cont); emit(" ]\n") let nn = emit_bind("load i32, ptr @sysreg_count") let go = emit_bind(`icmp slt i32 {iv}, {nn}`) emit(" br i1 "); emit(go); emit(", label %"); emit(body); emit(", label %"); emit(done); emit("\n") emit(body); emit(":\n") let pp = nreg(); emit(" "); emit(pp); emit(" = getelementptr inbounds ["); emit(cap); emit(" x i32], ptr @sysreg_phase, i32 0, i32 "); emit(iv); emit("\n") let ph = emit_bind(`load i32, ptr {pp}`) let m = emit_bind(`icmp eq i32 {ph}, {pid}`) emit(" br i1 "); emit(m); emit(", label %"); emit(doo); emit(", label %"); emit(cont); emit("\n") emit(doo); emit(":\n") let fp = nreg(); emit(" "); emit(fp); emit(" = getelementptr inbounds ["); emit(cap); emit(" x ptr], ptr @sysreg_fn, i32 0, i32 "); emit(iv); emit("\n") let fn = emit_bind(`load ptr, ptr {fp}`) emit(" call void "); emit(fn); emit("()\n") emit(" br label %"); emit(cont); emit("\n") emit(cont); emit(":\n "); emit(inext); emit(" = add i32 "); emit(iv); emit(", 1\n br label %"); emit(head); emit("\n") emit(done); emit(":\n") } 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 += 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 += 1 } if has_sc { 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 += 1 } } emit_engine_systems_for_phase(phase) # engine-owned systems run after every user handler emit_dyn_systems_for_phase(phase) # #64: mod-registered systems run last if (phase == "Overlay") { emit_scene_menu_render() } # a scene's `shows` menu paints last } # the menu of the live `scene X shows Menu`, drawn on top of everything else function emit_scene_menu_render() -> void { var any = false var i = 0 while i < len(g_scenes) { if (g_scenes[i].ty != null) { any = true }; i += 1 } if not any { return } let cs = emit_bind("load i32, ptr @L_scene") i = 0 while i < len(g_scenes) { let sc = g_scenes[i] if (sc.ty != null) { let ce = emit_bind(`icmp eq i32 {cs}, {itoa(sc.ival)}`) let run = lbl("mrun"); let skip = lbl("mskip") emit(" br i1 "); emit(ce); emit(", label %"); emit(run); emit(", label %"); emit(skip); emit("\n") emit(run); emit(":\n call void @fn_rt_ui_render()\n") emit(" br label %"); emit(skip); emit("\n"); emit(skip); emit(":\n") } 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, menu: pointer) -> 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 # `scene X shows Menu`: a free cursor and the menu open on enter, closed on exit if (menu != null) and (kind == "enter") { if (find_fn("input_cursor_mode") != null) { emit(" call void @fn_input_cursor_mode(i32 0)\n") } emit(" call void @fn_rt_ui_open(i32 "); emit(itoa(ui_index_of("UI_" + menu))); emit(")\n") } if (menu != null) and (kind == "exit") { emit(" call void @fn_rt_ui_close()\n") } 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, sc.ty) emit_scene_fn(sc.s, "exit", sc.b, sc.ty) i += 1 } g_cur_scene = null if len(g_scenes) > 0 { emit_scene_leave_fn() } # a program without scenes has no @L_scene } # @L_scene_leave(): run the on-exit hook of whichever scene is live — what a # `become` outside any scene's own handlers (a global handler, an @On listener, # a plain function) calls, since it cannot know the leaving scene at compile time. function emit_scene_leave_fn() -> void { emit("define void @L_scene_leave() {\nentry:\n") emit(" %s = load i32, ptr @L_scene\n") var i = 0 while i < len(g_scenes) { let sc = g_scenes[i] let k = itoa(i) emit(" %is"); emit(k); emit(" = icmp eq i32 %s, "); emit(itoa(sc.ival)); emit("\n") emit(" br i1 %is"); emit(k); emit(", label %leave"); emit(k); emit(", label %next"); emit(k); emit("\n") emit("leave"); emit(k); emit(":\n call void @scene_exit_"); emit(sc.s); emit("()\n ret void\n") emit("next"); emit(k); emit(":\n") i += 1 } emit(" ret void\n}\n\n") } # @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 += 1 } emit_despawn_all_fn() } # #84 — @fn_world_despawn(e): the runtime-callable despawn, used by the # world-bounds kill policy and World.despawn(e). Same per-kind @OnDespawn dispatch # as the `despawn` statement (emit_despawn) but reading a runtime %e, then the # entity-scoped-listener sweep and the slot free. Emitted from emit_program's tail # only when a world_despawn call was emitted (g_uses_world_despawn), after all uses # are seen — so a program that never despawns by id is byte-identical. function emit_world_despawn_fn() -> void { emit("define void @fn_world_despawn(i32 %e) {\nentry:\n") if len(g_ondespawn) > 0 { let me = itoa(MAX_ENT) emit(" %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\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 %e, i32 0)\n") # reason = Despawned let dev = `model_{mname}_despawn` if (find_event(dev) != null) { emit(" call void @ev_"); emit(dev); emit("(i32 %e, i32 0)\n") } emit(" br label %next"); emit(si); emit("\n") emit("next"); emit(si); emit(":\n") i += 1 } } if len(g_events) > 0 { emit(" call void @ludic_sweep_entity(i32 %e)\n") } # EV5: drop entity-scoped listeners emit(" call void @L_free_entity(i32 %e)\n") emit(" ret void\n}\n\n") } # 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 += 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 += 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 g_cur_scene = null # a listener belongs to no scene: `become` leaves the live one 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 += 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) { # a `return` ends this listener, not the dispatch: the next one and the foreign ones still run nloc = base g_term = false let next = lbl("evnext") g_ret_label = next g_err_file = g_onlisten[i].file vis_check(ev, en) emit_block(g_onlisten[i].a) g_ret_label = "ret" if not g_term { emit(` br label %{next}\n`) } emit(`{next}:\n`) g_term = false } 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 += 1 } emit(") {\nentry:\n") emit(buf_str(falloc)) emit(buf_str(fbody)) emit("}\n\n") 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(" %swinc"); emit(sk); emit(" = add i32 %swi"); emit(sk); emit(", 1\n store i32 %swinc"); 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 += 1 } emit("}\n\n") }