ludic/selfhost/backend/game/emit_world.ludic
Orkuncakilkaya 8384ad3215 feat(compiler): the built-in ECS's stores grow - 100 000 entities, where 1024 was the wall
Every per-entity store (@S_ components, @H_ flags, alive, kind, freelist, owners) is a heap block
L_grow doubles from 1024 as L_alloc hands out a slot past it, the new slots zeroed; each site loads
the store's base where it indexes it (ecs_base, its registers %ecsb* so a raw function's t0 labels
cannot collide). Prop.has bounds against @L_cap, Pool.capacity answers it, a mod's registered
stores grow with the rest, every main grows the stores once before anything reads them. A snapshot
records its slot count first and a load grows to it before reading back. The overflow stop of
1c7ce84 is gone with the wall. ludic-dev test 305 passed, selfhost-test 33 passed; 1000 / 5000 /
100000 entities spawn and count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:53:24 +03:00

533 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 @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 @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")
}
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")
}