Baseline: Ludic compiler + toolchain, Phase 1 syntax fixes complete

Self-hosted compiler (selfhost/*.ludic), runtime, examples, editor tooling,
and docs. Phase 1 of the syntax-redesign cohesion pass has landed:
edge-system fix, signature-query, when-alias, and the documentation truth-pass.
Suite green (14/14), C-free bootstrap fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-27 15:15:35 +03:00
commit 985f9ad8f2
418 changed files with 39065 additions and 0 deletions

68
selfhost/ast.ludic Normal file
View file

@ -0,0 +1,68 @@
# ast.ludic — the node kinds and the single Node shape they share, plus the
# declaration tables the emitter reads. Mirrors compiler/front/ast.h + sem/.
# declarations
const N_STRUCT: int = 0
const N_FIELD: int = 1
const N_VAR: int = 2
const N_CONST: int = 3
const N_FN: int = 4
const N_PARAM: int = 5
const N_MAIN: int = 6
const N_BLOCK: int = 7
const N_COMP: int = 8
const N_SYS: int = 9
const N_ARCH: int = 10
const N_EXTERN: int = 11
const N_UI: int = 12
# statements
const S_LET: int = 10
const S_ASSIGN: int = 11
const S_IF: int = 12
const S_WHILE: int = 13
const S_FOR: int = 14
const S_RETURN: int = 15
const S_EXPR: int = 16
const S_BREAK: int = 17
const S_CONTINUE: int = 18
const S_MATCH: int = 19
const S_MARM: int = 20
const S_QUERY: int = 21
const S_SPAWN: int = 22
const S_DESPAWN: int = 23
const S_MACHINE: int = 24
const S_STATE: int = 25
const S_BECOME: int = 26
# expressions
const E_INT: int = 30
const E_STR: int = 31
const E_BOOL: int = 32
const E_ID: int = 33
const E_CALL: int = 34
const E_MEMBER: int = 35
const E_INDEX: int = 36
const E_BIN: int = 37
const E_UN: int = 38
const E_NEW: int = 39
const E_FLOAT: int = 40
const E_REC: int = 41
const E_FINIT: int = 42
struct Node {
kind: int = 0
s: ptr = ptr_null() # name / operator / string / type-of-new
ival: int = 0 # int literal, bool, flags
ty: ptr = ptr_null() # declared type (let/param/field/fn/var/const)
a: Node # fixed children (meaning per kind)
b: Node
c: Node
kids: []Node # variadic children
line: int = 0
}
fn node(kind: int) -> Node {
let n = new Node
n.kind = kind
n.kids = new []Node
return n
}

40
selfhost/bootstrap-cfree.sh Executable file
View file

@ -0,0 +1,40 @@
#!/bin/bash
# bootstrap-cfree.sh — rebuild the self-hosted compiler with NO C compiler.
#
# The C `ludicc` is retired to a one-time seed: selfhost/ludicc.seed.ll is the
# self-host compiler's own LLVM IR, a proven fixed point. From it plus clang
# (an IR assembler, the same floor Rust and Swift stand on) the compiler
# rebuilds itself and reproduces its own IR — the C source is never invoked.
#
# seed.ll --clang--> sh_seed
# sh_seed compiles selfhost.ludic -> out.ll
# assert out.ll == seed.ll (the seed is a true fixed point)
set -u
cd "$(dirname "$0")/.."
CC="${LUDIC_CC:-clang}"
B=build/cfree
mkdir -p "$B"
# assemble the compiler straight from the checked-in seed — no C involved
$CC selfhost/ludicc.seed.ll -o "$B/sh_seed" 2>/dev/null || { echo "FAIL: assemble seed"; exit 1; }
echo " seed.ll --clang--> sh_seed (no C compiler used)"
# regenerate the concatenated source the same way build.sh does (pure shell)
FRAGS="selfhost/str.ludic selfhost/buf.ludic selfhost/io.ludic selfhost/ast.ludic
selfhost/lex.ludic selfhost/parse.ludic selfhost/parse_game.ludic selfhost/emit_core.ludic selfhost/emit_head.ludic
selfhost/emit_addr.ludic selfhost/emit_intrin.ludic selfhost/emit_intrin2.ludic selfhost/emit_math.ludic selfhost/emit_new.ludic
selfhost/emit_expr.ludic selfhost/emit_stmt.ludic selfhost/emit_ecs.ludic selfhost/emit_query.ludic selfhost/emit_spawn.ludic selfhost/emit_game.ludic selfhost/emit_machine.ludic selfhost/emit_save.ludic selfhost/emit_ui.ludic selfhost/emit_decl.ludic selfhost/main.ludic"
{ echo "game SelfHost {"; for f in $FRAGS; do cat "$f"; echo; done; echo "}"; } > "$B/selfhost.ludic"
# the seed-built compiler compiles its own source
"$B/sh_seed" "$B/selfhost.ludic" > "$B/out.ll" 2>/dev/null || { echo "FAIL: seed compiler self-compile"; exit 1; }
echo " sh_seed compiles selfhost.ludic -> out.ll ($(wc -l < "$B/out.ll" | tr -d ' ') lines)"
if cmp -s "$B/out.ll" selfhost/ludicc.seed.ll; then
echo " ✅ out.ll == seed.ll — the compiler rebuilds itself with no C compiler"
exit 0
else
echo " ❌ out.ll != seed.ll (seed is stale — regenerate with ./selfhost/reseed.sh)"
diff "$B/out.ll" selfhost/ludicc.seed.ll | head
exit 1
fi

40
selfhost/bootstrap.sh Executable file
View file

@ -0,0 +1,40 @@
#!/bin/bash
# bootstrap.sh — the self-hosting proof.
#
# stage0: the C compiler (build/ludicc) compiles the self-host source -> gen1
# stage1: gen1 compiles the self-host source -> gen2
# stage2: gen2 compiles the self-host source -> gen3
# assert: gen2.ll == gen3.ll (the compiler reproduces itself exactly)
#
# gen1 is built by a *different* compiler (the C one), so its IR legitimately
# differs; gen2 == gen3 is the fixpoint that proves independence.
set -u
cd "$(dirname "$0")/.."
CC="${LUDIC_CC:-clang}"
B=build/boot
mkdir -p "$B"
say() { printf " %s\n" "$1"; }
# stage 0: C compiler builds the self-host compiler (also writes build/selfhost.ludic)
./selfhost/build.sh build/ludicc "$B/gen1" >/dev/null 2>&1 || { echo "FAIL: stage0 build"; exit 1; }
say "stage0: C ludicc -> gen1 (self-host compiler)"
# stage 1: gen1 compiles the self-host source to IR, assemble -> gen2
"$B/gen1" build/selfhost.ludic > "$B/gen2.ll" 2>/dev/null || { echo "FAIL: gen1 self-compile"; exit 1; }
$CC "$B/gen2.ll" -o "$B/gen2" 2>/dev/null || { echo "FAIL: gen2 assemble"; exit 1; }
say "stage1: gen1 -> gen2.ll ($(wc -l < "$B/gen2.ll" | tr -d ' ') lines) -> gen2"
# stage 2: gen2 compiles the self-host source to IR -> gen3
"$B/gen2" build/selfhost.ludic > "$B/gen3.ll" 2>/dev/null || { echo "FAIL: gen2 self-compile"; exit 1; }
say "stage2: gen2 -> gen3.ll ($(wc -l < "$B/gen3.ll" | tr -d ' ') lines)"
# the fixpoint
if cmp -s "$B/gen2.ll" "$B/gen3.ll"; then
rm -f build/selfhost.ludic
echo " ✅ FIXPOINT: gen2.ll == gen3.ll — the self-hosted compiler reproduces itself"
exit 0
else
echo " ❌ gen2.ll != gen3.ll"
diff "$B/gen2.ll" "$B/gen3.ll" | head
exit 1
fi

29
selfhost/buf.ludic Normal file
View file

@ -0,0 +1,29 @@
# buf.ludic — a growable byte buffer for building IR text. The emitter writes
# module-level material (types, globals, string constants) into one buffer and
# function bodies into another, then prints them in order.
struct Buf { data: ptr = ptr_null(), len: int = 0, cap: int = 0 }
fn buf_new() -> Buf {
let b = new Buf
b.cap = 256
b.data = mem_alloc(b.cap)
b.len = 0
return b
}
fn buf_ensure(b: Buf, extra: int) -> void {
if b.len + extra + 1 <= b.cap { return }
while b.len + extra + 1 > b.cap { b.cap = b.cap * 2 }
b.data = mem_realloc(b.data, b.cap)
}
fn buf_putc(b: Buf, c: int) -> void {
buf_ensure(b, 1)
poke8(b.data, b.len, c)
b.len = b.len + 1
}
fn buf_puts(b: Buf, s: ptr) -> void {
let i = 0
while peek8(s, i) != 0 { buf_putc(b, peek8(s, i)) i = i + 1 }
}
fn buf_puti(b: Buf, n: int) -> void { buf_puts(b, itoa(n)) }
fn buf_str(b: Buf) -> ptr { poke8(b.data, b.len, 0) return b.data }

39
selfhost/build.sh Executable file
View file

@ -0,0 +1,39 @@
#!/bin/bash
# build.sh — assemble the self-host compiler from its fragments and compile it
# with the given ludicc (default: the C build/ludicc).
# ./selfhost/build.sh [ludicc] [outbin]
set -u
cd "$(dirname "$0")/.."
LC="${1:-build/ludicc}"
OUT="${2:-build/selfhost}"
mkdir -p build
FRAGS="selfhost/str.ludic
selfhost/buf.ludic
selfhost/io.ludic
selfhost/ast.ludic
selfhost/lex.ludic
selfhost/parse.ludic
selfhost/parse_game.ludic
selfhost/emit_core.ludic
selfhost/emit_head.ludic
selfhost/emit_addr.ludic
selfhost/emit_intrin.ludic
selfhost/emit_intrin2.ludic
selfhost/emit_math.ludic
selfhost/emit_new.ludic
selfhost/emit_expr.ludic
selfhost/emit_stmt.ludic
selfhost/emit_ecs.ludic
selfhost/emit_query.ludic
selfhost/emit_spawn.ludic
selfhost/emit_game.ludic
selfhost/emit_machine.ludic
selfhost/emit_save.ludic
selfhost/emit_ui.ludic
selfhost/emit_decl.ludic
selfhost/main.ludic"
SRC=build/selfhost.ludic
{ echo "game SelfHost {"; for f in $FRAGS; do cat "$f"; echo; done; echo "}"; } > "$SRC"
# the compiler emits IR to stdout; clang assembles and links it
"$LC" "$SRC" > "$OUT.ll" 2>/dev/null && ${LUDIC_CC:-clang} "$OUT.ll" -o "$OUT" 2>/dev/null
rc=$?; rm -f "$OUT.ll"; [ $rc -eq 0 ]

8
selfhost/compile.sh Executable file
View file

@ -0,0 +1,8 @@
#!/bin/bash
# compile.sh — compile a .ludic file with the self-hosted compiler and link it.
# ./selfhost/compile.sh <selfhost-binary> <input.ludic> <output-binary>
set -eu
cd "$(dirname "$0")/.."
SH="$1"; IN="$2"; OUT="$3"
"$SH" "$IN" > "$OUT.ll"
clang "$OUT.ll" -o "$OUT"

38
selfhost/emit_addr.ludic Normal file
View file

@ -0,0 +1,38 @@
# emit_addr.ludic — addresses of lvalues (struct fields and slice elements),
# shared by expression loads and assignment stores. Each returns the address
# register; the element/field type is written into g_addr_ty.
var g_addr_ty: ptr # out-param: the type at the computed address
# address of `base.field`
fn emit_member_addr(e: Node) -> ptr {
let base = emit_expr(e.a)
let s = layout_node(base.ty)
if ptr_is_null(s) { perr(sconcat("member access on non-aggregate ", base.ty)) }
let fidx = field_index(s, e.s)
if fidx < 0 { perr(sconcat("no such field ", e.s)) }
g_addr_ty = field_type(s, e.s)
let r = nreg()
emit(" ") emit(r) emit(" = getelementptr inbounds ") emit(layout_ty(base.ty))
emit(", ptr ") emit(base.code) emit(", i32 0, i32 ") emit(itoa(fidx)) emit("\n")
return r
}
# address of `base[index]` (slices only in this subset)
fn emit_index_addr(e: Node) -> ptr {
let base = emit_expr(e.a)
if not is_slice_ty(base.ty) { perr("indexing a non-slice") }
let el = slice_elem(base.ty)
g_addr_ty = el
# load the data pointer from the slice header (field 0)
let dp = nreg()
emit(" ") emit(dp) emit(" = getelementptr inbounds %LSlice, ptr ")
emit(base.code) emit(", i32 0, i32 0\n")
let data = nreg()
emit(" ") emit(data) emit(" = load ptr, ptr ") emit(dp) emit("\n")
let ix = emit_expr(e.b)
let r = nreg()
emit(" ") emit(r) emit(" = getelementptr inbounds ") emit(llty(el))
emit(", ptr ") emit(data) emit(", i32 ") emit(ix.code) emit("\n")
return r
}

139
selfhost/emit_core.ludic Normal file
View file

@ -0,0 +1,139 @@
# emit_core.ludic — emitter state, type mapping, struct/slice helpers, and the
# module header. Mirrors the pieces of compiler/back/ that this subset needs.
# structs and slices are references, so every non-scalar type lowers to `ptr`.
struct Val { code: ptr = ptr_null(), ty: ptr = ptr_null() }
fn val(code: ptr, ty: ptr) -> Val { let v = new Val v.code = code v.ty = ty return v }
var head: Buf # module-level: types, globals, string constants
var code: Buf # function bodies
var falloc: Buf # entry-block allocas for the current function
var ll_t: int = 0 # temp register counter (reset per function)
var ll_lbl: int = 0 # label counter
var ll_str: int = 0 # string-constant counter
# local environment (parallel slices), reset per function
var loc_name: []ptr
var loc_reg: []ptr
var loc_ty: []ptr
var nloc: int = 0
# loop targets for break/continue (innermost last)
var brk_lbl: []ptr
var cnt_lbl: []ptr
var nloop: int = 0
var ret_ty: ptr # current function's return type
var g_term: bool = false # did the current block end in a terminator?
var self_stk: []ptr # entity-index slot (ip) per enclosing query, for self()
var nself: int = 0
var mach_stk: []Node # enclosing `machine` nodes, so `become` finds its register
var nmach: int = 0
fn emit(s: ptr) -> void { buf_puts(code, s) }
fn emith(s: ptr) -> void { buf_puts(head, s) }
# stack slots MUST live in the entry block (an alloca in a loop walks the stack
# off its end), so they go into a per-function buffer spliced in at entry.
fn emit_alloca(llt: ptr) -> ptr {
let r = sconcat("%t", itoa(ll_t)) ll_t = ll_t + 1
buf_puts(falloc, " ") buf_puts(falloc, r) buf_puts(falloc, " = alloca ") buf_puts(falloc, llt) buf_puts(falloc, "\n")
return r
}
# "%t<n>" fresh register
fn sconcat(a: ptr, b: ptr) -> ptr {
let la = slen(a) let lb = slen(b)
let out = mem_alloc(la + lb + 1)
let i = 0
while i < la { poke8(out, i, peek8(a, i)) i = i + 1 }
let j = 0
while j < lb { poke8(out, la + j, peek8(b, j)) j = j + 1 }
poke8(out, la + lb, 0)
return out
}
fn nreg() -> ptr { let r = sconcat("%t", itoa(ll_t)) ll_t = ll_t + 1 return r }
fn lbl(pfx: ptr) -> ptr { let r = sconcat(pfx, itoa(ll_lbl)) ll_lbl = ll_lbl + 1 return r }
# Ludic type -> LLVM type. int/bool are i32; everything else (ptr/str/struct/
# slice) is a pointer; void is void.
fn llty(t: ptr) -> ptr {
if streq(t, "int") or streq(t, "bool") or streq(t, "fixed") { return "i32" }
if streq(t, "void") { return "void" }
return "ptr"
}
fn is_slice_ty(t: ptr) -> bool { return peek8(t, 0) == 91 and peek8(t, 1) == 93 } # "[]"
fn slice_elem(t: ptr) -> ptr { return substr(t, 2, slen(t) - 2) }
fn find_struct(name: ptr) -> Node {
let i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_STRUCT and streq(d.s, name) { return d }
i = i + 1
}
return ptr_null()
}
fn is_struct_ty(t: ptr) -> bool { return not ptr_is_null(find_struct(t)) }
fn find_arch(name: ptr) -> Node {
let i = 0
while i < len(prog) { let d = prog[i] if d.kind == N_ARCH and streq(d.s, name) { return d } i = i + 1 }
return ptr_null()
}
fn find_comp(name: ptr) -> Node {
let i = 0
while i < len(prog) { let d = prog[i] if d.kind == N_COMP and streq(d.s, name) { return d } i = i + 1 }
return ptr_null()
}
# a struct or a component — both have %Str_/%Cmp_ layouts with named fields
fn layout_node(name: ptr) -> Node {
let s = find_struct(name) if not ptr_is_null(s) { return s }
return find_comp(name)
}
fn layout_ty(name: ptr) -> ptr {
if not ptr_is_null(find_struct(name)) { return sconcat("%Str_", name) }
return sconcat("%Cmp_", name)
}
fn field_index(s: Node, fname: ptr) -> int {
let i = 0
while i < len(s.kids) { if streq(s.kids[i].s, fname) { return i } i = i + 1 }
return 0 - 1
}
fn field_type(s: Node, fname: ptr) -> ptr {
let i = 0
while i < len(s.kids) { if streq(s.kids[i].s, fname) { return s.kids[i].ty } i = i + 1 }
return "int"
}
# find a global var/const by name
fn find_global(name: ptr) -> Node {
let i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_VAR and streq(d.s, name) { return d }
if d.kind == N_CONST and streq(d.s, name) { return d }
i = i + 1
}
return ptr_null()
}
fn find_fn(name: ptr) -> Node {
let i = 0
while i < len(prog) { let d = prog[i] if d.kind == N_FN and streq(d.s, name) { return d } i = i + 1 }
return ptr_null()
}
# local variable environment
fn loc_reset() -> void { nloc = 0 }
fn loc_push(name: ptr, r: ptr, ty: ptr) -> void {
if nloc < len(loc_name) { loc_name[nloc] = name loc_reg[nloc] = r loc_ty[nloc] = ty }
else { push(loc_name, name) push(loc_reg, r) push(loc_ty, ty) }
nloc = nloc + 1
}
fn loc_find(name: ptr) -> int {
let i = nloc - 1
while i >= 0 { if streq(loc_name[i], name) { return i } i = i - 1 }
return 0 - 1
}

104
selfhost/emit_decl.ludic Normal file
View file

@ -0,0 +1,104 @@
# emit_decl.ludic — functions, main, and the whole-program driver. A function's
# body is built into a scratch buffer so entry-block allocas can be spliced in
# ahead of it (see emit_alloca). Returns go through a %retval slot + `ret` label.
fn emit_params_sig(d: Node) -> void {
let i = 0
while i < len(d.kids) {
if i > 0 { emit(", ") }
emit(llty(d.kids[i].ty)) emit(" %arg_") emit(d.kids[i].s)
i = i + 1
}
}
fn emit_fn(d: Node) -> void {
ll_t = 0 ll_lbl = 0 g_term = false loc_reset() nloop = 0
ret_ty = d.ty
let fbody = buf_new()
falloc = buf_new()
let saved = code
code = fbody
let rl = llty(ret_ty)
if not streq(rl, "void") { buf_puts(falloc, " %retval = alloca ") buf_puts(falloc, rl) buf_puts(falloc, "\n") }
# params: store each incoming argument into a stack slot
let i = 0
while i < len(d.kids) {
let p = d.kids[i]
let slot = emit_alloca(llty(p.ty))
emit(" store ") emit(llty(p.ty)) emit(" %arg_") emit(p.s) emit(", ptr ") emit(slot) emit("\n")
loc_push(p.s, slot, p.ty)
i = i + 1
}
emit_block(d.a)
if not g_term { emit(" br label %ret\n") }
emit("ret:\n")
if streq(rl, "void") { emit(" ret void\n") }
else { let r = emit_bind(sconcat("load ", sconcat(rl, ", ptr %retval"))) emit(" ret ") emit(rl) emit(" ") emit(r) emit("\n") }
code = saved
emit("define ") emit(rl) emit(" @fn_") emit(d.s) emit("(") emit_params_sig(d) emit(") {\nentry:\n")
emit(buf_str(falloc))
emit(buf_str(fbody))
emit("}\n\n")
}
fn emit_main(d: Node) -> void {
ll_t = 0 ll_lbl = 0 g_term = false loc_reset() nloop = 0
ret_ty = "int"
let fbody = buf_new()
falloc = buf_new()
let saved = code
code = fbody
buf_puts(falloc, " %retval = alloca i32\n")
emit(" store i32 %argc, ptr @L_argc\n")
emit(" store ptr %argv, ptr @L_argv\n")
emit(" store i32 0, ptr %retval\n")
emit_block(d.a)
if not g_term { emit(" br label %ret\n") }
emit("ret:\n")
let r = emit_bind("load i32, ptr %retval")
emit(" ret i32 ") emit(r) emit("\n")
code = saved
emit("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n")
emit(buf_str(falloc))
emit(buf_str(fbody))
emit("}\n")
}
fn emit_program() -> void {
head = buf_new()
code = buf_new()
loc_name = new []ptr loc_reg = new []ptr loc_ty = new []ptr
brk_lbl = new []ptr cnt_lbl = new []ptr
self_stk = new []ptr
mach_stk = new []Node
emit_header()
if has_ecs() { emit_ecs_storage() }
let i = 0
while i < len(prog) { if prog[i].kind == N_FN { emit_fn(prog[i]) } i = i + 1 }
if has_ecs() { emit_ecs_allocator() emit_snapshot() }
if has_ui() { emit_ui_build() }
if has_systems() { emit_game_main() }
else {
i = 0
while i < len(prog) { if prog[i].kind == N_MAIN { emit_main(prog[i]) } i = i + 1 }
}
}
# Flush the emitted IR. With a null path it goes to stdout (the pipe the shell
# drivers read); with a path it is written to that file so ludicc can hand it to
# clang itself.
fn ir_flush(path: ptr) -> bool {
let h = buf_str(head)
let c = buf_str(code)
if ptr_is_null(path) {
print_str(h)
print_str(c)
return true
}
let f = file_open(path, "wb")
if ptr_is_null(f) { return false }
file_write(f, h, slen(h))
file_write(f, c, slen(c))
file_close(f)
return true
}

92
selfhost/emit_ecs.ludic Normal file
View file

@ -0,0 +1,92 @@
# emit_ecs.ludic — ECS storage and the entity allocator. For each component:
# a %Cmp_ layout, a dense @S_ store, and an @H_ "has this component" array.
# Entities are integer handles; L_alloc reuses freed slots. Mirrors the ECS
# parts of compiler/back/ir_decl.c.
const MAX_ENT: int = 1024
fn has_ecs() -> bool {
let i = 0
while i < len(prog) { let k = prog[i].kind if k == N_COMP or k == N_SYS { return true } i = i + 1 }
return false
}
fn has_systems() -> bool {
let i = 0
while i < len(prog) { if prog[i].kind == N_SYS { return true } i = i + 1 }
return false
}
fn emit_ecs_storage() -> void {
emith("@L_running = internal global i32 1\n")
emith("@L_key = internal global i32 0\n")
emith("@L_entc = internal global i32 0\n")
let me = itoa(MAX_ENT)
emith(sconcat("@L_alive = internal global [", sconcat(me, " x i32] zeroinitializer\n")))
emith(sconcat("@L_kind = internal global [", sconcat(me, " x i32] zeroinitializer\n")))
emith(sconcat("@L_freelist = internal global [", sconcat(me, " x i32] zeroinitializer\n")))
emith("@L_freen = internal global i32 0\n")
let i = 0
while i < len(prog) {
let c = prog[i]
if c.kind == N_COMP {
emith(sconcat("%Cmp_", sconcat(c.s, " = type { ")))
if len(c.kids) == 0 { emith("i32") }
let f = 0
while f < len(c.kids) { if f > 0 { emith(", ") } emith(llty(c.kids[f].ty)) f = f + 1 }
emith(" }\n")
emith(sconcat("@S_", sconcat(c.s, sconcat(" = internal global [", sconcat(me, sconcat(" x %Cmp_", sconcat(c.s, "] zeroinitializer\n")))))))
emith(sconcat("@H_", sconcat(c.s, sconcat(" = internal global [", sconcat(me, " x i8] zeroinitializer\n")))))
}
i = i + 1
}
}
# L_reset(e): clear every has-flag and the archetype kind for entity e
fn emit_ecs_allocator() -> void {
let me = itoa(MAX_ENT)
emit("define void @L_reset(i32 %e) {\nentry:\n")
let i = 0
while i < len(prog) {
if prog[i].kind == N_COMP {
let hn = sconcat("%h", itoa(i))
emit(" ") emit(hn) emit(" = getelementptr inbounds [") emit(me) emit(" x i8], ptr @H_") emit(prog[i].s) emit(", i32 0, i32 %e\n")
emit(" store i8 0, ptr ") emit(hn) emit("\n")
}
i = i + 1
}
emit(" %k = getelementptr inbounds [") emit(me) emit(" x i32], ptr @L_kind, i32 0, i32 %e\n")
emit(" store i32 0, ptr %k\n ret void\n}\n\n")
emit("define i32 @L_alloc() {\nentry:\n")
emit(" %fn = load i32, ptr @L_freen\n")
emit(" %has = icmp sgt i32 %fn, 0\n")
emit(" br i1 %has, label %reuse, label %fresh\n")
emit("reuse:\n")
emit(" %fn1 = sub i32 %fn, 1\n")
emit(" store i32 %fn1, ptr @L_freen\n")
emit(" %fp = getelementptr inbounds [") emit(me) emit(" x i32], ptr @L_freelist, i32 0, i32 %fn1\n")
emit(" %re = load i32, ptr %fp\n")
emit(" br label %done\n")
emit("fresh:\n")
emit(" %ec = load i32, ptr @L_entc\n")
emit(" %ec1 = add i32 %ec, 1\n")
emit(" store i32 %ec1, ptr @L_entc\n")
emit(" br label %done\n")
emit("done:\n")
emit(" %e = phi i32 [ %re, %reuse ], [ %ec, %fresh ]\n")
emit(" call void @L_reset(i32 %e)\n")
emit(" %ap = getelementptr inbounds [") emit(me) emit(" x i32], ptr @L_alive, i32 0, i32 %e\n")
emit(" store i32 1, ptr %ap\n")
emit(" ret i32 %e\n}\n\n")
emit("define void @L_free_entity(i32 %e) {\nentry:\n")
emit(" %ap = getelementptr inbounds [") emit(me) emit(" x i32], ptr @L_alive, i32 0, i32 %e\n")
emit(" store i32 0, ptr %ap\n")
emit(" call void @L_reset(i32 %e)\n")
emit(" %fn = load i32, ptr @L_freen\n")
emit(" %fp = getelementptr inbounds [") emit(me) emit(" x i32], ptr @L_freelist, i32 0, i32 %fn\n")
emit(" store i32 %e, ptr %fp\n")
emit(" %fn1 = add i32 %fn, 1\n")
emit(" store i32 %fn1, ptr @L_freen\n")
emit(" ret void\n}\n\n")
}

168
selfhost/emit_expr.ludic Normal file
View file

@ -0,0 +1,168 @@
# emit_expr.ludic — lower an expression to IR, returning its register and type.
fn emit_load_at(addr: ptr, ty: ptr) -> Val {
let r = emit_bind(sconcat("load ", sconcat(llty(ty), sconcat(", ptr ", addr))))
return val(r, ty)
}
# short-circuit `and`/`or`: seed a slot with (left!=0), branch to decide whether
# to overwrite with (right!=0).
fn emit_logic(e: Node) -> Val {
let slot = emit_alloca("i32")
let la = emit_expr(e.a)
let lc = emit_bind(sconcat("icmp ne i32 ", sconcat(la.code, ", 0")))
let lz = emit_bind(sconcat("zext i1 ", sconcat(lc, " to i32")))
emit(" store i32 ") emit(lz) emit(", ptr ") emit(slot) emit("\n")
let ev = lbl("sc") let done = lbl("scend")
if streq(e.s, "and") { emit(" br i1 ") emit(lc) emit(", label %") emit(ev) emit(", label %") emit(done) emit("\n") }
else { emit(" br i1 ") emit(lc) emit(", label %") emit(done) emit(", label %") emit(ev) emit("\n") }
emit(ev) emit(":\n")
let rb = emit_expr(e.b)
let rc = emit_bind(sconcat("icmp ne i32 ", sconcat(rb.code, ", 0")))
let rz = emit_bind(sconcat("zext i1 ", sconcat(rc, " to i32")))
emit(" store i32 ") emit(rz) emit(", ptr ") emit(slot) emit("\n")
emit(" br label %") emit(done) emit("\n")
emit(done) emit(":\n")
return val(emit_bind(sconcat("load i32, ptr ", slot)), "bool")
}
fn cmp_code(op: ptr) -> ptr {
if streq(op, "<") { return "slt" }
if streq(op, "<=") { return "sle" }
if streq(op, ">") { return "sgt" }
if streq(op, ">=") { return "sge" }
if streq(op, "==") { return "eq" }
return "ne"
}
fn is_cmp(op: ptr) -> bool {
return streq(op,"<") or streq(op,"<=") or streq(op,">") or streq(op,">=") or streq(op,"==") or streq(op,"!=")
}
fn arith_code(op: ptr) -> ptr {
if streq(op, "+") { return "add" }
if streq(op, "-") { return "sub" }
if streq(op, "*") { return "mul" }
if streq(op, "/") { return "sdiv" }
return "srem"
}
# widen an int value to Q16.16 by shifting left 16 (a fixed value passes through)
fn to_fixed(v: Val) -> ptr {
if streq(v.ty, "fixed") { return v.code }
return emit_bind(sconcat("shl i32 ", sconcat(v.code, ", 16")))
}
fn emit_bin(e: Node) -> Val {
if streq(e.s, "and") or streq(e.s, "or") { return emit_logic(e) }
let a = emit_expr(e.a)
let b = emit_expr(e.b)
let fx = streq(a.ty, "fixed") or streq(b.ty, "fixed")
if is_cmp(e.s) {
let ac = a.code let bc = b.code
if fx { ac = to_fixed(a) bc = to_fixed(b) }
let c = emit_bind(sconcat("icmp ", sconcat(cmp_code(e.s), sconcat(" i32 ", sconcat(ac, sconcat(", ", bc))))))
return val(emit_bind(sconcat("zext i1 ", sconcat(c, " to i32"))), "bool")
}
if fx {
let af = to_fixed(a) let bf = to_fixed(b)
if streq(e.s, "*") {
let a64 = emit_bind(sconcat("sext i32 ", sconcat(af, " to i64")))
let b64 = emit_bind(sconcat("sext i32 ", sconcat(bf, " to i64")))
let m = emit_bind(sconcat("mul i64 ", sconcat(a64, sconcat(", ", b64))))
let sh = emit_bind(sconcat("ashr i64 ", sconcat(m, ", 16")))
return val(emit_bind(sconcat("trunc i64 ", sconcat(sh, " to i32"))), "fixed")
}
if streq(e.s, "/") {
let a64 = emit_bind(sconcat("sext i32 ", sconcat(af, " to i64")))
let ash = emit_bind(sconcat("shl i64 ", sconcat(a64, ", 16")))
let b64 = emit_bind(sconcat("sext i32 ", sconcat(bf, " to i64")))
let dv = emit_bind(sconcat("sdiv i64 ", sconcat(ash, sconcat(", ", b64))))
return val(emit_bind(sconcat("trunc i64 ", sconcat(dv, " to i32"))), "fixed")
}
let r = emit_bind(sconcat(arith_code(e.s), sconcat(" i32 ", sconcat(af, sconcat(", ", bf)))))
return val(r, "fixed")
}
let r = emit_bind(sconcat(arith_code(e.s), sconcat(" i32 ", sconcat(a.code, sconcat(", ", b.code)))))
return val(r, "int")
}
fn emit_call(e: Node) -> Val {
let name = e.a.s
if streq(name, "self") { if nself == 0 { return val("0", "entity") } return val(emit_bind(sconcat("load i32, ptr ", self_stk[nself - 1])), "entity") }
if streq(name, "key") { return val(emit_bind("load i32, ptr @L_key"), "int") }
if streq(name, "save") { emit(" call void @L_save()\n") return val("0", "void") }
if streq(name, "ui_build") { emit(" call void @ui_build()\n") return val("0", "void") }
if streq(name, "load") { return val(emit_bind("call i32 @L_load()"), "bool") }
if streq(name, "quit") { emit(" store i32 0, ptr @L_running\n") return val("0", "void") }
if streq(name, "len") { return emit_len(e) }
if streq(name, "push") { return emit_push(e) }
if streq(name, "fx") { let a = emit_expr(e.kids[0]) return val(emit_bind(sconcat("shl i32 ", sconcat(a.code, ", 16"))), "fixed") }
if streq(name, "flr") { let a = emit_expr(e.kids[0]) return val(emit_bind(sconcat("ashr i32 ", sconcat(a.code, ", 16"))), "int") }
if is_intrinsic(name) { return emit_intrinsic(name, e) }
if is_intrinsic2(name) { return emit_intrinsic2(name, e) }
if is_math_builtin(name) { return emit_math_builtin(name, e) }
let fn2 = find_fn(name)
let cname = name
if ptr_is_null(fn2) {
# a builtin like clear()/reg() is satisfied by its rt_ function
let rtname = sconcat("rt_", name)
fn2 = find_fn(rtname)
if ptr_is_null(fn2) { perr(sconcat("unknown function ", name)) }
cname = rtname
}
# evaluate args first (their IR is emitted before the call instruction)
let args = new []ptr
let atys = new []ptr
let i = 0
while i < len(e.kids) { let v = emit_expr(e.kids[i]) push(args, v.code) push(atys, v.ty) i = i + 1 }
let rl = llty(fn2.ty)
emit(" ")
let rreg = "0"
if not streq(rl, "void") { rreg = nreg() emit(rreg) emit(" = ") }
emit("call ") emit(rl) emit(" @fn_") emit(cname) emit("(")
i = 0
while i < len(args) {
if i > 0 { emit(", ") }
emit(llty(atys[i])) emit(" ") emit(args[i])
i = i + 1
}
emit(")
")
return val(rreg, fn2.ty)
}
fn emit_expr(e: Node) -> Val {
if ptr_is_null(e) { return val("0", "int") }
if e.kind == E_INT { return val(itoa(e.ival), "int") }
if e.kind == E_FLOAT { return val(itoa(e.ival), "fixed") }
if e.kind == E_BOOL { return val(itoa(e.ival), "bool") }
if e.kind == E_STR { return val(emit_str_const(e.s), "str") }
if e.kind == E_NEW {
if is_slice_ty(e.s) { return emit_new_slice(e.s) }
return emit_new_struct(e.s)
}
if e.kind == E_ID {
let li = loc_find(e.s)
if li >= 0 { return emit_load_at(loc_reg[li], loc_ty[li]) }
let g = find_global(e.s)
if not ptr_is_null(g) {
if g.kind == N_CONST { return val(itoa(g.a.ival), "int") }
let r = emit_bind(sconcat("load ", sconcat(llty(g.ty), sconcat(", ptr @g_", e.s))))
return val(r, g.ty)
}
# a UI_<name> that is not a const/var resolves to its widget index
if is_ui_ident(e.s) { return val(itoa(ui_index_of(e.s)), "int") }
perr(sconcat("unknown identifier ", e.s))
}
if e.kind == E_MEMBER { let a = emit_member_addr(e) return emit_load_at(a, g_addr_ty) }
if e.kind == E_INDEX { let a = emit_index_addr(e) return emit_load_at(a, g_addr_ty) }
if e.kind == E_CALL { return emit_call(e) }
if e.kind == E_BIN { return emit_bin(e) }
if e.kind == E_UN {
let a = emit_expr(e.a)
if streq(e.s, "-") { return val(emit_bind(sconcat("sub i32 0, ", a.code)), "int") }
let c = emit_bind(sconcat("icmp eq i32 ", sconcat(a.code, ", 0")))
return val(emit_bind(sconcat("zext i1 ", sconcat(c, " to i32"))), "bool")
}
perr("cannot emit expression")
return val("0", "int")
}

68
selfhost/emit_game.ludic Normal file
View file

@ -0,0 +1,68 @@
# 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.
fn emit_system_fn(sys: 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
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")
}
fn emit_calls_for_phase(phase: ptr) -> void {
let i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_SYS and streq(d.ty, phase) { emit(" call void @sys_") emit(d.s) emit("()\n") }
i = i + 1
}
}
fn emit_game_main() -> void {
# every system becomes a function first
let i = 0
while i < len(prog) { if prog[i].kind == N_SYS { emit_system_fn(prog[i]) } i = i + 1 }
emit("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n")
emit(" store i32 %argc, ptr @L_argc\n")
emit(" store ptr %argv, ptr @L_argv\n")
if not ptr_is_null(find_fn("rt_init")) { emit(" call void @fn_rt_init()\n") }
emit_calls_for_phase("Start")
emit(" br label %loop\n")
emit("loop:\n")
let r = emit_bind("load i32, ptr @L_running")
let rc = emit_bind(sconcat("icmp ne i32 ", sconcat(r, ", 0")))
if not ptr_is_null(find_fn("rt_running")) {
let pr = emit_bind("call i32 @fn_rt_running()")
let pc = emit_bind(sconcat("icmp ne i32 ", sconcat(pr, ", 0")))
let go = emit_bind(sconcat("and i1 ", sconcat(rc, sconcat(", ", pc))))
emit(" br i1 ") emit(go) emit(", label %body, label %done\n")
} else {
emit(" br i1 ") emit(rc) emit(", label %body, label %done\n")
}
emit("body:\n")
if not ptr_is_null(find_fn("rt_poll")) {
let k = emit_bind("call i32 @fn_rt_poll()")
emit(" store i32 ") emit(k) emit(", ptr @L_key\n")
}
emit_calls_for_phase("Input")
emit_calls_for_phase("FixedUpdate")
emit_calls_for_phase("Update")
emit_calls_for_phase("LateUpdate")
emit_calls_for_phase("Render")
emit(" br label %loop\n")
emit("done:\n")
if not ptr_is_null(find_fn("rt_shutdown")) { emit(" call void @fn_rt_shutdown()\n") }
emit(" ret i32 0\n}\n")
}

108
selfhost/emit_head.ludic Normal file
View file

@ -0,0 +1,108 @@
# emit_head.ludic — string constants and the module header (libc declarations,
# the slice header type, struct layouts, globals, argv, format strings).
fn hexdig(n: int) -> int { if n < 10 { return 48 + n } return 55 + n } # 0-9 A-F
# emit `@.strN = ... c"escaped\00"` and return its name; % and non-print -> \XX
fn emit_str_const(s: ptr) -> ptr {
let name = sconcat("@.str", itoa(ll_str))
ll_str = ll_str + 1
let n = slen(s)
emith(name) emith(" = private unnamed_addr constant [")
emith(itoa(n + 1)) emith(" x i8] c\"")
let i = 0
while i < n {
let c = peek8(s, i)
if c == 34 or c == 92 or c < 32 or c > 126 {
buf_putc(head, 92) # backslash
buf_putc(head, hexdig(c / 16))
buf_putc(head, hexdig(c % 16))
} else { buf_putc(head, c) }
i = i + 1
}
emith("\\00\"\n")
return name
}
# a fresh SSA register bound to a `getelementptr`, returned as its name
fn emit_gep_i8(base: ptr, idx: ptr) -> ptr {
let r = nreg()
emit(" ") emit(r) emit(" = getelementptr inbounds i8, ptr ")
emit(base) emit(", i32 ") emit(idx) emit("\n")
return r
}
# the constant initializer for a global var: a literal, or 0/null
fn global_init(d: Node) -> ptr {
if ptr_is_null(d.a) { if streq(llty(d.ty), "ptr") { return "null" } return "0" }
let e = d.a
if e.kind == E_INT or e.kind == E_FLOAT or e.kind == E_BOOL { return itoa(e.ival) }
if e.kind == E_UN and streq(e.s, "-") and e.a.kind == E_INT { return sconcat("-", itoa(e.a.ival)) }
if streq(llty(d.ty), "ptr") { return "null" }
return "0"
}
fn emit_header() -> void {
emith("; Ludic (self-hosted) -> LLVM IR\n")
emith("declare i32 @printf(ptr, ...)\n")
emith("declare ptr @malloc(i64)\n")
emith("declare ptr @realloc(ptr, i64)\n")
emith("declare void @free(ptr)\n")
emith("declare ptr @fopen(ptr, ptr)\n")
emith("declare i64 @fread(ptr, i64, i64, ptr)\n")
emith("declare i64 @fwrite(ptr, i64, i64, ptr)\n")
emith("declare i32 @fseek(ptr, i64, i32)\n")
emith("declare i64 @ftell(ptr)\n")
emith("declare i32 @fclose(ptr)\n")
emith("declare void @exit(i32)\n")
emith("declare i32 @system(ptr)\n")
emith("declare ptr @getenv(ptr)\n")
emith("declare ptr @memcpy(ptr, ptr, i64)\n")
emith("declare ptr @memset(ptr, i32, i64)\n")
emith("declare i64 @strlen(ptr)\n")
emith("declare i32 @getchar()\n")
emith("declare i32 @putchar(i32)\n")
emith("declare i64 @time(ptr)\n")
emith("declare void @win_open(i32, i32, i32, ptr)\n")
emith("declare i32 @win_poll()\n")
emith("declare void @win_present(ptr, i32, i32)\n")
emith("declare i32 @win_running()\n")
emith("declare void @win_close()\n")
emith("@__stderrp = external global ptr\n")
emith("@.fmt_str = private unnamed_addr constant [3 x i8] c\"%s\\00\"\n")
emith("@.fmt_int = private unnamed_addr constant [4 x i8] c\"%d\\0A\\00\"\n")
emith("@L_argc = internal global i32 0\n")
emith("@L_argv = internal global ptr null\n")
emith("@.gametitle = private unnamed_addr constant [")
emith(itoa(slen(g_game_name) + 1)) emith(" x i8] c\"") emith(g_game_name) emith("\\00\"\n")
emith("%LSlice = type { ptr, i32, i32 }\n")
# struct layouts
let i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_STRUCT {
emith("%Str_") emith(d.s) emith(" = type { ")
if len(d.kids) == 0 { emith("i32") }
let f = 0
while f < len(d.kids) {
if f > 0 { emith(", ") }
emith(llty(d.kids[f].ty))
f = f + 1
}
emith(" }\n")
}
i = i + 1
}
# globals (vars) — aggregates/pointers default to null, scalars to 0
i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_VAR {
emith("@g_") emith(d.s) emith(" = internal global ")
emith(llty(d.ty)) emith(" ")
emith(global_init(d))
emith("\n")
}
i = i + 1
}
}

142
selfhost/emit_intrin.ludic Normal file
View file

@ -0,0 +1,142 @@
# emit_intrin.ludic — the low-level intrinsics the self-host source uses, each
# lowered to the same libc/inline IR the C backend emits. Returns a Val via
# g_intrin_val and sets g_intrin_ok when `name` was an intrinsic.
var g_intrin_ok: bool = false
# is `name` a low-level intrinsic? A pure name check, so it can gate dispatch
# without evaluating arguments (which could clobber shared state).
fn is_intrinsic(name: ptr) -> bool {
if streq(name,"ptr_null") or streq(name,"mem_alloc") or streq(name,"mem_realloc") { return true }
if streq(name,"peek8") or streq(name,"poke8") or streq(name,"ptr_is_null") { return true }
if streq(name,"file_open") or streq(name,"file_read") or streq(name,"file_write") { return true }
if streq(name,"file_seek") or streq(name,"file_tell") or streq(name,"file_close") { return true }
if streq(name,"print_str") or streq(name,"print_int") { return true }
if streq(name,"os_argc") or streq(name,"os_arg") or streq(name,"os_exit") or streq(name,"file_stderr") { return true }
if streq(name,"os_system") or streq(name,"os_getenv") { return true }
if streq(name,"shl") or streq(name,"shr") or streq(name,"band") or streq(name,"bor") or streq(name,"bxor") or streq(name,"bnot") { return true }
if streq(name,"peek32") or streq(name,"poke32") { return true }
return false
}
# emit " <r> = <rest>\n" and return r
fn emit_bind(rest: ptr) -> ptr { let r = nreg() emit(" ") emit(r) emit(" = ") emit(rest) emit("\n") return r }
fn arg_code(e: Node, i: int) -> ptr { let v = emit_expr(e.kids[i]) return v.code }
fn emit_intrinsic(name: ptr, e: Node) -> Val {
g_intrin_ok = true
if streq(name, "ptr_null") { return val("null", "ptr") }
if streq(name, "mem_alloc") {
let n = arg_code(e, 0)
let w = emit_bind(sconcat("zext i32 ", sconcat(n, " to i64")))
return val(emit_bind(sconcat("call ptr @malloc(i64 ", sconcat(w, ")"))), "ptr")
}
if streq(name, "mem_realloc") {
let p = arg_code(e, 0) let n = arg_code(e, 1)
let w = emit_bind(sconcat("zext i32 ", sconcat(n, " to i64")))
return val(emit_bind(sconcat("call ptr @realloc(ptr ", sconcat(p, sconcat(", i64 ", sconcat(w, ")"))))), "ptr")
}
if streq(name, "peek8") {
let p = arg_code(e, 0) let i = arg_code(e, 1)
let a = emit_gep_i8(p, i)
let b = emit_bind(sconcat("load i8, ptr ", a))
return val(emit_bind(sconcat("zext i8 ", sconcat(b, " to i32"))), "int")
}
if streq(name, "poke8") {
let p = arg_code(e, 0) let i = arg_code(e, 1) let v = arg_code(e, 2)
let a = emit_gep_i8(p, i)
let t = emit_bind(sconcat("trunc i32 ", sconcat(v, " to i8")))
emit(" store i8 ") emit(t) emit(", ptr ") emit(a) emit("\n")
return val("0", "void")
}
if streq(name, "ptr_is_null") {
let p = arg_code(e, 0)
let c = emit_bind(sconcat("icmp eq ptr ", sconcat(p, ", null")))
return val(emit_bind(sconcat("zext i1 ", sconcat(c, " to i32"))), "bool")
}
if streq(name, "file_open") {
let p = arg_code(e, 0) let m = arg_code(e, 1)
return val(emit_bind(sconcat("call ptr @fopen(ptr ", sconcat(p, sconcat(", ptr ", sconcat(m, ")"))))), "ptr")
}
if streq(name, "file_read") or streq(name, "file_write") {
let f = arg_code(e, 0) let b = arg_code(e, 1) let n = arg_code(e, 2)
let w = emit_bind(sconcat("zext i32 ", sconcat(n, " to i64")))
let fn2 = "@fread"
if streq(name, "file_write") { fn2 = "@fwrite" }
let r = emit_bind(sconcat("call i64 ", sconcat(fn2, sconcat("(ptr ", sconcat(b, sconcat(", i64 1, i64 ", sconcat(w, sconcat(", ptr ", sconcat(f, ")")))))))))
return val(emit_bind(sconcat("trunc i64 ", sconcat(r, " to i32"))), "int")
}
if streq(name, "file_seek") {
let f = arg_code(e, 0) let off = arg_code(e, 1) let wh = arg_code(e, 2)
let o = emit_bind(sconcat("sext i32 ", sconcat(off, " to i64")))
return val(emit_bind(sconcat("call i32 @fseek(ptr ", sconcat(f, sconcat(", i64 ", sconcat(o, sconcat(", i32 ", sconcat(wh, ")"))))))), "int")
}
if streq(name, "file_tell") {
let f = arg_code(e, 0)
let r = emit_bind(sconcat("call i64 @ftell(ptr ", sconcat(f, ")")))
return val(emit_bind(sconcat("trunc i64 ", sconcat(r, " to i32"))), "int")
}
if streq(name, "file_close") {
let f = arg_code(e, 0)
emit(" call i32 @fclose(ptr ") emit(f) emit(")\n")
return val("0", "void")
}
if streq(name, "print_str") {
let s = arg_code(e, 0)
emit(" call i32 (ptr, ...) @printf(ptr @.fmt_str, ptr ") emit(s) emit(")\n")
return val("0", "void")
}
if streq(name, "print_int") {
let n = arg_code(e, 0)
emit(" call i32 (ptr, ...) @printf(ptr @.fmt_int, i32 ") emit(n) emit(")\n")
return val("0", "void")
}
if streq(name, "os_argc") { return val(emit_bind("load i32, ptr @L_argc"), "int") }
if streq(name, "os_arg") {
let i = arg_code(e, 0)
let v = emit_bind("load ptr, ptr @L_argv")
let q = emit_bind(sconcat("getelementptr ptr, ptr ", sconcat(v, sconcat(", i32 ", i))))
return val(emit_bind(sconcat("load ptr, ptr ", q)), "str")
}
if streq(name, "os_exit") {
let n = arg_code(e, 0)
emit(" call void @exit(i32 ") emit(n) emit(")\n")
emit(" unreachable\n")
g_term = true
return val("0", "void")
}
if streq(name, "file_stderr") { return val(emit_bind("load ptr, ptr @__stderrp"), "ptr") }
if streq(name, "os_system") {
let c = arg_code(e, 0)
return val(emit_bind(sconcat("call i32 @system(ptr ", sconcat(c, ")"))), "int")
}
if streq(name, "os_getenv") {
let n = arg_code(e, 0)
return val(emit_bind(sconcat("call ptr @getenv(ptr ", sconcat(n, ")"))), "str")
}
if streq(name, "bnot") { let a = arg_code(e, 0) return val(emit_bind(sconcat("xor i32 ", sconcat(a, ", -1"))), "int") }
if streq(name,"shl") or streq(name,"shr") or streq(name,"band") or streq(name,"bor") or streq(name,"bxor") {
let a = arg_code(e, 0) let b = arg_code(e, 1)
let opc = "shl"
if streq(name,"shr") { opc = "lshr" }
if streq(name,"band") { opc = "and" }
if streq(name,"bor") { opc = "or" }
if streq(name,"bxor") { opc = "xor" }
return val(emit_bind(sconcat(opc, sconcat(" i32 ", sconcat(a, sconcat(", ", b))))), "int")
}
if streq(name, "peek32") {
let p = arg_code(e, 0) let i = arg_code(e, 1)
let g = nreg()
emit(" ") emit(g) emit(" = getelementptr inbounds i32, ptr ") emit(p) emit(", i32 ") emit(i) emit("\n")
return val(emit_bind(sconcat("load i32, ptr ", g)), "int")
}
if streq(name, "poke32") {
let p = arg_code(e, 0) let i = arg_code(e, 1) let v = arg_code(e, 2)
let g = nreg()
emit(" ") emit(g) emit(" = getelementptr inbounds i32, ptr ") emit(p) emit(", i32 ") emit(i) emit("\n")
emit(" store i32 ") emit(v) emit(", ptr ") emit(g) emit("\n")
return val("0", "void")
}
g_intrin_ok = false
return val("0", "void")
}

View file

@ -0,0 +1,95 @@
# emit_intrin2.ludic — the rest of the low-level intrinsics: raw memory, fixed
# memory access, pointer arithmetic, stdio, time, and the windowing hooks.
# Matches compiler/back/ir_intrin.c. Reached from emit_intrinsic's fall-through.
var g_windowed: bool = false # headless by default (games read stdin / dump PPM)
fn is_intrinsic2(name: ptr) -> bool {
if streq(name,"mem_free") or streq(name,"mem_copy") or streq(name,"mem_set") { return true }
if streq(name,"ptr_add") or streq(name,"read_byte") or streq(name,"write_byte") { return true }
if streq(name,"str_len") or streq(name,"os_time") { return true }
if streq(name,"peekf") or streq(name,"pokef") or streq(name,"as_fixed") or streq(name,"as_int") { return true }
if streq(name,"peekp") or streq(name,"pokep") { return true }
if streq(name,"is_windowed") or streq(name,"game_title") { return true }
if streq(name,"win_open") or streq(name,"win_poll") or streq(name,"win_present") { return true }
if streq(name,"win_running") or streq(name,"win_close") { return true }
return false
}
fn emit_intrinsic2(name: ptr, e: Node) -> Val {
if streq(name, "mem_free") {
let p = arg_code(e, 0) emit(" call void @free(ptr ") emit(p) emit(")\n") return val("0", "void")
}
if streq(name, "mem_copy") {
let d = arg_code(e, 0) let s = arg_code(e, 1) let n = arg_code(e, 2)
let w = emit_bind(sconcat("zext i32 ", sconcat(n, " to i64")))
emit(" call ptr @memcpy(ptr ") emit(d) emit(", ptr ") emit(s) emit(", i64 ") emit(w) emit(")\n")
return val("0", "void")
}
if streq(name, "mem_set") {
let p = arg_code(e, 0) let v = arg_code(e, 1) let n = arg_code(e, 2)
let w = emit_bind(sconcat("zext i32 ", sconcat(n, " to i64")))
emit(" call ptr @memset(ptr ") emit(p) emit(", i32 ") emit(v) emit(", i64 ") emit(w) emit(")\n")
return val("0", "void")
}
if streq(name, "ptr_add") {
let p = arg_code(e, 0) let n = arg_code(e, 1)
let g = nreg()
emit(" ") emit(g) emit(" = getelementptr inbounds i8, ptr ") emit(p) emit(", i32 ") emit(n) emit("\n")
return val(g, "ptr")
}
if streq(name, "read_byte") { return val(emit_bind("call i32 @getchar()"), "int") }
if streq(name, "write_byte") {
let v = arg_code(e, 0) emit(" call i32 @putchar(i32 ") emit(v) emit(")\n") return val("0", "void")
}
if streq(name, "str_len") {
let s = arg_code(e, 0)
let r = emit_bind(sconcat("call i64 @strlen(ptr ", sconcat(s, ")")))
return val(emit_bind(sconcat("trunc i64 ", sconcat(r, " to i32"))), "int")
}
if streq(name, "os_time") {
let r = emit_bind("call i64 @time(ptr null)")
return val(emit_bind(sconcat("trunc i64 ", sconcat(r, " to i32"))), "int")
}
if streq(name, "peekf") {
let p = arg_code(e, 0) let i = arg_code(e, 1)
let g = nreg() emit(" ") emit(g) emit(" = getelementptr inbounds i32, ptr ") emit(p) emit(", i32 ") emit(i) emit("\n")
return val(emit_bind(sconcat("load i32, ptr ", g)), "fixed")
}
if streq(name, "pokef") {
let p = arg_code(e, 0) let i = arg_code(e, 1) let v = arg_code(e, 2)
let g = nreg() emit(" ") emit(g) emit(" = getelementptr inbounds i32, ptr ") emit(p) emit(", i32 ") emit(i) emit("\n")
emit(" store i32 ") emit(v) emit(", ptr ") emit(g) emit("\n")
return val("0", "void")
}
if streq(name, "peekp") {
let p = arg_code(e, 0) let i = arg_code(e, 1)
let g = nreg() emit(" ") emit(g) emit(" = getelementptr inbounds ptr, ptr ") emit(p) emit(", i32 ") emit(i) emit("\n")
return val(emit_bind(sconcat("load ptr, ptr ", g)), "ptr")
}
if streq(name, "pokep") {
let p = arg_code(e, 0) let i = arg_code(e, 1) let v = arg_code(e, 2)
let g = nreg() emit(" ") emit(g) emit(" = getelementptr inbounds ptr, ptr ") emit(p) emit(", i32 ") emit(i) emit("\n")
emit(" store ptr ") emit(v) emit(", ptr ") emit(g) emit("\n")
return val("0", "void")
}
if streq(name, "as_fixed") { let a = emit_expr(e.kids[0]) return val(a.code, "fixed") }
if streq(name, "as_int") { let a = emit_expr(e.kids[0]) return val(a.code, "int") }
if streq(name, "is_windowed") { if g_windowed { return val("1", "bool") } return val("0", "bool") }
if streq(name, "game_title") { return val("@.gametitle", "str") }
# windowing hooks — declared external; only reached on the is_windowed() branch
if streq(name, "win_poll") { return val(emit_bind("call i32 @win_poll()"), "int") }
if streq(name, "win_running") { return val(emit_bind("call i32 @win_running()"), "bool") }
if streq(name, "win_open") {
let a = arg_code(e, 0) let b = arg_code(e, 1) let c = arg_code(e, 2) let d = arg_code(e, 3)
emit(" call void @win_open(i32 ") emit(a) emit(", i32 ") emit(b) emit(", i32 ") emit(c) emit(", ptr ") emit(d) emit(")\n")
return val("0", "void")
}
if streq(name, "win_present") {
let a = arg_code(e, 0) let b = arg_code(e, 1) let c = arg_code(e, 2)
emit(" call void @win_present(ptr ") emit(a) emit(", i32 ") emit(b) emit(", i32 ") emit(c) emit(")\n")
return val("0", "void")
}
if streq(name, "win_close") { emit(" call void @win_close()\n") return val("0", "void") }
return val("0", "void")
}

View file

@ -0,0 +1,39 @@
# emit_machine.ludic — `machine <reg> { state Name = v { .. } }` dispatches on a
# register's value; `become Name` stores the target state's value back. Both are
# reg()/setreg() calls, resolved to the runtime like any other builtin.
fn emit_machine(st: Node) -> void {
let regv = emit_expr(st.a)
let s = emit_bind(sconcat("call i32 @fn_rt_reg(i32 ", sconcat(regv.code, ")")))
if nmach < len(mach_stk) { mach_stk[nmach] = st } else { push(mach_stk, st) }
nmach = nmach + 1
let endl = lbl("smend")
let i = 0
while i < len(st.kids) {
let state = st.kids[i]
let v = emit_expr(state.b)
let c = emit_bind(sconcat("icmp eq i32 ", sconcat(s, sconcat(", ", v.code))))
let body = lbl("sbody") let nxt = lbl("sarm")
emit(" br i1 ") emit(c) emit(", label %") emit(body) emit(", label %") emit(nxt) emit("\n")
emit(body) emit(":\n") g_term = false
emit_block(state.a)
if not g_term { emit(" br label %") emit(endl) emit("\n") }
emit(nxt) emit(":\n") g_term = false
i = i + 1
}
if not g_term { emit(" br label %") emit(endl) emit("\n") }
emit(endl) emit(":\n") g_term = false
nmach = nmach - 1
}
fn emit_become(st: Node) -> void {
if nmach == 0 { perr("'become' outside a machine") }
let m = mach_stk[nmach - 1]
let target: Node = ptr_null()
let i = 0
while i < len(m.kids) { if streq(m.kids[i].s, st.s) { target = m.kids[i] } i = i + 1 }
if ptr_is_null(target) { perr(sconcat("become: no state ", st.s)) }
let regv = emit_expr(m.a)
let sv = emit_expr(target.b)
emit(" call void @fn_rt_setreg(i32 ") emit(regv.code) emit(", i32 ") emit(sv.code) emit(")\n")
}

29
selfhost/emit_math.ludic Normal file
View file

@ -0,0 +1,29 @@
# emit_math.ludic — the math builtins that lower to inline IR rather than a
# runtime call: min, max, abs, clamp. Everything else named like a builtin
# (clear, fill_rect, reg, ...) is resolved to its rt_ function by emit_call.
fn is_math_builtin(name: ptr) -> bool {
return streq(name,"min") or streq(name,"max") or streq(name,"abs") or streq(name,"clamp")
}
fn emit_math_builtin(name: ptr, e: Node) -> Val {
if streq(name, "abs") {
let a = emit_expr(e.kids[0])
let c = emit_bind(sconcat("icmp slt i32 ", sconcat(a.code, ", 0")))
let n = emit_bind(sconcat("sub i32 0, ", a.code))
return val(emit_bind(sconcat("select i1 ", sconcat(c, sconcat(", i32 ", sconcat(n, sconcat(", i32 ", a.code)))))), "int")
}
if streq(name, "min") or streq(name, "max") {
let a = emit_expr(e.kids[0]) let b = emit_expr(e.kids[1])
let op = "slt"
if streq(name, "max") { op = "sgt" }
let c = emit_bind(sconcat("icmp ", sconcat(op, sconcat(" i32 ", sconcat(a.code, sconcat(", ", b.code))))))
return val(emit_bind(sconcat("select i1 ", sconcat(c, sconcat(", i32 ", sconcat(a.code, sconcat(", i32 ", b.code)))))), "int")
}
# clamp(v, lo, hi) = max(lo, min(v, hi))
let v = emit_expr(e.kids[0]) let lo = emit_expr(e.kids[1]) let hi = emit_expr(e.kids[2])
let c1 = emit_bind(sconcat("icmp slt i32 ", sconcat(v.code, sconcat(", ", hi.code))))
let t = emit_bind(sconcat("select i1 ", sconcat(c1, sconcat(", i32 ", sconcat(v.code, sconcat(", i32 ", hi.code))))))
let c2 = emit_bind(sconcat("icmp sgt i32 ", sconcat(lo.code, sconcat(", ", t))))
return val(emit_bind(sconcat("select i1 ", sconcat(c2, sconcat(", i32 ", sconcat(lo.code, sconcat(", i32 ", t)))))), "int")
}

88
selfhost/emit_new.ludic Normal file
View file

@ -0,0 +1,88 @@
# emit_new.ludic — heap construction and slice operations: `new S`, `new []T`,
# push(slice, v) and len(slice). Slices are a { data, len, cap } header the
# holder points at, so growth is visible to every holder.
fn emit_sizeof(llt: ptr) -> ptr {
let p = emit_bind(sconcat("getelementptr ", sconcat(llt, ", ptr null, i32 1")))
return emit_bind(sconcat("ptrtoint ptr ", sconcat(p, " to i64")))
}
fn emit_new_struct(name: ptr) -> Val {
let s = find_struct(name)
if ptr_is_null(s) { perr("unknown struct in new") }
let sz = emit_sizeof(sconcat("%Str_", name))
let obj = emit_bind(sconcat("call ptr @malloc(i64 ", sconcat(sz, ")")))
let f = 0
while f < len(s.kids) {
let fd = s.kids[f]
let addr = nreg()
emit(" ") emit(addr) emit(" = getelementptr inbounds %Str_") emit(name)
emit(", ptr ") emit(obj) emit(", i32 0, i32 ") emit(itoa(f)) emit("\n")
let lt = llty(fd.ty)
let v = "0"
if streq(lt, "ptr") { v = "null" }
if not ptr_is_null(fd.a) { let dv = emit_expr(fd.a) v = dv.code }
emit(" store ") emit(lt) emit(" ") emit(v) emit(", ptr ") emit(addr) emit("\n")
f = f + 1
}
return val(obj, name)
}
fn emit_new_slice(ty: ptr) -> Val {
let sz = emit_sizeof("%LSlice")
let h = emit_bind(sconcat("call ptr @malloc(i64 ", sconcat(sz, ")")))
let d0 = nreg() emit(" ") emit(d0) emit(" = getelementptr inbounds %LSlice, ptr ") emit(h) emit(", i32 0, i32 0\n")
emit(" store ptr null, ptr ") emit(d0) emit("\n")
let d1 = nreg() emit(" ") emit(d1) emit(" = getelementptr inbounds %LSlice, ptr ") emit(h) emit(", i32 0, i32 1\n")
emit(" store i32 0, ptr ") emit(d1) emit("\n")
let d2 = nreg() emit(" ") emit(d2) emit(" = getelementptr inbounds %LSlice, ptr ") emit(h) emit(", i32 0, i32 2\n")
emit(" store i32 0, ptr ") emit(d2) emit("\n")
return val(h, ty)
}
fn slice_field(h: ptr, i: int) -> ptr {
let r = nreg()
emit(" ") emit(r) emit(" = getelementptr inbounds %LSlice, ptr ") emit(h)
emit(", i32 0, i32 ") emit(itoa(i)) emit("\n")
return r
}
fn emit_len(e: Node) -> Val {
let s = emit_expr(e.kids[0])
let lp = slice_field(s.code, 1)
return val(emit_bind(sconcat("load i32, ptr ", lp)), "int")
}
fn emit_push(e: Node) -> Val {
let s = emit_expr(e.kids[0])
let el = slice_elem(s.ty)
let elt = llty(el)
let h = s.code
let lp = slice_field(h, 1) let cp = slice_field(h, 2) let dp = slice_field(h, 0)
let l = emit_bind(sconcat("load i32, ptr ", lp))
let c = emit_bind(sconcat("load i32, ptr ", cp))
let full = emit_bind(sconcat("icmp sge i32 ", sconcat(l, sconcat(", ", c))))
let grow = lbl("grow") let put = lbl("put")
emit(" br i1 ") emit(full) emit(", label %") emit(grow) emit(", label %") emit(put) emit("\n")
emit(grow) emit(":\n")
let dbl = emit_bind(sconcat("mul i32 ", sconcat(c, ", 2")))
let isz = emit_bind(sconcat("icmp eq i32 ", sconcat(c, ", 0")))
let nc = emit_bind(sconcat("select i1 ", sconcat(isz, sconcat(", i32 8, i32 ", dbl))))
let esz = emit_sizeof(elt)
let ncw = emit_bind(sconcat("zext i32 ", sconcat(nc, " to i64")))
let bytes = emit_bind(sconcat("mul i64 ", sconcat(ncw, sconcat(", ", esz))))
let old = emit_bind(sconcat("load ptr, ptr ", dp))
let nd = emit_bind(sconcat("call ptr @realloc(ptr ", sconcat(old, sconcat(", i64 ", sconcat(bytes, ")")))))
emit(" store ptr ") emit(nd) emit(", ptr ") emit(dp) emit("\n")
emit(" store i32 ") emit(nc) emit(", ptr ") emit(cp) emit("\n")
emit(" br label %") emit(put) emit("\n")
emit(put) emit(":\n")
let v = emit_expr(e.kids[1])
let data = emit_bind(sconcat("load ptr, ptr ", dp))
let slot = nreg()
emit(" ") emit(slot) emit(" = getelementptr inbounds ") emit(elt) emit(", ptr ") emit(data) emit(", i32 ") emit(l) emit("\n")
emit(" store ") emit(elt) emit(" ") emit(v.code) emit(", ptr ") emit(slot) emit("\n")
let l1 = emit_bind(sconcat("add i32 ", sconcat(l, ", 1")))
emit(" store i32 ") emit(l1) emit(", ptr ") emit(lp) emit("\n")
return val("0", "void")
}

98
selfhost/emit_query.ludic Normal file
View file

@ -0,0 +1,98 @@
# emit_query.ludic — the ECS query loop. Iterates live entities, filters by the
# components/archetypes each carries, binds the requested components, and runs
# the body once per match. Mirrors ll_query in compiler/back/ir_ecs.c.
fn find_arch_id(name: ptr) -> int {
let i = 0 let n = 1
while i < len(prog) { if prog[i].kind == N_ARCH { if streq(prog[i].s, name) { return n } n = n + 1 } i = i + 1 }
return 0
}
fn emit_query(st: Node) -> void {
let me = itoa(MAX_ENT)
let ip = emit_alloca("i32")
store_at("i32", "0", ip)
if nself < len(self_stk) { self_stk[nself] = ip } else { push(self_stk, ip) }
nself = nself + 1
let cond = lbl("qcond") let body = lbl("qbody") let nxt = lbl("qnext") let endl = lbl("qend")
emit(" br label %") emit(cond) emit("\n")
emit(cond) emit(":\n")
let i0 = emit_bind(sconcat("load i32, ptr ", ip))
let ec = emit_bind("load i32, ptr @L_entc")
let lt = emit_bind(sconcat("icmp slt i32 ", sconcat(i0, sconcat(", ", ec))))
emit(" br i1 ") emit(lt) emit(", label %") emit(body) emit(", label %") emit(endl) emit("\n")
emit(body) emit(":\n") g_term = false
let i1 = emit_bind(sconcat("load i32, ptr ", ip))
# alive?
let ap = nreg() emit(" ") emit(ap) emit(" = getelementptr inbounds [") emit(me) emit(" x i32], ptr @L_alive, i32 0, i32 ") emit(i1) emit("\n")
let al = emit_bind(sconcat("load i32, ptr ", ap))
let alc = emit_bind(sconcat("icmp ne i32 ", sconcat(al, ", 0")))
let ka = lbl("qa")
emit(" br i1 ") emit(alc) emit(", label %") emit(ka) emit(", label %") emit(nxt) emit("\n")
emit(ka) emit(":\n")
let terms = st.c
# component / archetype filters
let t = 0
while t < len(terms.kids) {
let tm = terms.kids[t]
let ak = find_arch_id(tm.s)
let ok = "0"
if ak > 0 {
let kp = nreg() emit(" ") emit(kp) emit(" = getelementptr inbounds [") emit(me) emit(" x i32], ptr @L_kind, i32 0, i32 ") emit(i1) emit("\n")
let kv = emit_bind(sconcat("load i32, ptr ", kp))
ok = emit_bind(sconcat("icmp eq i32 ", sconcat(kv, sconcat(", ", itoa(ak)))))
} else {
let hp = nreg() emit(" ") emit(hp) emit(" = getelementptr inbounds [") emit(me) emit(" x i8], ptr @H_") emit(tm.s) emit(", i32 0, i32 ") emit(i1) emit("\n")
let hv = emit_bind(sconcat("load i8, ptr ", hp))
ok = emit_bind(sconcat("icmp ne i8 ", sconcat(hv, ", 0")))
}
let keep = lbl("qt")
emit(" br i1 ") emit(ok) emit(", label %") emit(keep) emit(", label %") emit(nxt) emit("\n")
emit(keep) emit(":\n")
t = t + 1
}
# bind the requested components to the loop variables, in order
let save = nloc
let vi = 0
t = 0
while t < len(terms.kids) {
let tm = terms.kids[t]
if tm.ival == 0 and find_arch_id(tm.s) == 0 {
if vi < len(st.kids) {
let slot = nreg()
emit(" ") emit(slot) emit(" = getelementptr inbounds [") emit(me) emit(" x %Cmp_") emit(tm.s) emit("], ptr @S_") emit(tm.s) emit(", i32 0, i32 ") emit(i1) emit("\n")
let vslot = emit_alloca("ptr")
emit(" store ptr ") emit(slot) emit(", ptr ") emit(vslot) emit("\n")
loc_push(st.kids[vi].s, vslot, tm.s)
vi = vi + 1
}
}
t = t + 1
}
# optional where-clause
if not ptr_is_null(st.b) {
let w = emit_expr(st.b)
let wc = emit_bind(sconcat("icmp ne i32 ", sconcat(w.code, ", 0")))
let kw = lbl("qw")
emit(" br i1 ") emit(wc) emit(", label %") emit(kw) emit(", label %") emit(nxt) emit("\n")
emit(kw) emit(":\n")
}
loop_push(nxt, endl)
emit_block(st.a)
loop_pop()
nloc = save
if not g_term { emit(" br label %") emit(nxt) emit("\n") }
emit(nxt) emit(":\n")
let i2 = emit_bind(sconcat("load i32, ptr ", ip))
let i3 = emit_bind(sconcat("add i32 ", sconcat(i2, ", 1")))
store_at("i32", i3, ip)
emit(" br label %") emit(cond) emit("\n")
emit(endl) emit(":\n") g_term = false
nself = nself - 1
}

61
selfhost/emit_save.ludic Normal file
View file

@ -0,0 +1,61 @@
# emit_save.ludic — save() / load() snapshot of the whole ECS world. The
# compiler writes the entity/component half itself (it knows the shape) and
# hands the open file to rt_save_state/rt_load_state for the runtime's own
# state. Mirrors compiler/back/ir_save.c. save()->@L_save, load()->@L_load.
var g_iok: int = 0
fn emit_io(fn2: ptr, p: ptr, bytes: ptr) -> void {
let r = sconcat("%io", itoa(g_iok)) g_iok = g_iok + 1
emit(" ") emit(r) emit(" = call i64 @") emit(fn2) emit("(ptr ") emit(p) emit(", i64 1, i64 ") emit(bytes) emit(", ptr %f)\n")
}
fn emit_snapshot_blocks(fn2: ptr) -> void {
g_iok = 0
let me = itoa(MAX_ENT)
emit_io(fn2, "@L_entc", "4")
emit_io(fn2, "@L_freen", "4")
emit(" %nalive = mul i64 ") emit(me) emit(", 4\n")
emit_io(fn2, "@L_alive", "%nalive")
emit_io(fn2, "@L_freelist", "%nalive")
emit_io(fn2, "@L_kind", "%nalive")
let i = 0
while i < len(prog) { if prog[i].kind == N_VAR { emit_io(fn2, sconcat("@g_", prog[i].s), "4") } i = i + 1 }
let ci = 0
i = 0
while i < len(prog) {
if prog[i].kind == N_COMP {
let c = prog[i].s
let csz = sconcat("%csz", itoa(ci))
emit(" ") emit(csz) emit(" = ptrtoint ptr getelementptr (%Cmp_") emit(c) emit(", ptr null, i32 ") emit(me) emit(") to i64\n")
emit_io(fn2, sconcat("@S_", c), csz)
emit_io(fn2, sconcat("@H_", c), me)
ci = ci + 1
}
i = i + 1
}
}
fn emit_snapshot() -> void {
emith("@.sav_path = private unnamed_addr constant [10 x i8] c\"ludic.sav\\00\"\n")
emith("@.sav_wb = private unnamed_addr constant [3 x i8] c\"wb\\00\"\n")
emith("@.sav_rb = private unnamed_addr constant [3 x i8] c\"rb\\00\"\n")
let has_save = not ptr_is_null(find_fn("rt_save_state"))
let has_load = not ptr_is_null(find_fn("rt_load_state"))
emit("define void @L_save() {\nentry:\n")
emit(" %f = call ptr @fopen(ptr @.sav_path, ptr @.sav_wb)\n")
emit(" %bad = icmp eq ptr %f, null\n")
emit(" br i1 %bad, label %out, label %go\ngo:\n")
emit_snapshot_blocks("fwrite")
if has_save { emit(" call void @fn_rt_save_state(ptr %f)\n") }
emit(" %c = call i32 @fclose(ptr %f)\n br label %out\nout:\n ret void\n}\n\n")
emit("define i32 @L_load() {\nentry:\n")
emit(" %f = call ptr @fopen(ptr @.sav_path, ptr @.sav_rb)\n")
emit(" %bad = icmp eq ptr %f, null\n")
emit(" br i1 %bad, label %miss, label %go\nmiss:\n ret i32 0\ngo:\n")
emit_snapshot_blocks("fread")
if has_load { emit(" call void @fn_rt_load_state(ptr %f)\n") }
emit(" %c = call i32 @fclose(ptr %f)\n ret i32 1\n}\n\n")
}

68
selfhost/emit_spawn.ludic Normal file
View file

@ -0,0 +1,68 @@
# emit_spawn.ludic — spawn / despawn / self(), and seeding a component's fields
# from its declared defaults plus any per-spawn overrides.
fn emit_init_component(e: ptr, comp: ptr, rec: Node) -> void {
let me = itoa(MAX_ENT)
let c = find_comp(comp)
if ptr_is_null(c) { perr(sconcat("spawn: unknown component ", comp)) }
let hp = nreg() emit(" ") emit(hp) emit(" = getelementptr inbounds [") emit(me) emit(" x i8], ptr @H_") emit(comp) emit(", i32 0, i32 ") emit(e) emit("\n")
emit(" store i8 1, ptr ") emit(hp) emit("\n")
let slot = nreg()
emit(" ") emit(slot) emit(" = getelementptr inbounds [") emit(me) emit(" x %Cmp_") emit(comp) emit("], ptr @S_") emit(comp) emit(", i32 0, i32 ") emit(e) emit("\n")
# defaults
let f = 0
while f < len(c.kids) {
let fd = c.kids[f]
let addr = nreg()
emit(" ") emit(addr) emit(" = getelementptr inbounds %Cmp_") emit(comp) emit(", ptr ") emit(slot) emit(", i32 0, i32 ") emit(itoa(f)) emit("\n")
let lt = llty(fd.ty)
let v = "0"
if streq(lt, "ptr") { v = "null" }
if not ptr_is_null(fd.a) { let dv = emit_expr(fd.a) v = dv.code }
emit(" store ") emit(lt) emit(" ") emit(v) emit(", ptr ") emit(addr) emit("\n")
f = f + 1
}
# per-spawn overrides
if not ptr_is_null(rec) {
let j = 0
while j < len(rec.kids) {
let fi = rec.kids[j]
let fidx = field_index(c, fi.s)
if fidx >= 0 {
let addr = nreg()
emit(" ") emit(addr) emit(" = getelementptr inbounds %Cmp_") emit(comp) emit(", ptr ") emit(slot) emit(", i32 0, i32 ") emit(itoa(fidx)) emit("\n")
let dv = emit_expr(fi.a)
emit(" store ") emit(llty(field_type(c, fi.s))) emit(" ") emit(dv.code) emit(", ptr ") emit(addr) emit("\n")
}
j = j + 1
}
}
}
fn emit_spawn(st: Node) -> void {
let e = emit_bind("call i32 @L_alloc()")
let ak = find_arch_id(st.s)
if ak > 0 {
let me = itoa(MAX_ENT)
let kp = nreg() emit(" ") emit(kp) emit(" = getelementptr inbounds [") emit(me) emit(" x i32], ptr @L_kind, i32 0, i32 ") emit(e) emit("\n")
emit(" store i32 ") emit(itoa(ak)) emit(", ptr ") emit(kp) emit("\n")
let arch = find_arch(st.s)
let c = 0
while c < len(arch.kids) {
let cn = arch.kids[c].s
let rec = ptr_null()
let i = 0
while i < len(st.kids) { if streq(st.kids[i].s, cn) { rec = st.kids[i].a } i = i + 1 }
emit_init_component(e, cn, rec)
c = c + 1
}
} else {
let i = 0
while i < len(st.kids) { emit_init_component(e, st.kids[i].s, st.kids[i].a) i = i + 1 }
}
}
fn emit_despawn(st: Node) -> void {
let v = emit_expr(st.a)
emit(" call void @L_free_entity(i32 ") emit(v.code) emit(")\n")
}

193
selfhost/emit_stmt.ludic Normal file
View file

@ -0,0 +1,193 @@
# emit_stmt.ludic — lower statements. Terminators set g_term so the rest of a
# block is skipped until a new basic block opens.
fn emit_block(b: Node) -> void {
let i = 0
while i < len(b.kids) {
if g_term { return }
emit_stmt(b.kids[i])
i = i + 1
}
}
# store `val` (llvm type `lt`) into address `addr`
fn store_at(lt: ptr, v: ptr, addr: ptr) -> void {
emit(" store ") emit(lt) emit(" ") emit(v) emit(", ptr ") emit(addr) emit("\n")
}
fn emit_assign(st: Node) -> void {
# resolve the target's address and type
let t = st.a
let addr = "0"
let ty = "int"
if t.kind == E_ID {
let li = loc_find(t.s)
if li >= 0 { addr = loc_reg[li] ty = loc_ty[li] }
else {
let g = find_global(t.s)
if ptr_is_null(g) { perr(sconcat("assign to unknown ", t.s)) }
addr = sconcat("@g_", t.s) ty = g.ty
}
} else {
if t.kind == E_MEMBER { addr = emit_member_addr(t) ty = g_addr_ty }
else { if t.kind == E_INDEX { addr = emit_index_addr(t) ty = g_addr_ty }
else { perr("bad assignment target") } }
}
let lt = llty(ty)
let rv = emit_expr(st.b)
let v = rv.code
if not streq(st.s, "=") {
let cur = emit_bind(sconcat("load ", sconcat(lt, sconcat(", ptr ", addr))))
let opc = "add"
if streq(st.s, "-=") { opc = "sub" }
if streq(st.s, "*=") { opc = "mul" }
if streq(st.s, "/=") { opc = "sdiv" }
v = emit_bind(sconcat(opc, sconcat(" i32 ", sconcat(cur, sconcat(", ", v)))))
}
store_at(lt, v, addr)
}
fn emit_if(st: Node) -> void {
let c = emit_expr(st.a)
let cc = emit_bind(sconcat("icmp ne i32 ", sconcat(c.code, ", 0")))
let has_else = not ptr_is_null(st.c)
let tl = lbl("then") let el = lbl("else") let en = lbl("ifend")
if has_else { emit(" br i1 ") emit(cc) emit(", label %") emit(tl) emit(", label %") emit(el) emit("\n") }
else { emit(" br i1 ") emit(cc) emit(", label %") emit(tl) emit(", label %") emit(en) emit("\n") }
emit(tl) emit(":\n") g_term = false
emit_block(st.b)
if not g_term { emit(" br label %") emit(en) emit("\n") }
if has_else {
emit(el) emit(":\n") g_term = false
# else is either a block or a nested if-statement
if st.c.kind == S_IF { emit_stmt(st.c) } else { emit_block(st.c) }
if not g_term { emit(" br label %") emit(en) emit("\n") }
}
emit(en) emit(":\n") g_term = false
}
fn emit_while(st: Node) -> void {
let cl = lbl("wcond") let bl = lbl("wbody") let en = lbl("wend")
emit(" br label %") emit(cl) emit("\n")
emit(cl) emit(":\n")
let c = emit_expr(st.a)
let cc = emit_bind(sconcat("icmp ne i32 ", sconcat(c.code, ", 0")))
emit(" br i1 ") emit(cc) emit(", label %") emit(bl) emit(", label %") emit(en) emit("\n")
emit(bl) emit(":\n") g_term = false
loop_push(cl, en)
emit_block(st.b)
loop_pop()
if not g_term { emit(" br label %") emit(cl) emit("\n") }
emit(en) emit(":\n") g_term = false
}
fn emit_for(st: Node) -> void {
let slot = emit_alloca("i32")
let lo = emit_expr(st.a)
store_at("i32", lo.code, slot)
loc_push(st.s, slot, "int")
let cl = lbl("fcond") let bl = lbl("fbody") let ct = lbl("fcont") let en = lbl("fend")
emit(" br label %") emit(cl) emit("\n")
emit(cl) emit(":\n")
let iv = emit_bind(sconcat("load i32, ptr ", slot))
let hi = emit_expr(st.b)
let cc = emit_bind(sconcat("icmp slt i32 ", sconcat(iv, sconcat(", ", hi.code))))
emit(" br i1 ") emit(cc) emit(", label %") emit(bl) emit(", label %") emit(en) emit("\n")
emit(bl) emit(":\n") g_term = false
loop_push(ct, en)
emit_block(st.c)
loop_pop()
if not g_term { emit(" br label %") emit(ct) emit("\n") }
emit(ct) emit(":\n")
let i2 = emit_bind(sconcat("load i32, ptr ", slot))
let i3 = emit_bind(sconcat("add i32 ", sconcat(i2, ", 1")))
store_at("i32", i3, slot)
emit(" br label %") emit(cl) emit("\n")
emit(en) emit(":\n") g_term = false
}
fn emit_return(st: Node) -> void {
if not ptr_is_null(st.a) {
let v = emit_expr(st.a)
store_at(llty(ret_ty), v.code, "%retval")
}
emit(" br label %ret\n")
g_term = true
}
fn arm_is_default(arm: Node) -> bool {
let p = 0
while p < len(arm.kids) { if arm.kids[p].kind == E_ID and streq(arm.kids[p].s, "_") { return true } p = p + 1 }
return false
}
fn emit_match(st: Node) -> void {
let sv = emit_expr(st.a)
let endl = lbl("mend")
let deflt: Node = ptr_null()
let i = 0
while i < len(st.kids) {
let arm = st.kids[i]
if arm_is_default(arm) { deflt = arm }
else {
let acc = "0"
let first = true
let p = 0
while p < len(arm.kids) {
let pv = emit_expr(arm.kids[p])
let c = emit_bind(sconcat("icmp eq i32 ", sconcat(sv.code, sconcat(", ", pv.code))))
if first { acc = c first = false }
else { acc = emit_bind(sconcat("or i1 ", sconcat(acc, sconcat(", ", c)))) }
p = p + 1
}
let bodyl = lbl("mbody") let nextl = lbl("marm")
emit(" br i1 ") emit(acc) emit(", label %") emit(bodyl) emit(", label %") emit(nextl) emit("\n")
emit(bodyl) emit(":\n") g_term = false
emit_block(arm.a)
if not g_term { emit(" br label %") emit(endl) emit("\n") }
emit(nextl) emit(":\n") g_term = false
}
i = i + 1
}
if not ptr_is_null(deflt) { emit_block(deflt.a) }
if not g_term { emit(" br label %") emit(endl) emit("\n") }
emit(endl) emit(":\n") g_term = false
}
fn emit_stmt(st: Node) -> void {
if st.kind == S_LET {
let ty = st.ty
if ptr_is_null(ty) { let v0 = emit_expr(st.a) ty = v0.ty
let slot = emit_alloca(llty(ty)) store_at(llty(ty), v0.code, slot) loc_push(st.s, slot, ty) return }
let slot = emit_alloca(llty(ty))
if ptr_is_null(st.a) {
let z = "0"
if streq(llty(ty), "ptr") { z = "null" }
store_at(llty(ty), z, slot)
} else { let v = emit_expr(st.a) store_at(llty(ty), v.code, slot) }
loc_push(st.s, slot, ty)
return
}
if st.kind == S_ASSIGN { emit_assign(st) return }
if st.kind == S_IF { emit_if(st) return }
if st.kind == S_WHILE { emit_while(st) return }
if st.kind == S_FOR { emit_for(st) return }
if st.kind == S_RETURN { emit_return(st) return }
if st.kind == S_BREAK { emit(" br label %") emit(brk_lbl[nloop - 1]) emit("\n") g_term = true return }
if st.kind == S_CONTINUE { emit(" br label %") emit(cnt_lbl[nloop - 1]) emit("\n") g_term = true return }
if st.kind == S_MATCH { emit_match(st) return }
if st.kind == S_QUERY { emit_query(st) return }
if st.kind == S_SPAWN { emit_spawn(st) return }
if st.kind == S_DESPAWN { emit_despawn(st) return }
if st.kind == S_MACHINE { emit_machine(st) return }
if st.kind == S_BECOME { emit_become(st) return }
if st.kind == S_EXPR { let v = emit_expr(st.a) return }
perr("cannot emit statement")
}
fn loop_push(cont: ptr, brk: ptr) -> void {
if nloop < len(cnt_lbl) { cnt_lbl[nloop] = cont brk_lbl[nloop] = brk }
else { push(cnt_lbl, cont) push(brk_lbl, brk) }
nloop = nloop + 1
}
fn loop_pop() -> void { nloop = nloop - 1 }

125
selfhost/emit_ui.ludic Normal file
View file

@ -0,0 +1,125 @@
# emit_ui.ludic — a `ui` block is data: the compiler flattens the widget tree
# and emits @ui_build(), which hands every property to the Ludic UI runtime via
# rt_ui_set(index, key, value). Mirrors compiler/back/ir_ui.c. Layout/drawing/
# focus all live in runtime/native/ui.ludic.
var uiw: []Node # flattened widgets, pre-order
var uiw_parent: []int
var ui_roots: []int # first-widget index of each ui block
fn ui_wtype(w: Node) -> int {
let s = w.s
if streq(s, "panel") { return 0 } if streq(s, "col") { return 1 } if streq(s, "row") { return 2 }
if streq(s, "label") { return 3 } if streq(s, "button") { return 4 }
if streq(s, "image") { return 5 } if streq(s, "spacer") { return 6 }
return 0
}
fn ui_prop(w: Node, key: ptr) -> Node {
let i = 0
while i < len(w.b.kids) { if streq(w.b.kids[i].s, key) { return w.b.kids[i].a } i = i + 1 }
return ptr_null()
}
fn ui_flatten(w: Node, parent: int) -> void {
let idx = len(uiw)
push(uiw, w) push(uiw_parent, parent)
let i = 0
while i < len(w.kids) { ui_flatten(w.kids[i], idx) i = i + 1 }
}
fn ui_flatten_all() -> void {
uiw = new []Node uiw_parent = new []int ui_roots = new []int
let i = 0
while i < len(prog) {
if prog[i].kind == N_UI and prog[i].ival == 1 { push(ui_roots, len(uiw)) ui_flatten(prog[i].a, 0 - 1) }
i = i + 1
}
}
fn has_ui() -> bool {
let i = 0
while i < len(prog) { if prog[i].kind == N_UI and prog[i].ival == 1 { return true } i = i + 1 }
return false
}
# index of a UI_<name>: a ui block's root, or a widget's id=
fn ui_index_of(nm: ptr) -> int {
let s = substr(nm, 3, slen(nm) - 3) # strip "UI_"
let u = 0 let bi = 0
let i = 0
while i < len(prog) {
if prog[i].kind == N_UI and prog[i].ival == 1 { if streq(prog[i].s, s) { return ui_roots[bi] } bi = bi + 1 }
i = i + 1
}
i = 0
while i < len(uiw) {
let idp = ui_prop(uiw[i], "id")
if not ptr_is_null(idp) { if idp.kind == E_ID and streq(idp.s, s) { return i } }
i = i + 1
}
return 0
}
fn is_ui_ident(nm: ptr) -> bool {
return slen(nm) > 3 and peek8(nm, 0) == 85 and peek8(nm, 1) == 73 and peek8(nm, 2) == 95 # "UI_"
}
fn ll_ui_set(idx: int, key: int, val: ptr) -> void {
emit(" call void @fn_rt_ui_set(i32 ") emit(itoa(idx)) emit(", i32 ") emit(itoa(key)) emit(", i32 ") emit(val) emit(")\n")
}
fn ui_prop_key(k: ptr) -> int {
if streq(k,"w") { return 2 } if streq(k,"h") { return 3 } if streq(k,"x") { return 4 } if streq(k,"y") { return 5 }
if streq(k,"pad") { return 7 } if streq(k,"gap") { return 8 } if streq(k,"bg") { return 9 } if streq(k,"fg") { return 10 }
if streq(k,"border") { return 11 } if streq(k,"grow") { return 13 } if streq(k,"font") { return 14 } if streq(k,"size") { return 15 }
if streq(k,"inset") { return 16 } if streq(k,"focus") { return 17 }
return 0 - 1
}
fn emit_ui_build() -> void {
ui_flatten_all()
ll_t = 0 ll_lbl = 0 g_term = false loc_reset() nloop = 0
let fbody = buf_new()
falloc = buf_new()
let saved = code
code = fbody
emit(" call void @fn_rt_ui_reset(i32 ") emit(itoa(len(uiw))) emit(")\n")
let i = 0
while i < len(uiw) {
let w = uiw[i]
let t = ui_wtype(w)
ll_ui_set(i, 0, itoa(t))
ll_ui_set(i, 1, itoa(uiw_parent[i]))
if t == 4 { ll_ui_set(i, 17, "1") }
let p = 0
while p < len(w.b.kids) {
let pr = w.b.kids[p]
let k = pr.s
let v = pr.a
if streq(k, "id") { p = p + 1 continue }
if streq(k, "text") {
let sv = emit_expr(v)
emit(" call void @fn_rt_ui_static_text(i32 ") emit(itoa(i)) emit(", ptr ") emit(sv.code) emit(")\n")
} else { if streq(k, "skin") or streq(k, "image") {
let sv = emit_expr(v)
let r = emit_bind(sconcat("call i32 @fn_rt_image_load(ptr ", sconcat(sv.code, ")")))
if streq(k, "skin") { ll_ui_set(i, 20, r) } else { ll_ui_set(i, 19, r) }
} else { if streq(k, "align") {
if v.kind == E_ID {
let a = 0
if streq(v.s, "center") { a = 1 } if streq(v.s, "end") { a = 2 }
ll_ui_set(i, 12, itoa(a))
} else { let vv = emit_expr(v) ll_ui_set(i, 12, vv.code) }
} else {
let key = ui_prop_key(k)
if key >= 0 {
let vv = emit_expr(v)
ll_ui_set(i, key, vv.code)
if key == 4 or key == 5 { ll_ui_set(i, 6, "1") }
}
} } }
p = p + 1
}
i = i + 1
}
code = saved
emit("define void @ui_build() {\nentry:\n")
emit(buf_str(falloc))
emit(buf_str(fbody))
emit(" ret void\n}\n\n")
}

16
selfhost/game-build.sh Executable file
View file

@ -0,0 +1,16 @@
#!/bin/bash
# game-build.sh — compile a Ludic GAME with the self-hosted compiler. The
# compiler resolves the game's own `import`s and auto-splices the Ludic runtime
# (runtime/native/core.ludic + its imports), exactly as the C compiler does.
# Headless only for now (renders to out.ppm). Run from the repo root.
# ./selfhost/game-build.sh <selfhost-bin> <game.ludic> <out-binary>
set -u
cd "$(dirname "$0")/.."
SH="$1"; GAME="$2"; OUT="$3"
CC="${LUDIC_CC:-clang}"
ERR="$(mktemp)"
"$SH" "$GAME" > "$OUT.ll" 2>"$ERR" || { echo "self-host failed:"; head -5 "$ERR"; rm -f "$ERR"; exit 1; }
# -O2 removes the dead windowing branches (is_windowed()==0) so win_* need no defs
$CC -O2 "$OUT.ll" -o "$OUT" 2>"$ERR" || { echo "link failed:"; grep -i error "$ERR" | head -5; rm -f "$ERR"; exit 1; }
rm -f "$ERR" "$OUT.ll" # keep only the binary
echo "built $OUT"

File diff suppressed because one or more lines are too long

4
selfhost/golden/menu.ppm Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

22
selfhost/io.ludic Normal file
View file

@ -0,0 +1,22 @@
# io.ludic — reading the input file and writing the output, plus tiny stdio.
# The compiler reads one .ludic file whole and emits LLVM IR text to stdout.
fn read_file(path: str) -> ptr {
let f = file_open(path, "rb")
if ptr_is_null(f) { return ptr_null() }
file_seek(f, 0, 2)
let n = file_tell(f)
file_seek(f, 0, 0)
let buf = mem_alloc(n + 1)
file_read(f, buf, n)
poke8(buf, n, 0)
file_close(f)
return buf
}
# length of a NUL-terminated buffer
fn cstr_len(s: ptr) -> int {
let n = 0
while peek8(s, n) != 0 { n = n + 1 }
return n
}

138
selfhost/lex.ludic Normal file
View file

@ -0,0 +1,138 @@
# lex.ludic — source text -> a token slice. Mirrors compiler/front/lex.c.
# Tokens carry their kind, their text (identifiers, strings, operators),
# an integer value (numbers, char literals) and a line for diagnostics.
const TK_ID: int = 0
const TK_INT: int = 1
const TK_STR: int = 2
const TK_OP: int = 3
const TK_NL: int = 4
const TK_EOF: int = 5
const TK_FLOAT: int = 6
struct Tok { kind: int = 0, text: ptr = ptr_null(), ival: int = 0, line: int = 0 }
var toks: []Tok
fn tok_push(kind: int, text: ptr, ival: int, line: int) -> void {
let t = new Tok
t.kind = kind t.text = text t.ival = ival t.line = line
push(toks, t)
}
# does src match the 2-char operator op at position i?
fn two_at(src: ptr, i: int, a: int, b: int) -> bool {
return peek8(src, i) == a and peek8(src, i + 1) == b
}
fn is_op1(c: int) -> bool {
# + - * / % < > = ( ) { } [ ] , : . ! @
if c == 43 or c == 45 or c == 42 or c == 47 or c == 37 { return true }
if c == 60 or c == 62 or c == 61 { return true }
if c == 40 or c == 41 or c == 123 or c == 125 { return true }
if c == 91 or c == 93 or c == 44 or c == 58 or c == 46 { return true }
if c == 33 or c == 64 { return true }
return false
}
fn lex(src: ptr) -> void {
toks = new []Tok
let i = 0
let line = 1
let n = slen(src)
while i < n {
let c = peek8(src, i)
if c == 10 { tok_push(TK_NL, ptr_null(), 0, line) line = line + 1 i = i + 1 continue }
if c == 32 or c == 9 or c == 13 { i = i + 1 continue }
if c == 35 { # '#' comment to end of line
while i < n and peek8(src, i) != 10 { i = i + 1 }
continue
}
if c == 34 { # "string"
i = i + 1
let start = i
let out = mem_alloc(n)
let j = 0
while i < n and peek8(src, i) != 34 {
if peek8(src, i) == 92 { # backslash escape
let e = peek8(src, i + 1)
let r = e
if e == 110 { r = 10 }
if e == 116 { r = 9 }
if e == 48 { r = 0 }
poke8(out, j, r) j = j + 1 i = i + 2
} else { poke8(out, j, peek8(src, i)) j = j + 1 i = i + 1 }
}
i = i + 1
poke8(out, j, 0)
tok_push(TK_STR, out, 0, line)
continue
}
if c == 39 { # 'c' char literal -> int
i = i + 1
let v = 0
if peek8(src, i) == 92 {
let e = peek8(src, i + 1)
if e == 110 { v = 10 }
if e == 116 { v = 9 }
if e == 48 { v = 0 }
i = i + 2
} else { v = peek8(src, i) i = i + 1 }
if peek8(src, i) == 39 { i = i + 1 }
tok_push(TK_INT, ptr_null(), v, line)
continue
}
if char_is_digit(c) {
if c == 48 and peek8(src, i + 1) == 120 { # 0x hex
let v = 0
i = i + 2
while i < n {
let h = peek8(src, i)
let d = 0
if char_is_digit(h) { d = h - 48 }
else { if h >= 97 and h <= 102 { d = h - 87 }
else { if h >= 65 and h <= 70 { d = h - 55 } else { break } } }
v = v * 16 + d
i = i + 1
}
tok_push(TK_INT, ptr_null(), v, line)
continue
}
let v = 0
while i < n and char_is_digit(peek8(src, i)) { v = v * 10 + (peek8(src, i) - 48) i = i + 1 }
# a fractional part makes it a Q16.16 fixed literal
if i < n and peek8(src, i) == 46 and char_is_digit(peek8(src, i + 1)) {
i = i + 1
let fnum = 0 let fden = 1
while i < n and char_is_digit(peek8(src, i)) { fnum = fnum * 10 + (peek8(src, i) - 48) fden = fden * 10 i = i + 1 }
let bits = shl(v, 16) + shl(fnum, 16) / fden
tok_push(TK_FLOAT, ptr_null(), bits, line)
continue
}
tok_push(TK_INT, ptr_null(), v, line)
continue
}
if char_is_alpha(c) {
let start = i
while i < n and char_is_alnum(peek8(src, i)) { i = i + 1 }
tok_push(TK_ID, substr(src, start, i - start), 0, line)
continue
}
if c == 59 { tok_push(TK_NL, ptr_null(), 0, line) i = i + 1 continue } # ';'
# two-character operators
if two_at(src, i, 45, 62) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # ->
if two_at(src, i, 61, 62) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # =>
if two_at(src, i, 61, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # ==
if two_at(src, i, 33, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # !=
if two_at(src, i, 60, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # <=
if two_at(src, i, 62, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # >=
if two_at(src, i, 43, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # +=
if two_at(src, i, 45, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # -=
if two_at(src, i, 42, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # *=
if two_at(src, i, 47, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # /=
if two_at(src, i, 46, 46) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # ..
if is_op1(c) { tok_push(TK_OP, substr(src, i, 1), 0, line) i = i + 1 continue }
i = i + 1 # skip anything unrecognised
}
tok_push(TK_EOF, ptr_null(), 0, line)
}

19459
selfhost/ludicc.seed.ll Normal file

File diff suppressed because it is too large Load diff

153
selfhost/main.ludic Normal file
View file

@ -0,0 +1,153 @@
# main.ludic — the self-hosted Ludic front-end.
#
# ludicc lexes, parses, checks and lowers a .ludic file to LLVM IR, then drives
# clang (the IR assembler + system linker, the same role rustc/swiftc give it)
# to produce a native binary. No C is compiled: the emitted IR is Ludic's, and
# clang only assembles and links it.
#
# ludicc app.ludic -o bin/app # native binary (windowed if a game)
# ludicc app.ludic -o bin/app --headless # force headless
# ludicc app.ludic --emit-llvm -o app.ll # stop at the IR
# ludic app.ludic # compile to a temp binary and run it
#
# The multi-call twist: when argv[0] is "ludic" it defaults to compile-and-run.
# With no -o and no run, IR still goes to stdout — the contract build.sh and
# reseed.sh rely on, so the bootstrap is untouched.
# basename: the part of a path after the last '/'.
fn base_name(path: ptr) -> ptr {
let last = 0 - 1
let i = 0
while peek8(path, i) != 0 { if peek8(path, i) == 47 { last = i } i = i + 1 }
return substr(path, last + 1, i - (last + 1))
}
# drop a trailing ".ludic" if present
fn strip_ludic(name: ptr) -> ptr {
let n = slen(name)
if n > 6 {
if streq(substr(name, n - 6, 6), ".ludic") { return substr(name, 0, n - 6) }
}
return name
}
# env var with a fallback when unset
fn getenv_or(name: ptr, dflt: ptr) -> ptr {
let v = os_getenv(name)
if ptr_is_null(v) { return dflt }
return v
}
# guarantee a directory string ends in '/' so path_join concatenates cleanly
fn ensure_slash(d: ptr) -> ptr {
let n = slen(d)
if n == 0 { return d }
if peek8(d, n - 1) == 47 { return d }
return sconcat(d, "/")
}
fn die(msg: ptr) -> void {
file_write(file_stderr(), msg, slen(msg))
os_exit(1)
}
main {
let path = ptr_null()
let out = ptr_null()
let want = 0 # 0 = auto, 1 = windowed, 2 = headless
let emit_ir = false # --emit-llvm: stop after writing IR
let fmt = false # --fmt: lex + parse only, then exit (the doc-check gate)
let save = false # --save-temps: keep the intermediate .ll
let run = false # compile then execute the result
# multi-call: invoked as `ludic` -> run mode by default
if streq(base_name(os_arg(0)), "ludic") { run = true }
let ai = 1
while ai < os_argc() {
let a = os_arg(ai)
if streq(a, "--windowed") { want = 1 }
else { if streq(a, "--headless") { want = 2 }
else { if streq(a, "--emit-llvm") { emit_ir = true }
else { if streq(a, "--fmt") { fmt = true }
else { if streq(a, "--save-temps") { save = true }
else { if streq(a, "--run") { run = true }
else { if streq(a, "-o") { ai = ai + 1 if ai < os_argc() { out = os_arg(ai) } }
else {
# an unknown flag is ignored (with a note) rather than mistaken for the
# input file — '-' is ASCII 45
if peek8(a, 0) == 45 {
let m = sconcat("ludicc: ignoring unknown flag ", sconcat(a, "\n"))
file_write(file_stderr(), m, slen(m))
} else { path = a }
} } } } } } }
ai = ai + 1
}
if ptr_is_null(path) {
die("usage: ludicc <file.ludic> [-o out] [--windowed|--headless] [--emit-llvm] [--save-temps]\n")
}
let src = read_file(path)
if ptr_is_null(src) { die("ludicc: cannot open input\n") }
cur_dir = dir_of(path)
lex(src)
parse_program()
# --fmt is the doc-check gate: reaching here means it lexed and parsed. A parse
# error would already have exited nonzero, so a clean parse exits 0. (Canonical
# formatting output is not yet reimplemented on the self-hosted toolchain.)
if fmt { os_exit(0) }
maybe_splice_runtime()
# a game gets a window by default; a plain program stays headless. An explicit
# flag always wins. The stdout-IR path (no target) also stays headless, which
# is what reseed.sh compiles the compiler itself with.
let has_target = run or (not ptr_is_null(out))
if want == 1 { g_windowed = true }
else { if want == 2 { g_windowed = false }
else { g_windowed = has_target and has_systems() } }
emit_program()
# --emit-llvm, or no binary target: emit IR and stop (stdout when out is null).
if emit_ir { ir_flush(out) return }
if not has_target { ir_flush(ptr_null()) return }
# a run with no explicit -o lands in a temp file
if run and ptr_is_null(out) {
out = sconcat("/tmp/ludic-run-", strip_ludic(base_name(path)))
}
# make the output directory if the user asked for e.g. bin/app
let odir = dir_of(out)
if slen(odir) > 0 { os_system(sconcat("mkdir -p ", odir)) }
let ll = sconcat(out, ".ll")
if not ir_flush(ll) { die("ludicc: cannot write IR\n") }
let cc = getenv_or("LUDIC_CC", "clang")
let home = ensure_slash(getenv_or("LUDIC_HOME", dir_of(os_arg(0))))
# clang twice: assemble the IR, then link. A windowed build adds the macOS
# platform layer and the Cocoa framework.
# -Wno-override-module: our IR carries an explicit target triple, which clang
# would otherwise warn about on every build.
let cmd = sconcat(cc, sconcat(" -O2 -Wno-override-module ", ll))
if g_windowed {
let cocoa = path_join(home, "runtime/native/cocoa.ll")
cmd = sconcat(cmd, sconcat(" ", sconcat(cocoa, " -framework Cocoa -Wl,-rpath,@loader_path")))
}
cmd = sconcat(cmd, sconcat(" -o ", out))
let rc = os_system(cmd)
if not save { os_system(sconcat("rm -f ", ll)) }
if rc != 0 { die("ludicc: link failed\n") }
# run mode: execute the binary we just built and forward its exit code
if run {
let st = os_system(out)
os_exit(band(shr(st, 8), 255))
}
}

322
selfhost/parse.ludic Normal file
View file

@ -0,0 +1,322 @@
# parse.ludic — recursive-descent parser: the token slice -> an AST.
# Reduced grammar: structs, vars, consts, fns, main; no ECS/scene/ui/match.
# Mirrors compiler/front/parse.c. Uses the global `toks` and a cursor `pi`.
var pi: int = 0
var prog: []Node # the top-level declarations
var g_game_name: ptr # the `game`/`module` name
fn cur() -> Tok { return toks[pi] }
fn pk(o: int) -> Tok { return toks[pi + o] }
fn is_op(v: ptr) -> bool { let t = toks[pi] return t.kind == TK_OP and streq(t.text, v) }
fn is_id(v: ptr) -> bool { let t = toks[pi] return t.kind == TK_ID and streq(t.text, v) }
fn is_kw(v: ptr) -> bool { return is_id(v) }
fn perr(msg: ptr) -> void {
let e = file_stderr()
file_write(e, "ludicc(self): parse error: ", 27)
file_write(e, msg, slen(msg))
file_write(e, "\n", 1)
os_exit(1)
}
fn eat_op(v: ptr) -> void { if not is_op(v) { perr(v) } pi = pi + 1 }
fn eat_id() -> ptr {
let t = toks[pi]
if t.kind != TK_ID { perr("expected identifier") }
pi = pi + 1
return t.text
}
fn skipnl() -> void { while toks[pi].kind == TK_NL { pi = pi + 1 } }
# a type: `[]T` slice, or a plain name (int/ptr/str/bool/struct)
fn ptype() -> ptr {
if is_op("[") {
pi = pi + 1
eat_op("]")
let el = ptype()
let out = mem_alloc(slen(el) + 3)
poke8(out, 0, 91) poke8(out, 1, 93) # "[]"
let i = 0
while peek8(el, i) != 0 { poke8(out, 2 + i, peek8(el, i)) i = i + 1 }
poke8(out, 2 + i, 0)
return out
}
return eat_id()
}
# ---- expressions -----------------------------------------------------------
fn expr() -> Node { return p_or() }
fn args_call(call: Node) -> void {
eat_op("(") skipnl()
while not is_op(")") { push(call.kids, expr()) skipnl() if is_op(",") { pi = pi + 1 skipnl() } }
eat_op(")")
}
fn p_primary() -> Node {
let t = toks[pi]
if t.kind == TK_INT { let n = node(E_INT) n.ival = t.ival pi = pi + 1 return n }
if t.kind == TK_FLOAT { let n = node(E_FLOAT) n.ival = t.ival pi = pi + 1 return n }
if t.kind == TK_STR { let n = node(E_STR) n.s = t.text pi = pi + 1 return n }
if t.kind == TK_ID {
if streq(t.text, "true") { let n = node(E_BOOL) n.ival = 1 pi = pi + 1 return n }
if streq(t.text, "false") { let n = node(E_BOOL) n.ival = 0 pi = pi + 1 return n }
if streq(t.text, "new") { pi = pi + 1 let n = node(E_NEW) n.s = ptype() return n }
let n = node(E_ID) n.s = t.text pi = pi + 1 return n
}
if is_op("(") { pi = pi + 1 skipnl() let e = expr() skipnl() eat_op(")") return e }
perr("expected expression")
return node(E_INT)
}
fn p_postfix() -> Node {
let e = p_primary()
while true {
if is_op(".") { pi = pi + 1 let m = node(E_MEMBER) m.a = e m.s = eat_id() e = m }
else { if is_op("[") { pi = pi + 1 let ix = node(E_INDEX) ix.a = e ix.b = expr() eat_op("]") e = ix }
else { if is_op("(") { let c = node(E_CALL) c.a = e args_call(c) e = c } else { break } } }
}
return e
}
fn p_unary() -> Node {
if is_op("-") { pi = pi + 1 let n = node(E_UN) n.s = "-" n.a = p_unary() return n }
if is_id("not") { pi = pi + 1 let n = node(E_UN) n.s = "not" n.a = p_unary() return n }
return p_postfix()
}
fn mkbin(op: ptr, l: Node, r: Node) -> Node { let b = node(E_BIN) b.s = op b.a = l b.b = r return b }
fn p_mul() -> Node {
let l = p_unary()
while is_op("*") or is_op("/") or is_op("%") { let op = toks[pi].text pi = pi + 1 l = mkbin(op, l, p_unary()) }
return l
}
fn p_add() -> Node {
let l = p_mul()
while is_op("+") or is_op("-") { let op = toks[pi].text pi = pi + 1 l = mkbin(op, l, p_mul()) }
return l
}
fn p_cmp() -> Node {
let l = p_add()
while is_op("<") or is_op("<=") or is_op(">") or is_op(">=") or is_op("==") or is_op("!=") {
let op = toks[pi].text pi = pi + 1 l = mkbin(op, l, p_add())
}
return l
}
fn p_and() -> Node {
let l = p_cmp()
while is_id("and") { pi = pi + 1 l = mkbin("and", l, p_cmp()) }
return l
}
fn p_or() -> Node {
let l = p_and()
while is_id("or") { pi = pi + 1 l = mkbin("or", l, p_and()) }
return l
}
# a record literal `{ field = value, ... }` — used by spawn component inits
fn record() -> Node {
eat_op("{")
let r = node(E_REC)
while true { skipnl() if is_op("}") { break }
let fi = node(E_FINIT) fi.s = eat_id() eat_op("=") fi.a = expr() push(r.kids, fi)
if is_op(",") { pi = pi + 1 } }
eat_op("}") return r
}
# ---- statements ------------------------------------------------------------
fn block() -> Node {
skipnl() eat_op("{")
let b = node(N_BLOCK)
while true { skipnl() if is_op("}") { break } push(b.kids, stmt()) }
eat_op("}")
return b
}
fn stmt() -> Node {
let t = toks[pi]
if t.kind == TK_ID {
if streq(t.text, "let") {
pi = pi + 1 let n = node(S_LET) n.s = eat_id()
if is_op(":") { pi = pi + 1 n.ty = ptype() }
if is_op("=") { pi = pi + 1 n.a = expr() }
return n
}
if streq(t.text, "return") {
pi = pi + 1 let n = node(S_RETURN)
if toks[pi].kind != TK_NL and not is_op("}") { n.a = expr() }
return n
}
if streq(t.text, "if") {
pi = pi + 1 let n = node(S_IF) n.a = expr() n.b = block() skipnl()
if is_id("else") { pi = pi + 1 skipnl()
if is_id("if") { n.c = stmt() } else { n.c = block() } }
return n
}
if streq(t.text, "when") { # `when c { }` — an if with no else
pi = pi + 1 let n = node(S_IF) n.a = expr() n.b = block()
return n
}
if streq(t.text, "while") { pi = pi + 1 let n = node(S_WHILE) n.a = expr() n.b = block() return n }
if streq(t.text, "for") {
pi = pi + 1
if is_op("(") { return parse_query_for() }
let n = node(S_FOR) n.s = eat_id()
let inkw = eat_id() # 'in'
n.a = expr() eat_op("..") n.b = expr() n.c = block()
return n
}
if streq(t.text, "spawn") { return parse_spawn() }
if streq(t.text, "machine") {
pi = pi + 1 let n = node(S_MACHINE) n.a = expr() skipnl() eat_op("{")
while true { skipnl() if is_op("}") { break }
let stkw = eat_id() # 'state'
let s = node(S_STATE) s.s = eat_id() eat_op("=") s.b = expr() skipnl() s.a = block()
push(n.kids, s) }
eat_op("}") return n
}
if streq(t.text, "become") { pi = pi + 1 let n = node(S_BECOME) n.s = eat_id() return n }
if streq(t.text, "enter") { pi = pi + 1 let n = node(S_BECOME) n.s = eat_id() n.ival = 1 return n }
if streq(t.text, "despawn") { pi = pi + 1 let n = node(S_DESPAWN) n.a = expr() return n }
if streq(t.text, "break") { pi = pi + 1 return node(S_BREAK) }
if streq(t.text, "continue") { pi = pi + 1 return node(S_CONTINUE) }
if streq(t.text, "match") {
pi = pi + 1 let n = node(S_MATCH) n.a = expr() skipnl() eat_op("{")
while true {
skipnl() if is_op("}") { break }
let arm = node(S_MARM)
while true { push(arm.kids, expr()) if is_op(",") { pi = pi + 1 skipnl() continue } break }
eat_op("=>") skipnl()
if is_op("{") { arm.a = block() } else { let b = node(N_BLOCK) push(b.kids, stmt()) arm.a = b }
push(n.kids, arm)
}
eat_op("}") return n
}
}
let e = expr()
if is_op("=") or is_op("+=") or is_op("-=") or is_op("*=") or is_op("/=") {
let n = node(S_ASSIGN) n.s = toks[pi].text pi = pi + 1 n.a = e n.b = expr() return n
}
let n = node(S_EXPR) n.a = e return n
}
# ---- declarations ----------------------------------------------------------
fn parse_struct() -> Node {
pi = pi + 1 let n = node(N_STRUCT) n.s = eat_id() skipnl() eat_op("{")
while true { skipnl() if is_op("}") { break }
let f = node(N_FIELD) f.s = eat_id() eat_op(":") f.ty = ptype()
if is_op("=") { pi = pi + 1 f.a = expr() }
push(n.kids, f) if is_op(",") { pi = pi + 1 } }
eat_op("}") return n
}
fn parse_var() -> Node {
pi = pi + 1 let n = node(N_VAR) n.s = eat_id() eat_op(":") n.ty = ptype()
if is_op("=") { pi = pi + 1 n.a = expr() }
return n
}
fn parse_const() -> Node {
pi = pi + 1 let n = node(N_CONST) n.s = eat_id() eat_op(":") n.ty = ptype() eat_op("=") n.a = expr()
return n
}
fn parse_fn() -> Node {
pi = pi + 1 let n = node(N_FN) n.s = eat_id() eat_op("(")
while not is_op(")") {
let p = node(N_PARAM) p.s = eat_id() eat_op(":") p.ty = ptype() push(n.kids, p)
if is_op(",") { pi = pi + 1 }
}
eat_op(")")
n.ty = "void"
if is_op("->") { pi = pi + 1 n.ty = ptype() }
n.a = block()
return n
}
fn parse_main() -> Node { pi = pi + 1 let n = node(N_MAIN) n.a = block() return n }
# directory part of a path, including the trailing '/', or "" if none
fn dir_of(path: ptr) -> ptr {
let last = 0 - 1
let i = 0
while peek8(path, i) != 0 { if peek8(path, i) == 47 { last = i } i = i + 1 }
if last < 0 { return "" }
return substr(path, 0, last + 1)
}
fn path_join(dir: ptr, rel: ptr) -> ptr {
if peek8(rel, 0) == 47 { return rel } # absolute
return sconcat(dir, rel)
}
var loaded_paths: []ptr
var cur_dir: ptr
fn already_loaded(full: ptr) -> bool {
let i = 0
while i < len(loaded_paths) { if streq(loaded_paths[i], full) { return true } i = i + 1 }
return false
}
# parse one top-level declaration (or resolve an import) into `prog`
fn parse_one_decl() -> void {
if is_id("import") { pi = pi + 1
let t = toks[pi]
if t.kind != TK_STR { perr("expected \"path\" after import") }
let rel = t.text pi = pi + 1
do_import(rel)
return
}
if is_id("struct") { push(prog, parse_struct()) return }
if is_id("component") { push(prog, parse_component()) return }
if is_id("archetype") { push(prog, parse_archetype()) return }
if is_id("system") or is_id("edge") { push(prog, parse_system()) return }
if is_id("ui") { push(prog, parse_ui()) return }
if is_id("var") { push(prog, parse_var()) return }
if is_id("const") { push(prog, parse_const()) return }
if is_id("export") { pi = pi + 1 let f = parse_fn() f.ival = 1 push(prog, f) return }
if is_id("fn") or is_id("pure") { if is_id("pure") { pi = pi + 1 } push(prog, parse_fn()) return }
if is_id("extern") { push(prog, parse_extern()) return }
if is_id("main") { push(prog, parse_main()) return }
perr("expected declaration")
}
# lex and parse an imported fragment into `prog`, saving/restoring lexer state
fn do_import(rel: ptr) -> void {
let full = path_join(cur_dir, rel)
if already_loaded(full) { return }
push(loaded_paths, full)
let src = read_file(full)
if ptr_is_null(src) { perr(sconcat("cannot open import ", full)) }
let saved_toks = toks let saved_pi = pi let saved_dir = cur_dir
cur_dir = dir_of(full)
lex(src) # resets the global token stream
pi = 0
skipnl()
while toks[pi].kind != TK_EOF { parse_one_decl() skipnl() }
toks = saved_toks pi = saved_pi cur_dir = saved_dir
}
# a game (has systems/components) links the Ludic runtime; auto-splice it the
# way the C compiler does. Tools (a `main` block, no ECS) get nothing.
fn maybe_splice_runtime() -> void {
if not has_ecs() { return }
let saved = cur_dir
cur_dir = ""
do_import("runtime/native/core.ludic")
cur_dir = saved
}
fn parse_program() -> void {
prog = new []Node
loaded_paths = new []ptr
skipnl()
g_game_name = "Ludic"
# imports may precede the game block
while is_id("import") { pi = pi + 1 let t = toks[pi] let rel = t.text pi = pi + 1 do_import(rel) skipnl() }
if is_id("game") or is_id("module") { pi = pi + 1 g_game_name = eat_id() skipnl() eat_op("{") }
while true {
skipnl()
if toks[pi].kind == TK_EOF { break }
if is_op("}") { break }
parse_one_decl()
}
}

144
selfhost/parse_game.ludic Normal file
View file

@ -0,0 +1,144 @@
# parse_game.ludic — the ECS front-end: component and system declarations,
# `for (vars) in query [terms] where cond`, spawn and despawn. Mirrors the
# game-construct parsing in compiler/front/parse.c.
fn parse_component() -> Node {
pi = pi + 1 let n = node(N_COMP) n.s = eat_id() skipnl() eat_op("{")
while true { skipnl() if is_op("}") { break }
let f = node(N_FIELD) f.s = eat_id() eat_op(":") f.ty = ptype()
if is_op("=") { pi = pi + 1 f.a = expr() }
push(n.kids, f) if is_op(",") { pi = pi + 1 } }
eat_op("}") return n
}
# skip an @annotation or a reads/writes/query/... clause we don't model yet
fn skip_clause() -> void {
if is_op("[") { let depth = 0
while true { if is_op("[") { depth = depth + 1 } if is_op("]") { depth = depth - 1 }
pi = pi + 1 if depth == 0 { break } }
}
}
fn parse_system() -> Node {
if is_id("edge") { pi = pi + 1 } # optional `edge` modifier before `system`
pi = pi + 1 let n = node(N_SYS) n.s = eat_id() n.ty = "Update"
# clauses: @anno, phase X, reads/writes/needs/uses [..], query (..) [..]
# A `query (vars) [terms] where c` clause desugars to a body wrapped in one
# `for (vars) in query [terms] where c { ... }` — the same S_QUERY node the
# inline form builds, so `qdecl.ludic` and the inline form share a lowering.
let has_q = false
let qn = node(S_QUERY)
while true {
skipnl() # clauses may span several lines
if is_op("@") { pi = pi + 1 let a = eat_id() if is_op("(") { let d = 0 # @anno, one per turn so a
while true { if is_op("(") { d = d + 1 } if is_op(")") { d = d - 1 } pi = pi + 1 if d == 0 { break } } }
continue } # newline-separated @anno re-skips at the loop top
if is_id("phase") { pi = pi + 1 n.ty = eat_id() continue }
if is_id("reads") or is_id("writes") or is_id("needs") or is_id("uses") { pi = pi + 1 skip_clause() continue }
if is_id("query") {
pi = pi + 1 has_q = true
if is_op("(") { eat_op("(") # optional (vars); omitted when nothing binds
while not is_op(")") { let v = node(E_ID) v.s = eat_id() push(qn.kids, v) if is_op(",") { pi = pi + 1 } }
eat_op(")") }
qn.c = parse_query_tail()
qn.b = qn.c.a # where-expr (or null)
continue
}
break
}
skipnl()
let body = block()
if has_q {
qn.a = body
let wrap = node(N_BLOCK) push(wrap.kids, qn)
n.a = wrap
} else { n.a = body }
return n
}
# `[Term, ...]` with optional `where <expr>`, returning a node whose kids are
# the terms (E_ID with ival=1 for {Tag} filters) and .a the where-expr or null.
fn parse_query_tail() -> Node {
eat_op("[")
let q = node(N_BLOCK)
while not is_op("]") {
if is_op("{") { pi = pi + 1 let t = node(E_ID) t.s = eat_id() t.ival = 1 push(q.kids, t) eat_op("}") }
else { let t = node(E_ID) t.s = eat_id() t.ival = 0 push(q.kids, t) }
if is_op(",") { pi = pi + 1 }
}
eat_op("]")
if is_id("where") { pi = pi + 1 q.a = expr() }
return q
}
# `for (a, b) in query [Pos, Vel] where ... { body }`
fn parse_query_for() -> Node {
let n = node(S_QUERY)
eat_op("(")
while not is_op(")") { let v = node(E_ID) v.s = eat_id() push(n.kids, v) if is_op(",") { pi = pi + 1 } }
eat_op(")")
let inkw = eat_id() # 'in'
let qkw = eat_id() # 'query'
n.c = parse_query_tail()
n.b = n.c.a # where
n.a = block()
return n
}
fn parse_spawn() -> Node {
pi = pi + 1 let n = node(S_SPAWN) n.s = eat_id() skipnl() eat_op("{")
while true {
skipnl() if is_op("}") { break }
let ci = node(E_FINIT) ci.s = eat_id() eat_op("=")
ci.a = record() # { field = val, ... }
push(n.kids, ci)
if is_op(",") { pi = pi + 1 }
}
eat_op("}") return n
}
# archetype Name { CompA, CompB } — a named entity kind (bundle of components)
fn parse_archetype() -> Node {
pi = pi + 1 let n = node(N_ARCH) n.s = eat_id() skipnl() eat_op("{")
while true { skipnl() if is_op("}") { break }
let c = node(E_ID) c.s = eat_id() push(n.kids, c)
if is_op(",") { pi = pi + 1 } skipnl() }
eat_op("}") return n
}
# extern fn name(params) -> T = "symbol"
fn parse_extern() -> Node {
pi = pi + 1 # 'extern'
let fnkw = eat_id() # 'fn'
let n = node(N_EXTERN) n.s = eat_id() eat_op("(")
while not is_op(")") { let p = node(N_PARAM) p.s = eat_id() eat_op(":") p.ty = ptype() push(n.kids, p)
if is_op(",") { pi = pi + 1 } }
eat_op(")")
n.ty = "void"
if is_op("->") { pi = pi + 1 n.ty = ptype() }
eat_op("=")
let t = toks[pi] # "symbol"
n.a = node(E_STR) n.a.s = t.text pi = pi + 1
return n
}
# ui Name { widget-tree } — parsed into a widget node tree (emitted later)
fn parse_widget() -> Node {
let w = node(N_UI) w.s = eat_id() # widget type name
w.b = node(N_BLOCK) # b.kids = props (E_FINIT)
while toks[pi].kind == TK_ID and toks[pi + 1].kind == TK_OP and streq(toks[pi + 1].text, "=") {
let pr = node(E_FINIT) pr.s = eat_id() eat_op("=") pr.a = expr() push(w.b.kids, pr)
}
skipnl()
if is_op("{") { pi = pi + 1
while true { skipnl() if is_op("}") { break } push(w.kids, parse_widget()) }
eat_op("}") }
return w
}
fn parse_ui() -> Node {
pi = pi + 1 let n = node(N_UI) n.s = eat_id() n.ival = 1 # ival=1 marks the top ui block
skipnl() eat_op("{") skipnl()
n.a = parse_widget()
skipnl() eat_op("}")
return n
}

26
selfhost/reseed.sh Executable file
View file

@ -0,0 +1,26 @@
#!/bin/bash
# reseed.sh — regenerate the checked-in IR seed after a deliberate change to the
# self-host compiler. Uses the CURRENT seed (C-free) to produce the new one, so
# the C compiler is not reintroduced; falls back to the C ludicc if no seed
# builds. Always verify with ./selfhost/bootstrap-cfree.sh afterwards.
set -eu
cd "$(dirname "$0")/.."
CC="${LUDIC_CC:-clang}"
B=build/cfree; mkdir -p "$B"
FRAGS="selfhost/str.ludic selfhost/buf.ludic selfhost/io.ludic selfhost/ast.ludic
selfhost/lex.ludic selfhost/parse.ludic selfhost/parse_game.ludic selfhost/emit_core.ludic selfhost/emit_head.ludic
selfhost/emit_addr.ludic selfhost/emit_intrin.ludic selfhost/emit_intrin2.ludic selfhost/emit_math.ludic selfhost/emit_new.ludic
selfhost/emit_expr.ludic selfhost/emit_stmt.ludic selfhost/emit_ecs.ludic selfhost/emit_query.ludic selfhost/emit_spawn.ludic selfhost/emit_game.ludic selfhost/emit_machine.ludic selfhost/emit_save.ludic selfhost/emit_ui.ludic selfhost/emit_decl.ludic selfhost/main.ludic"
{ echo "game SelfHost {"; for f in $FRAGS; do cat "$f"; echo; done; echo "}"; } > "$B/selfhost.ludic"
if $CC selfhost/ludicc.seed.ll -o "$B/sh_old" 2>/dev/null; then
# compile once with the old seed, then AGAIN with the freshly built one so the
# seed is a fixed point of the NEW compiler, not a one-step image of the old.
"$B/sh_old" "$B/selfhost.ludic" > "$B/step1.ll"
$CC "$B/step1.ll" -o "$B/sh_new"
"$B/sh_new" "$B/selfhost.ludic" > selfhost/ludicc.seed.ll
else
echo "seed does not build; reseeding from the C ludicc"
./selfhost/build.sh build/ludicc "$B/sh_c"
"$B/sh_c" "$B/selfhost.ludic" > selfhost/ludicc.seed.ll
fi
echo "reseeded: $(wc -l < selfhost/ludicc.seed.ll | tr -d ' ') lines"

58
selfhost/str.ludic Normal file
View file

@ -0,0 +1,58 @@
# str.ludic — the string handling a compiler lives on: comparison, copying,
# slicing out substrings, and integer<->text. All over raw NUL-terminated
# byte buffers reached with peek8/poke8.
fn streq(a: ptr, b: ptr) -> bool {
let i = 0
while true {
let ca = peek8(a, i)
let cb = peek8(b, i)
if ca != cb { return false }
if ca == 0 { return true }
i = i + 1
}
return false
}
fn slen(s: ptr) -> int {
let n = 0
while peek8(s, n) != 0 { n = n + 1 }
return n
}
# a fresh NUL-terminated copy of src[start .. start+n]
fn substr(src: ptr, start: int, n: int) -> ptr {
let b = mem_alloc(n + 1)
let i = 0
while i < n { poke8(b, i, peek8(src, start + i)) i = i + 1 }
poke8(b, n, 0)
return b
}
fn char_is_digit(c: int) -> bool { return c >= 48 and c <= 57 }
fn char_is_alpha(c: int) -> bool {
if c >= 65 and c <= 90 { return true }
if c >= 97 and c <= 122 { return true }
return c == 95
}
fn char_is_alnum(c: int) -> bool { return char_is_alpha(c) or char_is_digit(c) }
# integer -> fresh decimal string
fn itoa(v: int) -> ptr {
if v == 0 { let z = mem_alloc(2) poke8(z, 0, 48) poke8(z, 1, 0) return z }
let neg = false
let x = v
if x < 0 { neg = true x = 0 - x }
let tmp = mem_alloc(16)
let n = 0
while x > 0 { poke8(tmp, n, 48 + x % 10) x = x / 10 n = n + 1 }
let total = n
if neg { total = total + 1 }
let out = mem_alloc(total + 1)
let k = 0
if neg { poke8(out, 0, 45) k = 1 }
let i = 0
while i < n { poke8(out, k + i, peek8(tmp, n - 1 - i)) i = i + 1 }
poke8(out, total, 0)
return out
}

52
selfhost/test.sh Executable file
View file

@ -0,0 +1,52 @@
#!/bin/bash
# test.sh — the self-hosting suite: the compiler is correct (compiles diverse
# programs to working binaries) and self-reproducing (the bootstrap fixpoint).
set -u
cd "$(dirname "$0")/.."
CC="${LUDIC_CC:-clang}"
PASS=0; FAIL=0
ok() { printf " \033[32mPASS\033[0m %s\n" "$1"; PASS=$((PASS+1)); }
bad() { printf " \033[31mFAIL\033[0m %s\n" "$1"; FAIL=$((FAIL+1)); }
echo "== build the self-host compiler (via the C ludicc) =="
[ -x build/ludicc ] || ./build.sh >/dev/null 2>&1
./build.sh examples/snake.ludic --headless >/dev/null 2>&1 && [ -x build/ludicc ] && ok "compiler builds from the IR seed (no C)" || { bad "self-host build"; exit 1; }
echo "== the self-host compiler is correct =="
run_case() { # name expected
local n="$1" exp="$2"
./build/ludicc "selfhost/tests/$n.ludic" > "/tmp/sh_$n.ll" 2>"/tmp/sh_$n.err" || { bad "$n: self-host errored: $(head -1 /tmp/sh_$n.err)"; return; }
$CC "/tmp/sh_$n.ll" -o "/tmp/sh_$n" 2>/dev/null || { bad "$n: IR did not assemble"; return; }
local got; got=$("/tmp/sh_$n" 2>/dev/null | tr '\n' ' ' | sed 's/ *$//')
[ "$got" = "$exp" ] && ok "$n ($got)" || bad "$n: got [$got] want [$exp]"
}
run_case structs "7 9 109 2 42"
run_case slices "0 20 361 777"
run_case control "55 4 15 1"
run_case match_bits "1 2 9 16 4 9999"
run_case fixed "2 3 0 6 1"
echo "== the self-host compiler compiles real games (vs golden output) =="
# The golden PPMs in selfhost/golden/ were produced by the original C compiler.
# The self-hosted compiler must reproduce them exactly.
game_case() { # name keys
local n="$1" keys="$2"
./selfhost/game-build.sh build/ludicc "examples/$n.ludic" "/tmp/ludic_${n}_sh" >/tmp/gb.out 2>&1 || { bad "$n: build ($(tail -1 /tmp/gb.out))"; return; }
printf '%s' "$keys" | "/tmp/ludic_${n}_sh" >/dev/null 2>&1; cp out.ppm "/tmp/${n}_sh.ppm"
cmp -s "selfhost/golden/$n.ppm" "/tmp/${n}_sh.ppm" && ok "$n matches the golden render" || bad "$n: differs from golden"
}
game_case snake "ddssaawwddss"
game_case menu "ssss"
game_case chronorift "ddddwwwwaassK"
echo "== the bootstrap fixpoint (seeded from the C compiler) =="
./selfhost/bootstrap.sh >/tmp/boot.out 2>&1
if grep -q "FIXPOINT" /tmp/boot.out; then ok "gen2.ll == gen3.ll (compiler reproduces itself)"; else bad "fixpoint not reached"; cat /tmp/boot.out; fi
echo "== the C-free bootstrap (from the checked-in IR seed, no C compiler) =="
./selfhost/bootstrap-cfree.sh >/tmp/cfree.out 2>&1
if grep -q "no C compiler" /tmp/cfree.out; then ok "seed.ll rebuilds the compiler and is its own fixed point"; else bad "C-free bootstrap failed (seed stale? run ./selfhost/reseed.sh)"; cat /tmp/cfree.out; fi
echo
printf "== %d passed, %d failed ==\n" "$PASS" "$FAIL"
[ "$FAIL" -eq 0 ]

View file

@ -0,0 +1,13 @@
game T {
fn fib(n: int) -> int { if n < 2 { return n } return fib(n-1) + fib(n-2) }
main {
print_int(fib(10)) # 55
let s = 0
for i in 0 .. 10 { if i == 5 { break } if i % 2 == 0 { continue } s = s + i }
print_int(s) # 1+3 = 4
let i = 0 let t = 0
while true { i = i + 1 if i > 5 { break } t = t + i }
print_int(t) # 15
if false and (1/0 == 0) { print_int(999) } else { print_int(1) } # short-circuit: no div by zero
}
}

View file

@ -0,0 +1,12 @@
game T {
main {
let half = 0.5
let a = 1.5
print_int(flr(a + half)) # flr(2.0) = 2
print_int(flr(a * 2.0)) # flr(3.0) = 3
let third = 1.0 / 3.0
print_int(flr(third * 3.0)) # ~flr(1.0) = 1 (may be 0 with rounding)
print_int(flr(fx(5) + a)) # flr(6.5) = 6
if a > half { print_int(1) } else { print_int(0) } # 1
}
}

View file

@ -0,0 +1,16 @@
game T {
fn classify(c: int) -> int {
match c { 65, 66 => { return 1 } 67 => { return 2 } _ => { return 9 } }
return 0
}
main {
print_int(classify(65)) # 1
print_int(classify(67)) # 2
print_int(classify(90)) # 9
print_int(shl(1, 4)) # 16
print_int(band(bor(4, 1), 6)) # 4
let p = mem_alloc(16)
poke32(p, 1, 9999)
print_int(peek32(p, 1)) # 9999
}
}

View file

@ -0,0 +1,11 @@
game T {
main {
let xs = new []int
print_int(len(xs)) # 0
for i in 0 .. 20 { push(xs, i * i) }
print_int(len(xs)) # 20
print_int(xs[19]) # 361
xs[3] = 777
print_int(xs[3]) # 777
}
}

View file

@ -0,0 +1,20 @@
game T {
struct P { x: int = 0, y: int = 7, next: P }
fn bump(p: P) -> void { p.x = p.x + 100 }
main {
let a = new P
print_int(a.y) # 7 default
a.x = 5
let b = a
b.x = 9
print_int(a.x) # 9 (reference)
bump(a)
print_int(a.x) # 109
let c = new P
c.x = 2
a.next = c
print_int(a.next.x) # 2 chained
a.next.x = 42
print_int(c.x) # 42
}
}