From 23726afa90451d7a2ca1c627efe5e2358f80d0ed Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Mon, 31 Aug 2026 00:26:02 +0300 Subject: [PATCH] refactor(selfhost): reorganise into concern-based subdirectories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- selfhost/{ => backend}/emit_addr.ludic | 0 .../emit_call.ludic} | 190 +-------- selfhost/{ => backend}/emit_core.ludic | 0 selfhost/{ => backend}/emit_decl.ludic | 0 selfhost/backend/emit_expr.ludic | 189 +++++++++ selfhost/{ => backend}/emit_head.ludic | 0 selfhost/{ => backend}/emit_intrin.ludic | 0 selfhost/{ => backend}/emit_intrin2.ludic | 0 selfhost/{ => backend}/emit_mem.ludic | 0 selfhost/{ => backend}/emit_new.ludic | 0 selfhost/{ => backend}/emit_stmt.ludic | 0 .../{ => backend/game}/emit_collide.ludic | 0 selfhost/{ => backend/game}/emit_ecs.ludic | 0 selfhost/backend/game/emit_game.ludic | 388 +++++++++++++++++ .../{ => backend/game}/emit_machine.ludic | 0 selfhost/{ => backend/game}/emit_query.ludic | 0 selfhost/{ => backend/game}/emit_save.ludic | 0 selfhost/{ => backend/game}/emit_spawn.ludic | 0 selfhost/{ => backend/game}/emit_ui.ludic | 0 .../game/emit_world.ludic} | 389 +----------------- .../{ => backend/stdlib}/emit_color.ludic | 0 .../{ => backend/stdlib}/emit_colorfn.ludic | 0 .../{ => backend/stdlib}/emit_crypto.ludic | 0 .../{ => backend/stdlib}/emit_datetime.ludic | 0 selfhost/{ => backend/stdlib}/emit_ease.ludic | 0 selfhost/{ => backend/stdlib}/emit_fs.ludic | 0 selfhost/{ => backend/stdlib}/emit_hash.ludic | 0 selfhost/{ => backend/stdlib}/emit_list.ludic | 0 selfhost/{ => backend/stdlib}/emit_log.ludic | 0 selfhost/{ => backend/stdlib}/emit_math.ludic | 0 selfhost/{ => backend/stdlib}/emit_net.ludic | 0 .../{ => backend/stdlib}/emit_noise.ludic | 0 selfhost/{ => backend/stdlib}/emit_os.ludic | 0 selfhost/backend/stdlib/emit_text.ludic | 137 ++++++ .../stdlib/emit_text_prelude.ludic} | 138 +------ selfhost/{ => backend/stdlib}/emit_time.ludic | 0 .../{ => backend/stdlib}/emit_unicode.ludic | 0 selfhost/{ => backend/stdlib}/emit_uuid.ludic | 0 .../{ => backend/stdlib}/emit_vector.ludic | 0 selfhost/{ => frontend}/ast.ludic | 0 selfhost/{ => frontend}/lex.ludic | 0 selfhost/{ => frontend}/parse.ludic | 0 selfhost/{ => frontend}/parse_game.ludic | 0 selfhost/{ => support}/buf.ludic | 0 selfhost/{ => support}/io.ludic | 0 selfhost/{ => support}/str.ludic | 0 tools/check-vocabulary.py | 4 +- tools/docgen/check-impl.py | 21 +- tools/docgen/palette.py | 4 +- tools/ludic-tools/lsp.ludic | 2 +- tools/x/selfhost.ludic | 89 ++-- 51 files changed, 780 insertions(+), 771 deletions(-) rename selfhost/{ => backend}/emit_addr.ludic (100%) rename selfhost/{emit_expr.ludic => backend/emit_call.ludic} (76%) rename selfhost/{ => backend}/emit_core.ludic (100%) rename selfhost/{ => backend}/emit_decl.ludic (100%) create mode 100644 selfhost/backend/emit_expr.ludic rename selfhost/{ => backend}/emit_head.ludic (100%) rename selfhost/{ => backend}/emit_intrin.ludic (100%) rename selfhost/{ => backend}/emit_intrin2.ludic (100%) rename selfhost/{ => backend}/emit_mem.ludic (100%) rename selfhost/{ => backend}/emit_new.ludic (100%) rename selfhost/{ => backend}/emit_stmt.ludic (100%) rename selfhost/{ => backend/game}/emit_collide.ludic (100%) rename selfhost/{ => backend/game}/emit_ecs.ludic (100%) create mode 100644 selfhost/backend/game/emit_game.ludic rename selfhost/{ => backend/game}/emit_machine.ludic (100%) rename selfhost/{ => backend/game}/emit_query.ludic (100%) rename selfhost/{ => backend/game}/emit_save.ludic (100%) rename selfhost/{ => backend/game}/emit_spawn.ludic (100%) rename selfhost/{ => backend/game}/emit_ui.ludic (100%) rename selfhost/{emit_game.ludic => backend/game/emit_world.ludic} (52%) rename selfhost/{ => backend/stdlib}/emit_color.ludic (100%) rename selfhost/{ => backend/stdlib}/emit_colorfn.ludic (100%) rename selfhost/{ => backend/stdlib}/emit_crypto.ludic (100%) rename selfhost/{ => backend/stdlib}/emit_datetime.ludic (100%) rename selfhost/{ => backend/stdlib}/emit_ease.ludic (100%) rename selfhost/{ => backend/stdlib}/emit_fs.ludic (100%) rename selfhost/{ => backend/stdlib}/emit_hash.ludic (100%) rename selfhost/{ => backend/stdlib}/emit_list.ludic (100%) rename selfhost/{ => backend/stdlib}/emit_log.ludic (100%) rename selfhost/{ => backend/stdlib}/emit_math.ludic (100%) rename selfhost/{ => backend/stdlib}/emit_net.ludic (100%) rename selfhost/{ => backend/stdlib}/emit_noise.ludic (100%) rename selfhost/{ => backend/stdlib}/emit_os.ludic (100%) create mode 100644 selfhost/backend/stdlib/emit_text.ludic rename selfhost/{emit_text.ludic => backend/stdlib/emit_text_prelude.ludic} (70%) rename selfhost/{ => backend/stdlib}/emit_time.ludic (100%) rename selfhost/{ => backend/stdlib}/emit_unicode.ludic (100%) rename selfhost/{ => backend/stdlib}/emit_uuid.ludic (100%) rename selfhost/{ => backend/stdlib}/emit_vector.ludic (100%) rename selfhost/{ => frontend}/ast.ludic (100%) rename selfhost/{ => frontend}/lex.ludic (100%) rename selfhost/{ => frontend}/parse.ludic (100%) rename selfhost/{ => frontend}/parse_game.ludic (100%) rename selfhost/{ => support}/buf.ludic (100%) rename selfhost/{ => support}/io.ludic (100%) rename selfhost/{ => support}/str.ludic (100%) diff --git a/selfhost/emit_addr.ludic b/selfhost/backend/emit_addr.ludic similarity index 100% rename from selfhost/emit_addr.ludic rename to selfhost/backend/emit_addr.ludic diff --git a/selfhost/emit_expr.ludic b/selfhost/backend/emit_call.ludic similarity index 76% rename from selfhost/emit_expr.ludic rename to selfhost/backend/emit_call.ludic index 8336c3eb..a1ce8a6c 100644 --- a/selfhost/emit_expr.ludic +++ b/selfhost/backend/emit_call.ludic @@ -1,192 +1,4 @@ -# 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 -} - +# 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). # ---- namespaced builtins: Screen.* / Random.* / Input.* -------------------- # 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; diff --git a/selfhost/emit_core.ludic b/selfhost/backend/emit_core.ludic similarity index 100% rename from selfhost/emit_core.ludic rename to selfhost/backend/emit_core.ludic diff --git a/selfhost/emit_decl.ludic b/selfhost/backend/emit_decl.ludic similarity index 100% rename from selfhost/emit_decl.ludic rename to selfhost/backend/emit_decl.ludic diff --git a/selfhost/backend/emit_expr.ludic b/selfhost/backend/emit_expr.ludic new file mode 100644 index 00000000..a9b367bc --- /dev/null +++ b/selfhost/backend/emit_expr.ludic @@ -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 +} + diff --git a/selfhost/emit_head.ludic b/selfhost/backend/emit_head.ludic similarity index 100% rename from selfhost/emit_head.ludic rename to selfhost/backend/emit_head.ludic diff --git a/selfhost/emit_intrin.ludic b/selfhost/backend/emit_intrin.ludic similarity index 100% rename from selfhost/emit_intrin.ludic rename to selfhost/backend/emit_intrin.ludic diff --git a/selfhost/emit_intrin2.ludic b/selfhost/backend/emit_intrin2.ludic similarity index 100% rename from selfhost/emit_intrin2.ludic rename to selfhost/backend/emit_intrin2.ludic diff --git a/selfhost/emit_mem.ludic b/selfhost/backend/emit_mem.ludic similarity index 100% rename from selfhost/emit_mem.ludic rename to selfhost/backend/emit_mem.ludic diff --git a/selfhost/emit_new.ludic b/selfhost/backend/emit_new.ludic similarity index 100% rename from selfhost/emit_new.ludic rename to selfhost/backend/emit_new.ludic diff --git a/selfhost/emit_stmt.ludic b/selfhost/backend/emit_stmt.ludic similarity index 100% rename from selfhost/emit_stmt.ludic rename to selfhost/backend/emit_stmt.ludic diff --git a/selfhost/emit_collide.ludic b/selfhost/backend/game/emit_collide.ludic similarity index 100% rename from selfhost/emit_collide.ludic rename to selfhost/backend/game/emit_collide.ludic diff --git a/selfhost/emit_ecs.ludic b/selfhost/backend/game/emit_ecs.ludic similarity index 100% rename from selfhost/emit_ecs.ludic rename to selfhost/backend/game/emit_ecs.ludic diff --git a/selfhost/backend/game/emit_game.ludic b/selfhost/backend/game/emit_game.ludic new file mode 100644 index 00000000..778d111d --- /dev/null +++ b/selfhost/backend/game/emit_game.ludic @@ -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_ (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_ / +# @scene_exit_, 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__enter / scene__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_(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_(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_ — the POD payload struct passed by pointer to foreign listeners +# @evL_ — a fixed-capacity [16 x ptr] array of foreign callbacks +# @evN_ — how many are registered (registration order = dispatch order) +# @ludic_on_(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_(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_(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_(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_(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") +} + diff --git a/selfhost/emit_machine.ludic b/selfhost/backend/game/emit_machine.ludic similarity index 100% rename from selfhost/emit_machine.ludic rename to selfhost/backend/game/emit_machine.ludic diff --git a/selfhost/emit_query.ludic b/selfhost/backend/game/emit_query.ludic similarity index 100% rename from selfhost/emit_query.ludic rename to selfhost/backend/game/emit_query.ludic diff --git a/selfhost/emit_save.ludic b/selfhost/backend/game/emit_save.ludic similarity index 100% rename from selfhost/emit_save.ludic rename to selfhost/backend/game/emit_save.ludic diff --git a/selfhost/emit_spawn.ludic b/selfhost/backend/game/emit_spawn.ludic similarity index 100% rename from selfhost/emit_spawn.ludic rename to selfhost/backend/game/emit_spawn.ludic diff --git a/selfhost/emit_ui.ludic b/selfhost/backend/game/emit_ui.ludic similarity index 100% rename from selfhost/emit_ui.ludic rename to selfhost/backend/game/emit_ui.ludic diff --git a/selfhost/emit_game.ludic b/selfhost/backend/game/emit_world.ludic similarity index 52% rename from selfhost/emit_game.ludic rename to selfhost/backend/game/emit_world.ludic index ffce40ed..6f8ad733 100644 --- a/selfhost/emit_game.ludic +++ b/selfhost/backend/game/emit_world.ludic @@ -1,391 +1,4 @@ -# 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_ (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_ / -# @scene_exit_, 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__enter / scene__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_(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_(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_ — the POD payload struct passed by pointer to foreign listeners -# @evL_ — a fixed-capacity [16 x ptr] array of foreign callbacks -# @evN_ — how many are registered (registration order = dispatch order) -# @ludic_on_(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_(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_(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_(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_(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") -} - +# 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). # 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 # "game table" that lets a modding layer be ported in. Generated from the diff --git a/selfhost/emit_color.ludic b/selfhost/backend/stdlib/emit_color.ludic similarity index 100% rename from selfhost/emit_color.ludic rename to selfhost/backend/stdlib/emit_color.ludic diff --git a/selfhost/emit_colorfn.ludic b/selfhost/backend/stdlib/emit_colorfn.ludic similarity index 100% rename from selfhost/emit_colorfn.ludic rename to selfhost/backend/stdlib/emit_colorfn.ludic diff --git a/selfhost/emit_crypto.ludic b/selfhost/backend/stdlib/emit_crypto.ludic similarity index 100% rename from selfhost/emit_crypto.ludic rename to selfhost/backend/stdlib/emit_crypto.ludic diff --git a/selfhost/emit_datetime.ludic b/selfhost/backend/stdlib/emit_datetime.ludic similarity index 100% rename from selfhost/emit_datetime.ludic rename to selfhost/backend/stdlib/emit_datetime.ludic diff --git a/selfhost/emit_ease.ludic b/selfhost/backend/stdlib/emit_ease.ludic similarity index 100% rename from selfhost/emit_ease.ludic rename to selfhost/backend/stdlib/emit_ease.ludic diff --git a/selfhost/emit_fs.ludic b/selfhost/backend/stdlib/emit_fs.ludic similarity index 100% rename from selfhost/emit_fs.ludic rename to selfhost/backend/stdlib/emit_fs.ludic diff --git a/selfhost/emit_hash.ludic b/selfhost/backend/stdlib/emit_hash.ludic similarity index 100% rename from selfhost/emit_hash.ludic rename to selfhost/backend/stdlib/emit_hash.ludic diff --git a/selfhost/emit_list.ludic b/selfhost/backend/stdlib/emit_list.ludic similarity index 100% rename from selfhost/emit_list.ludic rename to selfhost/backend/stdlib/emit_list.ludic diff --git a/selfhost/emit_log.ludic b/selfhost/backend/stdlib/emit_log.ludic similarity index 100% rename from selfhost/emit_log.ludic rename to selfhost/backend/stdlib/emit_log.ludic diff --git a/selfhost/emit_math.ludic b/selfhost/backend/stdlib/emit_math.ludic similarity index 100% rename from selfhost/emit_math.ludic rename to selfhost/backend/stdlib/emit_math.ludic diff --git a/selfhost/emit_net.ludic b/selfhost/backend/stdlib/emit_net.ludic similarity index 100% rename from selfhost/emit_net.ludic rename to selfhost/backend/stdlib/emit_net.ludic diff --git a/selfhost/emit_noise.ludic b/selfhost/backend/stdlib/emit_noise.ludic similarity index 100% rename from selfhost/emit_noise.ludic rename to selfhost/backend/stdlib/emit_noise.ludic diff --git a/selfhost/emit_os.ludic b/selfhost/backend/stdlib/emit_os.ludic similarity index 100% rename from selfhost/emit_os.ludic rename to selfhost/backend/stdlib/emit_os.ludic diff --git a/selfhost/backend/stdlib/emit_text.ludic b/selfhost/backend/stdlib/emit_text.ludic new file mode 100644 index 00000000..b50c1419 --- /dev/null +++ b/selfhost/backend/stdlib/emit_text.ludic @@ -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") +} + diff --git a/selfhost/emit_text.ludic b/selfhost/backend/stdlib/emit_text_prelude.ludic similarity index 70% rename from selfhost/emit_text.ludic rename to selfhost/backend/stdlib/emit_text_prelude.ludic index eb17500f..cbe703c3 100644 --- a/selfhost/emit_text.ludic +++ b/selfhost/backend/stdlib/emit_text_prelude.ludic @@ -1,140 +1,4 @@ -# 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") -} - +# 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). # emit_text_prelude — string builders that allocate: upper/lower/trim/ # repeat/pad. Emitted once per program that uses them (g_uses_textrt). Plain # libc (strlen/malloc/memcpy), deterministic, C-string in and out. diff --git a/selfhost/emit_time.ludic b/selfhost/backend/stdlib/emit_time.ludic similarity index 100% rename from selfhost/emit_time.ludic rename to selfhost/backend/stdlib/emit_time.ludic diff --git a/selfhost/emit_unicode.ludic b/selfhost/backend/stdlib/emit_unicode.ludic similarity index 100% rename from selfhost/emit_unicode.ludic rename to selfhost/backend/stdlib/emit_unicode.ludic diff --git a/selfhost/emit_uuid.ludic b/selfhost/backend/stdlib/emit_uuid.ludic similarity index 100% rename from selfhost/emit_uuid.ludic rename to selfhost/backend/stdlib/emit_uuid.ludic diff --git a/selfhost/emit_vector.ludic b/selfhost/backend/stdlib/emit_vector.ludic similarity index 100% rename from selfhost/emit_vector.ludic rename to selfhost/backend/stdlib/emit_vector.ludic diff --git a/selfhost/ast.ludic b/selfhost/frontend/ast.ludic similarity index 100% rename from selfhost/ast.ludic rename to selfhost/frontend/ast.ludic diff --git a/selfhost/lex.ludic b/selfhost/frontend/lex.ludic similarity index 100% rename from selfhost/lex.ludic rename to selfhost/frontend/lex.ludic diff --git a/selfhost/parse.ludic b/selfhost/frontend/parse.ludic similarity index 100% rename from selfhost/parse.ludic rename to selfhost/frontend/parse.ludic diff --git a/selfhost/parse_game.ludic b/selfhost/frontend/parse_game.ludic similarity index 100% rename from selfhost/parse_game.ludic rename to selfhost/frontend/parse_game.ludic diff --git a/selfhost/buf.ludic b/selfhost/support/buf.ludic similarity index 100% rename from selfhost/buf.ludic rename to selfhost/support/buf.ludic diff --git a/selfhost/io.ludic b/selfhost/support/io.ludic similarity index 100% rename from selfhost/io.ludic rename to selfhost/support/io.ludic diff --git a/selfhost/str.ludic b/selfhost/support/str.ludic similarity index 100% rename from selfhost/str.ludic rename to selfhost/support/str.ludic diff --git a/tools/check-vocabulary.py b/tools/check-vocabulary.py index aa4a8cd5..425c565a 100755 --- a/tools/check-vocabulary.py +++ b/tools/check-vocabulary.py @@ -153,7 +153,7 @@ compare("primitive types", h_types, grammar_alternation(grammar, kpath, "fixed") def parser_keywords(): kws = set() 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'streq\(t\.text,\s*"([a-z]+)"\)', text)) return kws @@ -161,7 +161,7 @@ def parser_keywords(): pkw = parser_keywords() h_reserved = c_string_list(syntax_h, "LUDIC_KW_RESERVED") 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: for label, kws in (("declaration keywords", h_decl), ("clause keywords", h_clause)): unparsed = (kws - pkw) - h_reserved diff --git a/tools/docgen/check-impl.py b/tools/docgen/check-impl.py index 39a33b5b..1f641cd1 100644 --- a/tools/docgen/check-impl.py +++ b/tools/docgen/check-impl.py @@ -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 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` predicates it delegates to (Math/Text/List) * keywords tools/ludic-tools/ludic_syntax.h LUDIC_KW_* (minus LUDIC_KW_RESERVED, which is not-yet-implemented) @@ -45,20 +45,23 @@ def fn_body(text, name): def all_selfhost_source(): src = "" d = os.path.join(ROOT, "selfhost") - for fn in sorted(os.listdir(d)): - if fn.endswith(".ludic"): - src += read("selfhost", fn) + "\n" + files = [] + for dirpath, _dirs, names in os.walk(d): + 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 def compiler_ns_methods(): """{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() + body = fn_body(allsrc, "emit_ns_call") + if not body: + problems.append("emit_ns_call not found in selfhost/ sources") + return {} result = {} # each `if (ns == "X")` opens a block that runs to the next such marker chunks = re.split(r'if \(ns == "', body) diff --git a/tools/docgen/palette.py b/tools/docgen/palette.py index ca0eadd6..e3ee1efd 100644 --- a/tools/docgen/palette.py +++ b/tools/docgen/palette.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # 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 # (Name, 0xRRGGBB, group). Names are PascalCase, unique. >200 entries. @@ -293,6 +293,6 @@ if __name__ == "__main__": names = guard_check() root = os.path.dirname(os.path.abspath(__file__)) 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")) print(f"OK {len(PALETTE)} colors ({len(set(names))} unique names)") diff --git a/tools/ludic-tools/lsp.ludic b/tools/ludic-tools/lsp.ludic index d61f90f4..24a76ca2 100644 --- a/tools/ludic-tools/lsp.ludic +++ b/tools/ludic-tools/lsp.ludic @@ -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 } # 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 # 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. diff --git a/tools/x/selfhost.ludic b/tools/x/selfhost.ludic index c767c5dd..07aab33b 100644 --- a/tools/x/selfhost.ludic +++ b/tools/x/selfhost.ludic @@ -12,49 +12,52 @@ function cc() -> pointer { return getenv_or("LUDIC_CC", "clang") } # the old scripts each carried their own copy. function selfhost_frags() -> []pointer { let f = new []pointer - push(f, "selfhost/str.ludic") - push(f, "selfhost/buf.ludic") - push(f, "selfhost/io.ludic") - push(f, "selfhost/ast.ludic") - push(f, "selfhost/lex.ludic") - push(f, "selfhost/parse.ludic") - push(f, "selfhost/parse_game.ludic") - push(f, "selfhost/emit_core.ludic") - push(f, "selfhost/emit_head.ludic") - push(f, "selfhost/emit_addr.ludic") - push(f, "selfhost/emit_intrin.ludic") - push(f, "selfhost/emit_intrin2.ludic") - push(f, "selfhost/emit_math.ludic") - push(f, "selfhost/emit_vector.ludic") - push(f, "selfhost/emit_text.ludic") - push(f, "selfhost/emit_hash.ludic") - push(f, "selfhost/emit_crypto.ludic") - push(f, "selfhost/emit_uuid.ludic") - push(f, "selfhost/emit_noise.ludic") - push(f, "selfhost/emit_log.ludic") - push(f, "selfhost/emit_os.ludic") - push(f, "selfhost/emit_unicode.ludic") - push(f, "selfhost/emit_fs.ludic") - push(f, "selfhost/emit_list.ludic") - push(f, "selfhost/emit_ease.ludic") - push(f, "selfhost/emit_collide.ludic") - push(f, "selfhost/emit_mem.ludic") - push(f, "selfhost/emit_time.ludic") - push(f, "selfhost/emit_datetime.ludic") - push(f, "selfhost/emit_colorfn.ludic") - push(f, "selfhost/emit_color.ludic") - push(f, "selfhost/emit_new.ludic") - push(f, "selfhost/emit_expr.ludic") - push(f, "selfhost/emit_stmt.ludic") - push(f, "selfhost/emit_ecs.ludic") - push(f, "selfhost/emit_query.ludic") - push(f, "selfhost/emit_spawn.ludic") - push(f, "selfhost/emit_game.ludic") - push(f, "selfhost/emit_machine.ludic") - push(f, "selfhost/emit_save.ludic") - push(f, "selfhost/emit_net.ludic") - push(f, "selfhost/emit_ui.ludic") - push(f, "selfhost/emit_decl.ludic") + push(f, "selfhost/support/str.ludic") + push(f, "selfhost/support/buf.ludic") + push(f, "selfhost/support/io.ludic") + push(f, "selfhost/frontend/ast.ludic") + push(f, "selfhost/frontend/lex.ludic") + push(f, "selfhost/frontend/parse.ludic") + push(f, "selfhost/frontend/parse_game.ludic") + push(f, "selfhost/backend/emit_core.ludic") + push(f, "selfhost/backend/emit_head.ludic") + push(f, "selfhost/backend/emit_addr.ludic") + push(f, "selfhost/backend/emit_intrin.ludic") + push(f, "selfhost/backend/emit_intrin2.ludic") + push(f, "selfhost/backend/stdlib/emit_math.ludic") + push(f, "selfhost/backend/stdlib/emit_vector.ludic") + push(f, "selfhost/backend/stdlib/emit_text.ludic") + push(f, "selfhost/backend/stdlib/emit_text_prelude.ludic") + push(f, "selfhost/backend/stdlib/emit_hash.ludic") + push(f, "selfhost/backend/stdlib/emit_crypto.ludic") + push(f, "selfhost/backend/stdlib/emit_uuid.ludic") + push(f, "selfhost/backend/stdlib/emit_noise.ludic") + push(f, "selfhost/backend/stdlib/emit_log.ludic") + push(f, "selfhost/backend/stdlib/emit_os.ludic") + push(f, "selfhost/backend/stdlib/emit_unicode.ludic") + push(f, "selfhost/backend/stdlib/emit_fs.ludic") + push(f, "selfhost/backend/stdlib/emit_list.ludic") + push(f, "selfhost/backend/stdlib/emit_ease.ludic") + push(f, "selfhost/backend/game/emit_collide.ludic") + push(f, "selfhost/backend/emit_mem.ludic") + push(f, "selfhost/backend/stdlib/emit_time.ludic") + push(f, "selfhost/backend/stdlib/emit_datetime.ludic") + push(f, "selfhost/backend/stdlib/emit_colorfn.ludic") + push(f, "selfhost/backend/stdlib/emit_color.ludic") + push(f, "selfhost/backend/emit_new.ludic") + push(f, "selfhost/backend/emit_expr.ludic") + push(f, "selfhost/backend/emit_call.ludic") + push(f, "selfhost/backend/emit_stmt.ludic") + push(f, "selfhost/backend/game/emit_ecs.ludic") + push(f, "selfhost/backend/game/emit_query.ludic") + push(f, "selfhost/backend/game/emit_spawn.ludic") + push(f, "selfhost/backend/game/emit_game.ludic") + push(f, "selfhost/backend/game/emit_world.ludic") + push(f, "selfhost/backend/game/emit_machine.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") return f }