Networking N2–N6, and a fully C-free toolchain
Implement the rest of NETWORKING-DESIGN.md (N2–N6) and eliminate every
`.c` file from the repo. clang remains only the LLVM-IR assembler; no C
is compiled anywhere.
Networking (selfhost/emit_net.ludic + parser/emit changes):
- N2 @Sync: per-model serialize/apply + by-kind dispatchers; POD-scalar
compile error and empty-participation warning; selective replication.
- N3 @Owned: @L_owner array + owner/set_owner/is_owner; owners snapshot.
- N4 @ToServer/@ToClients remote events: framed net_send + net_pump re-emit.
- N5 @Server/@Predicted role guards + drivable sim (tick_fixed/tick_render,
entry-owns-the-loop).
- Built-in loopback transport so multiplayer runs with zero foreign code;
extern fn net_send/net_poll still overrides it for a real socket.
- N6 blessed runtime (examples/net_rt.ludic) + end-to-end demo (net_demo).
- Fix: llty("entity") is now i32 (entities are i32 handles), so let e = self().
C elimination:
- Networking + foreign-mod-ABI tests rewritten as self-contained pure-Ludic
programs (examples/net_*, world_*, mod_events, scoped); tests/ removed.
- Reflection ABI exposed to Ludic as world_* builtins (Ludic-to-Ludic modding).
- Formatter rewritten C→Ludic: tools/ludic-tools/fmt.ludic.
- Language server rewritten C→Ludic: tools/ludic-tools/lsp.ludic (lexer, index
parser, cross-file workspace resolver, JSON, all LSP handlers).
- Obsolete migrate_*.c codemods deleted; ludic_syntax.h kept as vocabulary data.
Suites: ./test.sh 44/44, ./tools/test-tools.sh 28/28 (LSP 42/42), fixpoint holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
96d01e45ab
commit
bca8f126fc
67 changed files with 24066 additions and 9309 deletions
|
|
@ -17,6 +17,18 @@ const N_EXTERN: int = 11
|
|||
const N_UI: int = 12
|
||||
const N_ENUM: int = 27 # enum Name { A, B, ... } — named int constants
|
||||
const S_TOGGLE: int = 28 # enable/disable (ival: 1=enable 0=disable; s=target; a=entity or null)
|
||||
const N_SCENE: int = 45 # scene Name [start] { on enter{} on exit{} layer L { handlers } }
|
||||
# s=name ival=scene id a=on-enter block b=on-exit block
|
||||
const S_ATTACH: int = 46 # attach P on e [{ overrides }] — add a property to a live entity
|
||||
# s=property a=entity expr b=override record (E_REC) or null
|
||||
const S_DETACH: int = 47 # detach P on e — remove a property from a live entity
|
||||
# s=property a=entity expr
|
||||
const N_EVENT: int = 48 # event Name { field: T = default, ... } — a public event payload
|
||||
# s=name kids=payload fields (N_FIELD)
|
||||
const S_EMIT: int = 49 # emit E(field: v, ...) — fire event E (calls its @On listeners)
|
||||
# s=event name a=E_REC of named args; also usable as an
|
||||
# expression (a cancellable event returns its cancelled flag)
|
||||
const S_CANCEL: int = 50 # cancel — inside a listener, veto a `cancellable` event
|
||||
# statements
|
||||
const S_LET: int = 10
|
||||
const S_ASSIGN: int = 11
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ echo " seed.ll --clang--> sh_seed (no C compiler used)"
|
|||
FRAGS="selfhost/str.ludic selfhost/buf.ludic selfhost/io.ludic selfhost/ast.ludic
|
||||
selfhost/lex.ludic selfhost/parse.ludic selfhost/parse_game.ludic selfhost/emit_core.ludic selfhost/emit_head.ludic
|
||||
selfhost/emit_addr.ludic selfhost/emit_intrin.ludic selfhost/emit_intrin2.ludic selfhost/emit_math.ludic selfhost/emit_new.ludic
|
||||
selfhost/emit_expr.ludic selfhost/emit_stmt.ludic selfhost/emit_ecs.ludic selfhost/emit_query.ludic selfhost/emit_spawn.ludic selfhost/emit_game.ludic selfhost/emit_machine.ludic selfhost/emit_save.ludic selfhost/emit_ui.ludic selfhost/emit_decl.ludic selfhost/main.ludic"
|
||||
selfhost/emit_expr.ludic selfhost/emit_stmt.ludic selfhost/emit_ecs.ludic selfhost/emit_query.ludic selfhost/emit_spawn.ludic selfhost/emit_game.ludic selfhost/emit_machine.ludic selfhost/emit_save.ludic
|
||||
selfhost/emit_net.ludic selfhost/emit_ui.ludic selfhost/emit_decl.ludic selfhost/main.ludic"
|
||||
{ echo "program SelfHost {"; for f in $FRAGS; do cat "$f"; echo; done; echo "}"; } > "$B/selfhost.ludic"
|
||||
|
||||
# the seed-built compiler compiles its own source
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ selfhost/emit_spawn.ludic
|
|||
selfhost/emit_game.ludic
|
||||
selfhost/emit_machine.ludic
|
||||
selfhost/emit_save.ludic
|
||||
selfhost/emit_net.ludic
|
||||
selfhost/emit_ui.ludic
|
||||
selfhost/emit_decl.ludic
|
||||
selfhost/main.ludic"
|
||||
|
|
|
|||
|
|
@ -34,6 +34,20 @@ var nself: int = 0
|
|||
var mach_stk: []Node # enclosing `machine` nodes, so `become` finds its register
|
||||
var nmach: int = 0
|
||||
|
||||
# scenes: one implicit active-scene register (@L_scene). A scene lowers to a
|
||||
# machine the compiler writes for you — `become <Scene>` runs the source scene's
|
||||
# on-exit, stores the target id, and runs its on-enter.
|
||||
var g_scenes: []Node # every `scene` declaration, in source order (ival = id)
|
||||
var g_scene_count: int = 0 # parse-time id counter
|
||||
var g_start_scene: int = 0 # id of the scene marked `start` (else the first)
|
||||
var g_cur_scene: Node = null # scene owning the handler being emitted, for `become`
|
||||
|
||||
fn find_scene(name: ptr) -> Node {
|
||||
var i = 0
|
||||
while i < len(g_scenes) { if (g_scenes[i].s == name) { return g_scenes[i] }; i = i + 1 }
|
||||
return null
|
||||
}
|
||||
|
||||
fn emit(s: ptr) -> void { buf_puts(code, s) }
|
||||
fn emith(s: ptr) -> void { buf_puts(head, s) }
|
||||
|
||||
|
|
@ -52,7 +66,7 @@ fn lbl(pfx: ptr) -> ptr { let r = (pfx + itoa(ll_lbl)); ll_lbl = ll_lbl + 1; r
|
|||
# Ludic type -> LLVM type. int/bool are i32; everything else (ptr/str/struct/
|
||||
# slice) is a pointer; void is void.
|
||||
fn llty(t: ptr) -> ptr {
|
||||
if (t == "int") or (t == "bool") or (t == "fixed") { return "i32" }
|
||||
if (t == "int") or (t == "bool") or (t == "fixed") or (t == "entity") { return "i32" } # entity = an i32 handle (self())
|
||||
if (t == "byte") { return "i8" } # a single byte (p[i] on a raw ptr)
|
||||
if (t == "words") or (t == "fixeds") or (t == "ptrs") { return "ptr" } # typed buffers
|
||||
if (t == "void") { return "void" }
|
||||
|
|
@ -112,6 +126,13 @@ fn enum_ordinal(ename: ptr, vname: ptr) -> int {
|
|||
}
|
||||
i = i + 1
|
||||
}
|
||||
# LC1: the compiler owns `EndReason` — the reason bound by a reason-carrying
|
||||
# teardown (`@OnDespawn(M, reason: r)`). Each despawn site passes one of these.
|
||||
if (ename == "EndReason") {
|
||||
if (vname == "Despawned") { return 0 } # explicit `despawn e`
|
||||
if (vname == "SceneExit") { return 1 } # a scene tearing down its owned entities
|
||||
if (vname == "Quit") { return 2 } # program shutdown
|
||||
}
|
||||
return 0 - 1
|
||||
}
|
||||
fn find_fn(name: ptr) -> Node {
|
||||
|
|
@ -120,6 +141,17 @@ fn find_fn(name: ptr) -> Node {
|
|||
return null
|
||||
}
|
||||
|
||||
# `extern fn name(params) -> T = "sym"` binds a Ludic name to a link symbol. A
|
||||
# call to `name` lowers to a direct `@<sym>` call (no @fn_ prefix — the string is
|
||||
# the exact linked symbol), and emit_extern_decls emits a matching `declare`. This
|
||||
# is the transport seam (net_send/net_poll), the windowing/socket FFI, and any
|
||||
# C/Rust/Zig library binding — the same seam NETWORKING-DESIGN §5 names.
|
||||
fn find_extern(name: ptr) -> Node {
|
||||
var i = 0
|
||||
while i < len(prog) { let d = prog[i]; if d.kind == N_EXTERN and (d.s == name) { return d }; i = i + 1 }
|
||||
return null
|
||||
}
|
||||
|
||||
# @Computed derived fields: a per-property (Prop.field -> expression) registry.
|
||||
# These are NOT stored in the component layout; `x.field` expands inline to the
|
||||
# expression with its bare names read as fields of `x`. Populated at parse time.
|
||||
|
|
@ -162,8 +194,11 @@ fn onspawn_body(model: ptr) -> Node {
|
|||
var g_ondespawn: []Node # each: s = Model name, a = hook body block
|
||||
var g_onattach: []Node # each: s = Property name, a = hook body block
|
||||
|
||||
fn register_ondespawn(model: ptr, body: Node) -> void {
|
||||
let n = node(N_BLOCK); n.s = model; n.a = body; push(g_ondespawn, n)
|
||||
# LC1: `.ty` carries the optional `reason:` binding name (null if the hook took
|
||||
# no reason). The despawn hook function gains an `i32 %reason` parameter and each
|
||||
# teardown site passes a constant EndReason (see emit_despawn_hooks / emit_despawn).
|
||||
fn register_ondespawn(model: ptr, body: Node, reason: ptr) -> void {
|
||||
let n = node(N_BLOCK); n.s = model; n.a = body; n.ty = reason; push(g_ondespawn, n)
|
||||
}
|
||||
fn ondespawn_body(model: ptr) -> Node {
|
||||
var i = 0
|
||||
|
|
@ -179,6 +214,19 @@ fn onattach_body(prop: ptr) -> Node {
|
|||
return null
|
||||
}
|
||||
|
||||
# @OnDetach(Property): the teardown paired with @OnAttach — fires when a property
|
||||
# is removed from a live entity (`detach P on e`), with the property bound by name
|
||||
# so the body can read its outgoing value before it is cleared.
|
||||
var g_ondetach: []Node # each: s = Property name, a = hook body block
|
||||
fn register_ondetach(prop: ptr, body: Node) -> void {
|
||||
let n = node(N_BLOCK); n.s = prop; n.a = body; push(g_ondetach, n)
|
||||
}
|
||||
fn ondetach_body(prop: ptr) -> Node {
|
||||
var i = 0
|
||||
while i < len(g_ondetach) { if (g_ondetach[i].s == prop) { return g_ondetach[i].a }; i = i + 1 }
|
||||
return null
|
||||
}
|
||||
|
||||
# @OnEnable(Property) / @OnDisable(Property): run when a property is toggled on an
|
||||
# entity, with the property bound by name.
|
||||
var g_onenable: []Node
|
||||
|
|
@ -196,6 +244,59 @@ fn ondisable_body(prop: ptr) -> Node {
|
|||
return null
|
||||
}
|
||||
|
||||
# EV0 — the event bus core. `event E { fields }` declares a POD payload; `@On(E)
|
||||
# handler …` registers a compile-time listener; `emit E(…)` fires it. An event
|
||||
# lowers to a `@ev_<E>(payload)` function whose body is the concatenation of its
|
||||
# listeners in declaration order — a direct call at each `emit` site, no runtime.
|
||||
# Gated on `len(g_events) > 0`, so a program with no events is byte-identical.
|
||||
var g_events: []Node # each: an N_EVENT node (s = name, kids = payload fields, ival=1 if cancellable)
|
||||
var g_onlisten: []Node # each: N_BLOCK, s = event name, a = listener body block
|
||||
var g_cancel_addr: ptr = null # EV3: address of the current cancellable dispatch's flag (null outside one)
|
||||
|
||||
fn register_event(n: Node) -> void { push(g_events, n) }
|
||||
fn find_event(name: ptr) -> Node {
|
||||
var i = 0
|
||||
while i < len(g_events) { if (g_events[i].s == name) { return g_events[i] }; i = i + 1 }
|
||||
return null
|
||||
}
|
||||
fn register_onlisten(evt: ptr, body: Node) -> void {
|
||||
let n = node(N_BLOCK); n.s = evt; n.a = body; push(g_onlisten, n)
|
||||
}
|
||||
|
||||
# EV1 — `@Public` promotes a lifecycle hook to a public event. A promoted event
|
||||
# is synthesized here (payload = the entity, plus a reason for despawn); its
|
||||
# presence in g_events is what makes each lifecycle fire site also `emit` it, so
|
||||
# `find_event(name) != null` doubles as the "is this hook public?" test. Names are
|
||||
# the stable ABI contract: `model_<M>_spawn`, `model_<M>_despawn`, etc.
|
||||
fn ensure_event(name: ptr, with_reason: bool) -> void {
|
||||
if (find_event(name) != null) { return }
|
||||
let n = node(N_EVENT); n.s = name
|
||||
let ent = node(N_FIELD); ent.s = "entity"; ent.ty = "int"; push(n.kids, ent)
|
||||
if with_reason { let r = node(N_FIELD); r.s = "reason"; r.ty = "int"; push(n.kids, r) }
|
||||
register_event(n)
|
||||
}
|
||||
# a promoted scene/program event has no per-entity payload
|
||||
fn ensure_event_empty(name: ptr) -> void {
|
||||
if (find_event(name) != null) { return }
|
||||
let n = node(N_EVENT); n.s = name; register_event(n)
|
||||
}
|
||||
|
||||
# EV1/SCENES-E2 — layer toggle. A layer named in an `enable layer L`/`disable
|
||||
# layer L` statement becomes "managed": it gets an @LE_<L> enabled flag and its
|
||||
# handlers gate on it. Only managed layers pay for this, so a scene program that
|
||||
# never toggles a layer is byte-identical.
|
||||
var g_toggled_layers: []ptr
|
||||
fn note_toggled_layer(name: ptr) -> void {
|
||||
var i = 0
|
||||
while i < len(g_toggled_layers) { if (g_toggled_layers[i] == name) { return }; i = i + 1 }
|
||||
push(g_toggled_layers, name)
|
||||
}
|
||||
fn is_toggled_layer(name: ptr) -> bool {
|
||||
var i = 0
|
||||
while i < len(g_toggled_layers) { if (g_toggled_layers[i] == name) { return true }; i = i + 1 }
|
||||
return false
|
||||
}
|
||||
|
||||
# is `name` a model (archetype)? — chooses model-vs-handler for a bare enable/disable
|
||||
fn is_model(name: ptr) -> bool { return find_arch_id(name) > 0 }
|
||||
|
||||
|
|
|
|||
|
|
@ -70,21 +70,32 @@ fn emit_program() -> void {
|
|||
g_uses_str = false
|
||||
g_uses_intstr = false
|
||||
g_uses_strslice = false
|
||||
g_uses_loopback = false
|
||||
loc_name = new []ptr; loc_reg = new []ptr; loc_ty = new []ptr; loc_mut = new []int
|
||||
brk_lbl = new []ptr; cnt_lbl = new []ptr
|
||||
self_stk = new []ptr
|
||||
mach_stk = new []Node
|
||||
emit_header()
|
||||
emit_extern_decls()
|
||||
if has_ecs() { emit_ecs_storage() }
|
||||
var i = 0
|
||||
while i < len(prog) { if prog[i].kind == N_FN { emit_fn(prog[i]) }; i = i + 1 }
|
||||
if len(g_events) > 0 { emit_event_fns() } # EV0: @ev_<E> event-dispatch functions
|
||||
if has_ecs() and len(g_events) > 0 { emit_world_table() } # EV2: the mod reflection ABI
|
||||
if has_ecs() { emit_ecs_allocator(); emit_snapshot() }
|
||||
if has_ecs() { emit_net() } # N2/N3: @Sync serializers + @Owned storage (gated internally)
|
||||
if has_ui() { emit_ui_build() }
|
||||
if has_systems() { emit_game_main() }
|
||||
else {
|
||||
if has_systems() and has_entry() { # N5: game owns its loop via `entry`
|
||||
emit_game_defs() # system fns, hooks, tick helpers
|
||||
i = 0
|
||||
while i < len(prog) { if prog[i].kind == N_MAIN { emit_main(prog[i]) }; i = i + 1 }
|
||||
}
|
||||
else { if has_systems() { emit_game_main() } # the auto frame loop
|
||||
else {
|
||||
i = 0
|
||||
while i < len(prog) { if prog[i].kind == N_MAIN { emit_main(prog[i]) }; i = i + 1 }
|
||||
} }
|
||||
if g_uses_loopback { emit_loopback() } # built-in transport, after all net_send/net_poll uses are seen
|
||||
if g_uses_str { emit_str_prelude() } # @fn_str_eq / @fn_str_concat, after all uses are seen
|
||||
if g_uses_intstr { emit_int_str() } # @fn_int_str, for str(int) in interpolation
|
||||
if g_uses_strslice { emit_str_slice() } # @fn_str_slice, for s[a..b]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,14 @@ fn has_models() -> bool {
|
|||
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.
|
||||
fn 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
|
||||
|
|
@ -25,12 +33,22 @@ fn has_ecs() -> bool { return has_systems() or has_models() }
|
|||
fn emit_ecs_storage() -> void {
|
||||
emith("@L_running = internal global i32 1\n")
|
||||
emith("@L_key = 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]
|
||||
|
|
@ -46,6 +64,9 @@ fn emit_ecs_storage() -> void {
|
|||
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
|
||||
|
|
@ -62,7 +83,13 @@ fn emit_ecs_allocator() -> void {
|
|||
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 ret void\n}\n\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")
|
||||
|
|
|
|||
|
|
@ -123,7 +123,78 @@ fn emit_call(e: Node) -> Val {
|
|||
if (name == "save") { emit(" call void @L_save()\n"); return val("0", "void") }
|
||||
if (name == "ui_build") { emit(" call void @ui_build()\n"); return val("0", "void") }
|
||||
if (name == "load") { return val(emit_bind("call i32 @L_load()"), "bool") }
|
||||
if (name == "world_size") { return val(emit_bind("call i32 @L_world_size()"), "int") }
|
||||
if (name == "world_save") { # world_save(buf) -> bytes written
|
||||
let b = emit_expr(e.kids[0])
|
||||
return val(emit_bind(`call i32 @L_world_save(ptr {b.code})`), "int")
|
||||
}
|
||||
if (name == "world_load") { # world_load(buf, len)
|
||||
let b = emit_expr(e.kids[0])
|
||||
let l = emit_expr(e.kids[1])
|
||||
emit(" call void @L_world_load(ptr "); emit(b.code); emit(", i32 "); emit(l.code); emit(")\n")
|
||||
return val("0", "void")
|
||||
}
|
||||
if (name == "quit") { emit(" store i32 0, ptr @L_running\n"); return val("0", "void") }
|
||||
# NETWORKING (NETWORKING-DESIGN §5) — the low-level freedom layer, callable from
|
||||
# Ludic. serialize/apply/sync_size lower to the @Sync by-kind dispatchers (N2);
|
||||
# owner/set_owner/is_owner to the @Owned storage (N3); is_server/local_id read
|
||||
# the runtime-set role registers (N5). Offline these hold their single-player
|
||||
# default (@L_role=1 → is_server()==true), so guards collapse to "run here" (§8).
|
||||
if (name == "serialize") { # serialize(e, buf) -> bytes written
|
||||
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
||||
return val(emit_bind(`call i32 @ludic_serialize(i32 {a.code}, ptr {b.code})`), "int")
|
||||
}
|
||||
if (name == "apply") { # apply(e, buf, len)
|
||||
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let c = emit_expr(e.kids[2])
|
||||
emit(" call void @ludic_apply(i32 "); emit(a.code); emit(", ptr "); emit(b.code); emit(", i32 "); emit(c.code); emit(")\n")
|
||||
return val("0", "void")
|
||||
}
|
||||
if (name == "sync_size") { # sync_size(e) -> replicated byte count for e's model
|
||||
let a = emit_expr(e.kids[0])
|
||||
return val(emit_bind(`call i32 @ludic_sync_size(i32 {a.code})`), "int")
|
||||
}
|
||||
if (name == "owner") { # owner(e) -> peer id (-1 = unowned)
|
||||
let a = emit_expr(e.kids[0])
|
||||
return val(emit_bind(`call i32 @L_owner(i32 {a.code})`), "int")
|
||||
}
|
||||
if (name == "set_owner") { # set_owner(e, id)
|
||||
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
||||
emit(" call void @L_set_owner(i32 "); emit(a.code); emit(", i32 "); emit(b.code); emit(")\n")
|
||||
return val("0", "void")
|
||||
}
|
||||
if (name == "is_owner") { # is_owner(e) -> owner(e) == local_id()
|
||||
let a = emit_expr(e.kids[0])
|
||||
return val(emit_bind(`call i32 @L_is_owner(i32 {a.code})`), "bool")
|
||||
}
|
||||
if (name == "is_server") { # is_server() -> the local peer is the authority
|
||||
let r = emit_bind("load i32, ptr @L_role")
|
||||
let c = emit_bind(`icmp eq i32 {r}, 1`)
|
||||
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
||||
}
|
||||
if (name == "local_id") { return val(emit_bind("load i32, ptr @L_localid"), "int") }
|
||||
if (name == "net_pump") { emit(" call void @L_net_pump()\n"); return val("0", "void") } # N4: drain + re-emit inbound RPCs
|
||||
if (name == "tick_fixed") { emit(" call void @L_tick_fixed()\n"); return val("0", "void") } # N5: run the sim phases
|
||||
if (name == "tick_render") { emit(" call void @L_tick_render()\n"); return val("0", "void") } # N5: run the Render phase
|
||||
if (name == "set_role") { # N5: the runtime sets the peer's role (1=server, 0=client)
|
||||
let a = emit_expr(e.kids[0]); emit(" store i32 "); emit(a.code); emit(", ptr @L_role\n"); return val("0", "void")
|
||||
}
|
||||
if (name == "set_local_id") { # N5: the runtime sets this peer's id
|
||||
let a = emit_expr(e.kids[0]); emit(" store i32 "); emit(a.code); emit(", ptr @L_localid\n"); return val("0", "void")
|
||||
}
|
||||
# net_send(peer, buf, len) / net_poll(buf, cap): the transport seam. An
|
||||
# `extern fn` of the same name (a real socket) wins; absent one, these lower to
|
||||
# the compiler's built-in loopback so a game is networked with zero foreign code.
|
||||
if (name == "net_send") and (find_extern("net_send") == null) {
|
||||
g_uses_loopback = true
|
||||
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let c = emit_expr(e.kids[2])
|
||||
emit(" call void @L_net_send(i32 "); emit(a.code); emit(", ptr "); emit(b.code); emit(", i32 "); emit(c.code); emit(")\n")
|
||||
return val("0", "void")
|
||||
}
|
||||
if (name == "net_poll") and (find_extern("net_poll") == null) {
|
||||
g_uses_loopback = true
|
||||
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
||||
return val(emit_bind(`call i32 @L_net_poll(ptr {a.code}, i32 {b.code})`), "int")
|
||||
}
|
||||
if (name == "len") { return emit_len(e) }
|
||||
if (name == "push") { return emit_push(e) }
|
||||
if (name == "str") { # str(x): int/bool/fixed -> text, a string passes through
|
||||
|
|
@ -151,9 +222,55 @@ fn emit_call(e: Node) -> Val {
|
|||
}
|
||||
if (name == "fx") { let a = emit_expr(e.kids[0]); return val(emit_bind(`shl i32 {a.code}, 16`), "fixed") }
|
||||
if (name == "flr") { let a = emit_expr(e.kids[0]); return val(emit_bind(`ashr i32 {a.code}, 16`), "int") }
|
||||
# The EV2 reflection ABI (the world table), exposed to Ludic so a Ludic mod can
|
||||
# introspect the world by name — the same functions a foreign mod binds. Emitted
|
||||
# only for a modding program (ECS + events), so a plain game is unchanged.
|
||||
if (name == "world_prop_id") { let a = emit_expr(e.kids[0]); return val(emit_bind(`call i32 @ludic_prop_id(ptr {a.code})`), "int") }
|
||||
if (name == "world_field_id") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); return val(emit_bind(`call i32 @ludic_field_id(i32 {a.code}, ptr {b.code})`), "int") }
|
||||
if (name == "world_get") {
|
||||
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let c = emit_expr(e.kids[2])
|
||||
let r = emit_bind(`call i64 @ludic_get(i32 {a.code}, i32 {b.code}, i32 {c.code})`)
|
||||
return val(emit_bind(`trunc i64 {r} to i32`), "int")
|
||||
}
|
||||
if (name == "world_set") {
|
||||
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let c = emit_expr(e.kids[2]); let d = emit_expr(e.kids[3])
|
||||
let v64 = emit_bind(`sext i32 {d.code} to i64`)
|
||||
emit(" call void @ludic_set(i32 "); emit(a.code); emit(", i32 "); emit(b.code); emit(", i32 "); emit(c.code); emit(", i64 "); emit(v64); emit(")\n")
|
||||
return val("0", "void")
|
||||
}
|
||||
if (name == "world_has") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); return val(emit_bind(`call i32 @ludic_has(i32 {a.code}, i32 {b.code})`), "int") }
|
||||
if (name == "world_count") { return val(emit_bind("call i32 @ludic_entity_count()"), "int") }
|
||||
if (name == "world_kind") { let a = emit_expr(e.kids[0]); return val(emit_bind(`call i32 @ludic_kind(i32 {a.code})`), "int") }
|
||||
if (name == "world_model_id") { let a = emit_expr(e.kids[0]); return val(emit_bind(`call i32 @ludic_model_id(ptr {a.code})`), "int") }
|
||||
if (name == "world_query_next") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); return val(emit_bind(`call i32 @ludic_query_next(i32 {a.code}, i32 {b.code})`), "int") }
|
||||
if (name == "world_register_prop") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); return val(emit_bind(`call i32 @ludic_register_prop(ptr {a.code}, i32 {b.code})`), "int") }
|
||||
if (name == "world_attach_dyn") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); emit(" call void @ludic_attach_dyn(i32 "); emit(a.code); emit(", i32 "); emit(b.code); emit(")\n"); return val("0", "void") }
|
||||
if (name == "world_detach_dyn") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); emit(" call void @ludic_detach_dyn(i32 "); emit(a.code); emit(", i32 "); emit(b.code); emit(")\n"); return val("0", "void") }
|
||||
if (name == "world_spawn") { let a = emit_expr(e.kids[0]); return val(emit_bind(`call i32 @ludic_spawn(i32 {a.code})`), "int") }
|
||||
if is_intrinsic(name) { return emit_intrinsic(name, e) }
|
||||
if is_intrinsic2(name) { return emit_intrinsic2(name, e) }
|
||||
if is_math_builtin(name) { return emit_math_builtin(name, e) }
|
||||
# extern fn: a direct call to the declared link symbol (no @fn_ prefix)
|
||||
let ext = find_extern(name)
|
||||
if (ext != null) {
|
||||
let eargs = new []ptr
|
||||
let eatys = new []ptr
|
||||
var ei = 0
|
||||
while ei < len(e.kids) { let v = emit_expr(e.kids[ei]); push(eargs, v.code); push(eatys, v.ty); ei = ei + 1 }
|
||||
let erl = llty(ext.ty)
|
||||
emit(" ")
|
||||
var erreg = "0"
|
||||
if not (erl == "void") { erreg = nreg(); emit(erreg); emit(" = ") }
|
||||
emit("call "); emit(erl); emit(" @"); emit(ext.a.s); emit("(")
|
||||
ei = 0
|
||||
while ei < len(eargs) {
|
||||
if ei > 0 { emit(", ") }
|
||||
emit(llty(eatys[ei])); emit(" "); emit(eargs[ei])
|
||||
ei = ei + 1
|
||||
}
|
||||
emit(")\n")
|
||||
return val(erreg, ext.ty)
|
||||
}
|
||||
var fn2 = find_fn(name)
|
||||
var cname = name
|
||||
if (fn2 == null) {
|
||||
|
|
@ -234,6 +351,7 @@ fn emit_expr(e: Node) -> Val {
|
|||
}
|
||||
return emit_load_at(a, g_addr_ty)
|
||||
}
|
||||
if e.kind == S_EMIT { return emit_emit(e) } # emit as an expression -> cancelled flag
|
||||
if e.kind == E_CALL { return emit_call(e) }
|
||||
if e.kind == E_BIN { return emit_bin(e) }
|
||||
if e.kind == E_UN {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
# called only when the runtime defines them.
|
||||
|
||||
fn 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()
|
||||
|
|
@ -20,24 +21,101 @@ fn emit_system_fn(sys: Node) -> void {
|
|||
emit("}\n\n")
|
||||
}
|
||||
|
||||
# one enable-gated call to @sys_<d.s> (skipped while the handler is disabled).
|
||||
fn 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.
|
||||
fn emit_calls_for_phase(phase: ptr) -> void {
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
if d.kind == N_SYS and (d.ty == phase) { # skip a disabled handler
|
||||
let he = emit_bind(`load i32, ptr @HE_{d.s}`)
|
||||
let hc = emit_bind(`icmp ne i32 {he}, 0`)
|
||||
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")
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
# @OnDespawn(Model) hooks compile to `@on_despawn_<Model>(entity)` functions that
|
||||
# bind the model's properties and run the body — dispatched by kind at `despawn`.
|
||||
# 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.
|
||||
fn emit_scene_fn(name: ptr, kind: ptr, 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")
|
||||
}
|
||||
|
||||
fn 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.
|
||||
fn emit_despawn_hooks() -> void {
|
||||
var i = 0
|
||||
while i < len(g_ondespawn) {
|
||||
|
|
@ -49,30 +127,608 @@ fn emit_despawn_hooks() -> void {
|
|||
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) {\nentry:\n")
|
||||
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()
|
||||
}
|
||||
|
||||
fn emit_game_main() -> void {
|
||||
# every system becomes a function first
|
||||
# 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.
|
||||
fn 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
|
||||
|
||||
fn 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")
|
||||
}
|
||||
|
||||
# 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.
|
||||
fn 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.
|
||||
fn 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.
|
||||
fn 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()
|
||||
}
|
||||
|
||||
fn 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")
|
||||
|
|
@ -97,7 +753,9 @@ fn emit_game_main() -> void {
|
|||
emit_calls_for_phase("Render")
|
||||
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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,27 @@ fn emit_header() -> void {
|
|||
}
|
||||
}
|
||||
|
||||
# One `declare <ret> @<sym>(<argtys>)` per `extern fn`, so the linker resolves the
|
||||
# call to the bound symbol. Emitted after the header; a program with no `extern fn`
|
||||
# emits nothing here, so un-networked builds stay byte-identical.
|
||||
fn emit_extern_decls() -> void {
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
if d.kind == N_EXTERN {
|
||||
emith("declare "); emith(llty(d.ty)); emith(" @"); emith(d.a.s); emith("(")
|
||||
var f = 0
|
||||
while f < len(d.kids) {
|
||||
if f > 0 { emith(", ") }
|
||||
emith(llty(d.kids[f].ty))
|
||||
f = f + 1
|
||||
}
|
||||
emith(")\n")
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
# The string runtime, emitted (once) into any program that uses `+`/`==`/`!=`
|
||||
# on strings. Hand-written IR over NUL-terminated byte buffers: str_eq walks both
|
||||
# until a mismatch or a shared terminator; str_concat measures both, mallocs
|
||||
|
|
|
|||
|
|
@ -26,14 +26,26 @@ fn emit_machine(st: Node) -> void {
|
|||
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).
|
||||
fn emit_become(st: Node) -> void {
|
||||
if nmach == 0 { perr("'become' outside a machine") }
|
||||
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) { perr(`become: no state {st.s}`) }
|
||||
let regv = emit_expr(m.a)
|
||||
let sv = emit_expr(target.b)
|
||||
emit(" call void @fn_rt_set_reg(i32 "); emit(regv.code); emit(", i32 "); emit(sv.code); emit(")\n")
|
||||
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 regv = emit_expr(m.a)
|
||||
let sv = emit_expr(target.b)
|
||||
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")
|
||||
}
|
||||
|
|
|
|||
373
selfhost/emit_net.ludic
Normal file
373
selfhost/emit_net.ludic
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
# emit_net.ludic — NETWORKING N2–N6 codegen (NETWORKING-DESIGN.md).
|
||||
#
|
||||
# A program that calls net_send/net_poll with no `extern fn` override triggers the
|
||||
# built-in loopback transport; this flag defers its emission to end-of-module.
|
||||
var g_uses_loopback: bool = false
|
||||
|
||||
#
|
||||
# N2 (@Sync): per-model serialize/apply over the replicable-and-participating
|
||||
# fields, by-kind dispatchers a replication runtime calls, and the POD-scalar
|
||||
# compile error + empty-participation warning.
|
||||
# N3 (@Owned): the @L_owner array + owner()/set_owner()/is_owner().
|
||||
# N5 (roles): the @L_role / @L_localid registers + is_server()/local_id().
|
||||
#
|
||||
# Everything here is gated (net_has_sync / net_has_owned / len(g_events) etc.),
|
||||
# so a program that uses none of it is byte-identical to single-player (§8).
|
||||
|
||||
# ---- N2: what replicates -----------------------------------------------------
|
||||
# A field of an entity replicates iff the field is @Sync (field.ival==1) AND its
|
||||
# component participates in the entity's model (the model member is @Sync,
|
||||
# member.ival==1). Participation is decided per model use-site.
|
||||
|
||||
fn net_field_ibytes(ty: ptr) -> int { if (llty(ty) == "i8") { return 1 }; return 4 }
|
||||
fn net_field_bytes(ty: ptr) -> ptr { if (llty(ty) == "i8") { return "1" }; return "4" }
|
||||
|
||||
# total replicated bytes for model m (compile-time constant)
|
||||
fn net_model_bytes(m: Node) -> int {
|
||||
var total = 0
|
||||
var ci = 0
|
||||
while ci < len(m.kids) {
|
||||
if m.kids[ci].ival == 1 {
|
||||
let c = find_comp(m.kids[ci].s)
|
||||
if (c != null) {
|
||||
var fj = 0
|
||||
while fj < len(c.kids) { if c.kids[fj].ival == 1 { total = total + net_field_ibytes(c.kids[fj].ty) }; fj = fj + 1 }
|
||||
}
|
||||
}
|
||||
ci = ci + 1
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
fn net_model_syncs(m: Node) -> bool { return net_model_bytes(m) > 0 }
|
||||
|
||||
fn net_has_sync() -> bool {
|
||||
var i = 0
|
||||
while i < len(prog) { if prog[i].kind == N_ARCH { if net_model_syncs(prog[i]) { return true } }; i = i + 1 }
|
||||
return false
|
||||
}
|
||||
|
||||
# ---- N3: ownership -----------------------------------------------------------
|
||||
fn net_has_owned() -> bool {
|
||||
var i = 0
|
||||
while i < len(prog) { if prog[i].kind == N_ARCH and (prog[i].ival == 1) { return true }; i = i + 1 }
|
||||
return false
|
||||
}
|
||||
|
||||
# ---- N5: role-tagged handlers ------------------------------------------------
|
||||
# A handler tagged @Server (ival==1) or @Predicted (ival==2) has a network role.
|
||||
fn net_has_role() -> bool {
|
||||
var i = 0
|
||||
while i < len(prog) { if prog[i].kind == N_SYS and (prog[i].ival != 0) { return true }; i = i + 1 }
|
||||
return false
|
||||
}
|
||||
|
||||
# Any networking feature in use → emit the shared role registers (@L_role /
|
||||
# @L_localid). A runtime sets them; offline they keep their single-player default.
|
||||
fn net_any() -> bool { return net_has_sync() or net_has_owned() or net_has_role() }
|
||||
|
||||
# ---- diagnostics -------------------------------------------------------------
|
||||
fn net_warn(msg: ptr) -> void {
|
||||
let e = file_stderr()
|
||||
file_write(e, "ludicc(self): warning: ", 23)
|
||||
file_write(e, msg, len(msg))
|
||||
file_write(e, "\n", 1)
|
||||
}
|
||||
|
||||
# Validate @Sync usage: a participating member whose component replicates nothing
|
||||
# is a warning (participation that replicates nothing); a @Sync ptr field is a
|
||||
# hard error (footgun 3 — networked fields must be POD scalars).
|
||||
fn net_check() -> void {
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
if prog[i].kind == N_ARCH {
|
||||
let m = prog[i]
|
||||
var ci = 0
|
||||
while ci < len(m.kids) {
|
||||
if m.kids[ci].ival == 1 {
|
||||
let cn = m.kids[ci].s
|
||||
let c = find_comp(cn)
|
||||
if (c != null) {
|
||||
var any = false
|
||||
var fj = 0
|
||||
while fj < len(c.kids) {
|
||||
if c.kids[fj].ival == 1 {
|
||||
any = true
|
||||
if (llty(c.kids[fj].ty) == "ptr") { perr(`@Sync field {cn}.{c.kids[fj].s} is not a POD scalar (networked fields must be int/bool/fixed/byte)`) }
|
||||
}
|
||||
fj = fj + 1
|
||||
}
|
||||
if not any { net_warn(`model {m.s} @Syncs {cn} but it has no @Sync fields — nothing replicates`) }
|
||||
}
|
||||
}
|
||||
ci = ci + 1
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
# ---- N2: per-model serializer / applier --------------------------------------
|
||||
# serialize_<M>(e, buf) -> bytes written. Copies each replicated field, tightly
|
||||
# packed in member-then-field order, so apply reads the identical layout.
|
||||
fn emit_net_serialize(m: Node) -> void {
|
||||
ll_t = 0
|
||||
let me = itoa(MAX_ENT)
|
||||
emit("define i32 @L_serialize_"); emit(m.s); emit("(i32 %e, ptr %buf) {\nentry:\n")
|
||||
var off = 0
|
||||
var ci = 0
|
||||
while ci < len(m.kids) {
|
||||
if m.kids[ci].ival == 1 {
|
||||
let cn = m.kids[ci].s
|
||||
let c = find_comp(cn)
|
||||
if (c != null) {
|
||||
let s = nreg(); emit(" "); emit(s); 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) {
|
||||
if c.kids[fj].ival == 1 {
|
||||
let bytes = net_field_bytes(c.kids[fj].ty)
|
||||
let fa = nreg(); emit(" "); emit(fa); emit(" = getelementptr inbounds %Cmp_"); emit(cn); emit(", ptr "); emit(s); emit(", i32 0, i32 "); emit(itoa(fj)); emit("\n")
|
||||
let dst = nreg(); emit(" "); emit(dst); emit(" = getelementptr inbounds i8, ptr %buf, i32 "); emit(itoa(off)); emit("\n")
|
||||
emit(" call ptr @memcpy(ptr "); emit(dst); emit(", ptr "); emit(fa); emit(", i64 "); emit(bytes); emit(")\n")
|
||||
off = off + net_field_ibytes(c.kids[fj].ty)
|
||||
}
|
||||
fj = fj + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
ci = ci + 1
|
||||
}
|
||||
emit(" ret i32 "); emit(itoa(off)); emit("\n}\n\n")
|
||||
}
|
||||
|
||||
# apply_<M>(e, buf, len): the inverse — copy each replicated field back from the
|
||||
# buffer into component storage. `len` is accepted for symmetry (the runtime's
|
||||
# framing) but the layout is fixed, so it is not consulted.
|
||||
fn emit_net_apply(m: Node) -> void {
|
||||
ll_t = 0
|
||||
let me = itoa(MAX_ENT)
|
||||
emit("define void @L_apply_"); emit(m.s); emit("(i32 %e, ptr %buf, i32 %len) {\nentry:\n")
|
||||
var off = 0
|
||||
var ci = 0
|
||||
while ci < len(m.kids) {
|
||||
if m.kids[ci].ival == 1 {
|
||||
let cn = m.kids[ci].s
|
||||
let c = find_comp(cn)
|
||||
if (c != null) {
|
||||
let s = nreg(); emit(" "); emit(s); 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) {
|
||||
if c.kids[fj].ival == 1 {
|
||||
let bytes = net_field_bytes(c.kids[fj].ty)
|
||||
let fa = nreg(); emit(" "); emit(fa); emit(" = getelementptr inbounds %Cmp_"); emit(cn); emit(", ptr "); emit(s); emit(", i32 0, i32 "); emit(itoa(fj)); emit("\n")
|
||||
let src = nreg(); emit(" "); emit(src); emit(" = getelementptr inbounds i8, ptr %buf, i32 "); emit(itoa(off)); emit("\n")
|
||||
emit(" call ptr @memcpy(ptr "); emit(fa); emit(", ptr "); emit(src); emit(", i64 "); emit(bytes); emit(")\n")
|
||||
off = off + net_field_ibytes(c.kids[fj].ty)
|
||||
}
|
||||
fj = fj + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
ci = ci + 1
|
||||
}
|
||||
emit(" ret void\n}\n\n")
|
||||
}
|
||||
|
||||
# ---- N2: by-kind dispatchers (the runtime ABI) -------------------------------
|
||||
# ludic_serialize(e, buf) -> bytes / ludic_apply(e, buf, len) / ludic_sync_size(e)
|
||||
# route on the entity's model kind to the per-model function above, so a
|
||||
# replication runtime replicates any entity without knowing its type.
|
||||
fn emit_net_dispatch() -> void {
|
||||
let me = itoa(MAX_ENT)
|
||||
|
||||
emit("define i32 @ludic_serialize(i32 %e, ptr %buf) {\nentry:\n")
|
||||
emit(" %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n %k = load i32, ptr %kp\n")
|
||||
var k = 0
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
if prog[i].kind == N_ARCH and net_model_syncs(prog[i]) {
|
||||
let mn = prog[i].s; let sk = itoa(k)
|
||||
emit(" %c"); emit(sk); emit(" = icmp eq i32 %k, "); emit(itoa(find_arch_id(mn))); emit("\n")
|
||||
emit(" br i1 %c"); emit(sk); emit(", label %h"); emit(sk); emit(", label %n"); emit(sk); emit("\n")
|
||||
emit("h"); emit(sk); emit(":\n %r"); emit(sk); emit(" = call i32 @L_serialize_"); emit(mn); emit("(i32 %e, ptr %buf)\n ret i32 %r"); emit(sk); emit("\n")
|
||||
emit("n"); emit(sk); emit(":\n")
|
||||
k = k + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
emit(" ret i32 0\n}\n\n")
|
||||
|
||||
emit("define void @ludic_apply(i32 %e, ptr %buf, i32 %len) {\nentry:\n")
|
||||
emit(" %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n %k = load i32, ptr %kp\n")
|
||||
k = 0; i = 0
|
||||
while i < len(prog) {
|
||||
if prog[i].kind == N_ARCH and net_model_syncs(prog[i]) {
|
||||
let mn = prog[i].s; let sk = itoa(k)
|
||||
emit(" %c"); emit(sk); emit(" = icmp eq i32 %k, "); emit(itoa(find_arch_id(mn))); emit("\n")
|
||||
emit(" br i1 %c"); emit(sk); emit(", label %h"); emit(sk); emit(", label %n"); emit(sk); emit("\n")
|
||||
emit("h"); emit(sk); emit(":\n call void @L_apply_"); emit(mn); emit("(i32 %e, ptr %buf, i32 %len)\n ret void\n")
|
||||
emit("n"); emit(sk); emit(":\n")
|
||||
k = k + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
emit(" ret void\n}\n\n")
|
||||
|
||||
# ludic_sync_size(e): the replicated byte count for the entity's model — a
|
||||
# constant per kind, so a runtime can size a buffer before serialize.
|
||||
emit("define i32 @ludic_sync_size(i32 %e) {\nentry:\n")
|
||||
emit(" %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n %k = load i32, ptr %kp\n")
|
||||
k = 0; i = 0
|
||||
while i < len(prog) {
|
||||
if prog[i].kind == N_ARCH and net_model_syncs(prog[i]) {
|
||||
let mn = prog[i].s; let sk = itoa(k)
|
||||
emit(" %c"); emit(sk); emit(" = icmp eq i32 %k, "); emit(itoa(find_arch_id(mn))); emit("\n")
|
||||
emit(" br i1 %c"); emit(sk); emit(", label %h"); emit(sk); emit(", label %n"); emit(sk); emit("\n")
|
||||
emit("h"); emit(sk); emit(":\n ret i32 "); emit(itoa(net_model_bytes(prog[i]))); emit("\n")
|
||||
emit("n"); emit(sk); emit(":\n")
|
||||
k = k + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
emit(" ret i32 0\n}\n\n")
|
||||
}
|
||||
|
||||
# ---- N3: ownership storage + accessors ---------------------------------------
|
||||
# @L_owner: one i32 owner id per entity, -1 = unowned. Only emitted when a model
|
||||
# is @Owned, and L_reset clears it to -1 on alloc/free (see emit_ecs). owner()/
|
||||
# set_owner()/is_owner() read and write it; the authority assigns.
|
||||
fn emit_net_owner() -> void {
|
||||
let me = itoa(MAX_ENT)
|
||||
emit("define i32 @L_owner(i32 %e) {\nentry:\n")
|
||||
emit(" %p = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_owner_arr, i32 0, i32 %e\n %v = load i32, ptr %p\n ret i32 %v\n}\n\n")
|
||||
emit("define void @L_set_owner(i32 %e, i32 %id) {\nentry:\n")
|
||||
emit(" %p = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_owner_arr, i32 0, i32 %e\n store i32 %id, ptr %p\n ret void\n}\n\n")
|
||||
# is_owner(e): does the local peer own e? owner(e) == local_id().
|
||||
emit("define i32 @L_is_owner(i32 %e) {\nentry:\n")
|
||||
emit(" %p = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_owner_arr, i32 0, i32 %e\n %o = load i32, ptr %p\n")
|
||||
emit(" %lid = load i32, ptr @L_localid\n %eq = icmp eq i32 %o, %lid\n %r = zext i1 %eq to i32\n ret i32 %r\n}\n\n")
|
||||
}
|
||||
|
||||
# ---- built-in loopback transport (NETWORKING-DESIGN §5 N0) -------------------
|
||||
# The transport seam is net_send/net_poll. A production build binds them to a real
|
||||
# socket via `extern fn` (UDP native, WebRTC/WebSocket wasm). Absent that, the
|
||||
# compiler emits this in-process loopback — a single FIFO of datagrams, send
|
||||
# enqueues a copy and poll dequeues the oldest — so a game is networked end to end
|
||||
# with NO foreign host at all (the Ludic-native default). Datagram-preserving:
|
||||
# one message per poll, matching how replication/RPC frame. Emitted only when a
|
||||
# program actually calls net_send/net_poll without an extern override.
|
||||
fn emit_loopback() -> void {
|
||||
emith("@L_netq = internal global [64 x [2048 x i8]] zeroinitializer\n")
|
||||
emith("@L_netlen = internal global [64 x i32] zeroinitializer\n")
|
||||
emith("@L_nethead = internal global i32 0\n")
|
||||
emith("@L_nettail = internal global i32 0\n")
|
||||
|
||||
emit("define void @L_net_send(i32 %peer, ptr %buf, i32 %len) {\nentry:\n")
|
||||
emit(" %l0 = icmp slt i32 %len, 0\n %len1 = select i1 %l0, i32 0, i32 %len\n")
|
||||
emit(" %l1 = icmp sgt i32 %len1, 2048\n %n = select i1 %l1, i32 2048, i32 %len1\n")
|
||||
emit(" %t = load i32, ptr @L_nettail\n %h = load i32, ptr @L_nethead\n")
|
||||
emit(" %t1 = add i32 %t, 1\n %tn = srem i32 %t1, 64\n %full = icmp eq i32 %tn, %h\n")
|
||||
emit(" br i1 %full, label %drop, label %go\n")
|
||||
emit("go:\n")
|
||||
emit(" %row = getelementptr inbounds [64 x [2048 x i8]], ptr @L_netq, i32 0, i32 %t\n")
|
||||
emit(" %nz = zext i32 %n to i64\n call ptr @memcpy(ptr %row, ptr %buf, i64 %nz)\n")
|
||||
emit(" %lp = getelementptr inbounds [64 x i32], ptr @L_netlen, i32 0, i32 %t\n store i32 %n, ptr %lp\n")
|
||||
emit(" store i32 %tn, ptr @L_nettail\n br label %drop\n")
|
||||
emit("drop:\n ret void\n}\n\n")
|
||||
|
||||
emit("define i32 @L_net_poll(ptr %buf, i32 %cap) {\nentry:\n")
|
||||
emit(" %h = load i32, ptr @L_nethead\n %t = load i32, ptr @L_nettail\n %empty = icmp eq i32 %h, %t\n")
|
||||
emit(" br i1 %empty, label %none, label %go\n")
|
||||
emit("go:\n")
|
||||
emit(" %lp = getelementptr inbounds [64 x i32], ptr @L_netlen, i32 0, i32 %h\n %ln = load i32, ptr %lp\n")
|
||||
emit(" %big = icmp sgt i32 %ln, %cap\n %n = select i1 %big, i32 %cap, i32 %ln\n")
|
||||
emit(" %row = getelementptr inbounds [64 x [2048 x i8]], ptr @L_netq, i32 0, i32 %h\n")
|
||||
emit(" %nz = zext i32 %n to i64\n call ptr @memcpy(ptr %buf, ptr %row, i64 %nz)\n")
|
||||
emit(" %h1 = add i32 %h, 1\n %hn = srem i32 %h1, 64\n store i32 %hn, ptr @L_nethead\n ret i32 %n\n")
|
||||
emit("none:\n ret i32 0\n}\n\n")
|
||||
}
|
||||
|
||||
# ---- N4: remote events (RPCs) ------------------------------------------------
|
||||
# An `event` marked @ToServer / @ToClients (ev.ty set) crosses the wire. At an
|
||||
# `emit` site the POD payload is serialized as [i32 event_id][packed fields] and
|
||||
# net_send in the declared direction; net_pump() drains inbound frames and
|
||||
# re-emits each into the ordinary @ev_<E> dispatch on the far side. Reuses the
|
||||
# EV0 payload (already flat) and the transport seam — no new concept.
|
||||
|
||||
fn net_has_remote() -> bool {
|
||||
var i = 0
|
||||
while i < len(g_events) { if (g_events[i].ty != null) { return true }; i = i + 1 }
|
||||
return false
|
||||
}
|
||||
# stable wire id for an event = its index in g_events (same program both peers)
|
||||
fn net_event_id(name: ptr) -> int {
|
||||
var i = 0
|
||||
while i < len(g_events) { if (g_events[i].s == name) { return i }; i = i + 1 }
|
||||
return 0 - 1
|
||||
}
|
||||
# the transport symbols: an `extern fn` override, else the built-in loopback.
|
||||
fn net_send_sym() -> ptr { let x = find_extern("net_send"); if (x != null) { return x.a.s }; return "L_net_send" }
|
||||
fn net_poll_sym() -> ptr { let x = find_extern("net_poll"); if (x != null) { return x.a.s }; return "L_net_poll" }
|
||||
|
||||
# net_pump(): poll every pending frame and re-emit it locally. The receive path
|
||||
# of a remote event — the runtime/game calls this each tick.
|
||||
fn emit_net_pump() -> void {
|
||||
emith("@L_recvbuf = internal global [2048 x i8] zeroinitializer\n")
|
||||
if (find_extern("net_poll") == null) { g_uses_loopback = true }
|
||||
let psym = net_poll_sym()
|
||||
emit("define void @L_net_pump() {\nentry:\n br label %loop\n")
|
||||
emit("loop:\n %n = call i32 @"); emit(psym); emit("(ptr @L_recvbuf, i32 2048)\n")
|
||||
emit(" %done = icmp eq i32 %n, 0\n br i1 %done, label %fin, label %body\n")
|
||||
emit("body:\n %eid = load i32, ptr @L_recvbuf\n")
|
||||
var e = 0
|
||||
while e < len(g_events) {
|
||||
let ev = g_events[e]
|
||||
if (ev.ty != null) {
|
||||
let sk = itoa(e)
|
||||
emit(" %c"); emit(sk); emit(" = icmp eq i32 %eid, "); emit(itoa(net_event_id(ev.s))); emit("\n")
|
||||
emit(" br i1 %c"); emit(sk); emit(", label %h"); emit(sk); emit(", label %x"); emit(sk); emit("\n")
|
||||
emit("h"); emit(sk); emit(":\n")
|
||||
# decode each field from the frame (offset starts after the i32 event id)
|
||||
var off = 4
|
||||
var f = 0
|
||||
let acc = buf_new()
|
||||
while f < len(ev.kids) {
|
||||
let ft = llty(ev.kids[f].ty)
|
||||
let fk = `{sk}_{itoa(f)}`
|
||||
emit(" %fa"); emit(fk); emit(" = getelementptr inbounds i8, ptr @L_recvbuf, i32 "); emit(itoa(off)); emit("\n")
|
||||
emit(" %fv"); emit(fk); emit(" = load "); emit(ft); emit(", ptr %fa"); emit(fk); emit("\n")
|
||||
if f > 0 { buf_puts(acc, ", ") }
|
||||
buf_puts(acc, ft); buf_puts(acc, " %fv"); buf_puts(acc, fk)
|
||||
off = off + net_field_ibytes(ev.kids[f].ty)
|
||||
f = f + 1
|
||||
}
|
||||
emit(" call void @ev_"); emit(ev.s); emit("("); emit(buf_str(acc)); emit(")\n")
|
||||
emit(" br label %loop\n")
|
||||
emit("x"); emit(sk); emit(":\n")
|
||||
}
|
||||
e = e + 1
|
||||
}
|
||||
emit(" br label %loop\n") # unknown id: skip, keep draining
|
||||
emit("fin:\n ret void\n}\n\n")
|
||||
}
|
||||
|
||||
# ---- driver ------------------------------------------------------------------
|
||||
fn emit_net() -> void {
|
||||
if net_has_sync() {
|
||||
net_check()
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
if prog[i].kind == N_ARCH and net_model_syncs(prog[i]) {
|
||||
emit_net_serialize(prog[i])
|
||||
emit_net_apply(prog[i])
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
emit_net_dispatch()
|
||||
}
|
||||
if net_has_owned() { emit_net_owner() }
|
||||
if net_has_remote() {
|
||||
emith("@L_sendbuf = internal global [2048 x i8] zeroinitializer\n") # RPC send scratch
|
||||
emit_net_pump()
|
||||
}
|
||||
}
|
||||
|
|
@ -5,13 +5,47 @@
|
|||
|
||||
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: ptr = null # "file" | "save" | "load" | "size"
|
||||
var g_off: ptr = null # current byte-offset register, buffer modes
|
||||
|
||||
fn emit_io(fn2: ptr, p: ptr, bytes: ptr) -> void {
|
||||
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")
|
||||
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
|
||||
}
|
||||
|
||||
fn emit_snapshot_blocks(fn2: ptr) -> void {
|
||||
g_iok = 0
|
||||
g_off = "0"
|
||||
let me = itoa(MAX_ENT)
|
||||
emit_io(fn2, "@L_entc", "4")
|
||||
emit_io(fn2, "@L_freen", "4")
|
||||
|
|
@ -19,6 +53,9 @@ fn emit_snapshot_blocks(fn2: ptr) -> void {
|
|||
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
|
||||
|
|
@ -37,6 +74,7 @@ fn emit_snapshot_blocks(fn2: ptr) -> void {
|
|||
}
|
||||
|
||||
fn 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")
|
||||
|
|
@ -58,4 +96,32 @@ fn emit_snapshot() -> void {
|
|||
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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ fn emit_init_component(e: ptr, comp: ptr, rec: Node) -> void {
|
|||
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
|
||||
|
|
@ -65,7 +68,7 @@ fn emit_bind_props(model: Node, e: ptr) -> void {
|
|||
}
|
||||
}
|
||||
|
||||
fn emit_spawn(st: Node) -> void {
|
||||
fn emit_spawn(st: Node) -> ptr {
|
||||
let e = emit_bind("call i32 @L_alloc()")
|
||||
let ak = find_arch_id(st.s)
|
||||
if ak > 0 {
|
||||
|
|
@ -89,10 +92,15 @@ fn emit_spawn(st: Node) -> void {
|
|||
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>)
|
||||
}
|
||||
|
||||
fn emit_despawn(st: Node) -> void {
|
||||
|
|
@ -108,19 +116,76 @@ fn emit_despawn(st: Node) -> void {
|
|||
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(")\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).
|
||||
fn 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.
|
||||
fn 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.
|
||||
fn 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)
|
||||
|
|
@ -137,6 +202,9 @@ fn emit_toggle(st: Node) -> void {
|
|||
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")
|
||||
|
|
|
|||
|
|
@ -160,6 +160,67 @@ fn emit_match(st: Node) -> void {
|
|||
emit(endl); emit(":\n"); g_term = false
|
||||
}
|
||||
|
||||
# emit E(field: v, ...) — evaluate the payload args in the event's declared field
|
||||
# order (so call args line up with the `@ev_<E>` signature), then a direct call.
|
||||
# A missing arg falls back to the field's default; an unset scalar/ptr to 0/null.
|
||||
fn emit_emit(st: Node) -> Val {
|
||||
let ev = find_event(st.s)
|
||||
if (ev == null) { perr(`emit: unknown event {st.s}`) }
|
||||
# evaluate each payload field in declared order (default for a missing arg)
|
||||
let fcodes = new []ptr
|
||||
let ftys = new []ptr
|
||||
var f = 0
|
||||
while f < len(ev.kids) {
|
||||
let fd = ev.kids[f]
|
||||
var av: Node = null # the caller's value for this field, if given
|
||||
var j = 0
|
||||
while j < len(st.a.kids) { if (st.a.kids[j].s == fd.s) { av = st.a.kids[j].a }; j = j + 1 }
|
||||
let lt = llty(fd.ty)
|
||||
var code = "0"
|
||||
if (lt == "ptr") { code = "null" }
|
||||
if (av != null) { let v = emit_expr(av); code = v.code }
|
||||
else { if (fd.a != null) { let dv = emit_expr(fd.a); code = dv.code } } # declared default
|
||||
push(fcodes, code); push(ftys, lt)
|
||||
f = f + 1
|
||||
}
|
||||
# N4: a remote event (@ToServer/@ToClients) serializes its payload as
|
||||
# [i32 event_id][packed fields] and net_send in its direction — the far side's
|
||||
# net_pump() re-emits it. It is a remote call, so there is no local dispatch.
|
||||
if (ev.ty != null) {
|
||||
let idp = nreg(); emit(" "); emit(idp); emit(" = getelementptr inbounds i8, ptr @L_sendbuf, i32 0\n")
|
||||
emit(" store i32 "); emit(itoa(net_event_id(st.s))); emit(", ptr "); emit(idp); emit("\n")
|
||||
var off = 4
|
||||
var k = 0
|
||||
while k < len(ev.kids) {
|
||||
let dp = nreg(); emit(" "); emit(dp); emit(" = getelementptr inbounds i8, ptr @L_sendbuf, i32 "); emit(itoa(off)); emit("\n")
|
||||
emit(" store "); emit(ftys[k]); emit(" "); emit(fcodes[k]); emit(", ptr "); emit(dp); emit("\n")
|
||||
off = off + net_field_ibytes(ev.kids[k].ty)
|
||||
k = k + 1
|
||||
}
|
||||
var peer = "0"
|
||||
if (ev.ty == "toclients") { peer = "-1" } # broadcast (loopback ignores the peer id)
|
||||
if (find_extern("net_send") == null) { g_uses_loopback = true }
|
||||
emit(" call void @"); emit(net_send_sym()); emit("(i32 "); emit(peer); emit(", ptr @L_sendbuf, i32 "); emit(itoa(off)); emit(")\n")
|
||||
return val("0", "int")
|
||||
}
|
||||
# a local event: build the call args and dispatch. A cancellable event returns
|
||||
# its cancelled flag (i32); a plain event is void.
|
||||
let args = buf_new()
|
||||
var g = 0
|
||||
while g < len(fcodes) {
|
||||
if g > 0 { buf_puts(args, ", ") }
|
||||
buf_puts(args, ftys[g]); buf_puts(args, " "); buf_puts(args, fcodes[g])
|
||||
g = g + 1
|
||||
}
|
||||
if ev.ival == 1 {
|
||||
let r = nreg()
|
||||
emit(" "); emit(r); emit(" = call i32 @ev_"); emit(st.s); emit("("); emit(buf_str(args)); emit(")\n")
|
||||
return val(r, "int")
|
||||
}
|
||||
emit(" call void @ev_"); emit(st.s); emit("("); emit(buf_str(args)); emit(")\n")
|
||||
return val("0", "int")
|
||||
}
|
||||
|
||||
fn emit_stmt(st: Node) -> void {
|
||||
if st.kind == S_LET {
|
||||
var ty = st.ty
|
||||
|
|
@ -184,9 +245,16 @@ fn emit_stmt(st: Node) -> void {
|
|||
if st.kind == S_CONTINUE { emit(" br label %"); emit(cnt_lbl[nloop - 1]); emit("\n"); g_term = true; return }
|
||||
if st.kind == S_MATCH { emit_match(st); return }
|
||||
if st.kind == S_QUERY { emit_query(st); return }
|
||||
if st.kind == S_SPAWN { emit_spawn(st); return }
|
||||
if st.kind == S_SPAWN { let se = emit_spawn(st); return }
|
||||
if st.kind == S_DESPAWN { emit_despawn(st); return }
|
||||
if st.kind == S_TOGGLE { emit_toggle(st); return }
|
||||
if st.kind == S_ATTACH { emit_attach(st); return }
|
||||
if st.kind == S_DETACH { emit_detach(st); return }
|
||||
if st.kind == S_EMIT { let v = emit_emit(st); return } # statement form: discard the flag
|
||||
if st.kind == S_CANCEL { # veto the enclosing cancellable event
|
||||
if (g_cancel_addr == null) { perr("cancel outside a cancellable event listener") }
|
||||
emit(" store i32 1, ptr "); emit(g_cancel_addr); emit("\n"); return
|
||||
}
|
||||
if st.kind == S_MACHINE { emit_machine(st); return }
|
||||
if st.kind == S_BECOME { emit_become(st); return }
|
||||
if st.kind == S_EXPR { let v = emit_expr(st.a); return }
|
||||
|
|
|
|||
21758
selfhost/ludicc.seed.ll
21758
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -109,9 +109,26 @@ fn parse_interp(raw: ptr) -> Node {
|
|||
return acc
|
||||
}
|
||||
|
||||
# emit E(field: v, ...) — shared by the statement form and the expression form.
|
||||
# As an expression it yields a cancellable event's cancelled flag (0/1); a
|
||||
# non-cancellable event yields 0.
|
||||
fn parse_emit() -> Node {
|
||||
pi = pi + 1; let n = node(S_EMIT); n.s = eat_id()
|
||||
let r = node(E_REC)
|
||||
eat_op("("); skipnl()
|
||||
while not is_op(")") {
|
||||
let fi = node(E_FINIT); fi.s = eat_id(); eat_op(":"); fi.a = expr(); push(r.kids, fi)
|
||||
skipnl(); if is_op(",") { pi = pi + 1; skipnl() }
|
||||
}
|
||||
eat_op(")")
|
||||
n.a = r
|
||||
return n
|
||||
}
|
||||
|
||||
fn p_primary() -> Node {
|
||||
let t = toks[pi]
|
||||
if t.kind == TK_INTERP { pi = pi + 1; return parse_interp(t.text) }
|
||||
if t.kind == TK_ID and (t.text == "emit") and (toks[pi + 1].kind == TK_ID) { return parse_emit() }
|
||||
if t.kind == TK_INT { let n = node(E_INT); n.ival = t.ival; pi = pi + 1; return n }
|
||||
if t.kind == TK_FLOAT { let n = node(E_FLOAT); n.ival = t.ival; pi = pi + 1; return n }
|
||||
if t.kind == TK_STR { let n = node(E_STR); n.s = t.text; pi = pi + 1; return n }
|
||||
|
|
@ -253,16 +270,35 @@ fn stmt() -> Node {
|
|||
push(n.kids, s); sidx = sidx + 1 }
|
||||
eat_op("}"); return n
|
||||
}
|
||||
# `emit E(...)` fires an event, but a bare `emit(...)` is an ordinary call
|
||||
# (the compiler dogfoods a function named `emit`), so require an event name.
|
||||
if (t.text == "emit") and (toks[pi + 1].kind == TK_ID) { return parse_emit() }
|
||||
if (t.text == "become") { pi = pi + 1; let n = node(S_BECOME); n.s = eat_id(); return n }
|
||||
if (t.text == "despawn") { pi = pi + 1; let n = node(S_DESPAWN); n.a = expr(); return n }
|
||||
if (t.text == "enable") or (t.text == "disable") {
|
||||
var en = 0; if (t.text == "enable") { en = 1 }
|
||||
pi = pi + 1; let n = node(S_TOGGLE); n.ival = en; n.s = eat_id() # `enable P on e` / `disable Model` / `disable Handler`
|
||||
pi = pi + 1; let n = node(S_TOGGLE); n.ival = en
|
||||
if is_id("layer") { pi = pi + 1; n.ty = "layer"; n.s = eat_id(); note_toggled_layer(n.s); return n } # enable/disable layer L
|
||||
n.s = eat_id() # `enable P on e` / `disable Model` / `disable Handler`
|
||||
if is_id("on") { pi = pi + 1; n.a = expr() } # property on an entity
|
||||
return n
|
||||
}
|
||||
if (t.text == "attach") { # attach P on e [{ field: val, ... }]
|
||||
pi = pi + 1; let n = node(S_ATTACH); n.s = eat_id()
|
||||
if not is_id("on") { perr("attach needs 'on <entity>'") }
|
||||
pi = pi + 1; n.a = expr()
|
||||
if is_op("{") { n.b = record() } # optional field overrides (same-line)
|
||||
return n
|
||||
}
|
||||
if (t.text == "detach") { # detach P on e
|
||||
pi = pi + 1; let n = node(S_DETACH); n.s = eat_id()
|
||||
if not is_id("on") { perr("detach needs 'on <entity>'") }
|
||||
pi = pi + 1; n.a = expr()
|
||||
return n
|
||||
}
|
||||
if (t.text == "break") { pi = pi + 1; return node(S_BREAK) }
|
||||
if (t.text == "continue") { pi = pi + 1; return node(S_CONTINUE) }
|
||||
if (t.text == "cancel") { pi = pi + 1; return node(S_CANCEL) } # veto a cancellable event
|
||||
if (t.text == "match") {
|
||||
pi = pi + 1; let n = node(S_MATCH); n.a = expr(); skipnl(); eat_op("{")
|
||||
while true {
|
||||
|
|
@ -337,23 +373,42 @@ fn parse_one_decl() -> void {
|
|||
var qspec: Node = null
|
||||
var onspawn_model: ptr = null
|
||||
var ondespawn_model: ptr = null
|
||||
var ondespawn_reason: ptr = null # @OnDespawn(M, reason: r) — LC1 teardown reason binding
|
||||
var onattach_prop: ptr = null
|
||||
var ondetach_prop: ptr = null
|
||||
var onenable_prop: ptr = null
|
||||
var ondisable_prop: ptr = null
|
||||
var on_event: ptr = null # @On(Event) — a compile-time event listener
|
||||
var is_public = false # @Public — promote a lifecycle hook to an event
|
||||
var hook_phase: ptr = null # @OnStart / @OnQuit override the phase
|
||||
var is_sync_prop = false # @Sync property P — every field replicates (NETWORKING N2)
|
||||
var is_owned = false # @Owned model M — entities carry a network owner (N3)
|
||||
var role: ptr = null # @Server / @Predicted — a handler's network role (N5)
|
||||
var remote_dir: ptr = null # @ToServer / @ToClients — a remote event's direction (N4)
|
||||
while is_op("@") {
|
||||
pi = pi + 1; let a = eat_id() # collect a leading @annotation
|
||||
if (a == "export") { is_export = true }
|
||||
else { if (a == "Public") { is_public = true } # @Public hook promotion
|
||||
else { if (a == "On") { eat_op("("); on_event = eat_id(); eat_op(")") } # @On(Event) listener
|
||||
else { if (a == "Queries") { qspec = parse_queries_anno() } # @Queries(these: [...], on: ...)
|
||||
else { if (a == "OnSpawn") { eat_op("("); onspawn_model = eat_id(); eat_op(")") }
|
||||
else { if (a == "OnDespawn") { eat_op("("); ondespawn_model = eat_id(); eat_op(")") }
|
||||
else { if (a == "OnDespawn") { eat_op("("); ondespawn_model = eat_id()
|
||||
if is_op(",") { pi = pi + 1; eat_id(); eat_op(":"); ondespawn_reason = eat_id() } # , reason: r
|
||||
eat_op(")") }
|
||||
else { if (a == "OnAttach") { eat_op("("); onattach_prop = eat_id(); eat_op(")") }
|
||||
else { if (a == "OnDetach") { eat_op("("); ondetach_prop = eat_id(); eat_op(")") }
|
||||
else { if (a == "OnEnable") { eat_op("("); onenable_prop = eat_id(); eat_op(")") }
|
||||
else { if (a == "OnDisable") { eat_op("("); ondisable_prop = eat_id(); eat_op(")") }
|
||||
else { if (a == "OnStart") { hook_phase = "Start" } # boot
|
||||
else { if (a == "OnQuit") { hook_phase = "OnQuit" } # shutdown
|
||||
else { if (a == "Sync") { is_sync_prop = true } # @Sync property (N2)
|
||||
else { if (a == "Owned") { is_owned = true } # @Owned model (N3)
|
||||
else { if (a == "Server") { role = "server" } # @Server handler (N5)
|
||||
else { if (a == "Predicted") { role = "predicted" } # @Predicted handler (N5)
|
||||
else { if (a == "ToServer") { remote_dir = "toserver" } # @ToServer event (N4)
|
||||
else { if (a == "ToClients") { remote_dir = "toclients" } # @ToClients event (N4)
|
||||
else { if is_op("(") { var d = 0 # any other @anno(args) — parsed and skipped
|
||||
while true { if is_op("(") { d = d + 1 }; if is_op(")") { d = d - 1 }; pi = pi + 1; if d == 0 { break } } } } } } } } } } } }
|
||||
while true { if is_op("(") { d = d + 1 }; if is_op(")") { d = d - 1 }; pi = pi + 1; if d == 0 { break } } } } } } } } } } } } } } } } } } } } }
|
||||
skipnl()
|
||||
}
|
||||
if is_id("import") { pi = pi + 1
|
||||
|
|
@ -364,16 +419,47 @@ fn parse_one_decl() -> void {
|
|||
return
|
||||
}
|
||||
if is_id("enum") { push(prog, parse_enum()); return }
|
||||
if is_id("property") { push(prog, parse_component()); return }
|
||||
if is_id("model") { push(prog, parse_archetype()); return }
|
||||
if is_id("event") {
|
||||
let ev = parse_event()
|
||||
if (remote_dir != null) { ev.ty = remote_dir } # N4: a directional remote event (RPC)
|
||||
register_event(ev); return
|
||||
}
|
||||
if is_id("property") {
|
||||
let c = parse_component()
|
||||
if is_sync_prop { var fi = 0; while fi < len(c.kids) { c.kids[fi].ival = 1; fi = fi + 1 } } # N2: mark every field replicable
|
||||
push(prog, c); return
|
||||
}
|
||||
if is_id("model") {
|
||||
let m = parse_archetype()
|
||||
if is_owned { m.ival = 1 } # N3: this model's entities carry a network owner
|
||||
push(prog, m); return
|
||||
}
|
||||
if is_id("scene") { parse_scene(); return } # layers push handlers into prog; scene -> g_scenes
|
||||
if is_id("handler") {
|
||||
let h = parse_system()
|
||||
if (onspawn_model != null) { register_onspawn(onspawn_model, h.a); return } # spawn hook
|
||||
if (ondespawn_model != null) { register_ondespawn(ondespawn_model, h.a); return } # despawn hook
|
||||
if (onattach_prop != null) { register_onattach(onattach_prop, h.a); return } # attach hook
|
||||
if (onenable_prop != null) { register_onenable(onenable_prop, h.a); return } # enable hook
|
||||
if (ondisable_prop != null) { register_ondisable(ondisable_prop, h.a); return } # disable hook
|
||||
if (hook_phase != null) { h.ty = hook_phase } # @OnStart/@OnQuit
|
||||
if (role != null) { if (role == "server") { h.ival = 1 } else { h.ival = 2 } } # N5: @Server=1 / @Predicted=2
|
||||
if (on_event != null) { register_onlisten(on_event, h.a); return } # @On(Event) listener
|
||||
if (onspawn_model != null) {
|
||||
register_onspawn(onspawn_model, h.a) # spawn hook
|
||||
if is_public { ensure_event(`model_{onspawn_model}_spawn`, false) } # @Public -> model_<M>_spawn
|
||||
return
|
||||
}
|
||||
if (ondespawn_model != null) {
|
||||
register_ondespawn(ondespawn_model, h.a, ondespawn_reason) # despawn hook
|
||||
if is_public { ensure_event(`model_{ondespawn_model}_despawn`, true) } # @Public -> model_<M>_despawn (with reason)
|
||||
return
|
||||
}
|
||||
if (onattach_prop != null) { register_onattach(onattach_prop, h.a); if is_public { ensure_event(`prop_{onattach_prop}_attach`, false) }; return } # -> prop_<P>_attach
|
||||
if (ondetach_prop != null) { register_ondetach(ondetach_prop, h.a); if is_public { ensure_event(`prop_{ondetach_prop}_detach`, false) }; return } # -> prop_<P>_detach
|
||||
if (onenable_prop != null) { register_onenable(onenable_prop, h.a); if is_public { ensure_event(`prop_{onenable_prop}_enable`, false) }; return } # -> prop_<P>_enable
|
||||
if (ondisable_prop != null) { register_ondisable(ondisable_prop, h.a); if is_public { ensure_event(`prop_{ondisable_prop}_disable`, false) }; return } # -> prop_<P>_disable
|
||||
if (hook_phase != null) { # @OnStart/@OnQuit
|
||||
h.ty = hook_phase
|
||||
if is_public { # -> program_start / program_quit
|
||||
if (hook_phase == "Start") { ensure_event_empty("program_start") }
|
||||
else { ensure_event_empty("program_quit") }
|
||||
}
|
||||
}
|
||||
if (qspec != null) { # @Queries wraps the body in its S_QUERY
|
||||
qspec.a = h.a
|
||||
let wrap = node(N_BLOCK); push(wrap.kids, qspec); h.a = wrap
|
||||
|
|
@ -421,8 +507,15 @@ fn parse_program() -> void {
|
|||
g_onspawn = new []Node
|
||||
g_ondespawn = new []Node
|
||||
g_onattach = new []Node
|
||||
g_ondetach = new []Node
|
||||
g_onenable = new []Node
|
||||
g_ondisable = new []Node
|
||||
g_scenes = new []Node
|
||||
g_scene_count = 0
|
||||
g_start_scene = 0
|
||||
g_events = new []Node
|
||||
g_onlisten = new []Node
|
||||
g_toggled_layers = new []ptr
|
||||
loaded_paths = new []ptr
|
||||
skipnl()
|
||||
g_game_name = "Ludic"
|
||||
|
|
|
|||
|
|
@ -6,15 +6,32 @@ fn parse_component() -> Node {
|
|||
pi = pi + 1; let n = node(N_COMP); n.s = eat_id(); skipnl(); eat_op("{")
|
||||
while true { skipnl(); if is_op("}") { break }
|
||||
var is_computed = false
|
||||
if is_op("@") { pi = pi + 1; let ann = eat_id(); if (ann == "Computed") { is_computed = true }; skipnl() }
|
||||
var is_sync = false # @Sync — this field replicates (NETWORKING N2)
|
||||
if is_op("@") { pi = pi + 1; let ann = eat_id(); if (ann == "Computed") { is_computed = true } else { if (ann == "Sync") { is_sync = true } }; skipnl() }
|
||||
let f = node(N_FIELD); f.s = eat_id(); eat_op(":"); f.ty = ptype()
|
||||
if is_op("=") { pi = pi + 1; f.a = expr() }
|
||||
if is_sync { f.ival = 1 } # mark the field replicable (read by emit_net)
|
||||
if is_computed { register_computed(n.s, f.s, f.ty, f.a) } # derived: no storage
|
||||
else { push(n.kids, f) }
|
||||
if is_op(",") { pi = pi + 1 } }
|
||||
eat_op("}"); return n
|
||||
}
|
||||
|
||||
# event Name { field: T = default, ... } — a public event's POD payload. Same
|
||||
# field grammar as a `property`, but stored in g_events, not prog: an event is a
|
||||
# signal shape, not per-entity storage. Zero fields is allowed (`event Ping {}`).
|
||||
fn parse_event() -> Node {
|
||||
pi = pi + 1; let n = node(N_EVENT)
|
||||
if is_id("cancellable") { pi = pi + 1; n.ival = 1 } # a decision event: listeners may `cancel` it
|
||||
n.s = eat_id(); skipnl(); eat_op("{")
|
||||
while true { skipnl(); if is_op("}") { break }
|
||||
let f = node(N_FIELD); f.s = eat_id(); eat_op(":"); f.ty = ptype()
|
||||
if is_op("=") { pi = pi + 1; f.a = expr() }
|
||||
push(n.kids, f)
|
||||
if is_op(",") { pi = pi + 1 } }
|
||||
eat_op("}"); return n
|
||||
}
|
||||
|
||||
fn parse_system() -> Node {
|
||||
pi = pi + 1; let n = node(N_SYS); n.s = eat_id(); n.ty = "Update"
|
||||
# postfix clauses on `handler Name …`: @anno(...) (parsed and reserved, e.g.
|
||||
|
|
@ -139,6 +156,57 @@ fn parse_spawn() -> Node {
|
|||
eat_op("}"); return n
|
||||
}
|
||||
|
||||
# scene Name [start] { on enter {..} on exit {..} layer L { handler .. } .. }
|
||||
# A scene groups handlers behind an implicit active-scene register; only the
|
||||
# active scene's handlers run each phase. `on enter`/`on exit` are lifecycle
|
||||
# blocks (scene .a/.b); each layer's handlers are pushed straight into `prog` as
|
||||
# ordinary N_SYS nodes, tagged with the owning scene in `.c`, so the whole
|
||||
# system backend (functions, phases, enable/disable) is reused unchanged.
|
||||
fn parse_scene() -> void {
|
||||
pi = pi + 1 # 'scene'
|
||||
let n = node(N_SCENE); n.s = eat_id()
|
||||
n.ival = g_scene_count
|
||||
# optional modifiers after the name, any order: `start` (the boot scene) and
|
||||
# `public` (promote its on-enter/on-exit to scene_<S>_enter / scene_<S>_exit).
|
||||
var is_pub = false
|
||||
while is_id("start") or is_id("public") {
|
||||
if is_id("start") { pi = pi + 1; g_start_scene = g_scene_count }
|
||||
else { pi = pi + 1; is_pub = true }
|
||||
}
|
||||
g_scene_count = g_scene_count + 1
|
||||
if is_pub { ensure_event_empty(`scene_{n.s}_enter`); ensure_event_empty(`scene_{n.s}_exit`) }
|
||||
skipnl(); eat_op("{")
|
||||
while true {
|
||||
skipnl(); if is_op("}") { break }
|
||||
if is_id("on") { # on enter { .. } / on exit { .. }
|
||||
pi = pi + 1; let which = eat_id(); skipnl()
|
||||
if (which == "enter") { n.a = block() }
|
||||
else { if (which == "exit") { n.b = block() } else { perr("expected 'enter' or 'exit' after 'on'") } }
|
||||
continue
|
||||
}
|
||||
if is_id("layer") { # layer Name [public] { handler .. }
|
||||
pi = pi + 1; let lname = eat_id()
|
||||
if is_id("public") { pi = pi + 1; ensure_event_empty(`layer_{lname}_show`); ensure_event_empty(`layer_{lname}_hide`) }
|
||||
skipnl(); eat_op("{")
|
||||
let ltag = node(E_ID); ltag.s = lname # the layer name, tagged onto each handler
|
||||
while true {
|
||||
skipnl(); if is_op("}") { break }
|
||||
if not is_id("handler") { perr("expected 'handler' in layer") }
|
||||
let h = parse_system() # N_SYS: .s name, .ty phase, .a body
|
||||
h.c = n # tag the owning scene (null = global)
|
||||
h.b = ltag # tag the owning layer (for enable/disable layer)
|
||||
push(prog, h)
|
||||
skipnl()
|
||||
}
|
||||
eat_op("}")
|
||||
continue
|
||||
}
|
||||
perr("expected 'on', 'layer' or '}' in scene")
|
||||
}
|
||||
eat_op("}")
|
||||
push(g_scenes, n)
|
||||
}
|
||||
|
||||
# enum Name { A, B, C } — named int constants; a variant's value is its index.
|
||||
# Accessed as `Name.A` (a compile-time int), so it names magic-int value spaces
|
||||
# (state ids, menu selections, mode registers) without a runtime cost.
|
||||
|
|
@ -150,11 +218,17 @@ fn parse_enum() -> Node {
|
|||
eat_op("}"); return n
|
||||
}
|
||||
|
||||
# archetype Name { CompA, CompB } — a named entity kind (bundle of components)
|
||||
# archetype Name { CompA, @Sync CompB } — a named entity kind (bundle of
|
||||
# components). A member marked `@Sync` *participates* in replication (NETWORKING
|
||||
# N2): its @Sync-marked fields cross the wire for this model. Participation is
|
||||
# per model use-site — the same property syncs in one model, not another. The
|
||||
# per-member @Sync sets the member E_ID's ival=1 (read by emit_net).
|
||||
fn parse_archetype() -> Node {
|
||||
pi = pi + 1; let n = node(N_ARCH); n.s = eat_id(); skipnl(); eat_op("{")
|
||||
while true { skipnl(); if is_op("}") { break }
|
||||
let c = node(E_ID); c.s = eat_id(); push(n.kids, c)
|
||||
let c = node(E_ID)
|
||||
if is_op("@") { pi = pi + 1; let a = eat_id(); if (a == "Sync") { c.ival = 1 }; skipnl() }
|
||||
c.s = eat_id(); push(n.kids, c)
|
||||
if is_op(",") { pi = pi + 1 }; skipnl() }
|
||||
eat_op("}"); return n
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ B=build/cfree; mkdir -p "$B"
|
|||
FRAGS="selfhost/str.ludic selfhost/buf.ludic selfhost/io.ludic selfhost/ast.ludic
|
||||
selfhost/lex.ludic selfhost/parse.ludic selfhost/parse_game.ludic selfhost/emit_core.ludic selfhost/emit_head.ludic
|
||||
selfhost/emit_addr.ludic selfhost/emit_intrin.ludic selfhost/emit_intrin2.ludic selfhost/emit_math.ludic selfhost/emit_new.ludic
|
||||
selfhost/emit_expr.ludic selfhost/emit_stmt.ludic selfhost/emit_ecs.ludic selfhost/emit_query.ludic selfhost/emit_spawn.ludic selfhost/emit_game.ludic selfhost/emit_machine.ludic selfhost/emit_save.ludic selfhost/emit_ui.ludic selfhost/emit_decl.ludic selfhost/main.ludic"
|
||||
selfhost/emit_expr.ludic selfhost/emit_stmt.ludic selfhost/emit_ecs.ludic selfhost/emit_query.ludic selfhost/emit_spawn.ludic selfhost/emit_game.ludic selfhost/emit_machine.ludic selfhost/emit_save.ludic
|
||||
selfhost/emit_net.ludic selfhost/emit_ui.ludic selfhost/emit_decl.ludic selfhost/main.ludic"
|
||||
{ echo "program SelfHost {"; for f in $FRAGS; do cat "$f"; echo; done; echo "}"; } > "$B/selfhost.ludic"
|
||||
if $CC selfhost/ludicc.seed.ll -o "$B/sh_old" 2>/dev/null; then
|
||||
# compile once with the old seed, then AGAIN with the freshly built one so the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue