Implement the rest of NETWORKING-DESIGN.md (N2–N6) and eliminate every
`.c` file from the repo. clang remains only the LLVM-IR assembler; no C
is compiled anywhere.
Networking (selfhost/emit_net.ludic + parser/emit changes):
- N2 @Sync: per-model serialize/apply + by-kind dispatchers; POD-scalar
compile error and empty-participation warning; selective replication.
- N3 @Owned: @L_owner array + owner/set_owner/is_owner; owners snapshot.
- N4 @ToServer/@ToClients remote events: framed net_send + net_pump re-emit.
- N5 @Server/@Predicted role guards + drivable sim (tick_fixed/tick_render,
entry-owns-the-loop).
- Built-in loopback transport so multiplayer runs with zero foreign code;
extern fn net_send/net_poll still overrides it for a real socket.
- N6 blessed runtime (examples/net_rt.ludic) + end-to-end demo (net_demo).
- Fix: llty("entity") is now i32 (entities are i32 handles), so let e = self().
C elimination:
- Networking + foreign-mod-ABI tests rewritten as self-contained pure-Ludic
programs (examples/net_*, world_*, mod_events, scoped); tests/ removed.
- Reflection ABI exposed to Ludic as world_* builtins (Ludic-to-Ludic modding).
- Formatter rewritten C→Ludic: tools/ludic-tools/fmt.ludic.
- Language server rewritten C→Ludic: tools/ludic-tools/lsp.ludic (lexer, index
parser, cross-file workspace resolver, JSON, all LSP handlers).
- Obsolete migrate_*.c codemods deleted; ludic_syntax.h kept as vocabulary data.
Suites: ./test.sh 44/44, ./tools/test-tools.sh 28/28 (LSP 42/42), fixpoint holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
203 lines
9.6 KiB
Text
203 lines
9.6 KiB
Text
# 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 = `@.str{itoa(ll_str)}`
|
|
ll_str = ll_str + 1
|
|
let n = len(s)
|
|
emith(name); emith(" = private unnamed_addr constant [")
|
|
emith(itoa(n + 1)); emith(" x i8] c\"")
|
|
var i = 0
|
|
while i < n {
|
|
let c = 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
|
|
}
|
|
|
|
# the constant initializer for a global var: a literal, or 0/null
|
|
fn global_init(d: Node) -> ptr {
|
|
if (d.a == null) { if (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 (e.s == ("-")) and e.a.kind == E_INT { return (("-") + itoa(e.a.ival)) }
|
|
if (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("@__stdoutp = external global ptr\n")
|
|
emith("@.fmt_int = private unnamed_addr constant [4 x i8] c\"%d\\0A\\00\"\n")
|
|
emith("@.fmt_line = private unnamed_addr constant [4 x i8] c\"%s\\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(len(g_game_name) + 1)); emith(" x i8] c\""); emith(g_game_name); emith("\\00\"\n")
|
|
emith("%LSlice = type { ptr, i32, i32 }\n")
|
|
# property layouts — a %Cmp_ record of named fields, emitted here so `new`
|
|
# works whether or not the program runs the ECS. The per-entity @S_/@H_ arrays
|
|
# are separate (emit_ecs_storage), emitted only for a program that runs the ECS.
|
|
var i = 0
|
|
while i < len(prog) {
|
|
let d = prog[i]
|
|
if d.kind == N_COMP {
|
|
emith(layout_ty(d.s)); emith(" = type { ")
|
|
if len(d.kids) == 0 { emith("i32") }
|
|
var 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
|
|
}
|
|
}
|
|
|
|
# One `declare <ret> @<sym>(<argtys>)` per `extern fn`, so the linker resolves the
|
|
# call to the bound symbol. Emitted after the header; a program with no `extern fn`
|
|
# emits nothing here, so un-networked builds stay byte-identical.
|
|
fn emit_extern_decls() -> void {
|
|
var i = 0
|
|
while i < len(prog) {
|
|
let d = prog[i]
|
|
if d.kind == N_EXTERN {
|
|
emith("declare "); emith(llty(d.ty)); emith(" @"); emith(d.a.s); emith("(")
|
|
var 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
|
|
}
|
|
}
|
|
|
|
# The string runtime, emitted (once) into any program that uses `+`/`==`/`!=`
|
|
# on strings. Hand-written IR over NUL-terminated byte buffers: str_eq walks both
|
|
# until a mismatch or a shared terminator; str_concat measures both, mallocs
|
|
# len+len+1, copies each half, and NUL-terminates. @malloc is always declared.
|
|
fn emit_str_prelude() -> void {
|
|
emith("define i32 @fn_str_eq(ptr %a, ptr %b) {\n")
|
|
emith("entry:\n br label %loop\n")
|
|
emith("loop:\n %i = phi i32 [ 0, %entry ], [ %i1, %cont ]\n")
|
|
emith(" %pa = getelementptr inbounds i8, ptr %a, i32 %i\n")
|
|
emith(" %pb = getelementptr inbounds i8, ptr %b, i32 %i\n")
|
|
emith(" %ca = load i8, ptr %pa\n %cb = load i8, ptr %pb\n")
|
|
emith(" %df = icmp ne i8 %ca, %cb\n br i1 %df, label %ret0, label %chk\n")
|
|
emith("chk:\n %zt = icmp eq i8 %ca, 0\n br i1 %zt, label %ret1, label %cont\n")
|
|
emith("cont:\n %i1 = add i32 %i, 1\n br label %loop\n")
|
|
emith("ret1:\n ret i32 1\nret0:\n ret i32 0\n}\n")
|
|
|
|
emith("define ptr @fn_str_concat(ptr %a, ptr %b) {\n")
|
|
emith("entry:\n br label %al\n")
|
|
emith("al:\n %ia = phi i32 [ 0, %entry ], [ %ia1, %alb ]\n")
|
|
emith(" %pa = getelementptr inbounds i8, ptr %a, i32 %ia\n %cca = load i8, ptr %pa\n")
|
|
emith(" %za = icmp eq i8 %cca, 0\n br i1 %za, label %bl0, label %alb\n")
|
|
emith("alb:\n %ia1 = add i32 %ia, 1\n br label %al\n")
|
|
emith("bl0:\n br label %bl\n")
|
|
emith("bl:\n %ib = phi i32 [ 0, %bl0 ], [ %ib1, %blb ]\n")
|
|
emith(" %pb = getelementptr inbounds i8, ptr %b, i32 %ib\n %ccb = load i8, ptr %pb\n")
|
|
emith(" %zb = icmp eq i8 %ccb, 0\n br i1 %zb, label %alloc, label %blb\n")
|
|
emith("blb:\n %ib1 = add i32 %ib, 1\n br label %bl\n")
|
|
emith("alloc:\n %sum = add i32 %ia, %ib\n %sz = add i32 %sum, 1\n")
|
|
emith(" %sz64 = sext i32 %sz to i64\n %out = call ptr @malloc(i64 %sz64)\n br label %c1\n")
|
|
emith("c1:\n %i = phi i32 [ 0, %alloc ], [ %i1, %c1b ]\n")
|
|
emith(" %d1 = icmp slt i32 %i, %ia\n br i1 %d1, label %c1b, label %c2i\n")
|
|
emith("c1b:\n %s1 = getelementptr inbounds i8, ptr %a, i32 %i\n %v1 = load i8, ptr %s1\n")
|
|
emith(" %o1 = getelementptr inbounds i8, ptr %out, i32 %i\n store i8 %v1, ptr %o1\n")
|
|
emith(" %i1 = add i32 %i, 1\n br label %c1\n")
|
|
emith("c2i:\n br label %c2\n")
|
|
emith("c2:\n %j = phi i32 [ 0, %c2i ], [ %j1, %c2b ]\n")
|
|
emith(" %d2 = icmp slt i32 %j, %ib\n br i1 %d2, label %c2b, label %fin\n")
|
|
emith("c2b:\n %s2 = getelementptr inbounds i8, ptr %b, i32 %j\n %v2 = load i8, ptr %s2\n")
|
|
emith(" %oj = add i32 %ia, %j\n %o2 = getelementptr inbounds i8, ptr %out, i32 %oj\n store i8 %v2, ptr %o2\n")
|
|
emith(" %j1 = add i32 %j, 1\n br label %c2\n")
|
|
emith("fin:\n %pe = getelementptr inbounds i8, ptr %out, i32 %sum\n store i8 0, ptr %pe\n ret ptr %out\n}\n")
|
|
}
|
|
|
|
# int -> decimal string, emitted (once) into any program that uses str(int)
|
|
# (string interpolation of a number). Writes digits from the end of a 24-byte
|
|
# buffer, prepends '-' for negatives, and returns a pointer into the buffer.
|
|
fn emit_int_str() -> void {
|
|
emith("define ptr @fn_int_str(i32 %n0) {\n")
|
|
emith("entry:\n %buf = call ptr @malloc(i64 24)\n")
|
|
emith(" %isneg = icmp slt i32 %n0, 0\n %neg = sub i32 0, %n0\n")
|
|
emith(" %n = select i1 %isneg, i32 %neg, i32 %n0\n")
|
|
emith(" %term = getelementptr inbounds i8, ptr %buf, i32 23\n store i8 0, ptr %term\n")
|
|
emith(" %iszero = icmp eq i32 %n0, 0\n br i1 %iszero, label %zc, label %dl\n")
|
|
emith("zc:\n %zp = getelementptr inbounds i8, ptr %buf, i32 22\n store i8 48, ptr %zp\n ret ptr %zp\n")
|
|
emith("dl:\n br label %dloop\n")
|
|
emith("dloop:\n %pos = phi i32 [ 22, %dl ], [ %pos2, %dbody ]\n %cur = phi i32 [ %n, %dl ], [ %cur2, %dbody ]\n")
|
|
emith(" %done = icmp eq i32 %cur, 0\n br i1 %done, label %sign, label %dbody\n")
|
|
emith("dbody:\n %d = urem i32 %cur, 10\n %ch = add i32 %d, 48\n %ch8 = trunc i32 %ch to i8\n")
|
|
emith(" %pp = getelementptr inbounds i8, ptr %buf, i32 %pos\n store i8 %ch8, ptr %pp\n")
|
|
emith(" %cur2 = udiv i32 %cur, 10\n %pos2 = sub i32 %pos, 1\n br label %dloop\n")
|
|
emith("sign:\n br i1 %isneg, label %addneg, label %fin\n")
|
|
emith("addneg:\n %sp = getelementptr inbounds i8, ptr %buf, i32 %pos\n store i8 45, ptr %sp\n %posn = sub i32 %pos, 1\n br label %fin\n")
|
|
emith("fin:\n %fpos = phi i32 [ %pos, %sign ], [ %posn, %addneg ]\n")
|
|
emith(" %rpos = add i32 %fpos, 1\n %res = getelementptr inbounds i8, ptr %buf, i32 %rpos\n ret ptr %res\n}\n")
|
|
}
|
|
|
|
# s[a..b] -> a fresh NUL-terminated copy of the bytes [a, b), emitted (once) into
|
|
# any program that slices a string. Mallocs (b-a)+1, copies, terminates.
|
|
fn emit_str_slice() -> void {
|
|
emith("define ptr @fn_str_slice(ptr %s, i32 %start, i32 %end) {\n")
|
|
emith("entry:\n %len = sub i32 %end, %start\n %sz = add i32 %len, 1\n")
|
|
emith(" %sz64 = sext i32 %sz to i64\n %out = call ptr @malloc(i64 %sz64)\n br label %loop\n")
|
|
emith("loop:\n %i = phi i32 [ 0, %entry ], [ %i1, %body ]\n")
|
|
emith(" %d = icmp slt i32 %i, %len\n br i1 %d, label %body, label %fin\n")
|
|
emith("body:\n %si = add i32 %start, %i\n %sp = getelementptr inbounds i8, ptr %s, i32 %si\n %c = load i8, ptr %sp\n")
|
|
emith(" %op = getelementptr inbounds i8, ptr %out, i32 %i\n store i8 %c, ptr %op\n %i1 = add i32 %i, 1\n br label %loop\n")
|
|
emith("fin:\n %tp = getelementptr inbounds i8, ptr %out, i32 %len\n store i8 0, ptr %tp\n ret ptr %out\n}\n")
|
|
}
|