refactor(selfhost): reorganise into concern-based subdirectories
Split the flat 38-file selfhost/ into concern-based subdirectories:
frontend/ lex, parse, parse_game, ast
support/ str, buf, io
backend/ core IR + expression/statement lowering
backend/game/ ECS/scene/event/world lowering
backend/stdlib/ the namespaced Math.*/Text.*/Crypto.*/… intrinsics
and split the three oversized emitters at responsibility boundaries so
no file mixes concerns:
emit_game.ludic -> + emit_world.ludic (reflection world table,
tick helpers, @main synthesis)
emit_expr.ludic -> + emit_call.ludic (namespaced builtins, call
lowering, expr dispatch)
emit_text.ludic -> + emit_text_prelude.ludic (emitted string-builder runtime)
FRAGS in tools/x/selfhost.ludic is updated to the new paths with the link
order preserved, and the Python doc/vocabulary tooling is updated to walk
the new layout. Because the build is a plain in-order concatenation and
every split lands on a blank-line boundary, the regenerated seed is
byte-identical: `x reseed` leaves selfhost/ludicc.seed.ll unchanged,
`x bootstrap-cfree` still reaches its fixed point, and both `x test` (56)
and `x selfhost-test` (29, incl. golden renders) stay green.
Closes #29
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f55216af50
commit
23726afa90
51 changed files with 780 additions and 771 deletions
86
selfhost/backend/game/emit_collide.ludic
Normal file
86
selfhost/backend/game/emit_collide.ludic
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# emit_collide.ludic — the Collision.* namespace: 2D overlap tests on plain
|
||||
# integer coordinates (pixels or tiles). Rectangles are (x, y, w, h) with the
|
||||
# origin at the top-left; circles are (x, y, r). Squared distances use i64 so a
|
||||
# large coordinate can't overflow. Each returns a bool.
|
||||
|
||||
function is_collide_ns(meth: pointer) -> bool {
|
||||
if (meth == "rects") or (meth == "point_rect") { return true }
|
||||
if (meth == "circles") or (meth == "rect_circle") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
# dx*dx + dy*dy widened to i64 (no overflow for 32-bit deltas)
|
||||
function coll_sq_sum(dx: pointer, dy: pointer) -> pointer {
|
||||
let dx64 = emit_bind(`sext i32 {dx} to i64`)
|
||||
let dy64 = emit_bind(`sext i32 {dy} to i64`)
|
||||
let xx = emit_bind(`mul i64 {dx64}, {dx64}`)
|
||||
let yy = emit_bind(`mul i64 {dy64}, {dy64}`)
|
||||
return emit_bind(`add i64 {xx}, {yy}`)
|
||||
}
|
||||
|
||||
# max(lo, min(v, hi)) — clamp v into [lo, hi]
|
||||
function coll_clamp(v: pointer, lo: pointer, hi: pointer) -> pointer {
|
||||
let c1 = emit_bind(`icmp slt i32 {v}, {hi}`)
|
||||
let t = emit_bind(`select i1 {c1}, i32 {v}, i32 {hi}`)
|
||||
let c2 = emit_bind(`icmp sgt i32 {lo}, {t}`)
|
||||
return emit_bind(`select i1 {c2}, i32 {lo}, i32 {t}`)
|
||||
}
|
||||
|
||||
function emit_collide_ns(meth: pointer, e: Node) -> Val {
|
||||
if (meth == "rects") { # AABB overlap of two rects
|
||||
let ax = emit_expr(e.kids[0]); let ay = emit_expr(e.kids[1]); let aw = emit_expr(e.kids[2]); let ah = emit_expr(e.kids[3])
|
||||
let bx = emit_expr(e.kids[4]); let by = emit_expr(e.kids[5]); let bw = emit_expr(e.kids[6]); let bh = emit_expr(e.kids[7])
|
||||
let axw = emit_bind(`add i32 {ax.code}, {aw.code}`)
|
||||
let bxw = emit_bind(`add i32 {bx.code}, {bw.code}`)
|
||||
let ayh = emit_bind(`add i32 {ay.code}, {ah.code}`)
|
||||
let byh = emit_bind(`add i32 {by.code}, {bh.code}`)
|
||||
let c1 = emit_bind(`icmp slt i32 {ax.code}, {bxw}`)
|
||||
let c2 = emit_bind(`icmp slt i32 {bx.code}, {axw}`)
|
||||
let c3 = emit_bind(`icmp slt i32 {ay.code}, {byh}`)
|
||||
let c4 = emit_bind(`icmp slt i32 {by.code}, {ayh}`)
|
||||
let x = emit_bind(`and i1 {c1}, {c2}`)
|
||||
let y = emit_bind(`and i1 {c3}, {c4}`)
|
||||
let r = emit_bind(`and i1 {x}, {y}`)
|
||||
return val(emit_bind(`zext i1 {r} to i32`), "bool")
|
||||
}
|
||||
if (meth == "point_rect") { # is a point inside a rect
|
||||
let px = emit_expr(e.kids[0]); let py = emit_expr(e.kids[1])
|
||||
let rx = emit_expr(e.kids[2]); let ry = emit_expr(e.kids[3]); let rw = emit_expr(e.kids[4]); let rh = emit_expr(e.kids[5])
|
||||
let rxw = emit_bind(`add i32 {rx.code}, {rw.code}`)
|
||||
let ryh = emit_bind(`add i32 {ry.code}, {rh.code}`)
|
||||
let c1 = emit_bind(`icmp sge i32 {px.code}, {rx.code}`)
|
||||
let c2 = emit_bind(`icmp slt i32 {px.code}, {rxw}`)
|
||||
let c3 = emit_bind(`icmp sge i32 {py.code}, {ry.code}`)
|
||||
let c4 = emit_bind(`icmp slt i32 {py.code}, {ryh}`)
|
||||
let x = emit_bind(`and i1 {c1}, {c2}`)
|
||||
let y = emit_bind(`and i1 {c3}, {c4}`)
|
||||
let r = emit_bind(`and i1 {x}, {y}`)
|
||||
return val(emit_bind(`zext i1 {r} to i32`), "bool")
|
||||
}
|
||||
if (meth == "circles") { # do two circles overlap
|
||||
let ax = emit_expr(e.kids[0]); let ay = emit_expr(e.kids[1]); let ar = emit_expr(e.kids[2])
|
||||
let bx = emit_expr(e.kids[3]); let by = emit_expr(e.kids[4]); let br = emit_expr(e.kids[5])
|
||||
let dx = emit_bind(`sub i32 {ax.code}, {bx.code}`)
|
||||
let dy = emit_bind(`sub i32 {ay.code}, {by.code}`)
|
||||
let d2 = coll_sq_sum(dx, dy)
|
||||
let rs = emit_bind(`add i32 {ar.code}, {br.code}`)
|
||||
let rs64 = emit_bind(`sext i32 {rs} to i64`)
|
||||
let r2 = emit_bind(`mul i64 {rs64}, {rs64}`)
|
||||
let le = emit_bind(`icmp sle i64 {d2}, {r2}`)
|
||||
return val(emit_bind(`zext i1 {le} to i32`), "bool")
|
||||
}
|
||||
# rect_circle: nearest point on the rect to the circle centre, within radius
|
||||
let rx = emit_expr(e.kids[0]); let ry = emit_expr(e.kids[1]); let rw = emit_expr(e.kids[2]); let rh = emit_expr(e.kids[3])
|
||||
let cx = emit_expr(e.kids[4]); let cy = emit_expr(e.kids[5]); let cr = emit_expr(e.kids[6])
|
||||
let rxw = emit_bind(`add i32 {rx.code}, {rw.code}`)
|
||||
let ryh = emit_bind(`add i32 {ry.code}, {rh.code}`)
|
||||
let clx = coll_clamp(cx.code, rx.code, rxw)
|
||||
let cly = coll_clamp(cy.code, ry.code, ryh)
|
||||
let dx = emit_bind(`sub i32 {cx.code}, {clx}`)
|
||||
let dy = emit_bind(`sub i32 {cy.code}, {cly}`)
|
||||
let d2 = coll_sq_sum(dx, dy)
|
||||
let cr64 = emit_bind(`sext i32 {cr.code} to i64`)
|
||||
let r2 = emit_bind(`mul i64 {cr64}, {cr64}`)
|
||||
let le = emit_bind(`icmp sle i64 {d2}, {r2}`)
|
||||
return val(emit_bind(`zext i1 {le} to i32`), "bool")
|
||||
}
|
||||
127
selfhost/backend/game/emit_ecs.ludic
Normal file
127
selfhost/backend/game/emit_ecs.ludic
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# emit_ecs.ludic — ECS storage and the entity allocator. For each component:
|
||||
# a %Cmp_ layout, a dense @S_ store, and an @H_ "has this component" array.
|
||||
# Entities are integer handles; L_alloc reuses freed slots. Mirrors the ECS
|
||||
# parts of compiler/back/ir_decl.c.
|
||||
|
||||
const MAX_ENT: int = 1024
|
||||
|
||||
function has_systems() -> bool {
|
||||
var i = 0
|
||||
while i < len(prog) { if prog[i].kind == N_SYS { return true }; i = i + 1 }
|
||||
return false
|
||||
}
|
||||
function has_models() -> bool {
|
||||
var i = 0
|
||||
while i < len(prog) { if prog[i].kind == N_ARCH { return true }; i = i + 1 }
|
||||
return false
|
||||
}
|
||||
# N5: does the program have an `entry` block? A game with both handlers and an
|
||||
# `entry` drives its own loop (calling tick_fixed/tick_render), instead of the
|
||||
# compiler's auto frame loop. A game with handlers and no entry uses the auto-loop.
|
||||
function has_entry() -> bool {
|
||||
var i = 0
|
||||
while i < len(prog) { if prog[i].kind == N_MAIN { return true }; i = i + 1 }
|
||||
return false
|
||||
}
|
||||
# Does this program run the ECS? A property alone no longer answers that — the
|
||||
# same `property` keyword also declares plain `new`-allocated records (the merged
|
||||
# `struct`). A program uses the ECS when it has a handler or a model; a tool that
|
||||
# only declares record types and functions does not, and gets no entity storage,
|
||||
# allocator, snapshot or runtime splice.
|
||||
function has_ecs() -> bool { return has_systems() or has_models() }
|
||||
|
||||
function emit_ecs_storage() -> void {
|
||||
emith("@L_running = internal global i32 1\n")
|
||||
emith("@L_key = internal global i32 0\n")
|
||||
emith("@L_frame = internal global i32 0\n")
|
||||
if len(g_scenes) > 0 { emith("@L_scene = internal global i32 0\n") } # active base scene id
|
||||
emith("@L_entc = internal global i32 0\n")
|
||||
let me = itoa(MAX_ENT)
|
||||
emith(`@L_alive = internal global [{me} x i32] zeroinitializer\n`)
|
||||
emith(`@L_kind = internal global [{me} x i32] zeroinitializer\n`)
|
||||
emith(`@L_freelist = internal global [{me} x i32] zeroinitializer\n`)
|
||||
emith("@L_freen = internal global i32 0\n")
|
||||
# NETWORKING role registers (N3/N5): a runtime sets these; offline they hold the
|
||||
# single-player default — @L_role=1 (this peer is the authority), local id 0.
|
||||
# Emitted only when a networking feature is used, so non-networked builds are
|
||||
# byte-identical (§8). @L_owner_arr is the per-entity network owner (N3, @Owned).
|
||||
if net_any() {
|
||||
emith("@L_role = internal global i32 1\n") # 1 = server/authority (offline default)
|
||||
emith("@L_localid = internal global i32 0\n") # this peer's id
|
||||
}
|
||||
if net_has_owned() { emith(`@L_owner_arr = internal global [{me} x i32] zeroinitializer\n`) }
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
let c = prog[i]
|
||||
# per-entity storage for a property (its %Cmp_ layout is emitted in the
|
||||
# header). Every property in an ECS program is a component today; a property
|
||||
# used only via `new` would not need these, but no such program mixes the two.
|
||||
if c.kind == N_COMP {
|
||||
emith(`@S_{c.s} = internal global [{me} x %Cmp_{c.s}] zeroinitializer\n`)
|
||||
emith(`@H_{c.s} = internal global [{me} x i8] zeroinitializer\n`)
|
||||
}
|
||||
# one enabled-flag global per model and per handler (default enabled)
|
||||
if c.kind == N_ARCH { emith(`@ME_{c.s} = internal global i32 1\n`) }
|
||||
if c.kind == N_SYS { emith(`@HE_{c.s} = internal global i32 1\n`) }
|
||||
i = i + 1
|
||||
}
|
||||
# one enabled-flag global per toggled layer (default shown)
|
||||
var li = 0
|
||||
while li < len(g_toggled_layers) { emith(`@LE_{g_toggled_layers[li]} = internal global i32 1\n`); li = li + 1 }
|
||||
}
|
||||
|
||||
# L_reset(e): clear every has-flag and the archetype kind for entity e
|
||||
function emit_ecs_allocator() -> void {
|
||||
let me = itoa(MAX_ENT)
|
||||
emit("define void @L_reset(i32 %e) {\nentry:\n")
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
if prog[i].kind == N_COMP {
|
||||
let hn = `%h{itoa(i)}`
|
||||
emit(" "); emit(hn); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(prog[i].s); emit(", i32 0, i32 %e\n")
|
||||
emit(" store i8 0, ptr "); emit(hn); emit("\n")
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
emit(" %k = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n")
|
||||
emit(" store i32 0, ptr %k\n")
|
||||
# N3: reset an @Owned entity's network owner to -1 (unowned) on alloc/free
|
||||
if net_has_owned() {
|
||||
emit(" %ow = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_owner_arr, i32 0, i32 %e\n")
|
||||
emit(" store i32 -1, ptr %ow\n")
|
||||
}
|
||||
emit(" ret void\n}\n\n")
|
||||
|
||||
emit("define i32 @L_alloc() {\nentry:\n")
|
||||
emit(" %fn = load i32, ptr @L_freen\n")
|
||||
emit(" %has = icmp sgt i32 %fn, 0\n")
|
||||
emit(" br i1 %has, label %reuse, label %fresh\n")
|
||||
emit("reuse:\n")
|
||||
emit(" %fn1 = sub i32 %fn, 1\n")
|
||||
emit(" store i32 %fn1, ptr @L_freen\n")
|
||||
emit(" %fp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_freelist, i32 0, i32 %fn1\n")
|
||||
emit(" %re = load i32, ptr %fp\n")
|
||||
emit(" br label %done\n")
|
||||
emit("fresh:\n")
|
||||
emit(" %ec = load i32, ptr @L_entc\n")
|
||||
emit(" %ec1 = add i32 %ec, 1\n")
|
||||
emit(" store i32 %ec1, ptr @L_entc\n")
|
||||
emit(" br label %done\n")
|
||||
emit("done:\n")
|
||||
emit(" %e = phi i32 [ %re, %reuse ], [ %ec, %fresh ]\n")
|
||||
emit(" call void @L_reset(i32 %e)\n")
|
||||
emit(" %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %e\n")
|
||||
emit(" store i32 1, ptr %ap\n")
|
||||
emit(" ret i32 %e\n}\n\n")
|
||||
|
||||
emit("define void @L_free_entity(i32 %e) {\nentry:\n")
|
||||
emit(" %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %e\n")
|
||||
emit(" store i32 0, ptr %ap\n")
|
||||
emit(" call void @L_reset(i32 %e)\n")
|
||||
emit(" %fn = load i32, ptr @L_freen\n")
|
||||
emit(" %fp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_freelist, i32 0, i32 %fn\n")
|
||||
emit(" store i32 %e, ptr %fp\n")
|
||||
emit(" %fn1 = add i32 %fn, 1\n")
|
||||
emit(" store i32 %fn1, ptr @L_freen\n")
|
||||
emit(" ret void\n}\n\n")
|
||||
}
|
||||
388
selfhost/backend/game/emit_game.ludic
Normal file
388
selfhost/backend/game/emit_game.ludic
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
# emit_game.ludic — system functions and the frame loop. A system compiles to a
|
||||
# void function; main() boots (Start systems), then runs the per-frame phases in
|
||||
# order until the game stops. Runtime hooks (rt_init/poll/running/shutdown) are
|
||||
# called only when the runtime defines them.
|
||||
|
||||
function emit_system_fn(sys: Node) -> void {
|
||||
g_cur_scene = sys.c # scene owning this handler (null if global) — for `become`
|
||||
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
|
||||
ret_ty = "void"
|
||||
let fbody = buf_new()
|
||||
falloc = buf_new()
|
||||
let saved = code
|
||||
code = fbody
|
||||
emit_block(sys.a)
|
||||
if not g_term { emit(" br label %ret\n") }
|
||||
emit("ret:\n ret void\n")
|
||||
code = saved
|
||||
emit("define void @sys_"); emit(sys.s); emit("() {\nentry:\n")
|
||||
emit(buf_str(falloc))
|
||||
emit(buf_str(fbody))
|
||||
emit("}\n\n")
|
||||
}
|
||||
|
||||
# one enable-gated call to @sys_<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")
|
||||
}
|
||||
|
||||
# Global handlers run first, then the active scene's layer handlers in
|
||||
# declaration (draw) order. The active scene is snapshotted once per phase, so a
|
||||
# `become` mid-phase takes effect at the next phase boundary — exactly one scene
|
||||
# is active within any single phase.
|
||||
function emit_calls_for_phase(phase: pointer) -> void {
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
if d.kind == N_SYS and (d.ty == phase) and (d.c == null) { emit_call_one(d) }
|
||||
i = i + 1
|
||||
}
|
||||
# any scene-owned handlers in this phase? gate them on one @L_scene snapshot.
|
||||
var has_sc = false
|
||||
i = 0
|
||||
while i < len(prog) { let d = prog[i]; if d.kind == N_SYS and (d.ty == phase) and (d.c != null) { has_sc = true }; i = i + 1 }
|
||||
if not has_sc { return }
|
||||
let cs = emit_bind("load i32, ptr @L_scene")
|
||||
i = 0
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
if d.kind == N_SYS and (d.ty == phase) and (d.c != null) {
|
||||
let ce = emit_bind(`icmp eq i32 {cs}, {itoa(d.c.ival)}`)
|
||||
let run = lbl("scrun"); let skip = lbl("scskip")
|
||||
emit(" br i1 "); emit(ce); emit(", label %"); emit(run); emit(", label %"); emit(skip); emit("\n")
|
||||
emit(run); emit(":\n")
|
||||
emit_call_one(d)
|
||||
emit(" br label %"); emit(skip); emit("\n"); emit(skip); emit(":\n")
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
# on enter / on exit compile to void functions @scene_enter_<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) -> void {
|
||||
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
|
||||
ret_ty = "void"
|
||||
let fbody = buf_new()
|
||||
falloc = buf_new()
|
||||
let saved = code
|
||||
code = fbody
|
||||
if (body != null) { emit_block(body) }
|
||||
# EV1: a `public` scene fires scene_<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)
|
||||
emit_scene_fn(sc.s, "exit", sc.b)
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
# @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 = i + 1
|
||||
}
|
||||
emit_despawn_all_fn()
|
||||
}
|
||||
|
||||
# LC1 "no silent deaths": at program shutdown every still-live entity's despawn
|
||||
# hook fires with reason Quit, so teardown that must run on exit is not skipped.
|
||||
# @L_despawn_all(reason) walks the live set and dispatches each entity by kind —
|
||||
# the same per-model dispatch as `despawn`, but without freeing (the process is
|
||||
# ending). Emitted only when the program has @OnDespawn hooks, so despawn-free
|
||||
# programs are byte-for-byte unchanged.
|
||||
function emit_despawn_all_fn() -> void {
|
||||
if len(g_ondespawn) == 0 { return }
|
||||
let me = itoa(MAX_ENT)
|
||||
emit("define void @L_despawn_all(i32 %reason) {\nentry:\n br label %loop\n")
|
||||
emit("loop:\n %i = phi i32 [ 0, %entry ], [ %i1, %cont ]\n")
|
||||
emit(" %n = load i32, ptr @L_entc\n %go = icmp slt i32 %i, %n\n")
|
||||
emit(" br i1 %go, label %body, label %fin\n")
|
||||
emit("body:\n %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %i\n")
|
||||
emit(" %al = load i32, ptr %ap\n %isa = icmp ne i32 %al, 0\n")
|
||||
emit(" br i1 %isa, label %do, label %cont\n")
|
||||
emit("do:\n %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %i\n")
|
||||
emit(" %k = load i32, ptr %kp\n")
|
||||
var i = 0
|
||||
while i < len(g_ondespawn) {
|
||||
let mname = g_ondespawn[i].s
|
||||
let si = itoa(i)
|
||||
emit(" %c"); emit(si); emit(" = icmp eq i32 %k, "); emit(itoa(find_arch_id(mname))); emit("\n")
|
||||
emit(" br i1 %c"); emit(si); emit(", label %hit"); emit(si); emit(", label %next"); emit(si); emit("\n")
|
||||
emit("hit"); emit(si); emit(":\n call void @on_despawn_"); emit(mname); emit("(i32 %i, i32 %reason)\n")
|
||||
let dev = `model_{mname}_despawn` # EV1: @Public despawn event at shutdown
|
||||
if (find_event(dev) != null) { emit(" call void @ev_"); emit(dev); emit("(i32 %i, i32 %reason)\n") }
|
||||
emit(" br label %next"); emit(si); emit("\n")
|
||||
emit("next"); emit(si); emit(":\n")
|
||||
i = i + 1
|
||||
}
|
||||
emit(" br label %cont\n")
|
||||
emit("cont:\n %i1 = add i32 %i, 1\n br label %loop\n")
|
||||
emit("fin:\n ret void\n}\n\n")
|
||||
}
|
||||
|
||||
# EV0: each declared `event E` compiles to a `@ev_<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 = 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 %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
|
||||
ret_ty = "void"
|
||||
let fbody = buf_new()
|
||||
falloc = buf_new()
|
||||
let saved = code
|
||||
code = fbody
|
||||
# EV6: bound re-entrant emit — past EV_DEPTH_CAP, return without dispatching
|
||||
emit(" %evd = load i32, ptr @ev_depth\n")
|
||||
emit(" %evover = icmp sge i32 %evd, "); emit(itoa(EV_DEPTH_CAP)); emit("\n")
|
||||
emit(" br i1 %evover, label %evcap, label %evgo\n")
|
||||
emit("evcap:\n")
|
||||
if ev.ival == 1 { emit(" ret i32 0\n") } else { emit(" ret void\n") }
|
||||
emit("evgo:\n")
|
||||
emit(" %evd1 = add i32 %evd, 1\n store i32 %evd1, ptr @ev_depth\n")
|
||||
# a stack copy of the payload, passed by pointer to every foreign listener
|
||||
let pl = emit_alloca(`%Ev_{en}`)
|
||||
# bind each field: store the param into the payload struct AND a name slot the
|
||||
# compile-time listener bodies read bare.
|
||||
var f = 0
|
||||
while f < len(ev.kids) {
|
||||
let fd = ev.kids[f]
|
||||
let lt = llty(fd.ty)
|
||||
let pa = nreg(); emit(" "); emit(pa); emit(" = getelementptr inbounds %Ev_"); emit(en); emit(", ptr "); emit(pl); emit(", i32 0, i32 "); emit(itoa(f)); emit("\n")
|
||||
emit(" store "); emit(lt); emit(" %p"); emit(itoa(f)); emit(", ptr "); emit(pa); emit("\n")
|
||||
let slot = emit_alloca(lt)
|
||||
emit(" store "); emit(lt); emit(" %p"); emit(itoa(f)); emit(", ptr "); emit(slot); emit("\n")
|
||||
loc_push(fd.s, slot, fd.ty)
|
||||
f = f + 1
|
||||
}
|
||||
# cancellable: zero the flag and expose its address to `cancel` in the listeners
|
||||
var caddr = null
|
||||
if ev.ival == 1 {
|
||||
caddr = nreg(); emit(" "); emit(caddr); emit(" = getelementptr inbounds %Ev_"); emit(en); emit(", ptr "); emit(pl); emit(", i32 0, i32 "); emit(itoa(len(ev.kids))); emit("\n")
|
||||
emit(" store i32 0, ptr "); emit(caddr); emit("\n")
|
||||
g_cancel_addr = caddr
|
||||
}
|
||||
let base = nloc # listeners share the params but not each other's locals
|
||||
var i = 0
|
||||
while i < len(g_onlisten) {
|
||||
if (g_onlisten[i].s == en) { nloc = base; g_term = false; emit_block(g_onlisten[i].a) }
|
||||
i = i + 1
|
||||
}
|
||||
# the open half: walk the foreign callback array in registration order
|
||||
if not g_term {
|
||||
let ci = emit_alloca("i32"); emit(" store i32 0, ptr "); emit(ci); emit("\n")
|
||||
let L = lbl("evl"); let B = lbl("evb"); let D = lbl("evd")
|
||||
emit(" br label %"); emit(L); emit("\n")
|
||||
emit(L); emit(":\n")
|
||||
let iv = emit_bind(`load i32, ptr {ci}`)
|
||||
let nn = emit_bind(`load i32, ptr @evN_{en}`)
|
||||
let go = emit_bind(`icmp slt i32 {iv}, {nn}`)
|
||||
emit(" br i1 "); emit(go); emit(", label %"); emit(B); emit(", label %"); emit(D); emit("\n")
|
||||
emit(B); emit(":\n")
|
||||
let sp = nreg(); emit(" "); emit(sp); emit(" = getelementptr inbounds ["); emit(cap); emit(" x ptr], ptr @evL_"); emit(en); emit(", i32 0, i32 "); emit(iv); emit("\n")
|
||||
let cb = emit_bind(`load ptr, ptr {sp}`)
|
||||
let cbn = emit_bind(`icmp eq ptr {cb}, null`) # EV5: a removed (off) listener is null — skip it
|
||||
let doc = lbl("evdo"); let skp = lbl("evsk")
|
||||
emit(" br i1 "); emit(cbn); emit(", label %"); emit(skp); emit(", label %"); emit(doc); emit("\n")
|
||||
emit(doc); emit(":\n")
|
||||
emit(" call void "); emit(cb); emit("(ptr "); emit(pl); emit(")\n")
|
||||
emit(" br label %"); emit(skp); emit("\n")
|
||||
emit(skp); emit(":\n")
|
||||
let i2 = emit_bind(`add i32 {iv}, 1`)
|
||||
emit(" store i32 "); emit(i2); emit(", ptr "); emit(ci); emit("\n")
|
||||
emit(" br label %"); emit(L); emit("\n")
|
||||
emit(D); emit(":\n")
|
||||
emit(" br label %ret\n")
|
||||
}
|
||||
emit("ret:\n")
|
||||
emit(" %evdd = load i32, ptr @ev_depth\n %evdd1 = sub i32 %evdd, 1\n store i32 %evdd1, ptr @ev_depth\n") # EV6: leave one nesting level
|
||||
if ev.ival == 1 { # return the (possibly set) cancelled flag
|
||||
let cv = emit_bind(`load i32, ptr {caddr}`)
|
||||
emit(" ret i32 "); emit(cv); emit("\n")
|
||||
} else { emit(" ret void\n") }
|
||||
g_cancel_addr = null # leaves listener scope
|
||||
code = saved
|
||||
var rt = "void"; if ev.ival == 1 { rt = "i32" }
|
||||
emit("define "); emit(rt); emit(" @ev_"); emit(en); emit("(")
|
||||
var g = 0
|
||||
while g < len(ev.kids) {
|
||||
if g > 0 { emit(", ") }
|
||||
emit(llty(ev.kids[g].ty)); emit(" %p"); emit(itoa(g))
|
||||
g = g + 1
|
||||
}
|
||||
emit(") {\nentry:\n")
|
||||
emit(buf_str(falloc))
|
||||
emit(buf_str(fbody))
|
||||
emit("}\n\n")
|
||||
e = e + 1
|
||||
}
|
||||
|
||||
# EV5: @ludic_sweep_entity(owner) — remove every entity-scoped listener owned by
|
||||
# a despawning entity, across all events. Called from `despawn`, so a listener
|
||||
# bound to an entity cannot outlive it (the Node listener-leak footgun, gone).
|
||||
let capS = itoa(EV_CAP)
|
||||
emit("define void @ludic_sweep_entity(i32 %owner) {\nentry:\n %ci = alloca i32\n store i32 0, ptr %ci\n br label %sw0\n")
|
||||
var e2 = 0
|
||||
while e2 < len(g_events) {
|
||||
let en2 = g_events[e2].s; let sk = itoa(e2)
|
||||
emit("sw"); emit(sk); emit(":\n")
|
||||
emit(" %swi"); emit(sk); emit(" = load i32, ptr %ci\n")
|
||||
emit(" %swn"); emit(sk); emit(" = load i32, ptr @evN_"); emit(en2); emit("\n")
|
||||
emit(" %swg"); emit(sk); emit(" = icmp slt i32 %swi"); emit(sk); emit(", %swn"); emit(sk); emit("\n")
|
||||
emit(" br i1 %swg"); emit(sk); emit(", label %swb"); emit(sk); emit(", label %swd"); emit(sk); emit("\n")
|
||||
emit("swb"); emit(sk); emit(":\n")
|
||||
emit(" %swop"); emit(sk); emit(" = getelementptr inbounds ["); emit(capS); emit(" x i32], ptr @evO_"); emit(en2); emit(", i32 0, i32 %swi"); emit(sk); emit("\n")
|
||||
emit(" %swov"); emit(sk); emit(" = load i32, ptr %swop"); emit(sk); emit("\n")
|
||||
emit(" %swm"); emit(sk); emit(" = icmp eq i32 %swov"); emit(sk); emit(", %owner\n")
|
||||
emit(" br i1 %swm"); emit(sk); emit(", label %swh"); emit(sk); emit(", label %swx"); emit(sk); emit("\n")
|
||||
emit("swh"); emit(sk); emit(":\n")
|
||||
emit(" %swlp"); emit(sk); emit(" = getelementptr inbounds ["); emit(capS); emit(" x ptr], ptr @evL_"); emit(en2); emit(", i32 0, i32 %swi"); emit(sk); emit("\n")
|
||||
emit(" store ptr null, ptr %swlp"); emit(sk); emit("\n br label %swx"); emit(sk); emit("\n")
|
||||
emit("swx"); emit(sk); emit(":\n")
|
||||
emit(" %swi1"); emit(sk); emit(" = add i32 %swi"); emit(sk); emit(", 1\n store i32 %swi1"); emit(sk); emit(", ptr %ci\n br label %sw"); emit(sk); emit("\n")
|
||||
emit("swd"); emit(sk); emit(":\n store i32 0, ptr %ci\n")
|
||||
if (e2 + 1) < len(g_events) { emit(" br label %sw"); emit(itoa(e2 + 1)); emit("\n") }
|
||||
else { emit(" ret void\n") }
|
||||
e2 = e2 + 1
|
||||
}
|
||||
emit("}\n\n")
|
||||
}
|
||||
|
||||
71
selfhost/backend/game/emit_machine.ludic
Normal file
71
selfhost/backend/game/emit_machine.ludic
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# emit_machine.ludic — `machine <state> { state Name = v { .. } }` dispatches on
|
||||
# an int state store; `become Name` writes the target state's value back. The
|
||||
# store is either a named program-scope `var` (loaded/stored directly) or, for
|
||||
# older code, a register index (reg()/set_reg()).
|
||||
|
||||
# If `a` names a program-scope `var`, return it; else null (a register index).
|
||||
function machine_var(a: Node) -> Node {
|
||||
if a.kind == E_ID {
|
||||
let g = find_global(a.s)
|
||||
if (g != null) { if g.kind == N_VAR { return g } }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function emit_machine(st: Node) -> void {
|
||||
let gv = machine_var(st.a)
|
||||
var s = ""
|
||||
if (gv != null) { s = emit_bind(`load i32, ptr @g_{st.a.s}`) }
|
||||
else {
|
||||
let regv = emit_expr(st.a)
|
||||
s = emit_bind(`call i32 @fn_rt_reg(i32 {regv.code})`)
|
||||
}
|
||||
if nmach < len(mach_stk) { mach_stk[nmach] = st } else { push(mach_stk, st) }
|
||||
nmach = nmach + 1
|
||||
let endl = lbl("smend")
|
||||
var i = 0
|
||||
while i < len(st.kids) {
|
||||
let state = st.kids[i]
|
||||
let v = emit_expr(state.b)
|
||||
let c = emit_bind(`icmp eq i32 {s}, {v.code}`)
|
||||
let body = lbl("sbody"); let nxt = lbl("sarm")
|
||||
emit(" br i1 "); emit(c); emit(", label %"); emit(body); emit(", label %"); emit(nxt); emit("\n")
|
||||
emit(body); emit(":\n"); g_term = false
|
||||
emit_block(state.a)
|
||||
if not g_term { emit(" br label %"); emit(endl); emit("\n") }
|
||||
emit(nxt); emit(":\n"); g_term = false
|
||||
i = i + 1
|
||||
}
|
||||
if not g_term { emit(" br label %"); emit(endl); emit("\n") }
|
||||
emit(endl); emit(":\n"); g_term = false
|
||||
nmach = nmach - 1
|
||||
}
|
||||
|
||||
# `become Name` — a machine state transition when Name is a state of an
|
||||
# enclosing `machine`, otherwise a scene transition. A scene transition runs the
|
||||
# source scene's on-exit, stores the target scene id into @L_scene, and runs the
|
||||
# target's on-enter (two direct calls and a store — no dispatch table).
|
||||
function emit_become(st: Node) -> void {
|
||||
if nmach > 0 { # inside a machine: try a state first
|
||||
let m = mach_stk[nmach - 1]
|
||||
var target: Node = null
|
||||
var i = 0
|
||||
while i < len(m.kids) { if (m.kids[i].s == st.s) { target = m.kids[i] }; i = i + 1 }
|
||||
if (target != null) {
|
||||
let gv = machine_var(m.a)
|
||||
let sv = emit_expr(target.b)
|
||||
if (gv != null) {
|
||||
emit(" store i32 "); emit(sv.code); emit(", ptr @g_"); emit(m.a.s); emit("\n")
|
||||
} else {
|
||||
let regv = emit_expr(m.a)
|
||||
emit(" call void @fn_rt_set_reg(i32 "); emit(regv.code); emit(", i32 "); emit(sv.code); emit(")\n")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
let sc = find_scene(st.s) # else a scene transition
|
||||
if (sc == null) { perr(`become: no state or scene {st.s}`) }
|
||||
if (g_cur_scene != null) { emit(" call void @scene_exit_"); emit(g_cur_scene.s); emit("()\n") }
|
||||
emit(" store i32 "); emit(itoa(sc.ival)); emit(", ptr @L_scene\n")
|
||||
emit(" call void @scene_enter_"); emit(sc.s); emit("()\n")
|
||||
}
|
||||
101
selfhost/backend/game/emit_query.ludic
Normal file
101
selfhost/backend/game/emit_query.ludic
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# emit_query.ludic — the ECS query loop. Iterates live entities, filters by the
|
||||
# components/archetypes each carries, binds the requested components, and runs
|
||||
# the body once per match. Mirrors ll_query in compiler/back/ir_ecs.c.
|
||||
|
||||
function find_arch_id(name: pointer) -> int {
|
||||
var i = 0; var n = 1
|
||||
while i < len(prog) { if prog[i].kind == N_ARCH { if (prog[i].s == name) { return n }; n = n + 1 }; i = i + 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
function emit_query(st: Node) -> void {
|
||||
let me = itoa(MAX_ENT)
|
||||
let ip = emit_alloca("i32")
|
||||
store_at("i32", "0", ip)
|
||||
if nself < len(self_stk) { self_stk[nself] = ip } else { push(self_stk, ip) }
|
||||
nself = nself + 1
|
||||
|
||||
let cond = lbl("qcond"); let body = lbl("qbody"); let nxt = lbl("qnext"); let endl = lbl("qend")
|
||||
emit(" br label %"); emit(cond); emit("\n")
|
||||
emit(cond); emit(":\n")
|
||||
let i0 = emit_bind(`load i32, ptr {ip}`)
|
||||
let ec = emit_bind("load i32, ptr @L_entc")
|
||||
let lt = emit_bind(`icmp slt i32 {i0}, {ec}`)
|
||||
emit(" br i1 "); emit(lt); emit(", label %"); emit(body); emit(", label %"); emit(endl); emit("\n")
|
||||
emit(body); emit(":\n"); g_term = false
|
||||
let i1 = emit_bind(`load i32, ptr {ip}`)
|
||||
# alive?
|
||||
let ap = nreg(); emit(" "); emit(ap); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 "); emit(i1); emit("\n")
|
||||
let al = emit_bind(`load i32, ptr {ap}`)
|
||||
let alc = emit_bind(`icmp ne i32 {al}, 0`)
|
||||
let ka = lbl("qa")
|
||||
emit(" br i1 "); emit(alc); emit(", label %"); emit(ka); emit(", label %"); emit(nxt); emit("\n")
|
||||
emit(ka); emit(":\n")
|
||||
|
||||
let terms = st.c
|
||||
# component / archetype filters
|
||||
var t = 0
|
||||
while t < len(terms.kids) {
|
||||
let tm = terms.kids[t]
|
||||
let ak = find_arch_id(tm.s)
|
||||
var ok = "0"
|
||||
if ak > 0 {
|
||||
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 "); emit(i1); emit("\n")
|
||||
let kv = emit_bind(`load i32, ptr {kp}`)
|
||||
let kok = emit_bind(`icmp eq i32 {kv}, {itoa(ak)}`)
|
||||
let mev = emit_bind(`load i32, ptr @ME_{tm.s}`) # model-enabled flag
|
||||
let meok = emit_bind(`icmp ne i32 {mev}, 0`)
|
||||
ok = emit_bind(`and i1 {kok}, {meok}`)
|
||||
} else {
|
||||
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(tm.s); emit(", i32 0, i32 "); emit(i1); emit("\n")
|
||||
let hv = emit_bind(`load i8, ptr {hp}`)
|
||||
ok = emit_bind(`icmp ne i8 {hv}, 0`)
|
||||
}
|
||||
let keep = lbl("qt")
|
||||
emit(" br i1 "); emit(ok); emit(", label %"); emit(keep); emit(", label %"); emit(nxt); emit("\n")
|
||||
emit(keep); emit(":\n")
|
||||
t = t + 1
|
||||
}
|
||||
|
||||
# bind the requested components to the loop variables, in order
|
||||
let save = nloc
|
||||
var vi = 0
|
||||
t = 0
|
||||
while t < len(terms.kids) {
|
||||
let tm = terms.kids[t]
|
||||
if tm.ival == 0 and find_arch_id(tm.s) == 0 {
|
||||
if vi < len(st.kids) {
|
||||
let slot = nreg()
|
||||
emit(" "); emit(slot); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(tm.s); emit("], ptr @S_"); emit(tm.s); emit(", i32 0, i32 "); emit(i1); emit("\n")
|
||||
let vslot = emit_alloca("ptr")
|
||||
emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n")
|
||||
loc_push(st.kids[vi].s, vslot, tm.s)
|
||||
vi = vi + 1
|
||||
}
|
||||
}
|
||||
t = t + 1
|
||||
}
|
||||
|
||||
# optional where-clause
|
||||
if (st.b != null) {
|
||||
let w = emit_expr(st.b)
|
||||
let wc = emit_bind(`icmp ne i32 {w.code}, 0`)
|
||||
let kw = lbl("qw")
|
||||
emit(" br i1 "); emit(wc); emit(", label %"); emit(kw); emit(", label %"); emit(nxt); emit("\n")
|
||||
emit(kw); emit(":\n")
|
||||
}
|
||||
|
||||
loop_push(nxt, endl)
|
||||
emit_block(st.a)
|
||||
loop_pop()
|
||||
nloc = save
|
||||
if not g_term { emit(" br label %"); emit(nxt); emit("\n") }
|
||||
|
||||
emit(nxt); emit(":\n")
|
||||
let i2 = emit_bind(`load i32, ptr {ip}`)
|
||||
let i3 = emit_bind(`add i32 {i2}, 1`)
|
||||
store_at("i32", i3, ip)
|
||||
emit(" br label %"); emit(cond); emit("\n")
|
||||
emit(endl); emit(":\n"); g_term = false
|
||||
nself = nself - 1
|
||||
}
|
||||
127
selfhost/backend/game/emit_save.ludic
Normal file
127
selfhost/backend/game/emit_save.ludic
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# emit_save.ludic — save() / load() snapshot of the whole ECS world. The
|
||||
# compiler writes the entity/component half itself (it knows the shape) and
|
||||
# hands the open file to rt_save_state/rt_load_state for the runtime's own
|
||||
# state. Mirrors compiler/back/ir_save.c. save()->@L_save, load()->@L_load.
|
||||
|
||||
var g_iok: int = 0
|
||||
|
||||
# The snapshot is a fixed sequence of (region, byte-length) blocks; the same list
|
||||
# feeds two targets — a file (save/load via fwrite/fread) and a memory buffer
|
||||
# (world_save/world_load via memcpy, NETWORKING-DESIGN §5 N1). g_snap_mode picks
|
||||
# which; buffer modes thread a running i64 offset (@g_off) so world_save returns
|
||||
# the total byte count and world_load reads the identical layout back.
|
||||
# NOTE: a string initializer on a module `ptr` var lowers to null (global_init),
|
||||
# so these are seeded at runtime in emit_snapshot before first use — never read
|
||||
# them uninitialized (a null string `==` would deref and crash the compiler).
|
||||
var g_snap_mode: pointer = null # "file" | "save" | "load" | "size"
|
||||
var g_off: pointer = null # current byte-offset register, buffer modes
|
||||
|
||||
function emit_io(fn2: pointer, p: pointer, bytes: pointer) -> void {
|
||||
if (g_snap_mode == "file") {
|
||||
let r = `%io{itoa(g_iok)}`; g_iok = g_iok + 1
|
||||
emit(" "); emit(r); emit(" = call i64 @"); emit(fn2); emit("(ptr "); emit(p); emit(", i64 1, i64 "); emit(bytes); emit(", ptr %f)\n")
|
||||
return
|
||||
}
|
||||
if (g_snap_mode == "size") { # accumulate the offset only, no copy
|
||||
let noff = `%ioff{itoa(g_iok)}`
|
||||
emit(" "); emit(noff); emit(" = add i64 "); emit(g_off); emit(", "); emit(bytes); emit("\n")
|
||||
g_off = noff
|
||||
g_iok = g_iok + 1
|
||||
return
|
||||
}
|
||||
# buffer mode: dst/src is %buf + g_off, copy `bytes`, then advance the cursor
|
||||
let addr = `%ioa{itoa(g_iok)}`
|
||||
emit(" "); emit(addr); emit(" = getelementptr inbounds i8, ptr %buf, i64 "); emit(g_off); emit("\n")
|
||||
if (g_snap_mode == "save") {
|
||||
emit(" call ptr @memcpy(ptr "); emit(addr); emit(", ptr "); emit(p); emit(", i64 "); emit(bytes); emit(")\n")
|
||||
} else {
|
||||
emit(" call ptr @memcpy(ptr "); emit(p); emit(", ptr "); emit(addr); emit(", i64 "); emit(bytes); emit(")\n")
|
||||
}
|
||||
let noff = `%ioff{itoa(g_iok)}`
|
||||
emit(" "); emit(noff); emit(" = add i64 "); emit(g_off); emit(", "); emit(bytes); emit("\n")
|
||||
g_off = noff
|
||||
g_iok = g_iok + 1
|
||||
}
|
||||
|
||||
function emit_snapshot_blocks(fn2: pointer) -> void {
|
||||
g_iok = 0
|
||||
g_off = "0"
|
||||
let me = itoa(MAX_ENT)
|
||||
emit_io(fn2, "@L_entc", "4")
|
||||
emit_io(fn2, "@L_freen", "4")
|
||||
emit(" %nalive = mul i64 "); emit(me); emit(", 4\n")
|
||||
emit_io(fn2, "@L_alive", "%nalive")
|
||||
emit_io(fn2, "@L_freelist", "%nalive")
|
||||
emit_io(fn2, "@L_kind", "%nalive")
|
||||
# N3: an @Owned world snapshots its per-entity owners too, so rollback/replication
|
||||
# round-trips ownership (like @L_kind). Gated, so non-@Owned snapshots are unchanged.
|
||||
if net_has_owned() { emit_io(fn2, "@L_owner_arr", "%nalive") }
|
||||
var i = 0
|
||||
while i < len(prog) { if prog[i].kind == N_VAR { emit_io(fn2, `@g_{prog[i].s}`, "4") }; i = i + 1 }
|
||||
var ci = 0
|
||||
i = 0
|
||||
while i < len(prog) {
|
||||
if prog[i].kind == N_COMP {
|
||||
let c = prog[i].s
|
||||
let csz = `%csz{itoa(ci)}`
|
||||
emit(" "); emit(csz); emit(" = ptrtoint ptr getelementptr (%Cmp_"); emit(c); emit(", ptr null, i32 "); emit(me); emit(") to i64\n")
|
||||
emit_io(fn2, `@S_{c}`, csz)
|
||||
emit_io(fn2, `@H_{c}`, me)
|
||||
ci = ci + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
function emit_snapshot() -> void {
|
||||
g_snap_mode = "file" # seed (module ptr inits are null)
|
||||
emith("@.sav_path = private unnamed_addr constant [10 x i8] c\"ludic.sav\\00\"\n")
|
||||
emith("@.sav_wb = private unnamed_addr constant [3 x i8] c\"wb\\00\"\n")
|
||||
emith("@.sav_rb = private unnamed_addr constant [3 x i8] c\"rb\\00\"\n")
|
||||
let has_save = (find_fn("rt_save_state") != null)
|
||||
let has_load = (find_fn("rt_load_state") != null)
|
||||
|
||||
emit("define void @L_save() {\nentry:\n")
|
||||
emit(" %f = call ptr @fopen(ptr @.sav_path, ptr @.sav_wb)\n")
|
||||
emit(" %bad = icmp eq ptr %f, null\n")
|
||||
emit(" br i1 %bad, label %out, label %go\ngo:\n")
|
||||
emit_snapshot_blocks("fwrite")
|
||||
if has_save { emit(" call void @fn_rt_save_state(ptr %f)\n") }
|
||||
emit(" %c = call i32 @fclose(ptr %f)\n br label %out\nout:\n ret void\n}\n\n")
|
||||
|
||||
emit("define i32 @L_load() {\nentry:\n")
|
||||
emit(" %f = call ptr @fopen(ptr @.sav_path, ptr @.sav_rb)\n")
|
||||
emit(" %bad = icmp eq ptr %f, null\n")
|
||||
emit(" br i1 %bad, label %miss, label %go\nmiss:\n ret i32 0\ngo:\n")
|
||||
emit_snapshot_blocks("fread")
|
||||
if has_load { emit(" call void @fn_rt_load_state(ptr %f)\n") }
|
||||
emit(" %c = call i32 @fclose(ptr %f)\n ret i32 1\n}\n\n")
|
||||
|
||||
# world_save(buf) -> int / world_load(buf, len): the same whole-world snapshot,
|
||||
# to a caller-owned memory buffer instead of a file (NETWORKING-DESIGN §5 N1) —
|
||||
# the rollback/replication substrate. No rt_ hook: this is the ECS world only
|
||||
# (entities, components, vars), which is what a peer replicates or a rollback
|
||||
# restores; the runtime's windowing state stays local. world_save returns the
|
||||
# byte count written; the caller sizes the buffer with world_size().
|
||||
emit("define i32 @L_world_save(ptr %buf) {\nentry:\n")
|
||||
g_snap_mode = "save"
|
||||
emit_snapshot_blocks("")
|
||||
let sret = `%wsn{itoa(g_iok)}`
|
||||
emit(" "); emit(sret); emit(" = trunc i64 "); emit(g_off); emit(" to i32\n")
|
||||
emit(" ret i32 "); emit(sret); emit("\n}\n\n")
|
||||
|
||||
emit("define void @L_world_load(ptr %buf, i32 %len) {\nentry:\n")
|
||||
g_snap_mode = "load"
|
||||
emit_snapshot_blocks("")
|
||||
emit(" ret void\n}\n\n")
|
||||
|
||||
# world_size() -> int: the exact byte count a full snapshot needs, so a caller
|
||||
# can size the buffer before world_save. Same block walk, offset-only.
|
||||
emit("define i32 @L_world_size() {\nentry:\n")
|
||||
g_snap_mode = "size"
|
||||
emit_snapshot_blocks("")
|
||||
let zret = `%wzn{itoa(g_iok)}`
|
||||
emit(" "); emit(zret); emit(" = trunc i64 "); emit(g_off); emit(" to i32\n")
|
||||
emit(" ret i32 "); emit(zret); emit("\n}\n\n")
|
||||
g_snap_mode = "file"
|
||||
}
|
||||
212
selfhost/backend/game/emit_spawn.ludic
Normal file
212
selfhost/backend/game/emit_spawn.ludic
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
# emit_spawn.ludic — spawn / despawn / self(), and seeding a component's fields
|
||||
# from its declared defaults plus any per-spawn overrides.
|
||||
|
||||
function emit_init_component(e: pointer, comp: pointer, rec: Node) -> void {
|
||||
let me = itoa(MAX_ENT)
|
||||
let c = find_comp(comp)
|
||||
if (c == null) { perr(`spawn: unknown property {comp}`) }
|
||||
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(comp); emit(", i32 0, i32 "); emit(e); emit("\n")
|
||||
emit(" store i8 1, ptr "); emit(hp); emit("\n")
|
||||
let slot = nreg()
|
||||
emit(" "); emit(slot); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(comp); emit("], ptr @S_"); emit(comp); emit(", i32 0, i32 "); emit(e); emit("\n")
|
||||
# defaults
|
||||
var f = 0
|
||||
while f < len(c.kids) {
|
||||
let fd = c.kids[f]
|
||||
let addr = nreg()
|
||||
emit(" "); emit(addr); emit(" = getelementptr inbounds %Cmp_"); emit(comp); emit(", ptr "); emit(slot); emit(", i32 0, i32 "); emit(itoa(f)); emit("\n")
|
||||
let lt = llty(fd.ty)
|
||||
var v = "0"
|
||||
if (lt == "ptr") { v = "null" }
|
||||
if (fd.a != null) { let dv = emit_expr(fd.a); v = dv.code }
|
||||
emit(" store "); emit(lt); emit(" "); emit(v); emit(", ptr "); emit(addr); emit("\n")
|
||||
f = f + 1
|
||||
}
|
||||
# per-spawn overrides
|
||||
if (rec != null) {
|
||||
var j = 0
|
||||
while j < len(rec.kids) {
|
||||
let fi = rec.kids[j]
|
||||
let fidx = field_index(c, fi.s)
|
||||
if fidx >= 0 {
|
||||
let addr = nreg()
|
||||
emit(" "); emit(addr); emit(" = getelementptr inbounds %Cmp_"); emit(comp); emit(", ptr "); emit(slot); emit(", i32 0, i32 "); emit(itoa(fidx)); emit("\n")
|
||||
let dv = emit_expr(fi.a)
|
||||
emit(" store "); emit(llty(field_type(c, fi.s))); emit(" "); emit(dv.code); emit(", ptr "); emit(addr); emit("\n")
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
}
|
||||
# @OnAttach(Property) runs once the property is attached and seeded
|
||||
let ab = onattach_body(comp)
|
||||
if (ab != null) {
|
||||
let save = nloc
|
||||
let vslot = emit_alloca("ptr")
|
||||
emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n")
|
||||
loc_push(comp, vslot, comp)
|
||||
emit_block(ab)
|
||||
nloc = save
|
||||
}
|
||||
# EV1: a @Public attach hook fires prop_<P>_attach with the entity
|
||||
let aev = `prop_{comp}_attach`
|
||||
if (find_event(aev) != null) { emit(" call void @ev_"); emit(aev); emit("(i32 "); emit(e); emit(")\n") }
|
||||
}
|
||||
|
||||
# bind each of a model's properties to entity `e`'s component storage, so an
|
||||
# @OnSpawn hook body can address them by name (like a query binding for one entity).
|
||||
function emit_bind_props(model: Node, e: pointer) -> void {
|
||||
let me = itoa(MAX_ENT)
|
||||
var c = 0
|
||||
while c < len(model.kids) {
|
||||
let pname = model.kids[c].s
|
||||
let slot = nreg()
|
||||
emit(" "); emit(slot); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(pname); emit("], ptr @S_"); emit(pname); emit(", i32 0, i32 "); emit(e); emit("\n")
|
||||
let vslot = emit_alloca("ptr")
|
||||
emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n")
|
||||
loc_push(pname, vslot, pname)
|
||||
c = c + 1
|
||||
}
|
||||
}
|
||||
|
||||
function emit_spawn(st: Node) -> pointer {
|
||||
let e = emit_bind("call i32 @L_alloc()")
|
||||
let ak = find_arch_id(st.s)
|
||||
if ak > 0 {
|
||||
let me = itoa(MAX_ENT)
|
||||
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 "); emit(e); emit("\n")
|
||||
emit(" store i32 "); emit(itoa(ak)); emit(", ptr "); emit(kp); emit("\n")
|
||||
let arch = find_arch(st.s)
|
||||
var c = 0
|
||||
while c < len(arch.kids) {
|
||||
let cn = arch.kids[c].s
|
||||
var rec = null
|
||||
var i = 0
|
||||
while i < len(st.kids) { if (st.kids[i].s == cn) { rec = st.kids[i].a }; i = i + 1 }
|
||||
emit_init_component(e, cn, rec)
|
||||
c = c + 1
|
||||
}
|
||||
let ob = onspawn_body(st.s) # @OnSpawn(Model) hook runs after init
|
||||
if (ob != null) {
|
||||
let save = nloc
|
||||
emit_bind_props(arch, e)
|
||||
emit_block(ob)
|
||||
nloc = save
|
||||
}
|
||||
# EV1: a @Public spawn hook also fires the public event model_<M>_spawn, so
|
||||
# mods (native or foreign, over the ABI) see the entity born.
|
||||
let sev = `model_{st.s}_spawn`
|
||||
if (find_event(sev) != null) { emit(" call void @ev_"); emit(sev); emit("(i32 "); emit(e); emit(")\n") }
|
||||
} else {
|
||||
var i = 0
|
||||
while i < len(st.kids) { emit_init_component(e, st.kids[i].s, st.kids[i].a); i = i + 1 }
|
||||
}
|
||||
return e # the new entity id (for ludic_spawn_<M>)
|
||||
}
|
||||
|
||||
function emit_despawn(st: Node) -> void {
|
||||
let v = emit_expr(st.a)
|
||||
# @OnDespawn: dispatch on the entity's kind and run the matching model's hook
|
||||
if len(g_ondespawn) > 0 {
|
||||
let me = itoa(MAX_ENT)
|
||||
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 "); emit(v.code); emit("\n")
|
||||
let kind = emit_bind(`load i32, ptr {kp}`)
|
||||
var i = 0
|
||||
while i < len(g_ondespawn) {
|
||||
let mname = g_ondespawn[i].s
|
||||
let c = emit_bind(`icmp eq i32 {kind}, {itoa(find_arch_id(mname))}`)
|
||||
let yes = lbl("dh"); let no = lbl("dhn")
|
||||
emit(" br i1 "); emit(c); emit(", label %"); emit(yes); emit(", label %"); emit(no); emit("\n")
|
||||
emit(yes); emit(":\n call void @on_despawn_"); emit(mname); emit("(i32 "); emit(v.code); emit(", i32 0)\n") # reason = EndReason.Despawned
|
||||
let dev = `model_{mname}_despawn` # EV1: @Public despawn event
|
||||
if (find_event(dev) != null) { emit(" call void @ev_"); emit(dev); emit("(i32 "); emit(v.code); emit(", i32 0)\n") }
|
||||
emit(" br label %"); emit(no); emit("\n"); emit(no); emit(":\n")
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
if len(g_events) > 0 { emit(" call void @ludic_sweep_entity(i32 "); emit(v.code); emit(")\n") } # EV5: drop entity-scoped listeners
|
||||
emit(" call void @L_free_entity(i32 "); emit(v.code); emit(")\n")
|
||||
}
|
||||
|
||||
# attach P on e [{ overrides }] — add a property to a live entity. Structural
|
||||
# (not a toggle): seeds the property's fields and fires @OnAttach, but only on a
|
||||
# real transition — if the entity already has the property it is a no-op, so the
|
||||
# hook fires once per genuine attach (flecs/Bevy "real add" semantics).
|
||||
function emit_attach(st: Node) -> void {
|
||||
let ev = emit_expr(st.a)
|
||||
let me = itoa(MAX_ENT)
|
||||
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n")
|
||||
let cur = emit_bind(`load i8, ptr {hp}`)
|
||||
let isnew = emit_bind(`icmp eq i8 {cur}, 0`)
|
||||
let doit = lbl("attach"); let done = lbl("attdone")
|
||||
emit(" br i1 "); emit(isnew); emit(", label %"); emit(doit); emit(", label %"); emit(done); emit("\n")
|
||||
emit(doit); emit(":\n"); g_term = false
|
||||
emit_init_component(ev.code, st.s, st.b) # sets has=1, seeds defaults+overrides, fires @OnAttach
|
||||
if not g_term { emit(" br label %"); emit(done); emit("\n") }
|
||||
emit(done); emit(":\n"); g_term = false
|
||||
}
|
||||
|
||||
# detach P on e — remove a property from a live entity. Fires @OnDetach with the
|
||||
# property bound by name (its data still lives in @S_ storage, so the teardown
|
||||
# body reads the outgoing value), then clears the has-flag so queries skip it.
|
||||
# Only fires on a real transition; detaching an absent property is a no-op.
|
||||
function emit_detach(st: Node) -> void {
|
||||
let ev = emit_expr(st.a)
|
||||
let me = itoa(MAX_ENT)
|
||||
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n")
|
||||
let cur = emit_bind(`load i8, ptr {hp}`)
|
||||
let here = emit_bind(`icmp ne i8 {cur}, 0`)
|
||||
let doit = lbl("detach"); let done = lbl("detdone")
|
||||
emit(" br i1 "); emit(here); emit(", label %"); emit(doit); emit(", label %"); emit(done); emit("\n")
|
||||
emit(doit); emit(":\n"); g_term = false
|
||||
emit(" store i8 0, ptr "); emit(hp); emit("\n") # clear has-flag (data persists in @S_)
|
||||
let hb = ondetach_body(st.s) # @OnDetach reads the outgoing value
|
||||
if (hb != null) {
|
||||
let save = nloc
|
||||
let slot = nreg(); emit(" "); emit(slot); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(st.s); emit("], ptr @S_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n")
|
||||
let vslot = emit_alloca("ptr")
|
||||
emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n")
|
||||
loc_push(st.s, vslot, st.s)
|
||||
emit_block(hb)
|
||||
nloc = save
|
||||
}
|
||||
let dev = `prop_{st.s}_detach` # EV1: @Public detach event
|
||||
if (find_event(dev) != null) { emit(" call void @ev_"); emit(dev); emit("(i32 "); emit(ev.code); emit(")\n") }
|
||||
if not g_term { emit(" br label %"); emit(done); emit("\n") }
|
||||
emit(done); emit(":\n"); g_term = false
|
||||
}
|
||||
|
||||
# enable/disable. `<P> on <e>` toggles a property's has-flag on an entity (its
|
||||
# data persists, so re-enabling restores it, and queries already skip a cleared
|
||||
# flag). A bare `<Model>` / `<Handler>` flips a global enabled flag.
|
||||
function emit_toggle(st: Node) -> void {
|
||||
var val = "0"; if st.ival == 1 { val = "1" }
|
||||
if (st.ty != null) and (st.ty == "layer") { # enable/disable layer L
|
||||
emit(" store i32 "); emit(val); emit(", ptr @LE_"); emit(st.s); emit("\n")
|
||||
var lev = `layer_{st.s}_hide`; if st.ival == 1 { lev = `layer_{st.s}_show` } # public layer -> event
|
||||
if (find_event(lev) != null) { emit(" call void @ev_"); emit(lev); emit("()\n") }
|
||||
return
|
||||
}
|
||||
if (st.a != null) {
|
||||
let ev = emit_expr(st.a)
|
||||
let me = itoa(MAX_ENT)
|
||||
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n")
|
||||
emit(" store i8 "); emit(val); emit(", ptr "); emit(hp); emit("\n")
|
||||
var hb: Node = null
|
||||
if st.ival == 1 { hb = onenable_body(st.s) } else { hb = ondisable_body(st.s) }
|
||||
if (hb != null) {
|
||||
let save = nloc
|
||||
let slot = nreg(); emit(" "); emit(slot); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(st.s); emit("], ptr @S_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n")
|
||||
let vslot = emit_alloca("ptr")
|
||||
emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n")
|
||||
loc_push(st.s, vslot, st.s)
|
||||
emit_block(hb)
|
||||
nloc = save
|
||||
}
|
||||
# EV1: a @Public enable/disable hook fires prop_<P>_enable / prop_<P>_disable
|
||||
var tev = `prop_{st.s}_disable`; if st.ival == 1 { tev = `prop_{st.s}_enable` }
|
||||
if (find_event(tev) != null) { emit(" call void @ev_"); emit(tev); emit("(i32 "); emit(ev.code); emit(")\n") }
|
||||
} else {
|
||||
var g = "@HE_"; if is_model(st.s) { g = "@ME_" } # model vs handler
|
||||
emit(" store i32 "); emit(val); emit(", ptr "); emit(g); emit(st.s); emit("\n")
|
||||
}
|
||||
}
|
||||
125
selfhost/backend/game/emit_ui.ludic
Normal file
125
selfhost/backend/game/emit_ui.ludic
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# emit_ui.ludic — a `ui` block is data: the compiler flattens the widget tree
|
||||
# and emits @ui_build(), which hands every property to the Ludic UI runtime via
|
||||
# rt_ui_set(index, key, value). Mirrors compiler/back/ir_ui.c. Layout/drawing/
|
||||
# focus all live in runtime/native/ui.ludic.
|
||||
|
||||
var uiw: []Node # flattened widgets, pre-order
|
||||
var uiw_parent: []int
|
||||
var ui_roots: []int # first-widget index of each ui block
|
||||
|
||||
function ui_wtype(w: Node) -> int {
|
||||
let s = w.s
|
||||
if (s == "panel") { return 0 }; if (s == "col") { return 1 }; if (s == "row") { return 2 }
|
||||
if (s == "label") { return 3 }; if (s == "button") { return 4 }
|
||||
if (s == "image") { return 5 }; if (s == "spacer") { return 6 }
|
||||
return 0
|
||||
}
|
||||
function ui_prop(w: Node, key: pointer) -> Node {
|
||||
var i = 0
|
||||
while i < len(w.b.kids) { if (w.b.kids[i].s == key) { return w.b.kids[i].a }; i = i + 1 }
|
||||
return null
|
||||
}
|
||||
function ui_flatten(w: Node, parent: int) -> void {
|
||||
let idx = len(uiw)
|
||||
push(uiw, w); push(uiw_parent, parent)
|
||||
var i = 0
|
||||
while i < len(w.kids) { ui_flatten(w.kids[i], idx); i = i + 1 }
|
||||
}
|
||||
function ui_flatten_all() -> void {
|
||||
uiw = new []Node; uiw_parent = new []int; ui_roots = new []int
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
if prog[i].kind == N_UI and prog[i].ival == 1 { push(ui_roots, len(uiw)); ui_flatten(prog[i].a, 0 - 1) }
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
function has_ui() -> bool {
|
||||
var i = 0
|
||||
while i < len(prog) { if prog[i].kind == N_UI and prog[i].ival == 1 { return true }; i = i + 1 }
|
||||
return false
|
||||
}
|
||||
|
||||
# index of a UI_<name>: a ui block's root, or a widget's id=
|
||||
function ui_index_of(nm: pointer) -> int {
|
||||
let s = nm[3..len(nm)] # strip "UI_"
|
||||
let u = 0; var bi = 0
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
if prog[i].kind == N_UI and prog[i].ival == 1 { if (prog[i].s == s) { return ui_roots[bi] }; bi = bi + 1 }
|
||||
i = i + 1
|
||||
}
|
||||
i = 0
|
||||
while i < len(uiw) {
|
||||
let idp = ui_prop(uiw[i], "id")
|
||||
if (idp != null) { if idp.kind == E_ID and (idp.s == s) { return i } }
|
||||
i = i + 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
function is_ui_ident(nm: pointer) -> bool {
|
||||
return len(nm) > 3 and nm[0] == 85 and nm[1] == 73 and nm[2] == 95 # "UI_"
|
||||
}
|
||||
|
||||
function ll_ui_set(idx: int, key: int, val: pointer) -> void {
|
||||
emit(" call void @fn_rt_ui_set(i32 "); emit(itoa(idx)); emit(", i32 "); emit(itoa(key)); emit(", i32 "); emit(val); emit(")\n")
|
||||
}
|
||||
function ui_prop_key(k: pointer) -> int {
|
||||
if (k == "w") { return 2 }; if (k == "h") { return 3 }; if (k == "x") { return 4 }; if (k == "y") { return 5 }
|
||||
if (k == "pad") { return 7 }; if (k == "gap") { return 8 }; if (k == "bg") { return 9 }; if (k == "fg") { return 10 }
|
||||
if (k == "border") { return 11 }; if (k == "grow") { return 13 }; if (k == "font") { return 14 }; if (k == "size") { return 15 }
|
||||
if (k == "inset") { return 16 }; if (k == "focus") { return 17 }
|
||||
return 0 - 1
|
||||
}
|
||||
|
||||
function emit_ui_build() -> void {
|
||||
ui_flatten_all()
|
||||
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0
|
||||
let fbody = buf_new()
|
||||
falloc = buf_new()
|
||||
let saved = code
|
||||
code = fbody
|
||||
emit(" call void @fn_rt_ui_reset(i32 "); emit(itoa(len(uiw))); emit(")\n")
|
||||
var i = 0
|
||||
while i < len(uiw) {
|
||||
let w = uiw[i]
|
||||
let t = ui_wtype(w)
|
||||
ll_ui_set(i, 0, itoa(t))
|
||||
ll_ui_set(i, 1, itoa(uiw_parent[i]))
|
||||
if t == 4 { ll_ui_set(i, 17, "1") }
|
||||
var p = 0
|
||||
while p < len(w.b.kids) {
|
||||
let pr = w.b.kids[p]
|
||||
let k = pr.s
|
||||
let v = pr.a
|
||||
if (k == "id") { p = p + 1; continue }
|
||||
if (k == "text") {
|
||||
let sv = emit_expr(v)
|
||||
emit(" call void @fn_rt_ui_static_text(i32 "); emit(itoa(i)); emit(", ptr "); emit(sv.code); emit(")\n")
|
||||
} else { if (k == "skin") or (k == "image") {
|
||||
let sv = emit_expr(v)
|
||||
let r = emit_bind(`call i32 @fn_rt_image_load(ptr {sv.code})`)
|
||||
if (k == "skin") { ll_ui_set(i, 20, r) } else { ll_ui_set(i, 19, r) }
|
||||
} else { if (k == "align") {
|
||||
if v.kind == E_ID {
|
||||
var a = 0
|
||||
if (v.s == "center") { a = 1 }; if (v.s == "end") { a = 2 }
|
||||
ll_ui_set(i, 12, itoa(a))
|
||||
} else { let vv = emit_expr(v); ll_ui_set(i, 12, vv.code) }
|
||||
} else {
|
||||
let key = ui_prop_key(k)
|
||||
if key >= 0 {
|
||||
let vv = emit_expr(v)
|
||||
ll_ui_set(i, key, vv.code)
|
||||
if key == 4 or key == 5 { ll_ui_set(i, 6, "1") }
|
||||
}
|
||||
} } }
|
||||
p = p + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
code = saved
|
||||
emit("define void @ui_build() {\nentry:\n")
|
||||
emit(buf_str(falloc))
|
||||
emit(buf_str(fbody))
|
||||
emit(" ret void\n}\n\n")
|
||||
}
|
||||
377
selfhost/backend/game/emit_world.ludic
Normal file
377
selfhost/backend/game/emit_world.ludic
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
# 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)
|
||||
emith("declare i32 @strcmp(ptr, ptr)\n")
|
||||
|
||||
# EV7 — schema opening: a mod can register a brand-new component at runtime.
|
||||
# Compile-time components take prop ids 0..NC-1; mod-defined ones take NC.. and
|
||||
# live in these parallel registries (fixed capacity 32). Storage is a flat
|
||||
# malloc'd [MAX_ENT x nfields x i32] with a parallel has-flag array. get/set/has
|
||||
# and prop_id fall through to this table for a prop id >= NC.
|
||||
var ncomp = 0
|
||||
var ci0 = 0
|
||||
while ci0 < len(prog) { if prog[ci0].kind == N_COMP { ncomp = ncomp + 1 }; ci0 = ci0 + 1 }
|
||||
let NC = itoa(ncomp)
|
||||
emith("@dyn_count = global i32 0\n")
|
||||
emith("@dynS = global [32 x ptr] zeroinitializer\n") # storage base per dyn component
|
||||
emith("@dynH = global [32 x ptr] zeroinitializer\n") # has-flag array per dyn component
|
||||
emith("@dynF = global [32 x i32] zeroinitializer\n") # field count per dyn component
|
||||
emith("@dynName = global [32 x ptr] zeroinitializer\n") # name per dyn component
|
||||
|
||||
# ludic_prop_id(name): strcmp against each property's name constant
|
||||
emit("define i32 @ludic_prop_id(ptr %name) {\nentry:\n")
|
||||
var k = 0
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
if prog[i].kind == N_COMP {
|
||||
let sc = emit_str_const(prog[i].s); let sk = itoa(k)
|
||||
emit(" %c"); emit(sk); emit(" = call i32 @strcmp(ptr %name, ptr "); emit(sc); emit(")\n")
|
||||
emit(" %e"); emit(sk); emit(" = icmp eq i32 %c"); emit(sk); emit(", 0\n")
|
||||
emit(" br i1 %e"); emit(sk); emit(", label %h"); emit(sk); emit(", label %n"); emit(sk); emit("\n")
|
||||
emit("h"); emit(sk); emit(":\n ret i32 "); emit(sk); emit("\n")
|
||||
emit("n"); emit(sk); emit(":\n")
|
||||
k = k + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
# EV7: not a compile-time component — search the dynamic (mod-registered) names
|
||||
emit(" %dpi = alloca i32\n store i32 0, ptr %dpi\n br label %dpl\n")
|
||||
emit("dpl:\n %di = load i32, ptr %dpi\n %dn = load i32, ptr @dyn_count\n %dg = icmp slt i32 %di, %dn\n br i1 %dg, label %dpb, label %dpnone\n")
|
||||
emit("dpb:\n %dnp = getelementptr inbounds [32 x ptr], ptr @dynName, i32 0, i32 %di\n %dname = load ptr, ptr %dnp\n %dcmp = call i32 @strcmp(ptr %name, ptr %dname)\n %deq = icmp eq i32 %dcmp, 0\n br i1 %deq, label %dphit, label %dpnext\n")
|
||||
emit("dphit:\n %drid = add i32 %di, "); emit(NC); emit("\n ret i32 %drid\n")
|
||||
emit("dpnext:\n %di1 = add i32 %di, 1\n store i32 %di1, ptr %dpi\n br label %dpl\n")
|
||||
emit("dpnone:\n ret i32 -1\n}\n\n")
|
||||
|
||||
# ludic_field_id(prop, name): within the matched property, strcmp each field name
|
||||
emit("define i32 @ludic_field_id(i32 %p, ptr %name) {\nentry:\n")
|
||||
k = 0; i = 0
|
||||
while i < len(prog) {
|
||||
if prog[i].kind == N_COMP {
|
||||
let c = prog[i]; let sk = itoa(k)
|
||||
emit(" %pm"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n")
|
||||
emit(" br i1 %pm"); emit(sk); emit(", label %pk"); emit(sk); emit(", label %pn"); emit(sk); emit("\n")
|
||||
emit("pk"); emit(sk); emit(":\n")
|
||||
var f = 0
|
||||
while f < len(c.kids) {
|
||||
let fc = emit_str_const(c.kids[f].s); let fk = `{sk}_{itoa(f)}`
|
||||
emit(" %fc"); emit(fk); emit(" = call i32 @strcmp(ptr %name, ptr "); emit(fc); emit(")\n")
|
||||
emit(" %fe"); emit(fk); emit(" = icmp eq i32 %fc"); emit(fk); emit(", 0\n")
|
||||
emit(" br i1 %fe"); emit(fk); emit(", label %fh"); emit(fk); emit(", label %fn"); emit(fk); emit("\n")
|
||||
emit("fh"); emit(fk); emit(":\n ret i32 "); emit(itoa(f)); emit("\n")
|
||||
emit("fn"); emit(fk); emit(":\n")
|
||||
f = f + 1
|
||||
}
|
||||
emit(" ret i32 -1\n")
|
||||
emit("pn"); emit(sk); emit(":\n")
|
||||
k = k + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
emit(" ret i32 -1\n}\n\n")
|
||||
|
||||
# ludic_get / ludic_set / ludic_has dispatch prop -> @S_/@H_ storage; the field
|
||||
# address is slot + field*4 (integer fields).
|
||||
# ludic_get/ludic_set dispatch prop -> component storage, then the field id to a
|
||||
# constant struct GEP (so mixed layouts and ptr/byte fields are addressed
|
||||
# correctly, not assumed 4-byte). Values cross the ABI as i64: int/bool/fixed
|
||||
# sign-extend, byte zero-extends, ptr round-trips through ptrtoint/inttoptr.
|
||||
emit("define i64 @ludic_get(i32 %e, i32 %p, i32 %f) {\nentry:\n")
|
||||
k = 0; i = 0
|
||||
while i < len(prog) {
|
||||
if prog[i].kind == N_COMP {
|
||||
let c = prog[i]; let cn = c.s; let sk = itoa(k)
|
||||
emit(" %m"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n")
|
||||
emit(" br i1 %m"); emit(sk); emit(", label %g"); emit(sk); emit(", label %gn"); emit(sk); emit("\n")
|
||||
emit("g"); emit(sk); emit(":\n")
|
||||
emit(" %s"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(cn); emit("], ptr @S_"); emit(cn); emit(", i32 0, i32 %e\n")
|
||||
var fj = 0
|
||||
while fj < len(c.kids) {
|
||||
let ft = llty(c.kids[fj].ty); let fk = `{sk}_{itoa(fj)}`
|
||||
emit(" %gm"); emit(fk); emit(" = icmp eq i32 %f, "); emit(itoa(fj)); emit("\n")
|
||||
emit(" br i1 %gm"); emit(fk); emit(", label %gf"); emit(fk); emit(", label %gk"); emit(fk); emit("\n")
|
||||
emit("gf"); emit(fk); emit(":\n")
|
||||
emit(" %ga"); emit(fk); emit(" = getelementptr inbounds %Cmp_"); emit(cn); emit(", ptr %s"); emit(sk); emit(", i32 0, i32 "); emit(itoa(fj)); emit("\n")
|
||||
if (ft == "ptr") {
|
||||
emit(" %gl"); emit(fk); emit(" = load ptr, ptr %ga"); emit(fk); emit("\n")
|
||||
emit(" %gr"); emit(fk); emit(" = ptrtoint ptr %gl"); emit(fk); emit(" to i64\n ret i64 %gr"); emit(fk); emit("\n")
|
||||
} else { if (ft == "i8") {
|
||||
emit(" %gl"); emit(fk); emit(" = load i8, ptr %ga"); emit(fk); emit("\n")
|
||||
emit(" %gr"); emit(fk); emit(" = zext i8 %gl"); emit(fk); emit(" to i64\n ret i64 %gr"); emit(fk); emit("\n")
|
||||
} else {
|
||||
emit(" %gl"); emit(fk); emit(" = load i32, ptr %ga"); emit(fk); emit("\n")
|
||||
emit(" %gr"); emit(fk); emit(" = sext i32 %gl"); emit(fk); emit(" to i64\n ret i64 %gr"); emit(fk); emit("\n")
|
||||
} }
|
||||
emit("gk"); emit(fk); emit(":\n")
|
||||
fj = fj + 1
|
||||
}
|
||||
emit(" ret i64 0\n")
|
||||
emit("gn"); emit(sk); emit(":\n")
|
||||
k = k + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
# EV7: prop id >= NC -> a mod-registered component; index its flat storage
|
||||
emit(" %gdyn = sub i32 %p, "); emit(NC); emit("\n")
|
||||
emit(" %gdlo = icmp sge i32 %gdyn, 0\n %gdc = load i32, ptr @dyn_count\n %gdhi = icmp slt i32 %gdyn, %gdc\n %gdok = and i1 %gdlo, %gdhi\n br i1 %gdok, label %gdyng, label %gdnone\n")
|
||||
emit("gdyng:\n %gsp = getelementptr inbounds [32 x ptr], ptr @dynS, i32 0, i32 %gdyn\n %gbase = load ptr, ptr %gsp\n")
|
||||
emit(" %gfp = getelementptr inbounds [32 x i32], ptr @dynF, i32 0, i32 %gdyn\n %gnf = load i32, ptr %gfp\n %grow = mul i32 %e, %gnf\n %gidx = add i32 %grow, %f\n")
|
||||
emit(" %gaddr = getelementptr inbounds i32, ptr %gbase, i32 %gidx\n %gv = load i32, ptr %gaddr\n %gr = sext i32 %gv to i64\n ret i64 %gr\n")
|
||||
emit("gdnone:\n ret i64 0\n}\n\n")
|
||||
|
||||
emit("define void @ludic_set(i32 %e, i32 %p, i32 %f, i64 %val) {\nentry:\n")
|
||||
k = 0; i = 0
|
||||
while i < len(prog) {
|
||||
if prog[i].kind == N_COMP {
|
||||
let c = prog[i]; let cn = c.s; let sk = itoa(k)
|
||||
emit(" %m"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n")
|
||||
emit(" br i1 %m"); emit(sk); emit(", label %g"); emit(sk); emit(", label %gn"); emit(sk); emit("\n")
|
||||
emit("g"); emit(sk); emit(":\n")
|
||||
emit(" %s"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(cn); emit("], ptr @S_"); emit(cn); emit(", i32 0, i32 %e\n")
|
||||
var fj = 0
|
||||
while fj < len(c.kids) {
|
||||
let ft = llty(c.kids[fj].ty); let fk = `{sk}_{itoa(fj)}`
|
||||
emit(" %sm"); emit(fk); emit(" = icmp eq i32 %f, "); emit(itoa(fj)); emit("\n")
|
||||
emit(" br i1 %sm"); emit(fk); emit(", label %sf"); emit(fk); emit(", label %sk"); emit(fk); emit("\n")
|
||||
emit("sf"); emit(fk); emit(":\n")
|
||||
emit(" %sa"); emit(fk); emit(" = getelementptr inbounds %Cmp_"); emit(cn); emit(", ptr %s"); emit(sk); emit(", i32 0, i32 "); emit(itoa(fj)); emit("\n")
|
||||
if (ft == "ptr") {
|
||||
emit(" %sp"); emit(fk); emit(" = inttoptr i64 %val to ptr\n")
|
||||
emit(" store ptr %sp"); emit(fk); emit(", ptr %sa"); emit(fk); emit("\n ret void\n")
|
||||
} else { if (ft == "i8") {
|
||||
emit(" %sb"); emit(fk); emit(" = trunc i64 %val to i8\n")
|
||||
emit(" store i8 %sb"); emit(fk); emit(", ptr %sa"); emit(fk); emit("\n ret void\n")
|
||||
} else {
|
||||
emit(" %sw"); emit(fk); emit(" = trunc i64 %val to i32\n")
|
||||
emit(" store i32 %sw"); emit(fk); emit(", ptr %sa"); emit(fk); emit("\n ret void\n")
|
||||
} }
|
||||
emit("sk"); emit(fk); emit(":\n")
|
||||
fj = fj + 1
|
||||
}
|
||||
emit(" ret void\n")
|
||||
emit("gn"); emit(sk); emit(":\n")
|
||||
k = k + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
# EV7: prop id >= NC -> a mod-registered component
|
||||
emit(" %sdyn = sub i32 %p, "); emit(NC); emit("\n")
|
||||
emit(" %sdlo = icmp sge i32 %sdyn, 0\n %sdc = load i32, ptr @dyn_count\n %sdhi = icmp slt i32 %sdyn, %sdc\n %sdok = and i1 %sdlo, %sdhi\n br i1 %sdok, label %sdyng, label %sdnone\n")
|
||||
emit("sdyng:\n %ssp = getelementptr inbounds [32 x ptr], ptr @dynS, i32 0, i32 %sdyn\n %sbase = load ptr, ptr %ssp\n")
|
||||
emit(" %sfp = getelementptr inbounds [32 x i32], ptr @dynF, i32 0, i32 %sdyn\n %snf = load i32, ptr %sfp\n %srow = mul i32 %e, %snf\n %sidx = add i32 %srow, %f\n")
|
||||
emit(" %saddr = getelementptr inbounds i32, ptr %sbase, i32 %sidx\n %sv = trunc i64 %val to i32\n store i32 %sv, ptr %saddr\n ret void\n")
|
||||
emit("sdnone:\n ret void\n}\n\n")
|
||||
|
||||
emit("define i32 @ludic_has(i32 %e, i32 %p) {\nentry:\n")
|
||||
k = 0; i = 0
|
||||
while i < len(prog) {
|
||||
if prog[i].kind == N_COMP {
|
||||
let cn = prog[i].s; let sk = itoa(k)
|
||||
emit(" %m"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n")
|
||||
emit(" br i1 %m"); emit(sk); emit(", label %g"); emit(sk); emit(", label %gn"); emit(sk); emit("\n")
|
||||
emit("g"); emit(sk); emit(":\n")
|
||||
emit(" %hp"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(cn); emit(", i32 0, i32 %e\n")
|
||||
emit(" %hv"); emit(sk); emit(" = load i8, ptr %hp"); emit(sk); emit("\n")
|
||||
emit(" %hr"); emit(sk); emit(" = zext i8 %hv"); emit(sk); emit(" to i32\n ret i32 %hr"); emit(sk); emit("\n")
|
||||
emit("gn"); emit(sk); emit(":\n")
|
||||
k = k + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
# EV7: prop id >= NC -> a mod-registered component's has-flag array
|
||||
emit(" %hdyn = sub i32 %p, "); emit(NC); emit("\n")
|
||||
emit(" %hdlo = icmp sge i32 %hdyn, 0\n %hdc = load i32, ptr @dyn_count\n %hdhi = icmp slt i32 %hdyn, %hdc\n %hdok = and i1 %hdlo, %hdhi\n br i1 %hdok, label %hdyng, label %hdnone\n")
|
||||
emit("hdyng:\n %hhp = getelementptr inbounds [32 x ptr], ptr @dynH, i32 0, i32 %hdyn\n %hh = load ptr, ptr %hhp\n %hslot = getelementptr inbounds i8, ptr %hh, i32 %e\n %hval = load i8, ptr %hslot\n %hres = zext i8 %hval to i32\n ret i32 %hres\n")
|
||||
emit("hdnone:\n ret i32 0\n}\n\n")
|
||||
|
||||
# ludic_register_prop(name, nfields) -> prop id — a mod declares a NEW component.
|
||||
# Allocates flat [MAX_ENT x nfields x i32] storage + a MAX_ENT has-flag array,
|
||||
# zeroed. The returned id works with get/set/has/attach exactly like a built-in.
|
||||
emit("define i32 @ludic_register_prop(ptr %name, i32 %nfields) {\nentry:\n")
|
||||
emit(" %dc = load i32, ptr @dyn_count\n %full = icmp slt i32 %dc, 32\n br i1 %full, label %do, label %rej\n")
|
||||
emit("do:\n %nf4 = mul i32 %nfields, 4\n %sz = mul i32 %nf4, "); emit(me); emit("\n %szl = sext i32 %sz to i64\n")
|
||||
emit(" %buf = call ptr @malloc(i64 %szl)\n call ptr @memset(ptr %buf, i32 0, i64 %szl)\n")
|
||||
emit(" %sp = getelementptr inbounds [32 x ptr], ptr @dynS, i32 0, i32 %dc\n store ptr %buf, ptr %sp\n")
|
||||
emit(" %hbuf = call ptr @malloc(i64 "); emit(me); emit(")\n call ptr @memset(ptr %hbuf, i32 0, i64 "); emit(me); emit(")\n")
|
||||
emit(" %hp = getelementptr inbounds [32 x ptr], ptr @dynH, i32 0, i32 %dc\n store ptr %hbuf, ptr %hp\n")
|
||||
emit(" %fp = getelementptr inbounds [32 x i32], ptr @dynF, i32 0, i32 %dc\n store i32 %nfields, ptr %fp\n")
|
||||
emit(" %np = getelementptr inbounds [32 x ptr], ptr @dynName, i32 0, i32 %dc\n store ptr %name, ptr %np\n")
|
||||
emit(" %id = add i32 %dc, "); emit(NC); emit("\n %dc1 = add i32 %dc, 1\n store i32 %dc1, ptr @dyn_count\n ret i32 %id\n")
|
||||
emit("rej:\n ret i32 -1\n}\n\n")
|
||||
|
||||
# ludic_attach_dyn / ludic_detach_dyn(entity, prop) — set/clear a mod-registered
|
||||
# component's has-flag on an entity (the dynamic analogue of attach/detach).
|
||||
emit("define void @ludic_attach_dyn(i32 %e, i32 %p) {\nentry:\n")
|
||||
emit(" %dyn = sub i32 %p, "); emit(NC); emit("\n %hp = getelementptr inbounds [32 x ptr], ptr @dynH, i32 0, i32 %dyn\n %h = load ptr, ptr %hp\n %slot = getelementptr inbounds i8, ptr %h, i32 %e\n store i8 1, ptr %slot\n ret void\n}\n\n")
|
||||
emit("define void @ludic_detach_dyn(i32 %e, i32 %p) {\nentry:\n")
|
||||
emit(" %dyn = sub i32 %p, "); emit(NC); emit("\n %hp = getelementptr inbounds [32 x ptr], ptr @dynH, i32 0, i32 %dyn\n %h = load ptr, ptr %hp\n %slot = getelementptr inbounds i8, ptr %h, i32 %e\n store i8 0, ptr %slot\n ret void\n}\n\n")
|
||||
|
||||
# ludic_entity_count / ludic_kind / ludic_model_id — a mod scans the world and
|
||||
# identifies each entity's model, then reads/writes it with get/set/has above.
|
||||
emit("define i32 @ludic_entity_count() {\nentry:\n %n = load i32, ptr @L_entc\n ret i32 %n\n}\n\n")
|
||||
emit("define i32 @ludic_kind(i32 %e) {\nentry:\n")
|
||||
emit(" %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n")
|
||||
emit(" %k = load i32, ptr %kp\n ret i32 %k\n}\n\n")
|
||||
emit("define i32 @ludic_model_id(ptr %name) {\nentry:\n")
|
||||
k = 0; i = 0
|
||||
while i < len(prog) {
|
||||
if prog[i].kind == N_ARCH {
|
||||
let sc = emit_str_const(prog[i].s); let sk = itoa(k)
|
||||
emit(" %mdc"); emit(sk); emit(" = call i32 @strcmp(ptr %name, ptr "); emit(sc); emit(")\n")
|
||||
emit(" %mde"); emit(sk); emit(" = icmp eq i32 %mdc"); emit(sk); emit(", 0\n")
|
||||
emit(" br i1 %mde"); emit(sk); emit(", label %mdh"); emit(sk); emit(", label %mdn"); emit(sk); emit("\n")
|
||||
emit("mdh"); emit(sk); emit(":\n ret i32 "); emit(itoa(find_arch_id(prog[i].s))); emit("\n")
|
||||
emit("mdn"); emit(sk); emit(":\n")
|
||||
k = k + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
emit(" ret i32 -1\n}\n\n")
|
||||
|
||||
# ludic_spawn(model_id) -> entity — a mod creates a new entity. Each model gets a
|
||||
# @ludic_spawn_<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 = i + 1
|
||||
}
|
||||
emit("define i32 @ludic_spawn(i32 %m) {\nentry:\n")
|
||||
k = 0; i = 0
|
||||
while i < len(prog) {
|
||||
if prog[i].kind == N_ARCH {
|
||||
let m = prog[i].s; let sk = itoa(k)
|
||||
emit(" %sm"); emit(sk); emit(" = icmp eq i32 %m, "); emit(itoa(find_arch_id(m))); emit("\n")
|
||||
emit(" br i1 %sm"); emit(sk); emit(", label %sh"); emit(sk); emit(", label %sn"); emit(sk); emit("\n")
|
||||
emit("sh"); emit(sk); emit(":\n %sr"); emit(sk); emit(" = call i32 @ludic_spawn_"); emit(m); emit("()\n ret i32 %sr"); emit(sk); emit("\n")
|
||||
emit("sn"); emit(sk); emit(":\n")
|
||||
k = k + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
emit(" ret i32 -1\n}\n\n")
|
||||
|
||||
# ludic_query_next(prop_id, from) -> the next live entity (>= from) that has the
|
||||
# property, or -1. A mod iterates: for (e = query_next(p, 0); e >= 0; e =
|
||||
# query_next(p, e+1)). Reuses ludic_has for the membership test.
|
||||
emit("define i32 @ludic_query_next(i32 %p, i32 %from) {\nentry:\n")
|
||||
emit(" %n = load i32, ptr @L_entc\n br label %loop\n")
|
||||
emit("loop:\n %e = phi i32 [ %from, %entry ], [ %e1, %cont ]\n")
|
||||
emit(" %go = icmp slt i32 %e, %n\n br i1 %go, label %body, label %none\n")
|
||||
emit("body:\n %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %e\n")
|
||||
emit(" %al = load i32, ptr %ap\n %isa = icmp ne i32 %al, 0\n br i1 %isa, label %chk, label %cont\n")
|
||||
emit("chk:\n %h = call i32 @ludic_has(i32 %e, i32 %p)\n %hit = icmp ne i32 %h, 0\n br i1 %hit, label %hitb, label %cont\n")
|
||||
emit("hitb:\n ret i32 %e\n")
|
||||
emit("cont:\n %e1 = add i32 %e, 1\n br label %loop\n")
|
||||
emit("none:\n ret i32 -1\n}\n\n")
|
||||
}
|
||||
|
||||
# N5 — the drivable sim (NETWORKING-DESIGN §5). The per-frame phases the auto-loop
|
||||
# runs are also exposed as callables, so a game that owns its `entry` loop can
|
||||
# drive the simulation itself (for prediction/rollback, replay, headless tests, or
|
||||
# AI). tick_fixed() runs the sim phases; tick_render() runs Render.
|
||||
function emit_tick_helpers() -> void {
|
||||
emit("define void @L_tick_fixed() {\nentry:\n")
|
||||
ll_t = 0; ll_lbl = 0
|
||||
emit_calls_for_phase("Input")
|
||||
emit_calls_for_phase("FixedUpdate")
|
||||
emit_calls_for_phase("Update")
|
||||
emit_calls_for_phase("LateUpdate")
|
||||
emit(" ret void\n}\n\n")
|
||||
emit("define void @L_tick_render() {\nentry:\n")
|
||||
ll_t = 0; ll_lbl = 0
|
||||
emit_calls_for_phase("Render")
|
||||
emit(" ret void\n}\n\n")
|
||||
}
|
||||
|
||||
# system functions + lifecycle hooks + the drivable tick helpers — shared by the
|
||||
# auto-loop game (emit_game_main) and an entry-driven game that owns its own loop.
|
||||
function emit_game_defs() -> void {
|
||||
var i = 0
|
||||
while i < len(prog) { if prog[i].kind == N_SYS { emit_system_fn(prog[i]) }; i = i + 1 }
|
||||
emit_despawn_hooks()
|
||||
emit_scene_hooks()
|
||||
emit_tick_helpers()
|
||||
}
|
||||
|
||||
function emit_game_main() -> void {
|
||||
emit_game_defs()
|
||||
|
||||
emit("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n")
|
||||
emit(" store i32 %argc, ptr @L_argc\n")
|
||||
emit(" store ptr %argv, ptr @L_argv\n")
|
||||
if (find_fn("rt_init") != null) { emit(" call void @fn_rt_init()\n") }
|
||||
emit_calls_for_phase("Start")
|
||||
# enter the start scene once, after boot: store its id and run its on-enter.
|
||||
if len(g_scenes) > 0 {
|
||||
emit(" store i32 "); emit(itoa(g_start_scene)); emit(", ptr @L_scene\n")
|
||||
var si = 0
|
||||
while si < len(g_scenes) {
|
||||
if (g_scenes[si].ival == g_start_scene) { emit(" call void @scene_enter_"); emit(g_scenes[si].s); emit("()\n") }
|
||||
si = si + 1
|
||||
}
|
||||
}
|
||||
if (find_event("program_start") != null) { emit(" call void @ev_program_start()\n") } # EV1: @Public @OnStart
|
||||
emit(" br label %loop\n")
|
||||
emit("loop:\n")
|
||||
let r = emit_bind("load i32, ptr @L_running")
|
||||
let rc = emit_bind(`icmp ne i32 {r}, 0`)
|
||||
if (find_fn("rt_running") != null) {
|
||||
let pr = emit_bind("call i32 @fn_rt_running()")
|
||||
let pc = emit_bind(`icmp ne i32 {pr}, 0`)
|
||||
let go = emit_bind(`and i1 {rc}, {pc}`)
|
||||
emit(" br i1 "); emit(go); emit(", label %body, label %done\n")
|
||||
} else {
|
||||
emit(" br i1 "); emit(rc); emit(", label %body, label %done\n")
|
||||
}
|
||||
emit("body:\n")
|
||||
if (find_fn("rt_poll") != null) {
|
||||
let k = emit_bind("call i32 @fn_rt_poll()")
|
||||
emit(" store i32 "); emit(k); emit(", ptr @L_key\n")
|
||||
}
|
||||
emit_calls_for_phase("Input")
|
||||
emit_calls_for_phase("FixedUpdate")
|
||||
emit_calls_for_phase("Update")
|
||||
emit_calls_for_phase("LateUpdate")
|
||||
emit_calls_for_phase("Render")
|
||||
let fcur = emit_bind("load i32, ptr @L_frame") # Time.frame(): count completed frames
|
||||
let fnext = emit_bind(`add i32 {fcur}, 1`)
|
||||
emit(" store i32 "); emit(fnext); emit(", ptr @L_frame\n")
|
||||
emit(" br label %loop\n")
|
||||
emit("done:\n")
|
||||
if len(g_ondespawn) > 0 { emit(" call void @L_despawn_all(i32 2)\n") } # LC1: every survivor's @OnDespawn fires with reason Quit
|
||||
emit_calls_for_phase("OnQuit") # @OnQuit shutdown hooks run once, before teardown
|
||||
if (find_event("program_quit") != null) { emit(" call void @ev_program_quit()\n") } # EV1: @Public @OnQuit
|
||||
if (find_fn("rt_shutdown") != null) { emit(" call void @fn_rt_shutdown()\n") }
|
||||
emit(" ret i32 0\n}\n")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue