refactor(selfhost): reorganise into concern-based subdirectories
Split the flat 38-file selfhost/ into concern-based subdirectories:
frontend/ lex, parse, parse_game, ast
support/ str, buf, io
backend/ core IR + expression/statement lowering
backend/game/ ECS/scene/event/world lowering
backend/stdlib/ the namespaced Math.*/Text.*/Crypto.*/… intrinsics
and split the three oversized emitters at responsibility boundaries so
no file mixes concerns:
emit_game.ludic -> + emit_world.ludic (reflection world table,
tick helpers, @main synthesis)
emit_expr.ludic -> + emit_call.ludic (namespaced builtins, call
lowering, expr dispatch)
emit_text.ludic -> + emit_text_prelude.ludic (emitted string-builder runtime)
FRAGS in tools/x/selfhost.ludic is updated to the new paths with the link
order preserved, and the Python doc/vocabulary tooling is updated to walk
the new layout. Because the build is a plain in-order concatenation and
every split lands on a blank-line boundary, the regenerated seed is
byte-identical: `x reseed` leaves selfhost/ludicc.seed.ll unchanged,
`x bootstrap-cfree` still reaches its fixed point, and both `x test` (56)
and `x selfhost-test` (29, incl. golden renders) stay green.
Closes #29
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f55216af50
commit
23726afa90
51 changed files with 780 additions and 771 deletions
|
|
@ -1,192 +1,4 @@
|
||||||
# emit_expr.ludic — lower an expression to IR, returning its register and type.
|
# emit_call.ludic — call lowering: namespaced builtins (Screen.*/Random.*/Input.* …), ordinary/user call emission, and the top-level emit_expr dispatch. Split out of emit_expr.ludic (concern: calls & expression dispatch, vs. emit_expr.ludic's operators/binary/coercion machinery).
|
||||||
|
|
||||||
function emit_load_at(addr: pointer, ty: pointer) -> Val {
|
|
||||||
let r = emit_bind(`load {llty(ty)}, 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).
|
|
||||||
function emit_logic(e: Node) -> Val {
|
|
||||||
let slot = emit_alloca("i32")
|
|
||||||
let la = emit_expr(e.a)
|
|
||||||
let lc = emit_bind(`icmp ne i32 {la.code}, 0`)
|
|
||||||
let lz = emit_bind(`zext i1 {lc} to i32`)
|
|
||||||
emit(" store i32 "); emit(lz); emit(", ptr "); emit(slot); emit("\n")
|
|
||||||
let ev = lbl("sc"); let done = lbl("scend")
|
|
||||||
if (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(`icmp ne i32 {rb.code}, 0`)
|
|
||||||
let rz = emit_bind(`zext i1 {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(`load i32, ptr {slot}`), "bool")
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmp_code(op: pointer) -> pointer {
|
|
||||||
if (op == ("<")) { return "slt" }
|
|
||||||
if (op == ("<=")) { return "sle" }
|
|
||||||
if (op == (">")) { return "sgt" }
|
|
||||||
if (op == (">=")) { return "sge" }
|
|
||||||
if (op == ("==")) { return "eq" }
|
|
||||||
return "ne"
|
|
||||||
}
|
|
||||||
function is_cmp(op: pointer) -> bool {
|
|
||||||
return (op == ("<")) or (op == ("<=")) or (op == (">")) or (op == (">=")) or (op == ("==")) or (op == ("!="))
|
|
||||||
}
|
|
||||||
function arith_code(op: pointer) -> pointer {
|
|
||||||
if (op == ("+")) { return "add" }
|
|
||||||
if (op == ("-")) { return "sub" }
|
|
||||||
if (op == ("*")) { return "mul" }
|
|
||||||
if (op == ("/")) { return "sdiv" }
|
|
||||||
if (op == ("&")) { return "and" }
|
|
||||||
if (op == ("|")) { return "or" }
|
|
||||||
if (op == ("^")) { return "xor" }
|
|
||||||
if (op == ("<<")) { return "shl" }
|
|
||||||
if (op == (">>")) { return "lshr" } # logical shift (unsigned)
|
|
||||||
return "srem"
|
|
||||||
}
|
|
||||||
|
|
||||||
# widen an int value to Q16.16 by shifting left 16 (a fixed value passes through)
|
|
||||||
function to_fixed(v: Val) -> pointer {
|
|
||||||
if (v.ty == "fixed") { return v.code }
|
|
||||||
return emit_bind(`shl i32 {v.code}, 16`)
|
|
||||||
}
|
|
||||||
|
|
||||||
# coerce a value's code to the LLVM type of `target`, for the only cross-width
|
|
||||||
# pair the language has: int (i32) <-> long (i64). int widens with sext, long
|
|
||||||
# narrows with trunc; everything else (same width, or ptr) passes through.
|
|
||||||
function coerce_code(v: Val, target: pointer) -> pointer {
|
|
||||||
let lt = llty(target)
|
|
||||||
let vt = llty(v.ty)
|
|
||||||
if (lt == vt) { return v.code }
|
|
||||||
if (lt == "i64") and (vt == "i32") { return emit_bind(`sext i32 {v.code} to i64`) }
|
|
||||||
if (lt == "i32") and (vt == "i64") { return emit_bind(`trunc i64 {v.code} to i32`) }
|
|
||||||
return v.code
|
|
||||||
}
|
|
||||||
|
|
||||||
# widen an int value to i64 (a long passes through) — the long analogue of to_fixed
|
|
||||||
function to_long(v: Val) -> pointer {
|
|
||||||
if (llty(v.ty) == "i64") { return v.code }
|
|
||||||
return emit_bind(`sext i32 {v.code} to i64`)
|
|
||||||
}
|
|
||||||
|
|
||||||
# string operators: `a + b` concatenates, `a == b` / `a != b` compare by content.
|
|
||||||
# Both call the @fn_str_* prelude (emitted once per program that uses them).
|
|
||||||
function emit_str_op(op: pointer, a: Val, b: Val) -> Val {
|
|
||||||
g_uses_str = true
|
|
||||||
if (op == ("+")) {
|
|
||||||
return val(emit_bind(`call ptr @fn_str_concat(ptr {a.code}, ptr {b.code})`), "string")
|
|
||||||
}
|
|
||||||
let r = emit_bind(`call i32 @fn_str_eq(ptr {a.code}, ptr {b.code})`)
|
|
||||||
if (op == ("!=")) {
|
|
||||||
let c = emit_bind(`icmp eq i32 {r}, 0`)
|
|
||||||
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
|
||||||
}
|
|
||||||
return val(r, "bool")
|
|
||||||
}
|
|
||||||
|
|
||||||
function emit_bin(e: Node) -> Val {
|
|
||||||
if (e.s == "and") or (e.s == "or") { return emit_logic(e) }
|
|
||||||
let a = emit_expr(e.a)
|
|
||||||
let b = emit_expr(e.b)
|
|
||||||
# strings are pointer-typed, so any `+` with a pointer operand is concatenation,
|
|
||||||
# and `==`/`!=` between pointers is content comparison — except `x == null`,
|
|
||||||
# which is a pointer-identity test and falls through to the icmp below.
|
|
||||||
let ptrish = (llty(a.ty) == "ptr") or (llty(b.ty) == "ptr")
|
|
||||||
let isnull = e.a.kind == E_NULL or e.b.kind == E_NULL
|
|
||||||
if ptrish {
|
|
||||||
if (e.s == ("+")) { return emit_str_op("+", a, b) }
|
|
||||||
if ((e.s == ("==")) or (e.s == ("!="))) and not isnull { return emit_str_op(e.s, a, b) }
|
|
||||||
}
|
|
||||||
let fx = (a.ty == "fixed") or (b.ty == "fixed")
|
|
||||||
# a 64-bit operand (and no fixed/ptr involved) promotes the whole expression to
|
|
||||||
# i64: the other side widens with sext, and the result stays `long`.
|
|
||||||
let lng = ((llty(a.ty) == "i64") or (llty(b.ty) == "i64")) and not fx and not ptrish
|
|
||||||
if is_cmp(e.s) {
|
|
||||||
var ac = a.code; var bc = b.code
|
|
||||||
var ct = "i32"
|
|
||||||
if fx { ac = to_fixed(a); bc = to_fixed(b) }
|
|
||||||
else { if lng { ct = "i64"; ac = to_long(a); bc = to_long(b) }
|
|
||||||
else { if (llty(a.ty) == "ptr") or (llty(b.ty) == "ptr") { ct = "ptr" } } } # `p == null`, str/record identity
|
|
||||||
let c = emit_bind(`icmp {cmp_code(e.s)} {ct} {ac}, {bc}`)
|
|
||||||
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
|
||||||
}
|
|
||||||
if lng {
|
|
||||||
let al = to_long(a); let bl = to_long(b)
|
|
||||||
return val(emit_bind(`{arith_code(e.s)} i64 {al}, {bl}`), "long")
|
|
||||||
}
|
|
||||||
if fx {
|
|
||||||
let af = to_fixed(a); let bf = to_fixed(b)
|
|
||||||
if (e.s == ("*")) {
|
|
||||||
let a64 = emit_bind(`sext i32 {af} to i64`)
|
|
||||||
let b64 = emit_bind(`sext i32 {bf} to i64`)
|
|
||||||
let m = emit_bind(`mul i64 {a64}, {b64}`)
|
|
||||||
let sh = emit_bind(`ashr i64 {m}, 16`)
|
|
||||||
return val(emit_bind(`trunc i64 {sh} to i32`), "fixed")
|
|
||||||
}
|
|
||||||
if (e.s == ("/")) {
|
|
||||||
let a64 = emit_bind(`sext i32 {af} to i64`)
|
|
||||||
let ash = emit_bind(`shl i64 {a64}, 16`)
|
|
||||||
let b64 = emit_bind(`sext i32 {bf} to i64`)
|
|
||||||
let dv = emit_bind(`sdiv i64 {ash}, {b64}`)
|
|
||||||
return val(emit_bind(`trunc i64 {dv} to i32`), "fixed")
|
|
||||||
}
|
|
||||||
let r = emit_bind(`{arith_code(e.s)} i32 {af}, {bf}`)
|
|
||||||
return val(r, "fixed")
|
|
||||||
}
|
|
||||||
let r = emit_bind(`{arith_code(e.s)} i32 {a.code}, {b.code}`)
|
|
||||||
return val(r, "int")
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---- named arguments -------------------------------------------------------
|
|
||||||
# An argument list is either all-positional or all-named. When named, each kid
|
|
||||||
# is an E_FINIT (label -> value); this rewrites e.kids into plain value exprs in
|
|
||||||
# the order the callee declares its parameters, so the rest of emit_call is
|
|
||||||
# oblivious to whether the caller used names.
|
|
||||||
function args_are_named(e: Node) -> bool {
|
|
||||||
var i = 0
|
|
||||||
while i < len(e.kids) { if e.kids[i].kind == E_FINIT { return true }; i = i + 1 }
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
function reorder_named(e: Node, labels: []pointer) -> void {
|
|
||||||
if not args_are_named(e) { return }
|
|
||||||
var i = 0
|
|
||||||
while i < len(e.kids) {
|
|
||||||
if e.kids[i].kind != E_FINIT { perr("named and positional arguments cannot be mixed in one call") }
|
|
||||||
i = i + 1
|
|
||||||
}
|
|
||||||
if len(e.kids) != len(labels) { perr("wrong number of arguments") }
|
|
||||||
let out = new []Node
|
|
||||||
var li = 0
|
|
||||||
while li < len(labels) {
|
|
||||||
var found: Node = null
|
|
||||||
var k = 0
|
|
||||||
while k < len(e.kids) { if (e.kids[k].s == labels[li]) { found = e.kids[k] }; k = k + 1 }
|
|
||||||
if (found == null) { perr(`no argument named {labels[li]}`) }
|
|
||||||
push(out, found.a)
|
|
||||||
li = li + 1
|
|
||||||
}
|
|
||||||
e.kids = out
|
|
||||||
}
|
|
||||||
# The parameter labels of a resolved fn/extern, in declaration order.
|
|
||||||
function param_labels(fn: Node) -> []pointer {
|
|
||||||
let out = new []pointer
|
|
||||||
var i = 0
|
|
||||||
while i < len(fn.kids) { if fn.kids[i].kind == N_PARAM { push(out, fn.kids[i].s) }; i = i + 1 }
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
function param_types(fn: Node) -> []pointer {
|
|
||||||
let out = new []pointer
|
|
||||||
var i = 0
|
|
||||||
while i < len(fn.kids) { if fn.kids[i].kind == N_PARAM { push(out, fn.kids[i].ty) }; i = i + 1 }
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---- namespaced builtins: Screen.* / Random.* / Input.* --------------------
|
# ---- namespaced builtins: Screen.* / Random.* / Input.* --------------------
|
||||||
# The game-facing API reads as `subject.action(...)`. Each method maps to a bare
|
# The game-facing API reads as `subject.action(...)`. Each method maps to a bare
|
||||||
# runtime builtin plus the parameter labels callers may use as named arguments;
|
# runtime builtin plus the parameter labels callers may use as named arguments;
|
||||||
189
selfhost/backend/emit_expr.ludic
Normal file
189
selfhost/backend/emit_expr.ludic
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
# emit_expr.ludic — lower an expression to IR, returning its register and type.
|
||||||
|
|
||||||
|
function emit_load_at(addr: pointer, ty: pointer) -> Val {
|
||||||
|
let r = emit_bind(`load {llty(ty)}, 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).
|
||||||
|
function emit_logic(e: Node) -> Val {
|
||||||
|
let slot = emit_alloca("i32")
|
||||||
|
let la = emit_expr(e.a)
|
||||||
|
let lc = emit_bind(`icmp ne i32 {la.code}, 0`)
|
||||||
|
let lz = emit_bind(`zext i1 {lc} to i32`)
|
||||||
|
emit(" store i32 "); emit(lz); emit(", ptr "); emit(slot); emit("\n")
|
||||||
|
let ev = lbl("sc"); let done = lbl("scend")
|
||||||
|
if (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(`icmp ne i32 {rb.code}, 0`)
|
||||||
|
let rz = emit_bind(`zext i1 {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(`load i32, ptr {slot}`), "bool")
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmp_code(op: pointer) -> pointer {
|
||||||
|
if (op == ("<")) { return "slt" }
|
||||||
|
if (op == ("<=")) { return "sle" }
|
||||||
|
if (op == (">")) { return "sgt" }
|
||||||
|
if (op == (">=")) { return "sge" }
|
||||||
|
if (op == ("==")) { return "eq" }
|
||||||
|
return "ne"
|
||||||
|
}
|
||||||
|
function is_cmp(op: pointer) -> bool {
|
||||||
|
return (op == ("<")) or (op == ("<=")) or (op == (">")) or (op == (">=")) or (op == ("==")) or (op == ("!="))
|
||||||
|
}
|
||||||
|
function arith_code(op: pointer) -> pointer {
|
||||||
|
if (op == ("+")) { return "add" }
|
||||||
|
if (op == ("-")) { return "sub" }
|
||||||
|
if (op == ("*")) { return "mul" }
|
||||||
|
if (op == ("/")) { return "sdiv" }
|
||||||
|
if (op == ("&")) { return "and" }
|
||||||
|
if (op == ("|")) { return "or" }
|
||||||
|
if (op == ("^")) { return "xor" }
|
||||||
|
if (op == ("<<")) { return "shl" }
|
||||||
|
if (op == (">>")) { return "lshr" } # logical shift (unsigned)
|
||||||
|
return "srem"
|
||||||
|
}
|
||||||
|
|
||||||
|
# widen an int value to Q16.16 by shifting left 16 (a fixed value passes through)
|
||||||
|
function to_fixed(v: Val) -> pointer {
|
||||||
|
if (v.ty == "fixed") { return v.code }
|
||||||
|
return emit_bind(`shl i32 {v.code}, 16`)
|
||||||
|
}
|
||||||
|
|
||||||
|
# coerce a value's code to the LLVM type of `target`, for the only cross-width
|
||||||
|
# pair the language has: int (i32) <-> long (i64). int widens with sext, long
|
||||||
|
# narrows with trunc; everything else (same width, or ptr) passes through.
|
||||||
|
function coerce_code(v: Val, target: pointer) -> pointer {
|
||||||
|
let lt = llty(target)
|
||||||
|
let vt = llty(v.ty)
|
||||||
|
if (lt == vt) { return v.code }
|
||||||
|
if (lt == "i64") and (vt == "i32") { return emit_bind(`sext i32 {v.code} to i64`) }
|
||||||
|
if (lt == "i32") and (vt == "i64") { return emit_bind(`trunc i64 {v.code} to i32`) }
|
||||||
|
return v.code
|
||||||
|
}
|
||||||
|
|
||||||
|
# widen an int value to i64 (a long passes through) — the long analogue of to_fixed
|
||||||
|
function to_long(v: Val) -> pointer {
|
||||||
|
if (llty(v.ty) == "i64") { return v.code }
|
||||||
|
return emit_bind(`sext i32 {v.code} to i64`)
|
||||||
|
}
|
||||||
|
|
||||||
|
# string operators: `a + b` concatenates, `a == b` / `a != b` compare by content.
|
||||||
|
# Both call the @fn_str_* prelude (emitted once per program that uses them).
|
||||||
|
function emit_str_op(op: pointer, a: Val, b: Val) -> Val {
|
||||||
|
g_uses_str = true
|
||||||
|
if (op == ("+")) {
|
||||||
|
return val(emit_bind(`call ptr @fn_str_concat(ptr {a.code}, ptr {b.code})`), "string")
|
||||||
|
}
|
||||||
|
let r = emit_bind(`call i32 @fn_str_eq(ptr {a.code}, ptr {b.code})`)
|
||||||
|
if (op == ("!=")) {
|
||||||
|
let c = emit_bind(`icmp eq i32 {r}, 0`)
|
||||||
|
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
||||||
|
}
|
||||||
|
return val(r, "bool")
|
||||||
|
}
|
||||||
|
|
||||||
|
function emit_bin(e: Node) -> Val {
|
||||||
|
if (e.s == "and") or (e.s == "or") { return emit_logic(e) }
|
||||||
|
let a = emit_expr(e.a)
|
||||||
|
let b = emit_expr(e.b)
|
||||||
|
# strings are pointer-typed, so any `+` with a pointer operand is concatenation,
|
||||||
|
# and `==`/`!=` between pointers is content comparison — except `x == null`,
|
||||||
|
# which is a pointer-identity test and falls through to the icmp below.
|
||||||
|
let ptrish = (llty(a.ty) == "ptr") or (llty(b.ty) == "ptr")
|
||||||
|
let isnull = e.a.kind == E_NULL or e.b.kind == E_NULL
|
||||||
|
if ptrish {
|
||||||
|
if (e.s == ("+")) { return emit_str_op("+", a, b) }
|
||||||
|
if ((e.s == ("==")) or (e.s == ("!="))) and not isnull { return emit_str_op(e.s, a, b) }
|
||||||
|
}
|
||||||
|
let fx = (a.ty == "fixed") or (b.ty == "fixed")
|
||||||
|
# a 64-bit operand (and no fixed/ptr involved) promotes the whole expression to
|
||||||
|
# i64: the other side widens with sext, and the result stays `long`.
|
||||||
|
let lng = ((llty(a.ty) == "i64") or (llty(b.ty) == "i64")) and not fx and not ptrish
|
||||||
|
if is_cmp(e.s) {
|
||||||
|
var ac = a.code; var bc = b.code
|
||||||
|
var ct = "i32"
|
||||||
|
if fx { ac = to_fixed(a); bc = to_fixed(b) }
|
||||||
|
else { if lng { ct = "i64"; ac = to_long(a); bc = to_long(b) }
|
||||||
|
else { if (llty(a.ty) == "ptr") or (llty(b.ty) == "ptr") { ct = "ptr" } } } # `p == null`, str/record identity
|
||||||
|
let c = emit_bind(`icmp {cmp_code(e.s)} {ct} {ac}, {bc}`)
|
||||||
|
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
||||||
|
}
|
||||||
|
if lng {
|
||||||
|
let al = to_long(a); let bl = to_long(b)
|
||||||
|
return val(emit_bind(`{arith_code(e.s)} i64 {al}, {bl}`), "long")
|
||||||
|
}
|
||||||
|
if fx {
|
||||||
|
let af = to_fixed(a); let bf = to_fixed(b)
|
||||||
|
if (e.s == ("*")) {
|
||||||
|
let a64 = emit_bind(`sext i32 {af} to i64`)
|
||||||
|
let b64 = emit_bind(`sext i32 {bf} to i64`)
|
||||||
|
let m = emit_bind(`mul i64 {a64}, {b64}`)
|
||||||
|
let sh = emit_bind(`ashr i64 {m}, 16`)
|
||||||
|
return val(emit_bind(`trunc i64 {sh} to i32`), "fixed")
|
||||||
|
}
|
||||||
|
if (e.s == ("/")) {
|
||||||
|
let a64 = emit_bind(`sext i32 {af} to i64`)
|
||||||
|
let ash = emit_bind(`shl i64 {a64}, 16`)
|
||||||
|
let b64 = emit_bind(`sext i32 {bf} to i64`)
|
||||||
|
let dv = emit_bind(`sdiv i64 {ash}, {b64}`)
|
||||||
|
return val(emit_bind(`trunc i64 {dv} to i32`), "fixed")
|
||||||
|
}
|
||||||
|
let r = emit_bind(`{arith_code(e.s)} i32 {af}, {bf}`)
|
||||||
|
return val(r, "fixed")
|
||||||
|
}
|
||||||
|
let r = emit_bind(`{arith_code(e.s)} i32 {a.code}, {b.code}`)
|
||||||
|
return val(r, "int")
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- named arguments -------------------------------------------------------
|
||||||
|
# An argument list is either all-positional or all-named. When named, each kid
|
||||||
|
# is an E_FINIT (label -> value); this rewrites e.kids into plain value exprs in
|
||||||
|
# the order the callee declares its parameters, so the rest of emit_call is
|
||||||
|
# oblivious to whether the caller used names.
|
||||||
|
function args_are_named(e: Node) -> bool {
|
||||||
|
var i = 0
|
||||||
|
while i < len(e.kids) { if e.kids[i].kind == E_FINIT { return true }; i = i + 1 }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
function reorder_named(e: Node, labels: []pointer) -> void {
|
||||||
|
if not args_are_named(e) { return }
|
||||||
|
var i = 0
|
||||||
|
while i < len(e.kids) {
|
||||||
|
if e.kids[i].kind != E_FINIT { perr("named and positional arguments cannot be mixed in one call") }
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
if len(e.kids) != len(labels) { perr("wrong number of arguments") }
|
||||||
|
let out = new []Node
|
||||||
|
var li = 0
|
||||||
|
while li < len(labels) {
|
||||||
|
var found: Node = null
|
||||||
|
var k = 0
|
||||||
|
while k < len(e.kids) { if (e.kids[k].s == labels[li]) { found = e.kids[k] }; k = k + 1 }
|
||||||
|
if (found == null) { perr(`no argument named {labels[li]}`) }
|
||||||
|
push(out, found.a)
|
||||||
|
li = li + 1
|
||||||
|
}
|
||||||
|
e.kids = out
|
||||||
|
}
|
||||||
|
# The parameter labels of a resolved fn/extern, in declaration order.
|
||||||
|
function param_labels(fn: Node) -> []pointer {
|
||||||
|
let out = new []pointer
|
||||||
|
var i = 0
|
||||||
|
while i < len(fn.kids) { if fn.kids[i].kind == N_PARAM { push(out, fn.kids[i].s) }; i = i + 1 }
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function param_types(fn: Node) -> []pointer {
|
||||||
|
let out = new []pointer
|
||||||
|
var i = 0
|
||||||
|
while i < len(fn.kids) { if fn.kids[i].kind == N_PARAM { push(out, fn.kids[i].ty) }; i = i + 1 }
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
388
selfhost/backend/game/emit_game.ludic
Normal file
388
selfhost/backend/game/emit_game.ludic
Normal file
|
|
@ -0,0 +1,388 @@
|
||||||
|
# emit_game.ludic — system functions and the frame loop. A system compiles to a
|
||||||
|
# void function; main() boots (Start systems), then runs the per-frame phases in
|
||||||
|
# order until the game stops. Runtime hooks (rt_init/poll/running/shutdown) are
|
||||||
|
# called only when the runtime defines them.
|
||||||
|
|
||||||
|
function 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()
|
||||||
|
falloc = buf_new()
|
||||||
|
let saved = code
|
||||||
|
code = fbody
|
||||||
|
emit_block(sys.a)
|
||||||
|
if not g_term { emit(" br label %ret\n") }
|
||||||
|
emit("ret:\n ret void\n")
|
||||||
|
code = saved
|
||||||
|
emit("define void @sys_"); emit(sys.s); emit("() {\nentry:\n")
|
||||||
|
emit(buf_str(falloc))
|
||||||
|
emit(buf_str(fbody))
|
||||||
|
emit("}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
# one enable-gated call to @sys_<d.s> (skipped while the handler is disabled).
|
||||||
|
function 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.
|
||||||
|
function emit_calls_for_phase(phase: pointer) -> void {
|
||||||
|
var i = 0
|
||||||
|
while i < len(prog) {
|
||||||
|
let d = prog[i]
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
function emit_scene_fn(name: pointer, kind: pointer, 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
function 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.
|
||||||
|
function emit_despawn_hooks() -> void {
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_ondespawn) {
|
||||||
|
let hk = g_ondespawn[i]
|
||||||
|
let model = find_arch(hk.s)
|
||||||
|
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 (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, i32 %reason) {\nentry:\n")
|
||||||
|
emit(buf_str(falloc))
|
||||||
|
emit(buf_str(fbody))
|
||||||
|
emit("}\n\n")
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
emit_despawn_all_fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
function 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
|
||||||
|
|
||||||
|
function 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,391 +1,4 @@
|
||||||
# emit_game.ludic — system functions and the frame loop. A system compiles to a
|
# emit_world.ludic — the generated reflection ABI (the "world table" a mod reads/writes entity state by name through), the per-frame tick helpers, and the synthesized @main. Split out of emit_game.ludic (concern: runtime world/entry synthesis, vs. emit_game.ludic's system/scene/event lowering).
|
||||||
# void function; main() boots (Start systems), then runs the per-frame phases in
|
|
||||||
# order until the game stops. Runtime hooks (rt_init/poll/running/shutdown) are
|
|
||||||
# called only when the runtime defines them.
|
|
||||||
|
|
||||||
function 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()
|
|
||||||
falloc = buf_new()
|
|
||||||
let saved = code
|
|
||||||
code = fbody
|
|
||||||
emit_block(sys.a)
|
|
||||||
if not g_term { emit(" br label %ret\n") }
|
|
||||||
emit("ret:\n ret void\n")
|
|
||||||
code = saved
|
|
||||||
emit("define void @sys_"); emit(sys.s); emit("() {\nentry:\n")
|
|
||||||
emit(buf_str(falloc))
|
|
||||||
emit(buf_str(fbody))
|
|
||||||
emit("}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
# one enable-gated call to @sys_<d.s> (skipped while the handler is disabled).
|
|
||||||
function 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.
|
|
||||||
function emit_calls_for_phase(phase: pointer) -> void {
|
|
||||||
var i = 0
|
|
||||||
while i < len(prog) {
|
|
||||||
let d = prog[i]
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
function emit_scene_fn(name: pointer, kind: pointer, 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
function 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.
|
|
||||||
function emit_despawn_hooks() -> void {
|
|
||||||
var i = 0
|
|
||||||
while i < len(g_ondespawn) {
|
|
||||||
let hk = g_ondespawn[i]
|
|
||||||
let model = find_arch(hk.s)
|
|
||||||
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 (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, i32 %reason) {\nentry:\n")
|
|
||||||
emit(buf_str(falloc))
|
|
||||||
emit(buf_str(fbody))
|
|
||||||
emit("}\n\n")
|
|
||||||
i = i + 1
|
|
||||||
}
|
|
||||||
emit_despawn_all_fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
function 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
|
|
||||||
|
|
||||||
function 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
|
# 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
|
# 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
|
# "game table" that lets a modding layer be ported in. Generated from the
|
||||||
137
selfhost/backend/stdlib/emit_text.ludic
Normal file
137
selfhost/backend/stdlib/emit_text.ludic
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
# emit_text.ludic — the Text.* namespace over `str` (null-terminated byte
|
||||||
|
# strings). The libc-backed queries (length/char_at/starts_with/ends_with/
|
||||||
|
# contains/index_of/to_int) allocate nothing; slice/from_int/equals/concat reuse
|
||||||
|
# the string preludes that the `+`, `s[a..b]` and string(int) operators emit.
|
||||||
|
|
||||||
|
function is_text_ns(meth: pointer) -> bool {
|
||||||
|
if (meth == "length") or (meth == "char_at") or (meth == "slice") { return true }
|
||||||
|
if (meth == "equals") or (meth == "concat") or (meth == "to_int") or (meth == "from_int") { return true }
|
||||||
|
if (meth == "starts_with") or (meth == "ends_with") { return true }
|
||||||
|
if (meth == "contains") or (meth == "index_of") { return true }
|
||||||
|
if (meth == "upper") or (meth == "lower") or (meth == "trim") or (meth == "repeat") { return true }
|
||||||
|
if (meth == "pad_left") or (meth == "pad_right") { return true }
|
||||||
|
if (meth == "split") or (meth == "join") or (meth == "replace") { return true }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function emit_text_ns(meth: pointer, e: Node) -> Val {
|
||||||
|
if (meth == "from_int") { # int -> string, same as string(n)
|
||||||
|
let n = emit_expr(e.kids[0])
|
||||||
|
g_uses_intstr = true
|
||||||
|
return val(emit_bind(`call ptr @fn_int_str(i32 {n.code})`), "string")
|
||||||
|
}
|
||||||
|
if (meth == "slice") { # s[a..b], same substring helper
|
||||||
|
let s0 = emit_expr(e.kids[0]); let a = emit_expr(e.kids[1]); let b = emit_expr(e.kids[2])
|
||||||
|
g_uses_strslice = true
|
||||||
|
return val(emit_bind(`call ptr @fn_str_slice(ptr {s0.code}, i32 {a.code}, i32 {b.code})`), "string")
|
||||||
|
}
|
||||||
|
if (meth == "equals") { # byte-wise equality, same as ==
|
||||||
|
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
||||||
|
g_uses_str = true
|
||||||
|
return val(emit_bind(`call i32 @fn_str_eq(ptr {a.code}, ptr {b.code})`), "bool")
|
||||||
|
}
|
||||||
|
if (meth == "concat") { # a + b, same as the + operator
|
||||||
|
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
||||||
|
g_uses_str = true
|
||||||
|
return val(emit_bind(`call ptr @fn_str_concat(ptr {a.code}, ptr {b.code})`), "string")
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = emit_expr(e.kids[0])
|
||||||
|
if (meth == "length") { # byte length
|
||||||
|
let r = emit_bind(`call i64 @strlen(ptr {s.code})`)
|
||||||
|
return val(emit_bind(`trunc i64 {r} to i32`), "int")
|
||||||
|
}
|
||||||
|
if (meth == "char_at") { # the byte at index i, 0..255
|
||||||
|
let i = emit_expr(e.kids[1])
|
||||||
|
let a = emit_bind(`getelementptr inbounds i8, ptr {s.code}, i32 {i.code}`)
|
||||||
|
let c = emit_bind(`load i8, ptr {a}`)
|
||||||
|
return val(emit_bind(`zext i8 {c} to i32`), "int")
|
||||||
|
}
|
||||||
|
if (meth == "to_int") { # parse a leading integer, 0 if none
|
||||||
|
return val(emit_bind(`call i32 @atoi(ptr {s.code})`), "int")
|
||||||
|
}
|
||||||
|
if (meth == "upper") { # ASCII a-z -> A-Z, fresh string
|
||||||
|
g_uses_textrt = true
|
||||||
|
return val(emit_bind(`call ptr @fn_str_upper(ptr {s.code})`), "string")
|
||||||
|
}
|
||||||
|
if (meth == "lower") { # ASCII A-Z -> a-z, fresh string
|
||||||
|
g_uses_textrt = true
|
||||||
|
return val(emit_bind(`call ptr @fn_str_lower(ptr {s.code})`), "string")
|
||||||
|
}
|
||||||
|
if (meth == "trim") { # drop leading/trailing whitespace
|
||||||
|
g_uses_textrt = true
|
||||||
|
return val(emit_bind(`call ptr @fn_str_trim(ptr {s.code})`), "string")
|
||||||
|
}
|
||||||
|
if (meth == "repeat") { # s repeated n times
|
||||||
|
g_uses_textrt = true
|
||||||
|
let n = emit_expr(e.kids[1])
|
||||||
|
return val(emit_bind(`call ptr @fn_str_repeat(ptr {s.code}, i32 {n.code})`), "string")
|
||||||
|
}
|
||||||
|
if (meth == "pad_left") { # pad with spaces to width, on the left
|
||||||
|
g_uses_textrt = true
|
||||||
|
let w = emit_expr(e.kids[1])
|
||||||
|
return val(emit_bind(`call ptr @fn_str_pad(ptr {s.code}, i32 {w.code}, i1 1)`), "string")
|
||||||
|
}
|
||||||
|
if (meth == "pad_right") { # pad with spaces to width, on the right
|
||||||
|
g_uses_textrt = true
|
||||||
|
let w = emit_expr(e.kids[1])
|
||||||
|
return val(emit_bind(`call ptr @fn_str_pad(ptr {s.code}, i32 {w.code}, i1 0)`), "string")
|
||||||
|
}
|
||||||
|
if (meth == "replace") { # replace every `from` with `to`
|
||||||
|
g_uses_textrt2 = true
|
||||||
|
let from = emit_expr(e.kids[1]); let to = emit_expr(e.kids[2])
|
||||||
|
return val(emit_bind(`call ptr @fn_str_replace(ptr {s.code}, ptr {from.code}, ptr {to.code})`), "string")
|
||||||
|
}
|
||||||
|
if (meth == "split") { # split on a separator -> []string
|
||||||
|
g_uses_textrt2 = true
|
||||||
|
let sep = emit_expr(e.kids[1])
|
||||||
|
return val(emit_bind(`call ptr @fn_str_split(ptr {s.code}, ptr {sep.code})`), "[]string")
|
||||||
|
}
|
||||||
|
if (meth == "join") { # join a []string with a separator (s is the slice)
|
||||||
|
g_uses_textrt2 = true
|
||||||
|
let sep = emit_expr(e.kids[1])
|
||||||
|
return val(emit_bind(`call ptr @fn_str_join(ptr {s.code}, ptr {sep.code})`), "string")
|
||||||
|
}
|
||||||
|
if (meth == "contains") or (meth == "index_of") { # substring search
|
||||||
|
let sub = emit_expr(e.kids[1])
|
||||||
|
let p = emit_bind(`call ptr @strstr(ptr {s.code}, ptr {sub.code})`)
|
||||||
|
if (meth == "contains") {
|
||||||
|
let nn = emit_bind(`icmp ne ptr {p}, null`)
|
||||||
|
return val(emit_bind(`zext i1 {nn} to i32`), "bool")
|
||||||
|
}
|
||||||
|
let isnull = emit_bind(`icmp eq ptr {p}, null`) # index_of -> byte offset or -1
|
||||||
|
let pi = emit_bind(`ptrtoint ptr {p} to i64`)
|
||||||
|
let si = emit_bind(`ptrtoint ptr {s.code} to i64`)
|
||||||
|
let d = emit_bind(`sub i64 {pi}, {si}`)
|
||||||
|
let d32 = emit_bind(`trunc i64 {d} to i32`)
|
||||||
|
return val(emit_bind(`select i1 {isnull}, i32 -1, i32 {d32}`), "int")
|
||||||
|
}
|
||||||
|
|
||||||
|
# starts_with / ends_with: compare against the affix over its own length
|
||||||
|
let affix = emit_expr(e.kids[1])
|
||||||
|
let la = emit_bind(`call i64 @strlen(ptr {affix.code})`)
|
||||||
|
if (meth == "starts_with") { # strncmp of the head is null-safe
|
||||||
|
let cmp = emit_bind(`call i32 @strncmp(ptr {s.code}, ptr {affix.code}, i64 {la})`)
|
||||||
|
let eqz = emit_bind(`icmp eq i32 {cmp}, 0`)
|
||||||
|
return val(emit_bind(`zext i1 {eqz} to i32`), "bool")
|
||||||
|
}
|
||||||
|
# ends_with: compare the tail, but only when the affix fits (else a negative
|
||||||
|
# offset would read before the string) — branch so the strncmp never underflows
|
||||||
|
let ls = emit_bind(`call i64 @strlen(ptr {s.code})`)
|
||||||
|
let off = emit_bind(`sub i64 {ls}, {la}`)
|
||||||
|
let res = emit_alloca("i32")
|
||||||
|
store_at("i32", "0", res)
|
||||||
|
let neg = emit_bind(`icmp slt i64 {off}, 0`)
|
||||||
|
let cmpl = lbl("ew_cmp"); let en = lbl("ew_end")
|
||||||
|
emit(" br i1 "); emit(neg); emit(", label %"); emit(en); emit(", label %"); emit(cmpl); emit("\n")
|
||||||
|
emit(cmpl); emit(":\n")
|
||||||
|
let tail = emit_bind(`getelementptr inbounds i8, ptr {s.code}, i64 {off}`)
|
||||||
|
let cmp = emit_bind(`call i32 @strncmp(ptr {tail}, ptr {affix.code}, i64 {la})`)
|
||||||
|
let eqz = emit_bind(`icmp eq i32 {cmp}, 0`)
|
||||||
|
let z = emit_bind(`zext i1 {eqz} to i32`)
|
||||||
|
store_at("i32", z, res)
|
||||||
|
emit(" br label %"); emit(en); emit("\n")
|
||||||
|
emit(en); emit(":\n")
|
||||||
|
return val(emit_bind(`load i32, ptr {res}`), "bool")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,140 +1,4 @@
|
||||||
# emit_text.ludic — the Text.* namespace over `str` (null-terminated byte
|
# emit_text_prelude.ludic — the emitted Text runtime prelude: the string-builder helpers (upper/lower/trim/repeat/pad and the split/join machinery) written into each program that uses Text.*. Split out of emit_text.ludic (concern: emitted runtime code, vs. emit_text.ludic's Text.* namespace dispatch).
|
||||||
# strings). The libc-backed queries (length/char_at/starts_with/ends_with/
|
|
||||||
# contains/index_of/to_int) allocate nothing; slice/from_int/equals/concat reuse
|
|
||||||
# the string preludes that the `+`, `s[a..b]` and string(int) operators emit.
|
|
||||||
|
|
||||||
function is_text_ns(meth: pointer) -> bool {
|
|
||||||
if (meth == "length") or (meth == "char_at") or (meth == "slice") { return true }
|
|
||||||
if (meth == "equals") or (meth == "concat") or (meth == "to_int") or (meth == "from_int") { return true }
|
|
||||||
if (meth == "starts_with") or (meth == "ends_with") { return true }
|
|
||||||
if (meth == "contains") or (meth == "index_of") { return true }
|
|
||||||
if (meth == "upper") or (meth == "lower") or (meth == "trim") or (meth == "repeat") { return true }
|
|
||||||
if (meth == "pad_left") or (meth == "pad_right") { return true }
|
|
||||||
if (meth == "split") or (meth == "join") or (meth == "replace") { return true }
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function emit_text_ns(meth: pointer, e: Node) -> Val {
|
|
||||||
if (meth == "from_int") { # int -> string, same as string(n)
|
|
||||||
let n = emit_expr(e.kids[0])
|
|
||||||
g_uses_intstr = true
|
|
||||||
return val(emit_bind(`call ptr @fn_int_str(i32 {n.code})`), "string")
|
|
||||||
}
|
|
||||||
if (meth == "slice") { # s[a..b], same substring helper
|
|
||||||
let s0 = emit_expr(e.kids[0]); let a = emit_expr(e.kids[1]); let b = emit_expr(e.kids[2])
|
|
||||||
g_uses_strslice = true
|
|
||||||
return val(emit_bind(`call ptr @fn_str_slice(ptr {s0.code}, i32 {a.code}, i32 {b.code})`), "string")
|
|
||||||
}
|
|
||||||
if (meth == "equals") { # byte-wise equality, same as ==
|
|
||||||
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
|
||||||
g_uses_str = true
|
|
||||||
return val(emit_bind(`call i32 @fn_str_eq(ptr {a.code}, ptr {b.code})`), "bool")
|
|
||||||
}
|
|
||||||
if (meth == "concat") { # a + b, same as the + operator
|
|
||||||
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
|
||||||
g_uses_str = true
|
|
||||||
return val(emit_bind(`call ptr @fn_str_concat(ptr {a.code}, ptr {b.code})`), "string")
|
|
||||||
}
|
|
||||||
|
|
||||||
let s = emit_expr(e.kids[0])
|
|
||||||
if (meth == "length") { # byte length
|
|
||||||
let r = emit_bind(`call i64 @strlen(ptr {s.code})`)
|
|
||||||
return val(emit_bind(`trunc i64 {r} to i32`), "int")
|
|
||||||
}
|
|
||||||
if (meth == "char_at") { # the byte at index i, 0..255
|
|
||||||
let i = emit_expr(e.kids[1])
|
|
||||||
let a = emit_bind(`getelementptr inbounds i8, ptr {s.code}, i32 {i.code}`)
|
|
||||||
let c = emit_bind(`load i8, ptr {a}`)
|
|
||||||
return val(emit_bind(`zext i8 {c} to i32`), "int")
|
|
||||||
}
|
|
||||||
if (meth == "to_int") { # parse a leading integer, 0 if none
|
|
||||||
return val(emit_bind(`call i32 @atoi(ptr {s.code})`), "int")
|
|
||||||
}
|
|
||||||
if (meth == "upper") { # ASCII a-z -> A-Z, fresh string
|
|
||||||
g_uses_textrt = true
|
|
||||||
return val(emit_bind(`call ptr @fn_str_upper(ptr {s.code})`), "string")
|
|
||||||
}
|
|
||||||
if (meth == "lower") { # ASCII A-Z -> a-z, fresh string
|
|
||||||
g_uses_textrt = true
|
|
||||||
return val(emit_bind(`call ptr @fn_str_lower(ptr {s.code})`), "string")
|
|
||||||
}
|
|
||||||
if (meth == "trim") { # drop leading/trailing whitespace
|
|
||||||
g_uses_textrt = true
|
|
||||||
return val(emit_bind(`call ptr @fn_str_trim(ptr {s.code})`), "string")
|
|
||||||
}
|
|
||||||
if (meth == "repeat") { # s repeated n times
|
|
||||||
g_uses_textrt = true
|
|
||||||
let n = emit_expr(e.kids[1])
|
|
||||||
return val(emit_bind(`call ptr @fn_str_repeat(ptr {s.code}, i32 {n.code})`), "string")
|
|
||||||
}
|
|
||||||
if (meth == "pad_left") { # pad with spaces to width, on the left
|
|
||||||
g_uses_textrt = true
|
|
||||||
let w = emit_expr(e.kids[1])
|
|
||||||
return val(emit_bind(`call ptr @fn_str_pad(ptr {s.code}, i32 {w.code}, i1 1)`), "string")
|
|
||||||
}
|
|
||||||
if (meth == "pad_right") { # pad with spaces to width, on the right
|
|
||||||
g_uses_textrt = true
|
|
||||||
let w = emit_expr(e.kids[1])
|
|
||||||
return val(emit_bind(`call ptr @fn_str_pad(ptr {s.code}, i32 {w.code}, i1 0)`), "string")
|
|
||||||
}
|
|
||||||
if (meth == "replace") { # replace every `from` with `to`
|
|
||||||
g_uses_textrt2 = true
|
|
||||||
let from = emit_expr(e.kids[1]); let to = emit_expr(e.kids[2])
|
|
||||||
return val(emit_bind(`call ptr @fn_str_replace(ptr {s.code}, ptr {from.code}, ptr {to.code})`), "string")
|
|
||||||
}
|
|
||||||
if (meth == "split") { # split on a separator -> []string
|
|
||||||
g_uses_textrt2 = true
|
|
||||||
let sep = emit_expr(e.kids[1])
|
|
||||||
return val(emit_bind(`call ptr @fn_str_split(ptr {s.code}, ptr {sep.code})`), "[]string")
|
|
||||||
}
|
|
||||||
if (meth == "join") { # join a []string with a separator (s is the slice)
|
|
||||||
g_uses_textrt2 = true
|
|
||||||
let sep = emit_expr(e.kids[1])
|
|
||||||
return val(emit_bind(`call ptr @fn_str_join(ptr {s.code}, ptr {sep.code})`), "string")
|
|
||||||
}
|
|
||||||
if (meth == "contains") or (meth == "index_of") { # substring search
|
|
||||||
let sub = emit_expr(e.kids[1])
|
|
||||||
let p = emit_bind(`call ptr @strstr(ptr {s.code}, ptr {sub.code})`)
|
|
||||||
if (meth == "contains") {
|
|
||||||
let nn = emit_bind(`icmp ne ptr {p}, null`)
|
|
||||||
return val(emit_bind(`zext i1 {nn} to i32`), "bool")
|
|
||||||
}
|
|
||||||
let isnull = emit_bind(`icmp eq ptr {p}, null`) # index_of -> byte offset or -1
|
|
||||||
let pi = emit_bind(`ptrtoint ptr {p} to i64`)
|
|
||||||
let si = emit_bind(`ptrtoint ptr {s.code} to i64`)
|
|
||||||
let d = emit_bind(`sub i64 {pi}, {si}`)
|
|
||||||
let d32 = emit_bind(`trunc i64 {d} to i32`)
|
|
||||||
return val(emit_bind(`select i1 {isnull}, i32 -1, i32 {d32}`), "int")
|
|
||||||
}
|
|
||||||
|
|
||||||
# starts_with / ends_with: compare against the affix over its own length
|
|
||||||
let affix = emit_expr(e.kids[1])
|
|
||||||
let la = emit_bind(`call i64 @strlen(ptr {affix.code})`)
|
|
||||||
if (meth == "starts_with") { # strncmp of the head is null-safe
|
|
||||||
let cmp = emit_bind(`call i32 @strncmp(ptr {s.code}, ptr {affix.code}, i64 {la})`)
|
|
||||||
let eqz = emit_bind(`icmp eq i32 {cmp}, 0`)
|
|
||||||
return val(emit_bind(`zext i1 {eqz} to i32`), "bool")
|
|
||||||
}
|
|
||||||
# ends_with: compare the tail, but only when the affix fits (else a negative
|
|
||||||
# offset would read before the string) — branch so the strncmp never underflows
|
|
||||||
let ls = emit_bind(`call i64 @strlen(ptr {s.code})`)
|
|
||||||
let off = emit_bind(`sub i64 {ls}, {la}`)
|
|
||||||
let res = emit_alloca("i32")
|
|
||||||
store_at("i32", "0", res)
|
|
||||||
let neg = emit_bind(`icmp slt i64 {off}, 0`)
|
|
||||||
let cmpl = lbl("ew_cmp"); let en = lbl("ew_end")
|
|
||||||
emit(" br i1 "); emit(neg); emit(", label %"); emit(en); emit(", label %"); emit(cmpl); emit("\n")
|
|
||||||
emit(cmpl); emit(":\n")
|
|
||||||
let tail = emit_bind(`getelementptr inbounds i8, ptr {s.code}, i64 {off}`)
|
|
||||||
let cmp = emit_bind(`call i32 @strncmp(ptr {tail}, ptr {affix.code}, i64 {la})`)
|
|
||||||
let eqz = emit_bind(`icmp eq i32 {cmp}, 0`)
|
|
||||||
let z = emit_bind(`zext i1 {eqz} to i32`)
|
|
||||||
store_at("i32", z, res)
|
|
||||||
emit(" br label %"); emit(en); emit("\n")
|
|
||||||
emit(en); emit(":\n")
|
|
||||||
return val(emit_bind(`load i32, ptr {res}`), "bool")
|
|
||||||
}
|
|
||||||
|
|
||||||
# emit_text_prelude — string builders that allocate: upper/lower/trim/
|
# emit_text_prelude — string builders that allocate: upper/lower/trim/
|
||||||
# repeat/pad. Emitted once per program that uses them (g_uses_textrt). Plain
|
# repeat/pad. Emitted once per program that uses them (g_uses_textrt). Plain
|
||||||
# libc (strlen/malloc/memcpy), deterministic, C-string in and out.
|
# libc (strlen/malloc/memcpy), deterministic, C-string in and out.
|
||||||
|
|
@ -153,7 +153,7 @@ compare("primitive types", h_types, grammar_alternation(grammar, kpath, "fixed")
|
||||||
def parser_keywords():
|
def parser_keywords():
|
||||||
kws = set()
|
kws = set()
|
||||||
for name in ("parse.ludic", "parse_game.ludic"):
|
for name in ("parse.ludic", "parse_game.ludic"):
|
||||||
text = open(os.path.join(ROOT, "selfhost", name), encoding="utf-8").read()
|
text = open(os.path.join(ROOT, "selfhost", "frontend", name), encoding="utf-8").read()
|
||||||
kws |= set(re.findall(r'is_id\("([a-z]+)"\)', text))
|
kws |= set(re.findall(r'is_id\("([a-z]+)"\)', text))
|
||||||
kws |= set(re.findall(r'streq\(t\.text,\s*"([a-z]+)"\)', text))
|
kws |= set(re.findall(r'streq\(t\.text,\s*"([a-z]+)"\)', text))
|
||||||
return kws
|
return kws
|
||||||
|
|
@ -161,7 +161,7 @@ def parser_keywords():
|
||||||
pkw = parser_keywords()
|
pkw = parser_keywords()
|
||||||
h_reserved = c_string_list(syntax_h, "LUDIC_KW_RESERVED")
|
h_reserved = c_string_list(syntax_h, "LUDIC_KW_RESERVED")
|
||||||
if not pkw:
|
if not pkw:
|
||||||
problems.append("could not extract any keywords from selfhost/parse*.ludic")
|
problems.append("could not extract any keywords from selfhost/frontend/parse*.ludic")
|
||||||
else:
|
else:
|
||||||
for label, kws in (("declaration keywords", h_decl), ("clause keywords", h_clause)):
|
for label, kws in (("declaration keywords", h_decl), ("clause keywords", h_clause)):
|
||||||
unparsed = (kws - pkw) - h_reserved
|
unparsed = (kws - pkw) - h_reserved
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ never written. So the set of things that MUST have a doc page is read straight
|
||||||
from the implementation, and every one is required to have a page (and every
|
from the implementation, and every one is required to have a page (and every
|
||||||
namespace-method page is required to correspond to something real):
|
namespace-method page is required to correspond to something real):
|
||||||
|
|
||||||
* namespace methods selfhost/emit_expr.ludic `emit_ns_call` + the
|
* namespace methods selfhost/backend/emit_call.ludic `emit_ns_call` + the
|
||||||
`is_<ns>_ns` predicates it delegates to (Math/Text/List)
|
`is_<ns>_ns` predicates it delegates to (Math/Text/List)
|
||||||
* keywords tools/ludic-tools/ludic_syntax.h LUDIC_KW_* (minus
|
* keywords tools/ludic-tools/ludic_syntax.h LUDIC_KW_* (minus
|
||||||
LUDIC_KW_RESERVED, which is not-yet-implemented)
|
LUDIC_KW_RESERVED, which is not-yet-implemented)
|
||||||
|
|
@ -45,20 +45,23 @@ def fn_body(text, name):
|
||||||
def all_selfhost_source():
|
def all_selfhost_source():
|
||||||
src = ""
|
src = ""
|
||||||
d = os.path.join(ROOT, "selfhost")
|
d = os.path.join(ROOT, "selfhost")
|
||||||
for fn in sorted(os.listdir(d)):
|
files = []
|
||||||
if fn.endswith(".ludic"):
|
for dirpath, _dirs, names in os.walk(d):
|
||||||
src += read("selfhost", fn) + "\n"
|
for fn in names:
|
||||||
|
if fn.endswith(".ludic"):
|
||||||
|
files.append(os.path.join(dirpath, fn))
|
||||||
|
for path in sorted(files):
|
||||||
|
src += open(path, encoding="utf-8").read() + "\n"
|
||||||
return src
|
return src
|
||||||
|
|
||||||
|
|
||||||
def compiler_ns_methods():
|
def compiler_ns_methods():
|
||||||
"""{Namespace: set(method)} the compiler actually dispatches on."""
|
"""{Namespace: set(method)} the compiler actually dispatches on."""
|
||||||
expr = read("selfhost", "emit_expr.ludic")
|
|
||||||
body = fn_body(expr, "emit_ns_call")
|
|
||||||
if not body:
|
|
||||||
problems.append("emit_ns_call not found in selfhost/emit_expr.ludic")
|
|
||||||
return {}
|
|
||||||
allsrc = all_selfhost_source()
|
allsrc = all_selfhost_source()
|
||||||
|
body = fn_body(allsrc, "emit_ns_call")
|
||||||
|
if not body:
|
||||||
|
problems.append("emit_ns_call not found in selfhost/ sources")
|
||||||
|
return {}
|
||||||
result = {}
|
result = {}
|
||||||
# each `if (ns == "X")` opens a block that runs to the next such marker
|
# each `if (ns == "X")` opens a block that runs to the next such marker
|
||||||
chunks = re.split(r'if \(ns == "', body)
|
chunks = re.split(r'if \(ns == "', body)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# Single source of truth for Ludic's named color palette.
|
# Single source of truth for Ludic's named color palette.
|
||||||
# Emits: selfhost/emit_color.ludic (compiler lookup) and palette.json (docs).
|
# Emits: selfhost/backend/stdlib/emit_color.ludic (compiler lookup) and palette.json (docs).
|
||||||
import json, sys, os
|
import json, sys, os
|
||||||
|
|
||||||
# (Name, 0xRRGGBB, group). Names are PascalCase, unique. >200 entries.
|
# (Name, 0xRRGGBB, group). Names are PascalCase, unique. >200 entries.
|
||||||
|
|
@ -293,6 +293,6 @@ if __name__ == "__main__":
|
||||||
names = guard_check()
|
names = guard_check()
|
||||||
root = os.path.dirname(os.path.abspath(__file__))
|
root = os.path.dirname(os.path.abspath(__file__))
|
||||||
repo = "/Users/orkuncakilkaya/workspace/gpp"
|
repo = "/Users/orkuncakilkaya/workspace/gpp"
|
||||||
emit_ludic(os.path.join(repo, "selfhost", "emit_color.ludic"))
|
emit_ludic(os.path.join(repo, "selfhost", "backend", "stdlib", "emit_color.ludic"))
|
||||||
emit_json(os.path.join(root, "palette.json"))
|
emit_json(os.path.join(root, "palette.json"))
|
||||||
print(f"OK {len(PALETTE)} colors ({len(set(names))} unique names)")
|
print(f"OK {len(PALETTE)} colors ({len(set(names))} unique names)")
|
||||||
|
|
|
||||||
|
|
@ -1190,7 +1190,7 @@ program LudicLsp {
|
||||||
function builtin_sig(name: pointer) -> pointer { var i = 0; while i < len(g_builtins) { if (g_builtins[i] == name) { return g_bsigs[i] }; i = i + 1 }; return null }
|
function builtin_sig(name: pointer) -> pointer { var i = 0; while i < len(g_builtins) { if (g_builtins[i] == name) { return g_bsigs[i] }; i = i + 1 }; return null }
|
||||||
|
|
||||||
# The namespaced standard-library surface: `Namespace.method`, mirroring the
|
# The namespaced standard-library surface: `Namespace.method`, mirroring the
|
||||||
# compiler's emit_ns_call (selfhost/emit_expr.ludic). Screen/Random/Input/Map
|
# compiler's emit_ns_call (selfhost/backend/emit_call.ludic). Screen/Random/Input/Map
|
||||||
# methods resolve to the underlying bare builtin's signature; Math/Text/List
|
# methods resolve to the underlying bare builtin's signature; Math/Text/List
|
||||||
# are pure fixed-point / string / slice ops with their own signatures. Returns
|
# are pure fixed-point / string / slice ops with their own signatures. Returns
|
||||||
# the signature label for signature help, or null if it is not a known method.
|
# the signature label for signature help, or null if it is not a known method.
|
||||||
|
|
|
||||||
|
|
@ -12,49 +12,52 @@ function cc() -> pointer { return getenv_or("LUDIC_CC", "clang") }
|
||||||
# the old scripts each carried their own copy.
|
# the old scripts each carried their own copy.
|
||||||
function selfhost_frags() -> []pointer {
|
function selfhost_frags() -> []pointer {
|
||||||
let f = new []pointer
|
let f = new []pointer
|
||||||
push(f, "selfhost/str.ludic")
|
push(f, "selfhost/support/str.ludic")
|
||||||
push(f, "selfhost/buf.ludic")
|
push(f, "selfhost/support/buf.ludic")
|
||||||
push(f, "selfhost/io.ludic")
|
push(f, "selfhost/support/io.ludic")
|
||||||
push(f, "selfhost/ast.ludic")
|
push(f, "selfhost/frontend/ast.ludic")
|
||||||
push(f, "selfhost/lex.ludic")
|
push(f, "selfhost/frontend/lex.ludic")
|
||||||
push(f, "selfhost/parse.ludic")
|
push(f, "selfhost/frontend/parse.ludic")
|
||||||
push(f, "selfhost/parse_game.ludic")
|
push(f, "selfhost/frontend/parse_game.ludic")
|
||||||
push(f, "selfhost/emit_core.ludic")
|
push(f, "selfhost/backend/emit_core.ludic")
|
||||||
push(f, "selfhost/emit_head.ludic")
|
push(f, "selfhost/backend/emit_head.ludic")
|
||||||
push(f, "selfhost/emit_addr.ludic")
|
push(f, "selfhost/backend/emit_addr.ludic")
|
||||||
push(f, "selfhost/emit_intrin.ludic")
|
push(f, "selfhost/backend/emit_intrin.ludic")
|
||||||
push(f, "selfhost/emit_intrin2.ludic")
|
push(f, "selfhost/backend/emit_intrin2.ludic")
|
||||||
push(f, "selfhost/emit_math.ludic")
|
push(f, "selfhost/backend/stdlib/emit_math.ludic")
|
||||||
push(f, "selfhost/emit_vector.ludic")
|
push(f, "selfhost/backend/stdlib/emit_vector.ludic")
|
||||||
push(f, "selfhost/emit_text.ludic")
|
push(f, "selfhost/backend/stdlib/emit_text.ludic")
|
||||||
push(f, "selfhost/emit_hash.ludic")
|
push(f, "selfhost/backend/stdlib/emit_text_prelude.ludic")
|
||||||
push(f, "selfhost/emit_crypto.ludic")
|
push(f, "selfhost/backend/stdlib/emit_hash.ludic")
|
||||||
push(f, "selfhost/emit_uuid.ludic")
|
push(f, "selfhost/backend/stdlib/emit_crypto.ludic")
|
||||||
push(f, "selfhost/emit_noise.ludic")
|
push(f, "selfhost/backend/stdlib/emit_uuid.ludic")
|
||||||
push(f, "selfhost/emit_log.ludic")
|
push(f, "selfhost/backend/stdlib/emit_noise.ludic")
|
||||||
push(f, "selfhost/emit_os.ludic")
|
push(f, "selfhost/backend/stdlib/emit_log.ludic")
|
||||||
push(f, "selfhost/emit_unicode.ludic")
|
push(f, "selfhost/backend/stdlib/emit_os.ludic")
|
||||||
push(f, "selfhost/emit_fs.ludic")
|
push(f, "selfhost/backend/stdlib/emit_unicode.ludic")
|
||||||
push(f, "selfhost/emit_list.ludic")
|
push(f, "selfhost/backend/stdlib/emit_fs.ludic")
|
||||||
push(f, "selfhost/emit_ease.ludic")
|
push(f, "selfhost/backend/stdlib/emit_list.ludic")
|
||||||
push(f, "selfhost/emit_collide.ludic")
|
push(f, "selfhost/backend/stdlib/emit_ease.ludic")
|
||||||
push(f, "selfhost/emit_mem.ludic")
|
push(f, "selfhost/backend/game/emit_collide.ludic")
|
||||||
push(f, "selfhost/emit_time.ludic")
|
push(f, "selfhost/backend/emit_mem.ludic")
|
||||||
push(f, "selfhost/emit_datetime.ludic")
|
push(f, "selfhost/backend/stdlib/emit_time.ludic")
|
||||||
push(f, "selfhost/emit_colorfn.ludic")
|
push(f, "selfhost/backend/stdlib/emit_datetime.ludic")
|
||||||
push(f, "selfhost/emit_color.ludic")
|
push(f, "selfhost/backend/stdlib/emit_colorfn.ludic")
|
||||||
push(f, "selfhost/emit_new.ludic")
|
push(f, "selfhost/backend/stdlib/emit_color.ludic")
|
||||||
push(f, "selfhost/emit_expr.ludic")
|
push(f, "selfhost/backend/emit_new.ludic")
|
||||||
push(f, "selfhost/emit_stmt.ludic")
|
push(f, "selfhost/backend/emit_expr.ludic")
|
||||||
push(f, "selfhost/emit_ecs.ludic")
|
push(f, "selfhost/backend/emit_call.ludic")
|
||||||
push(f, "selfhost/emit_query.ludic")
|
push(f, "selfhost/backend/emit_stmt.ludic")
|
||||||
push(f, "selfhost/emit_spawn.ludic")
|
push(f, "selfhost/backend/game/emit_ecs.ludic")
|
||||||
push(f, "selfhost/emit_game.ludic")
|
push(f, "selfhost/backend/game/emit_query.ludic")
|
||||||
push(f, "selfhost/emit_machine.ludic")
|
push(f, "selfhost/backend/game/emit_spawn.ludic")
|
||||||
push(f, "selfhost/emit_save.ludic")
|
push(f, "selfhost/backend/game/emit_game.ludic")
|
||||||
push(f, "selfhost/emit_net.ludic")
|
push(f, "selfhost/backend/game/emit_world.ludic")
|
||||||
push(f, "selfhost/emit_ui.ludic")
|
push(f, "selfhost/backend/game/emit_machine.ludic")
|
||||||
push(f, "selfhost/emit_decl.ludic")
|
push(f, "selfhost/backend/game/emit_save.ludic")
|
||||||
|
push(f, "selfhost/backend/stdlib/emit_net.ludic")
|
||||||
|
push(f, "selfhost/backend/game/emit_ui.ludic")
|
||||||
|
push(f, "selfhost/backend/emit_decl.ludic")
|
||||||
push(f, "selfhost/main.ludic")
|
push(f, "selfhost/main.ludic")
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue