feat(input,ecs): #79 Input.axis_i directional int + #84 world bounds
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 23s
ci / build-and-test (push) Successful in 2m23s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 28s

#79 — Input.axis_i(neg,pos) -> int returns a -1/0/1 movement intent from the
multi-key device set, so WASD-to-movement needs no bool->int glue and feeds an
int mover directly (dx = Input.axis_i('a','d')).

#84 — a Bounds config entity (rect + policy, from ludic.core) drives the
engine-owned world-bounds system (LateUpdate): clamp / wrap / bounce (clamp +
flip Body velocity) / kill (despawn a body fully outside). Reads the Collider
size so the box stays inside; off by default, spliced only when Bounds is
declared (byte-identical otherwise). Adds World.despawn(e) — the by-id
reflective despawn (runs @OnDespawn + frees) via a new world_despawn intrinsic
whose @fn_world_despawn helper is emitted in emit_program's tail once a use is
seen (the g_uses_* prelude pattern), used by the kill policy and callable from
any system.

Examples input_movement + world_bounds. Full suite 112/0, goldens
byte-identical, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-02 07:44:41 +03:00
parent 0497029dae
commit ad3be0c53c
17 changed files with 30435 additions and 29731 deletions

View file

@ -293,6 +293,37 @@ function emit_despawn_hooks() -> void {
emit_despawn_all_fn()
}
# #84 — @fn_world_despawn(e): the runtime-callable despawn, used by the
# world-bounds kill policy and World.despawn(e). Same per-kind @OnDespawn dispatch
# as the `despawn` statement (emit_despawn) but reading a runtime %e, then the
# entity-scoped-listener sweep and the slot free. Emitted from emit_program's tail
# only when a world_despawn call was emitted (g_uses_world_despawn), after all uses
# are seen — so a program that never despawns by id is byte-identical.
function emit_world_despawn_fn() -> void {
emit("define void @fn_world_despawn(i32 %e) {\nentry:\n")
if len(g_ondespawn) > 0 {
let me = itoa(MAX_ENT)
emit(" %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n")
emit(" %k = load i32, ptr %kp\n")
var i = 0
while i < len(g_ondespawn) {
let mname = g_ondespawn[i].s
let si = itoa(i)
emit(" %c"); emit(si); emit(" = icmp eq i32 %k, "); emit(itoa(find_arch_id(mname))); emit("\n")
emit(" br i1 %c"); emit(si); emit(", label %hit"); emit(si); emit(", label %next"); emit(si); emit("\n")
emit("hit"); emit(si); emit(":\n call void @on_despawn_"); emit(mname); emit("(i32 %e, i32 0)\n") # reason = Despawned
let dev = `model_{mname}_despawn`
if (find_event(dev) != null) { emit(" call void @ev_"); emit(dev); emit("(i32 %e, i32 0)\n") }
emit(" br label %next"); emit(si); emit("\n")
emit("next"); emit(si); emit(":\n")
i = i + 1
}
}
if len(g_events) > 0 { emit(" call void @ludic_sweep_entity(i32 %e)\n") } # EV5: drop entity-scoped listeners
emit(" call void @L_free_entity(i32 %e)\n")
emit(" ret void\n}\n\n")
}
# LC1 "no silent deaths": at program shutdown every still-live entity's despawn
# hook fires with reason Quit, so teardown that must run on exit is not skipped.
# @L_despawn_all(reason) walks the live set and dispatches each entity by kind —