ludic/selfhost/emit_core.ludic
Orkuncakilkaya 5a9e8cb2da Phase 8a: finish typed indexing — fixeds/ptrs buffers (retire peekf/pokef/peekp/pokep/str_len)
The last peek/poke pairs were the same "typed buffer access wearing a C name" as
peek8/peek32, so they become indexing too:

  peekf(sc_x, i)     -> sc_x[i]        (a `fixeds` buffer -> a fixed)
  pokep(gc_bmp, s,b) -> gc_bmp[s] = b  (a `ptrs` buffer -> a pointer)
  str_len(s)         -> len(s)         (len is polymorphic since 7f; str_len was dead)

Two new element-typed buffers join words: `fixeds` (32-bit fixed) and `ptrs`
(pointer, 8-byte stride). emit_index_addr dispatches on the base type; IR is
byte-identical to the old intrinsics.

The peekf/peekp buffers (ed_x0/ed_x1/ol_x/sc_x fixed coords; gc_bmp/img_px/
tt_data/ui_text pointer arrays) were retyped scope-aware, then the 33 sites
migrated to indexing. Two bugs found via a menu golden diff / a link-time type
error and fixed: the buffer-name regex truncated digit suffixes (ed_x0 -> ed_x),
and the char-literal brace-miscount skipped a few module declarations (same class
as 7j).

Reseeded (22371 lines); C-free fixpoint holds; goldens byte-identical; 18/18;
vocab (fixeds/ptrs types in, 5 intrinsics out) + doc-fences clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 02:49:40 +03:00

217 lines
8.8 KiB
Text

# 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<n>" 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") or (t == "fixeds") or (t == "ptrs") { return "ptr" } # typed buffers
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
}