Adds two annotations that give properties/models a scriptable feel WITHOUT reattaching behavior to data — both reduce to code the data-oriented model already emits: - @Computed field on a property: a derived value that is NOT stored; x.field expands inline to its expression (bare names read as fields of x) at each use. Zero storage, zero runtime dispatch. Reuses qualify_fields (now non-destructive, base-node based); a g_computed registry keeps derived fields out of the layout. - @OnSpawn(Model) on a handler: a constructor that runs at each spawn of Model with the model's properties bound by name. Spawn statically knows the model, so no runtime dispatch; emit_spawn binds the properties and inlines the hook body. examples/annotations.ludic now exercises @Handles/@Queries/@Computed/@OnSpawn (output 3 25 0 0); test.sh 15/15, fixpoint holds, goldens byte-identical. Deferred (need more machinery, by design): @OnDespawn (despawn doesn't statically know the entity's model) and @OnChange (needs change-tracking). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
178 lines
7.8 KiB
Text
178 lines
7.8 KiB
Text
# emit_expr.ludic — lower an expression to IR, returning its register and type.
|
|
|
|
fn emit_load_at(addr: ptr, ty: ptr) -> Val {
|
|
let r = emit_bind(sconcat("load ", sconcat(llty(ty), sconcat(", ptr ", addr))))
|
|
return val(r, ty)
|
|
}
|
|
|
|
# short-circuit `and`/`or`: seed a slot with (left!=0), branch to decide whether
|
|
# to overwrite with (right!=0).
|
|
fn emit_logic(e: Node) -> Val {
|
|
let slot = emit_alloca("i32")
|
|
let la = emit_expr(e.a)
|
|
let lc = emit_bind(sconcat("icmp ne i32 ", sconcat(la.code, ", 0")))
|
|
let lz = emit_bind(sconcat("zext i1 ", sconcat(lc, " to i32")))
|
|
emit(" store i32 "); emit(lz); emit(", ptr "); emit(slot); emit("\n")
|
|
let ev = lbl("sc"); let done = lbl("scend")
|
|
if streq(e.s, "and") { emit(" br i1 "); emit(lc); emit(", label %"); emit(ev); emit(", label %"); emit(done); emit("\n") }
|
|
else { emit(" br i1 "); emit(lc); emit(", label %"); emit(done); emit(", label %"); emit(ev); emit("\n") }
|
|
emit(ev); emit(":\n")
|
|
let rb = emit_expr(e.b)
|
|
let rc = emit_bind(sconcat("icmp ne i32 ", sconcat(rb.code, ", 0")))
|
|
let rz = emit_bind(sconcat("zext i1 ", sconcat(rc, " to i32")))
|
|
emit(" store i32 "); emit(rz); emit(", ptr "); emit(slot); emit("\n")
|
|
emit(" br label %"); emit(done); emit("\n")
|
|
emit(done); emit(":\n")
|
|
return val(emit_bind(sconcat("load i32, ptr ", slot)), "bool")
|
|
}
|
|
|
|
fn cmp_code(op: ptr) -> ptr {
|
|
if streq(op, "<") { return "slt" }
|
|
if streq(op, "<=") { return "sle" }
|
|
if streq(op, ">") { return "sgt" }
|
|
if streq(op, ">=") { return "sge" }
|
|
if streq(op, "==") { return "eq" }
|
|
return "ne"
|
|
}
|
|
fn is_cmp(op: ptr) -> bool {
|
|
return streq(op,"<") or streq(op,"<=") or streq(op,">") or streq(op,">=") or streq(op,"==") or streq(op,"!=")
|
|
}
|
|
fn arith_code(op: ptr) -> ptr {
|
|
if streq(op, "+") { return "add" }
|
|
if streq(op, "-") { return "sub" }
|
|
if streq(op, "*") { return "mul" }
|
|
if streq(op, "/") { return "sdiv" }
|
|
return "srem"
|
|
}
|
|
|
|
# widen an int value to Q16.16 by shifting left 16 (a fixed value passes through)
|
|
fn to_fixed(v: Val) -> ptr {
|
|
if streq(v.ty, "fixed") { return v.code }
|
|
return emit_bind(sconcat("shl i32 ", sconcat(v.code, ", 16")))
|
|
}
|
|
|
|
fn emit_bin(e: Node) -> Val {
|
|
if streq(e.s, "and") or streq(e.s, "or") { return emit_logic(e) }
|
|
let a = emit_expr(e.a)
|
|
let b = emit_expr(e.b)
|
|
let fx = streq(a.ty, "fixed") or streq(b.ty, "fixed")
|
|
if is_cmp(e.s) {
|
|
let ac = a.code; let bc = b.code
|
|
if fx { ac = to_fixed(a); bc = to_fixed(b) }
|
|
let c = emit_bind(sconcat("icmp ", sconcat(cmp_code(e.s), sconcat(" i32 ", sconcat(ac, sconcat(", ", bc))))))
|
|
return val(emit_bind(sconcat("zext i1 ", sconcat(c, " to i32"))), "bool")
|
|
}
|
|
if fx {
|
|
let af = to_fixed(a); let bf = to_fixed(b)
|
|
if streq(e.s, "*") {
|
|
let a64 = emit_bind(sconcat("sext i32 ", sconcat(af, " to i64")))
|
|
let b64 = emit_bind(sconcat("sext i32 ", sconcat(bf, " to i64")))
|
|
let m = emit_bind(sconcat("mul i64 ", sconcat(a64, sconcat(", ", b64))))
|
|
let sh = emit_bind(sconcat("ashr i64 ", sconcat(m, ", 16")))
|
|
return val(emit_bind(sconcat("trunc i64 ", sconcat(sh, " to i32"))), "fixed")
|
|
}
|
|
if streq(e.s, "/") {
|
|
let a64 = emit_bind(sconcat("sext i32 ", sconcat(af, " to i64")))
|
|
let ash = emit_bind(sconcat("shl i64 ", sconcat(a64, ", 16")))
|
|
let b64 = emit_bind(sconcat("sext i32 ", sconcat(bf, " to i64")))
|
|
let dv = emit_bind(sconcat("sdiv i64 ", sconcat(ash, sconcat(", ", b64))))
|
|
return val(emit_bind(sconcat("trunc i64 ", sconcat(dv, " to i32"))), "fixed")
|
|
}
|
|
let r = emit_bind(sconcat(arith_code(e.s), sconcat(" i32 ", sconcat(af, sconcat(", ", bf)))))
|
|
return val(r, "fixed")
|
|
}
|
|
let r = emit_bind(sconcat(arith_code(e.s), sconcat(" i32 ", sconcat(a.code, sconcat(", ", b.code)))))
|
|
return val(r, "int")
|
|
}
|
|
|
|
fn emit_call(e: Node) -> Val {
|
|
let name = e.a.s
|
|
if streq(name, "self") { if nself == 0 { return val("0", "entity") }; return val(emit_bind(sconcat("load i32, ptr ", self_stk[nself - 1])), "entity") }
|
|
if streq(name, "key") { return val(emit_bind("load i32, ptr @L_key"), "int") }
|
|
if streq(name, "save") { emit(" call void @L_save()\n"); return val("0", "void") }
|
|
if streq(name, "ui_build") { emit(" call void @ui_build()\n"); return val("0", "void") }
|
|
if streq(name, "load") { return val(emit_bind("call i32 @L_load()"), "bool") }
|
|
if streq(name, "quit") { emit(" store i32 0, ptr @L_running\n"); return val("0", "void") }
|
|
if streq(name, "len") { return emit_len(e) }
|
|
if streq(name, "push") { return emit_push(e) }
|
|
if streq(name, "fx") { let a = emit_expr(e.kids[0]); return val(emit_bind(sconcat("shl i32 ", sconcat(a.code, ", 16"))), "fixed") }
|
|
if streq(name, "flr") { let a = emit_expr(e.kids[0]); return val(emit_bind(sconcat("ashr i32 ", sconcat(a.code, ", 16"))), "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) }
|
|
let fn2 = find_fn(name)
|
|
let cname = name
|
|
if ptr_is_null(fn2) {
|
|
# a builtin like clear()/reg() is satisfied by its rt_ function
|
|
let rtname = sconcat("rt_", name)
|
|
fn2 = find_fn(rtname)
|
|
if ptr_is_null(fn2) { perr(sconcat("unknown function ", name)) }
|
|
cname = rtname
|
|
}
|
|
# evaluate args first (their IR is emitted before the call instruction)
|
|
let args = new []ptr
|
|
let atys = new []ptr
|
|
let i = 0
|
|
while i < len(e.kids) { let v = emit_expr(e.kids[i]); push(args, v.code); push(atys, v.ty); i = i + 1 }
|
|
let rl = llty(fn2.ty)
|
|
emit(" ")
|
|
let rreg = "0"
|
|
if not streq(rl, "void") { rreg = nreg(); emit(rreg); emit(" = ") }
|
|
emit("call "); emit(rl); emit(" @fn_"); emit(cname); emit("(")
|
|
i = 0
|
|
while i < len(args) {
|
|
if i > 0 { emit(", ") }
|
|
emit(llty(atys[i])); emit(" "); emit(args[i])
|
|
i = i + 1
|
|
}
|
|
emit(")\n")
|
|
return val(rreg, fn2.ty)
|
|
}
|
|
|
|
fn emit_expr(e: Node) -> Val {
|
|
if ptr_is_null(e) { return val("0", "int") }
|
|
if e.kind == E_INT { return val(itoa(e.ival), "int") }
|
|
if e.kind == E_FLOAT { return val(itoa(e.ival), "fixed") }
|
|
if e.kind == E_BOOL { return val(itoa(e.ival), "bool") }
|
|
if e.kind == E_STR { return val(emit_str_const(e.s), "str") }
|
|
if e.kind == E_NEW {
|
|
if is_slice_ty(e.s) { return emit_new_slice(e.s) }
|
|
return emit_new_struct(e.s)
|
|
}
|
|
if e.kind == E_ID {
|
|
let li = loc_find(e.s)
|
|
if li >= 0 { return emit_load_at(loc_reg[li], loc_ty[li]) }
|
|
let g = find_global(e.s)
|
|
if not ptr_is_null(g) {
|
|
if g.kind == N_CONST { return val(itoa(g.a.ival), "int") }
|
|
let r = emit_bind(sconcat("load ", sconcat(llty(g.ty), sconcat(", ptr @g_", e.s))))
|
|
return val(r, g.ty)
|
|
}
|
|
# a UI_<name> that is not a const/var resolves to its widget index
|
|
if is_ui_ident(e.s) { return val(itoa(ui_index_of(e.s)), "int") }
|
|
perr(sconcat("unknown identifier ", e.s))
|
|
}
|
|
if e.kind == E_MEMBER {
|
|
if e.a.kind == E_ID { # `Enum.Variant` -> its ordinal, a compile-time int
|
|
let ord = enum_ordinal(e.a.s, e.s)
|
|
if ord >= 0 { return val(itoa(ord), "int") }
|
|
}
|
|
let bt = static_type(e.a) # `x.field` where field is @Computed -> inline it
|
|
if not ptr_is_null(bt) {
|
|
let cx = computed_expr(bt, e.s)
|
|
if not ptr_is_null(cx) { return emit_expr(qualify_fields(cx, e.a)) }
|
|
}
|
|
let a = emit_member_addr(e); return emit_load_at(a, g_addr_ty)
|
|
}
|
|
if e.kind == E_INDEX { let a = emit_index_addr(e); return emit_load_at(a, g_addr_ty) }
|
|
if e.kind == E_CALL { return emit_call(e) }
|
|
if e.kind == E_BIN { return emit_bin(e) }
|
|
if e.kind == E_UN {
|
|
let a = emit_expr(e.a)
|
|
if streq(e.s, "-") { return val(emit_bind(sconcat("sub i32 0, ", a.code)), "int") }
|
|
let c = emit_bind(sconcat("icmp eq i32 ", sconcat(a.code, ", 0")))
|
|
return val(emit_bind(sconcat("zext i1 ", sconcat(c, " to i32"))), "bool")
|
|
}
|
|
perr("cannot emit expression")
|
|
return val("0", "int")
|
|
}
|