Every allocation the compiler emits goes through @lp_malloc/@lp_calloc/@lp_realloc/@lp_free, and a Ludic-level one first stores its site (function, file, line, kind) in @lp_site. Off, that is one load and a predictable branch (30 M allocations: 0.87-0.91 s against 0.87-0.90 s on leaks2). On (the default in a headless build, and windowed under R3D_DEV), tracking starts at the first frame on its own and judging once R3D_ALLOC_WARM frames in a row kept nothing (600) or R3D_ALLOC_WARM_MAX after (re)start; Mem.play()/Mem.rewarm() sends a load back to its warm-up. A judged frame that ends holding more than it began with is reported by site with its callers (the unwinder, taken only once judging) and fails the run with exit 86 (R3D_ALLOC_FENCE=off|count|warn|fail). R3D_ALLOC_CENSUS writes the totals and top sites at exit. The build's defaults are --fence=, --fence-warm=, --fence-census= or a fence line in the program's package.ludic; the environment overrides them. The runtime is IR (emit_fence_ir.ludic, generated from a template); tracking is a side table in one calloc'd region, so no block carries a header and pointers crossing to natives stay safe. Examples alloc_fence, alloc_fence_leak and alloc_fence_auto with cases in ludic-dev test; reseeded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
762 lines
38 KiB
Text
762 lines
38 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.
|
|
|
|
function emit_system_fn(sys: Node) -> void {
|
|
det_enter(sys.s)
|
|
fence_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_<d.s> (skipped while the handler is disabled).
|
|
function emit_call_one(d: Node) -> void {
|
|
let he = emit_bind(`load i32, ptr @HE_{d.s}`)
|
|
var hc = emit_bind(`icmp ne i32 {he}, 0`)
|
|
# a handler in a toggled layer also gates on its layer's @LE_ flag (byte-identical
|
|
# for layers that are never enabled/disabled, since d.b is only read when managed)
|
|
if (d.b != null) and is_toggled_layer(d.b.s) {
|
|
let le = emit_bind(`load i32, ptr @LE_{d.b.s}`)
|
|
let lc = emit_bind(`icmp ne i32 {le}, 0`)
|
|
hc = emit_bind(`and i1 {hc}, {lc}`)
|
|
}
|
|
# N5: an @Server handler (d.ival==1) runs only on the authority (@L_role==1).
|
|
# Unmarked and @Predicted handlers run on every peer. Offline @L_role defaults to
|
|
# 1, so the guard collapses to "run here" and a non-networked build is unchanged.
|
|
if d.ival == 1 {
|
|
let rv = emit_bind("load i32, ptr @L_role")
|
|
let rc = emit_bind(`icmp eq i32 {rv}, 1`)
|
|
hc = emit_bind(`and i1 {hc}, {rc}`)
|
|
}
|
|
let run = lbl("hrun"); let skip = lbl("hskip")
|
|
emit(" br i1 "); emit(hc); emit(", label %"); emit(run); emit(", label %"); emit(skip); emit("\n")
|
|
emit(run); emit(":\n call void @sys_"); emit(d.s); emit("()\n")
|
|
emit(" br label %"); emit(skip); emit("\n"); emit(skip); emit(":\n")
|
|
}
|
|
|
|
# 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 <fn>` drops the tick
|
|
if (find_comp(comp) != null) and (find_fn(fn) != null) {
|
|
emit(` call void {fn_sym(fn)}()\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_sym("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_sym("fx_tick") + "()\n") } # Fx.* age and move
|
|
}
|
|
if (phase == "Render") {
|
|
if g_uses_fx and (find_fn("fx_draw") != null) { emit(" call void " + fn_sym("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")
|
|
let eb1 = ecs_base("L_alive")
|
|
emit(" %ap = getelementptr inbounds i32, ptr "); emit(eb1); emit(", 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")
|
|
let eb2 = ecs_base(`H_{d.s}`)
|
|
emit(" %h"); emit(sk); emit(" = getelementptr inbounds i8, ptr "); emit(eb2); emit(", 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")
|
|
let eb3 = ecs_base(`S_{d.s}`)
|
|
emit(" %s"); emit(sk); emit(" = getelementptr inbounds %Cmp_"); emit(d.s); emit(", ptr "); emit(eb3); emit(", 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_<Name>(): 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_<name> 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
|
|
if len(g_act_names) > 0 { emit(` call void {fn_sym("drain_actions")}()\n`) } # 0.R: this phase's actions, reduced
|
|
}
|
|
|
|
# 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_sym("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_<Name> /
|
|
# @scene_exit_<Name>, called at the transition point (and enter at boot for the
|
|
# start scene). Emitted for every scene, empty body when the hook is absent.
|
|
function emit_scene_fn(name: pointer, kind: pointer, body: Node, menu: pointer) -> void {
|
|
fence_enter(`scene {name}`)
|
|
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_sym("input_cursor_mode") + "(i32 0)\n") }
|
|
emit(" call void " + fn_sym("rt_ui_open") + "(i32 "); emit(itoa(ui_index_of("UI_" + menu))); emit(")\n")
|
|
}
|
|
if (menu != null) and (kind == "exit") { emit(" call void " + fn_sym("rt_ui_close") + "()\n") }
|
|
if (body != null) { emit_block(body) }
|
|
# EV1: a `public` scene fires scene_<S>_enter / scene_<S>_exit after its block
|
|
let sev = `scene_{name}_{kind}`
|
|
if (not g_term) and (find_event(sev) != null) { emit(" call void @ev_"); emit(sev); emit("()\n") }
|
|
if not g_term { emit(" br label %ret\n") }
|
|
emit("ret:\n ret void\n")
|
|
code = saved
|
|
emit("define void @scene_"); emit(kind); emit("_"); emit(name); emit("() {\nentry:\n")
|
|
emit(buf_str(falloc))
|
|
emit(buf_str(fbody))
|
|
emit("}\n\n")
|
|
}
|
|
|
|
function emit_scene_hooks() -> void {
|
|
var i = 0
|
|
while i < len(g_scenes) {
|
|
let sc = g_scenes[i]
|
|
g_cur_scene = sc
|
|
emit_scene_fn(sc.s, "enter", sc.a, 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_<Model>(entity, reason)`
|
|
# functions that bind the model's properties and run the body — dispatched by
|
|
# kind at `despawn`. LC1: the `i32 %reason` parameter carries an EndReason; when
|
|
# the hook declared `reason: r`, `r` is bound as an int local reading it.
|
|
function emit_despawn_hooks() -> void {
|
|
var i = 0
|
|
while i < len(g_ondespawn) {
|
|
let hk = g_ondespawn[i]
|
|
let model = find_arch(hk.s)
|
|
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
|
|
ret_ty = "void"
|
|
let fbody = buf_new()
|
|
falloc = buf_new()
|
|
let saved = code
|
|
code = fbody
|
|
if (hk.ty != null) { # bind the reason: r name to %reason
|
|
let rslot = emit_alloca("i32")
|
|
emit(" store i32 %reason, ptr "); emit(rslot); emit("\n")
|
|
loc_push(hk.ty, rslot, "int")
|
|
}
|
|
emit_bind_props(model, "%e")
|
|
emit_block(hk.a)
|
|
if not g_term { emit(" br label %ret\n") }
|
|
emit("ret:\n ret void\n")
|
|
code = saved
|
|
emit("define void @on_despawn_"); emit(hk.s); emit("(i32 %e, i32 %reason) {\nentry:\n")
|
|
emit(buf_str(falloc))
|
|
emit(buf_str(fbody))
|
|
emit("}\n\n")
|
|
i += 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)
|
|
let eb4 = ecs_base("L_kind")
|
|
emit(" %kp = getelementptr inbounds i32, ptr "); emit(eb4); emit(", 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")
|
|
let eb5 = ecs_base("L_alive")
|
|
emit(" %ap = getelementptr inbounds i32, ptr "); emit(eb5); emit(", 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")
|
|
let eb6 = ecs_base("L_kind")
|
|
emit(" %kp = getelementptr inbounds i32, ptr "); emit(eb6); emit(", 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_<E>(payload…)` function whose
|
|
# body is (1) its `@On(E)` listeners concatenated in declaration order — the
|
|
# closed, compile-time half — then (2) a loop over a runtime listener array, the
|
|
# open half a mod in another language joins through the C ABI. The payload fields
|
|
# arrive as params (%p0, %p1, …), bound by name so a listener body reads them bare
|
|
# (like a query/hook binding). Emitted only when g_events is non-empty, so an
|
|
# event-free program is byte-for-byte unchanged.
|
|
#
|
|
# The runtime half is the deliberate opt-in exception to "no dispatch tables":
|
|
# %Ev_<E> — the POD payload struct passed by pointer to foreign listeners
|
|
# @evL_<E> — a fixed-capacity [16 x ptr] array of foreign callbacks
|
|
# @evN_<E> — how many are registered (registration order = dispatch order)
|
|
# @ludic_on_<E>(ptr cb) -> i32 — the C ABI: a mod appends its callback
|
|
# A native Ludic listener costs a direct call; a foreign one costs one indirect
|
|
# call. With no foreign listeners registered the loop runs zero times (one branch).
|
|
const EV_CAP: int = 16
|
|
# EV6 — re-entrant emit is bounded: a listener may `emit` another event, but the
|
|
# nesting is capped so an event cycle traps as an early return instead of hanging
|
|
# the frame. @ev_depth counts the live dispatch nesting; past the cap a dispatch
|
|
# returns immediately (a cancellable event returns "not cancelled").
|
|
const EV_DEPTH_CAP: int = 32
|
|
|
|
function emit_event_fns() -> void {
|
|
emith("@ev_depth = global i32 0\n") # EV6 re-entrancy counter
|
|
var e = 0
|
|
while e < len(g_events) {
|
|
let ev = g_events[e]
|
|
let en = ev.s
|
|
let cap = itoa(EV_CAP)
|
|
|
|
# --- module-level: payload struct + the foreign listener registry (into head)
|
|
# A cancellable event's payload carries a trailing i32 `cancelled` flag that a
|
|
# listener (native or foreign) can set; the caller reads it back.
|
|
emith("%Ev_"); emith(en); emith(" = type { ")
|
|
var t = 0
|
|
while t < len(ev.kids) {
|
|
if t > 0 { emith(", ") }
|
|
emith(llty(ev.kids[t].ty))
|
|
t += 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")
|
|
emit(" %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
|
|
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)
|
|
fence_enter(`@On({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")
|
|
}
|