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:
commit
985f9ad8f2
418 changed files with 39065 additions and 0 deletions
68
selfhost/ast.ludic
Normal file
68
selfhost/ast.ludic
Normal 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
40
selfhost/bootstrap-cfree.sh
Executable 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
40
selfhost/bootstrap.sh
Executable 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
29
selfhost/buf.ludic
Normal 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
39
selfhost/build.sh
Executable 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
8
selfhost/compile.sh
Executable 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
38
selfhost/emit_addr.ludic
Normal 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
139
selfhost/emit_core.ludic
Normal 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
104
selfhost/emit_decl.ludic
Normal 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
92
selfhost/emit_ecs.ludic
Normal 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
168
selfhost/emit_expr.ludic
Normal 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
68
selfhost/emit_game.ludic
Normal 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
108
selfhost/emit_head.ludic
Normal 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
142
selfhost/emit_intrin.ludic
Normal 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")
|
||||
}
|
||||
95
selfhost/emit_intrin2.ludic
Normal file
95
selfhost/emit_intrin2.ludic
Normal 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")
|
||||
}
|
||||
39
selfhost/emit_machine.ludic
Normal file
39
selfhost/emit_machine.ludic
Normal 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
29
selfhost/emit_math.ludic
Normal 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
88
selfhost/emit_new.ludic
Normal 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
98
selfhost/emit_query.ludic
Normal 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
61
selfhost/emit_save.ludic
Normal 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
68
selfhost/emit_spawn.ludic
Normal 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
193
selfhost/emit_stmt.ludic
Normal 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
125
selfhost/emit_ui.ludic
Normal 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
16
selfhost/game-build.sh
Executable 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"
|
||||
4
selfhost/golden/chronorift.ppm
Normal file
4
selfhost/golden/chronorift.ppm
Normal file
File diff suppressed because one or more lines are too long
4
selfhost/golden/menu.ppm
Normal file
4
selfhost/golden/menu.ppm
Normal file
File diff suppressed because one or more lines are too long
4
selfhost/golden/snake.ppm
Normal file
4
selfhost/golden/snake.ppm
Normal file
File diff suppressed because one or more lines are too long
22
selfhost/io.ludic
Normal file
22
selfhost/io.ludic
Normal 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
138
selfhost/lex.ludic
Normal 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
19459
selfhost/ludicc.seed.ll
Normal file
File diff suppressed because it is too large
Load diff
153
selfhost/main.ludic
Normal file
153
selfhost/main.ludic
Normal 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
322
selfhost/parse.ludic
Normal 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
144
selfhost/parse_game.ludic
Normal 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
26
selfhost/reseed.sh
Executable 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
58
selfhost/str.ludic
Normal 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
52
selfhost/test.sh
Executable 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 ]
|
||||
13
selfhost/tests/control.ludic
Normal file
13
selfhost/tests/control.ludic
Normal 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
|
||||
}
|
||||
}
|
||||
12
selfhost/tests/fixed.ludic
Normal file
12
selfhost/tests/fixed.ludic
Normal 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
|
||||
}
|
||||
}
|
||||
16
selfhost/tests/match_bits.ludic
Normal file
16
selfhost/tests/match_bits.ludic
Normal 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
|
||||
}
|
||||
}
|
||||
11
selfhost/tests/slices.ludic
Normal file
11
selfhost/tests/slices.ludic
Normal 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
|
||||
}
|
||||
}
|
||||
20
selfhost/tests/structs.ludic
Normal file
20
selfhost/tests/structs.ludic
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue