# emit_core.ludic — emitter state, type mapping, struct/slice helpers, and the # module header. Mirrors the pieces of compiler/back/ that this subset needs. # structs and slices are references, so every non-scalar type lowers to `ptr`. property Val { code: ptr = null, ty: ptr = null } fn val(code: ptr, ty: ptr) -> Val { let v = new Val; v.code = code; v.ty = ty; return v } var head: Buf # module-level: types, globals, string constants var code: Buf # function bodies var falloc: Buf # entry-block allocas for the current function var ll_t: int = 0 # temp register counter (reset per function) var ll_lbl: int = 0 # label counter var ll_str: int = 0 # string-constant counter # local environment (parallel slices), reset per function var loc_name: []ptr var loc_reg: []ptr var loc_ty: []ptr var loc_mut: []int # 1 = mutable (var / param / loop-var), 0 = immutable (let) var nloc: int = 0 var g_uses_str: bool = false # a `str + str` / `str == str` was emitted -> emit the prelude var g_uses_intstr: bool = false # `str(int)` was emitted -> emit the int->string prelude var g_uses_strslice: bool = false # `s[a..b]` was emitted -> emit the substring prelude # loop targets for break/continue (innermost last) var brk_lbl: []ptr var cnt_lbl: []ptr var nloop: int = 0 var ret_ty: ptr # current function's return type var g_term: bool = false # did the current block end in a terminator? var self_stk: []ptr # entity-index slot (ip) per enclosing query, for self() var nself: int = 0 var mach_stk: []Node # enclosing `machine` nodes, so `become` finds its register var nmach: int = 0 fn emit(s: ptr) -> void { buf_puts(code, s) } fn emith(s: ptr) -> void { buf_puts(head, s) } # stack slots MUST live in the entry block (an alloca in a loop walks the stack # off its end), so they go into a per-function buffer spliced in at entry. fn emit_alloca(llt: ptr) -> ptr { let r = `%t{itoa(ll_t)}`; ll_t = ll_t + 1 buf_puts(falloc, " "); buf_puts(falloc, r); buf_puts(falloc, " = alloca "); buf_puts(falloc, llt); buf_puts(falloc, "\n") return r } # "%t" fresh register fn nreg() -> ptr { let r = `%t{itoa(ll_t)}`; ll_t = ll_t + 1; return r } fn lbl(pfx: ptr) -> ptr { let r = (pfx + itoa(ll_lbl)); ll_lbl = ll_lbl + 1; return 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 == "byte") { return "i8" } # a single byte (p[i] on a raw ptr) if (t == "words") { return "ptr" } # a 32-bit-word buffer (w[i] is i32) if (t == "void") { return "void" } return "ptr" } fn is_slice_ty(t: ptr) -> bool { return t[0] == 91 and t[1] == 93 } # "[]" fn slice_elem(t: ptr) -> ptr { return t[2..len(t)] } fn find_arch(name: ptr) -> Node { var i = 0 while i < len(prog) { let d = prog[i]; if d.kind == N_ARCH and (d.s == name) { return d }; i = i + 1 } return null } fn find_comp(name: ptr) -> Node { var i = 0 while i < len(prog) { let d = prog[i]; if d.kind == N_COMP and (d.s == name) { return d }; i = i + 1 } return null } # every record is a `property` with a %Cmp_ layout of named fields — whether it # is stored per-entity by the ECS or heap-allocated by `new` is a matter of use. fn layout_node(name: ptr) -> Node { return find_comp(name) } fn layout_ty(name: ptr) -> ptr { return (("%Cmp_") + name) } fn field_index(s: Node, fname: ptr) -> int { var i = 0 while i < len(s.kids) { if (s.kids[i].s == fname) { return i }; i = i + 1 } return 0 - 1 } fn field_type(s: Node, fname: ptr) -> ptr { var i = 0 while i < len(s.kids) { if (s.kids[i].s == fname) { return s.kids[i].ty }; i = i + 1 } return "int" } # find a global var/const by name fn find_global(name: ptr) -> Node { var i = 0 while i < len(prog) { let d = prog[i] if d.kind == N_VAR and (d.s == name) { return d } if d.kind == N_CONST and (d.s == name) { return d } i = i + 1 } return null } # `Enum.Variant` -> the variant's ordinal (its index), or -1 if `ename` names no # enum with that variant. Enum names live in `prog` like any other declaration. fn enum_ordinal(ename: ptr, vname: ptr) -> int { var i = 0 while i < len(prog) { let d = prog[i] if d.kind == N_ENUM and (d.s == ename) { var j = 0 while j < len(d.kids) { if (d.kids[j].s == vname) { return j }; j = j + 1 } } i = i + 1 } return 0 - 1 } fn find_fn(name: ptr) -> Node { var i = 0 while i < len(prog) { let d = prog[i]; if d.kind == N_FN 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. var g_computed: []Node # each: s = "Prop.field", ty = result type, a = expr fn register_computed(prop: ptr, field: ptr, ty: ptr, e: Node) -> void { let cf = node(N_FIELD); cf.s = `{prop}.{field}`; cf.ty = ty; cf.a = e push(g_computed, cf) } fn computed_expr(prop: ptr, field: ptr) -> Node { if (prop == null) { return null } let key = `{prop}.{field}` var i = 0 while i < len(g_computed) { if (g_computed[i].s == key) { return g_computed[i].a }; i = i + 1 } return null } # best-effort static type of an expression (for computed-field lookup; emits nothing) fn static_type(e: Node) -> ptr { if e.kind == E_ID { let li = loc_find(e.s); if li >= 0 { return loc_ty[li] } } return null } # @OnSpawn(Model) hooks: a Model -> hook-body registry. Populated at parse time; # `spawn Model { … }` runs the body with the model's properties bound (like a # constructor). Spawn statically knows the model, so no runtime dispatch is needed. var g_onspawn: []Node # each: s = Model name, a = hook body block fn register_onspawn(model: ptr, body: Node) -> void { let n = node(N_BLOCK); n.s = model; n.a = body; push(g_onspawn, n) } fn onspawn_body(model: ptr) -> Node { var i = 0 while i < len(g_onspawn) { if (g_onspawn[i].s == model) { return g_onspawn[i].a }; i = i + 1 } return null } # @OnDespawn(Model): a Model -> hook-body registry. Despawn does not statically # know an entity's model, so these are emitted as functions and dispatched on the # entity's kind at each `despawn`. @OnAttach(Property) fires per property-attach. 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) } fn ondespawn_body(model: ptr) -> Node { var i = 0 while i < len(g_ondespawn) { if (g_ondespawn[i].s == model) { return g_ondespawn[i].a }; i = i + 1 } return null } fn register_onattach(prop: ptr, body: Node) -> void { let n = node(N_BLOCK); n.s = prop; n.a = body; push(g_onattach, n) } fn onattach_body(prop: ptr) -> Node { var i = 0 while i < len(g_onattach) { if (g_onattach[i].s == prop) { return g_onattach[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 var g_ondisable: []Node fn register_onenable(prop: ptr, body: Node) -> void { let n = node(N_BLOCK); n.s = prop; n.a = body; push(g_onenable, n) } fn register_ondisable(prop: ptr, body: Node) -> void { let n = node(N_BLOCK); n.s = prop; n.a = body; push(g_ondisable, n) } fn onenable_body(prop: ptr) -> Node { var i = 0 while i < len(g_onenable) { if (g_onenable[i].s == prop) { return g_onenable[i].a }; i = i + 1 } return null } fn ondisable_body(prop: ptr) -> Node { var i = 0 while i < len(g_ondisable) { if (g_ondisable[i].s == prop) { return g_ondisable[i].a }; i = i + 1 } return null } # 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 } # local variable environment fn loc_reset() -> void { nloc = 0 } # push a local. Defaults to mutable (params, loop and query bindings are all # reassignable/rebindable); a `let` binding marks its slot immutable afterward # via loc_set_mut, so a later `name = …` can be rejected. fn loc_push(name: ptr, r: ptr, ty: ptr) -> void { if nloc < len(loc_name) { loc_name[nloc] = name; loc_reg[nloc] = r; loc_ty[nloc] = ty; loc_mut[nloc] = 1 } else { push(loc_name, name); push(loc_reg, r); push(loc_ty, ty); push(loc_mut, 1) } nloc = nloc + 1 } fn loc_set_mut(m: int) -> void { if nloc > 0 { loc_mut[nloc - 1] = m } } fn loc_find(name: ptr) -> int { var i = nloc - 1 while i >= 0 { if (loc_name[i] == name) { return i }; i = i - 1 } return 0 - 1 }