Phase 2: enforce statement separators (Rule B)

The parser now requires a newline or ';' between statements (block() in
selfhost/parse.ludic); two statements may no longer sit adjacent with only
spaces. Also fixed if-without-else swallowing its trailing separator.

Migration: tools/ludic-tools/migrate_separators.c inserts ';' at statement
boundaries corpus-wide (examples, runtime, 25 self-host fragments, ~1100
boundaries). Verified semantically identical — the migrated compiler compiles
itself to IR byte-identical to the pre-migration seed, and every golden game
renders identically. Reseeded to the strict compiler; C-free fixpoint holds;
test.sh 14/14.

Docs: Rule B documented in LANGUAGE.md; BOOTSTRAP.md R1 + stale fences updated;
check-docs green across all docs. Fixed a multi-line string literal in
emit_expr.ludic (byte-identical \n escape) that the C toolchain lexer mis-lexes.

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

View file

@ -3,7 +3,7 @@
# 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 }
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
@ -36,24 +36,24 @@ 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")
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 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 }
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 }
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 }
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.
@ -79,17 +79,17 @@ 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 }
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 }
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 }
let s = find_struct(name); if not ptr_is_null(s) { return s }
return find_comp(name)
}
fn layout_ty(name: ptr) -> ptr {
@ -99,12 +99,12 @@ fn layout_ty(name: ptr) -> ptr {
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 }
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 }
while i < len(s.kids) { if streq(s.kids[i].s, fname) { return s.kids[i].ty }; i = i + 1 }
return "int"
}
@ -121,19 +121,19 @@ fn find_global(name: ptr) -> Node {
}
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 }
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) }
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 }
while i >= 0 { if streq(loc_name[i], name) { return i }; i = i - 1 }
return 0 - 1
}