ludic/selfhost/backend/game/emit_world.ludic
Orkuncakilkaya a8d54e9878 fence (25.1): every allocation goes through the fence - sites, frame judging, census, callers
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>
2026-09-28 15:35:29 +03:00

534 lines
29 KiB
Text

# emit_world.ludic — the generated reflection ABI (the "world table" a mod reads/writes entity state by name through), the per-frame tick helpers, and the synthesized @main. Split out of emit_game.ludic (concern: runtime world/entry synthesis, vs. emit_game.ludic's system/scene/event lowering).
# EV2 — the world table: a generated reflection ABI so a mod reads and writes
# entity state *by name*, without having compiled against the game. This is the
# "game table" that lets a modding layer be ported in. Generated from the
# 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)
# 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 += 1 }; 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 += 1
}
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 += 1
}
emit(" ret i32 -1\n")
emit("pn"); emit(sk); emit(":\n")
k += 1
}
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")
let eb1 = ecs_base(`S_{cn}`)
emit(" %s"); emit(sk); emit(" = getelementptr inbounds %Cmp_"); emit(cn); emit(", ptr "); emit(eb1); emit(", 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 += 1
}
emit(" ret i64 0\n")
emit("gn"); emit(sk); emit(":\n")
k += 1
}
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")
let eb2 = ecs_base(`S_{cn}`)
emit(" %s"); emit(sk); emit(" = getelementptr inbounds %Cmp_"); emit(cn); emit(", ptr "); emit(eb2); emit(", 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 += 1
}
emit(" ret void\n")
emit("gn"); emit(sk); emit(":\n")
k += 1
}
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")
let eb3 = ecs_base(`H_{cn}`)
emit(" %hp"); emit(sk); emit(" = getelementptr inbounds i8, ptr "); emit(eb3); emit(", 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 += 1
}
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 %rcap = load i32, ptr @L_cap\n %nf4 = mul i32 %nfields, 4\n %sz = mul i32 %nf4, %rcap\n %szl = sext i32 %sz to i64\n")
emit(" %buf = call ptr @lp_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(" %hcap = sext i32 %rcap to i64\n %hbuf = call ptr @lp_malloc(i64 %hcap)\n call ptr @memset(ptr %hbuf, i32 0, i64 %hcap)\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")
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 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 += 1
}
i += 1
}
emit(" ret i32 -1\n}\n\n")
# ludic_spawn(model_id) -> entity — a mod creates a new entity. Each model gets a
# @ludic_spawn_<M> that reuses the compiler's own spawn lowering (alloc, kind,
# component defaults, @OnSpawn, and the model_<M>_spawn event), so a mod-spawned
# entity is indistinguishable from one born in source. A dispatcher routes the id.
i = 0
while i < len(prog) {
if prog[i].kind == N_ARCH {
let m = prog[i].s
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
ret_ty = "int"
let fbody = buf_new()
falloc = buf_new()
let saved = code
code = fbody
let syn = node(S_SPAWN); syn.s = m # a defaults-only spawn of model m
let se = emit_spawn(syn)
emit(" ret i32 "); emit(se); emit("\n")
code = saved
emit("define i32 @ludic_spawn_"); emit(m); emit("() {\nentry:\n")
emit(buf_str(falloc))
emit(buf_str(fbody))
emit("}\n\n")
}
i += 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 += 1
}
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")
let eb5 = ecs_base("L_alive")
emit(" %ap = getelementptr inbounds i32, ptr "); emit(eb5); emit(", 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")
emit_world_reflect_enum()
}
# EV8 — schema enumeration: walk the property/field metadata by index, not just
# by name. Powers the Reflect.* namespace (auto-serialization, debug inspectors)
# so a mod can list every component and field without knowing them up front.
# Compile-time props take ids 0..NC-1; ids >= NC are mod-registered (dynamic),
# whose field names are unknown (i32 fields) so they report "" / "int".
function emit_world_reflect_enum() -> void {
let empty = emit_str_const("")
let tint = emit_str_const("int")
# count the compile-time components (== the first dynamic prop id)
var ncomp = 0
var i = 0
while i < len(prog) { if prog[i].kind == N_COMP { ncomp += 1 }; i += 1 }
let NC = itoa(ncomp)
# ludic_prop_count() -> total property count (compile-time + mod-registered)
emit("define i32 @ludic_prop_count() {\nentry:\n")
emit(" %d = load i32, ptr @dyn_count\n %n = add i32 %d, "); emit(NC); emit("\n ret i32 %n\n}\n\n")
# ludic_prop_name(i) -> the i-th property's name (or "" if out of range)
emit("define ptr @ludic_prop_name(i32 %i) {\nentry:\n")
var k = 0; 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(" %pe"); emit(sk); emit(" = icmp eq i32 %i, "); emit(sk); emit("\n")
emit(" br i1 %pe"); emit(sk); emit(", label %ph"); emit(sk); emit(", label %pn"); emit(sk); emit("\n")
emit("ph"); emit(sk); emit(":\n ret ptr "); emit(sc); emit("\n")
emit("pn"); emit(sk); emit(":\n")
k += 1
}
i += 1
}
emit(" %dyn = sub i32 %i, "); emit(NC); emit("\n %dc = load i32, ptr @dyn_count\n")
emit(" %ib = icmp uge i32 %dyn, %dc\n br i1 %ib, label %pbad, label %pdyn\n")
emit("pdyn:\n %np = getelementptr inbounds [32 x ptr], ptr @dynName, i32 0, i32 %dyn\n %nm = load ptr, ptr %np\n ret ptr %nm\n")
emit("pbad:\n ret ptr "); emit(empty); emit("\n}\n\n")
# ludic_field_count(prop) -> the number of fields of a property
emit("define i32 @ludic_field_count(i32 %p) {\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(" %fce"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n")
emit(" br i1 %fce"); emit(sk); emit(", label %fch"); emit(sk); emit(", label %fcn"); emit(sk); emit("\n")
emit("fch"); emit(sk); emit(":\n ret i32 "); emit(itoa(len(c.kids))); emit("\n")
emit("fcn"); emit(sk); emit(":\n")
k += 1
}
i += 1
}
emit(" %dyn = sub i32 %p, "); emit(NC); emit("\n %dc = load i32, ptr @dyn_count\n")
emit(" %ib = icmp uge i32 %dyn, %dc\n br i1 %ib, label %fcbad, label %fcdyn\n")
emit("fcdyn:\n %fp = getelementptr inbounds [32 x i32], ptr @dynF, i32 0, i32 %dyn\n %fn = load i32, ptr %fp\n ret i32 %fn\n")
emit("fcbad:\n ret i32 0\n}\n\n")
# ludic_field_name(prop, f) -> the f-th field's name of a property (or "")
emit("define ptr @ludic_field_name(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 sk = itoa(k)
emit(" %fne"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n")
emit(" br i1 %fne"); emit(sk); emit(", label %fnk"); emit(sk); emit(", label %fnn"); emit(sk); emit("\n")
emit("fnk"); 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(" %fnfe"); emit(fk); emit(" = icmp eq i32 %f, "); emit(itoa(f)); emit("\n")
emit(" br i1 %fnfe"); emit(fk); emit(", label %fnfh"); emit(fk); emit(", label %fnfn"); emit(fk); emit("\n")
emit("fnfh"); emit(fk); emit(":\n ret ptr "); emit(fc); emit("\n")
emit("fnfn"); emit(fk); emit(":\n")
f += 1
}
emit(" ret ptr "); emit(empty); emit("\n")
emit("fnn"); emit(sk); emit(":\n")
k += 1
}
i += 1
}
emit(" ret ptr "); emit(empty); emit("\n}\n\n")
# ludic_field_type(prop, f) -> the f-th field's type name (or "int" fallback)
emit("define ptr @ludic_field_type(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 sk = itoa(k)
emit(" %fte"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n")
emit(" br i1 %fte"); emit(sk); emit(", label %ftk"); emit(sk); emit(", label %ftn"); emit(sk); emit("\n")
emit("ftk"); emit(sk); emit(":\n")
var f = 0
while f < len(c.kids) {
let tc = emit_str_const(c.kids[f].ty); let fk = `{sk}_{itoa(f)}`
emit(" %ftfe"); emit(fk); emit(" = icmp eq i32 %f, "); emit(itoa(f)); emit("\n")
emit(" br i1 %ftfe"); emit(fk); emit(", label %ftfh"); emit(fk); emit(", label %ftfn"); emit(fk); emit("\n")
emit("ftfh"); emit(fk); emit(":\n ret ptr "); emit(tc); emit("\n")
emit("ftfn"); emit(fk); emit(":\n")
f += 1
}
emit(" ret ptr "); emit(tint); emit("\n")
emit("ftn"); emit(sk); emit(":\n")
k += 1
}
i += 1
}
emit(" ret ptr "); emit(tint); emit("\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_calls_for_phase("Overlay")
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 {
emit_system_registry() # #64: @ludic_register_system + the registry globals
var i = 0
while i < len(prog) { if prog[i].kind == N_SYS { emit_system_fn(prog[i]) }; i += 1 }
emit_despawn_hooks()
emit_scene_hooks()
if has_countdowns() { emit_countdown_system() } # `countdown` fields (see emit_game.ludic)
if has_prefabs() { emit_prefab_fns() } # @L_prefab_<Name> + @L_spawn_prefab(name)
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")
emit(" call void @L_init_runtime()\n")
if has_ecs() { emit(" call void @L_grow(i32 1)\n") }
if (find_fn("rt_init") != null) { emit(" call void " + fn_sym("rt_init") + "()\n") }
emit(" call void @L_init_globals()\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 += 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_sym("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")
# #83: when the game uses the input action-map / device layer, the frame loop
# commits the device layer automatically each frame by calling input_poll (which
# reads the live key via rt_poll, records/replays, and rebuilds the held-key set,
# mouse and gamepad state). Its return is the frame key, so Input.key still works
# and the game no longer has to call Input.poll by hand. A game that uses no Input
# runtime keeps the plain rt_poll path, byte-identical.
if g_uses_input and (find_fn("input_drive") != null) {
let k = emit_bind("call i32 " + fn_sym("input_drive") + "()")
emit(" store i32 "); emit(k); emit(", ptr @L_key\n")
} else {
if (find_fn("rt_poll") != null) {
let k = emit_bind("call i32 " + fn_sym("rt_poll") + "()")
emit(" store i32 "); emit(k); emit(", ptr @L_key\n")
}
}
# a program with `ui` blocks: navigate the open UI from this frame's key and emit
# UiClicked on activation — so a scene never polls ui_tick by hand.
if has_ui() and (find_fn("rt_ui_tick") != null) {
let uk = emit_bind("load i32, ptr @L_key")
emit(" call void " + fn_sym("rt_ui_tick") + "(i32 "); emit(uk); emit(")\n")
}
emit_calls_for_phase("Input")
emit_calls_for_phase("FixedUpdate")
emit_calls_for_phase("Update")
emit_calls_for_phase("LateUpdate")
# #86: @ClearColor(N) makes the Render phase auto-clear to N at the top and
# auto-present after the handlers run, so a game drops the clear/show boilerplate
# (the clear colour is declared, not written in the handler body). Opt-in — a game
# with no @ClearColor is byte-identical (it clears/shows itself, or the light
# system owns the present).
if g_has_clear_color and (find_fn("rt_clear") != null) {
let colour = emit_expr(g_clear_color) # a literal, a const, or a Color.Name
emit(" call void " + fn_sym("rt_clear") + "(i32 "); emit(colour.code); emit(")\n")
}
emit_calls_for_phase("Render")
# Overlay: HUD / menus drawn after every engine Render system (sprites, lights), so
# game UI is never painted under an actor. Presented together with the frame.
emit_calls_for_phase("Overlay")
if g_has_clear_color and (find_fn("rt_present") != null) {
emit(" call void " + fn_sym("rt_present") + "()\n")
}
emit(" call void @lp_mem_frame()\n") # 25.1: the fence judges the frame that ended
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_sym("rt_shutdown") + "()\n") }
emit(" ret i32 0\n}\n")
}