Three enable/disable statement scopes, each a reversible flag flip:
- `disable P on e` / `enable P on e` — one property on one entity. Clears
the has-flag so queries stop matching; field data persists in storage, so
enable restores it untouched. @OnDisable(P)/@OnEnable(P) handler hooks run
at the toggle point with the property bound by name.
- `disable Model` / `enable Model` — @ME_<Model> global flag; the model's
entities drop out of every query while disabled.
- `disable Handler` / `enable Handler` — @HE_<Handler> global flag; the
handler stops being called each phase while disabled.
Nothing is copied or freed — each toggle is one global store or one has-flag
store. Reduces entirely to existing ECS machinery (has-flags, kind filter,
per-phase call guards), so the data-oriented model is untouched.
New AST node S_TOGGLE; emit_toggle lowers it. Query {Model} filter now ANDs
@ME_; phase calls now guard on @HE_. Parser gains enable/disable statements
and @OnEnable/@OnDisable annotations.
Vocabulary: `on` promoted from RESERVED to CLAUSE (parser now dispatches on
it); enable/disable added as STMT keywords — synced across ludic_syntax.h,
the TextMate grammar, and LudicTokens.kt (check-vocabulary.py clean).
examples/toggle.ludic demonstrates all three scopes (prints 6 0 7 1 0);
test.sh smoke asserts it. Reseeded; C-free fixpoint holds; goldens identical.
Also: stop tracking tools/.idea/ (gitignored).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
233 lines
8.8 KiB
Text
233 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`.
|
|
|
|
struct Val { code: ptr = ptr_null(), ty: ptr = 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 nloc: int = 0
|
|
|
|
# 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 = sconcat("%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 sconcat(a: ptr, b: ptr) -> ptr {
|
|
let la = slen(a); let lb = slen(b)
|
|
let out = mem_alloc(la + lb + 1)
|
|
let i = 0
|
|
while i < la { poke8(out, i, peek8(a, i)); i = i + 1 }
|
|
let j = 0
|
|
while j < lb { poke8(out, la + j, peek8(b, j)); j = j + 1 }
|
|
poke8(out, la + lb, 0)
|
|
return out
|
|
}
|
|
fn nreg() -> ptr { let r = sconcat("%t", itoa(ll_t)); ll_t = ll_t + 1; return r }
|
|
fn lbl(pfx: ptr) -> ptr { let r = sconcat(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 streq(t, "int") or streq(t, "bool") or streq(t, "fixed") { return "i32" }
|
|
if streq(t, "void") { return "void" }
|
|
return "ptr"
|
|
}
|
|
|
|
fn is_slice_ty(t: ptr) -> bool { return peek8(t, 0) == 91 and peek8(t, 1) == 93 } # "[]"
|
|
fn slice_elem(t: ptr) -> ptr { return substr(t, 2, slen(t) - 2) }
|
|
|
|
fn find_struct(name: ptr) -> Node {
|
|
let i = 0
|
|
while i < len(prog) {
|
|
let d = prog[i]
|
|
if d.kind == N_STRUCT and streq(d.s, name) { return d }
|
|
i = i + 1
|
|
}
|
|
return ptr_null()
|
|
}
|
|
fn is_struct_ty(t: ptr) -> bool { return not ptr_is_null(find_struct(t)) }
|
|
|
|
fn find_arch(name: ptr) -> Node {
|
|
let i = 0
|
|
while i < len(prog) { let d = prog[i]; if d.kind == N_ARCH and streq(d.s, name) { return d }; i = i + 1 }
|
|
return ptr_null()
|
|
}
|
|
fn find_comp(name: ptr) -> Node {
|
|
let i = 0
|
|
while i < len(prog) { let d = prog[i]; if d.kind == N_COMP and streq(d.s, name) { return d }; i = i + 1 }
|
|
return ptr_null()
|
|
}
|
|
# a struct or a component — both have %Str_/%Cmp_ layouts with named fields
|
|
fn layout_node(name: ptr) -> Node {
|
|
let s = find_struct(name); if not ptr_is_null(s) { return s }
|
|
return find_comp(name)
|
|
}
|
|
fn layout_ty(name: ptr) -> ptr {
|
|
if not ptr_is_null(find_struct(name)) { return sconcat("%Str_", name) }
|
|
return sconcat("%Cmp_", name)
|
|
}
|
|
|
|
fn field_index(s: Node, fname: ptr) -> int {
|
|
let i = 0
|
|
while i < len(s.kids) { if streq(s.kids[i].s, fname) { return i }; i = i + 1 }
|
|
return 0 - 1
|
|
}
|
|
fn field_type(s: Node, fname: ptr) -> ptr {
|
|
let i = 0
|
|
while i < len(s.kids) { if streq(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 {
|
|
let i = 0
|
|
while i < len(prog) {
|
|
let d = prog[i]
|
|
if d.kind == N_VAR and streq(d.s, name) { return d }
|
|
if d.kind == N_CONST and streq(d.s, name) { return d }
|
|
i = i + 1
|
|
}
|
|
return ptr_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 {
|
|
let i = 0
|
|
while i < len(prog) {
|
|
let d = prog[i]
|
|
if d.kind == N_ENUM and streq(d.s, ename) {
|
|
let j = 0
|
|
while j < len(d.kids) { if streq(d.kids[j].s, vname) { return j }; j = j + 1 }
|
|
}
|
|
i = i + 1
|
|
}
|
|
return 0 - 1
|
|
}
|
|
fn find_fn(name: ptr) -> Node {
|
|
let i = 0
|
|
while i < len(prog) { let d = prog[i]; if d.kind == N_FN and streq(d.s, name) { return d }; i = i + 1 }
|
|
return ptr_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 = sconcat(prop, sconcat(".", field)); cf.ty = ty; cf.a = e
|
|
push(g_computed, cf)
|
|
}
|
|
fn computed_expr(prop: ptr, field: ptr) -> Node {
|
|
if ptr_is_null(prop) { return ptr_null() }
|
|
let key = sconcat(prop, sconcat(".", field))
|
|
let i = 0
|
|
while i < len(g_computed) { if streq(g_computed[i].s, key) { return g_computed[i].a }; i = i + 1 }
|
|
return ptr_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 ptr_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 {
|
|
let i = 0
|
|
while i < len(g_onspawn) { if streq(g_onspawn[i].s, model) { return g_onspawn[i].a }; i = i + 1 }
|
|
return ptr_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 {
|
|
let i = 0
|
|
while i < len(g_ondespawn) { if streq(g_ondespawn[i].s, model) { return g_ondespawn[i].a }; i = i + 1 }
|
|
return ptr_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 {
|
|
let i = 0
|
|
while i < len(g_onattach) { if streq(g_onattach[i].s, prop) { return g_onattach[i].a }; i = i + 1 }
|
|
return ptr_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 {
|
|
let i = 0
|
|
while i < len(g_onenable) { if streq(g_onenable[i].s, prop) { return g_onenable[i].a }; i = i + 1 }
|
|
return ptr_null()
|
|
}
|
|
fn ondisable_body(prop: ptr) -> Node {
|
|
let i = 0
|
|
while i < len(g_ondisable) { if streq(g_ondisable[i].s, prop) { return g_ondisable[i].a }; i = i + 1 }
|
|
return ptr_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 }
|
|
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 }
|
|
else { push(loc_name, name); push(loc_reg, r); push(loc_ty, ty) }
|
|
nloc = nloc + 1
|
|
}
|
|
fn loc_find(name: ptr) -> int {
|
|
let i = nloc - 1
|
|
while i >= 0 { if streq(loc_name[i], name) { return i }; i = i - 1 }
|
|
return 0 - 1
|
|
}
|