ludic/selfhost/backend/emit_head.ludic
Orkuncakilkaya a8d54e9878 fence (25.1): every allocation goes through the fence - sites, frame judging, census, callers
Every allocation the compiler emits goes through @lp_malloc/@lp_calloc/@lp_realloc/@lp_free, and a
Ludic-level one first stores its site (function, file, line, kind) in @lp_site. Off, that is one load
and a predictable branch (30 M allocations: 0.87-0.91 s against 0.87-0.90 s on leaks2).

On (the default in a headless build, and windowed under R3D_DEV), tracking starts at the first frame
on its own and judging once R3D_ALLOC_WARM frames in a row kept nothing (600) or R3D_ALLOC_WARM_MAX
after (re)start; Mem.play()/Mem.rewarm() sends a load back to its warm-up. A judged frame that ends
holding more than it began with is reported by site with its callers (the unwinder, taken only once
judging) and fails the run with exit 86 (R3D_ALLOC_FENCE=off|count|warn|fail). R3D_ALLOC_CENSUS
writes the totals and top sites at exit. The build's defaults are --fence=, --fence-warm=,
--fence-census= or a fence line in the program's package.ludic; the environment overrides them.

The runtime is IR (emit_fence_ir.ludic, generated from a template); tracking is a side table in one
calloc'd region, so no block carries a header and pointers crossing to natives stay safe. Examples
alloc_fence, alloc_fence_leak and alloc_fence_auto with cases in ludic-dev test; reseeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:35:29 +03:00

356 lines
17 KiB
Text

# emit_head.ludic — string constants and the module header (libc declarations,
# the slice header type, struct layouts, globals, argv, format strings).
function 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
function emit_str_const(s: pointer) -> pointer {
let name = `@.str{itoa(ll_str)}`
ll_str += 1
let n = len(s)
emith(name); emith(" = private unnamed_addr constant [")
emith(itoa(n + 1)); emith(" x i8] c\"")
emit_escaped(s)
emith("\\00\"\n")
return name
}
# the bytes of s as the body of an IR c"..." string: " \ and non-print -> \XX
function emit_escaped(s: pointer) -> void {
let n = len(s)
var i = 0
while i < n {
let c = s[i]
if c == '"' or c == CH_BACKSLASH or c < ' ' or c > '~' {
buf_putc(head, CH_BACKSLASH) # \XX hex escape
buf_putc(head, hexdig(c / 16))
buf_putc(head, hexdig(c % 16))
} else { buf_putc(head, c) }
i += 1
}
}
# an int literal's LLVM constant: its value, or a long literal's own digits
function int_lit_code(e: Node) -> pointer {
if e.s != null { return e.s }
return itoa(e.ival)
}
# the constant initializer for a global var: a literal, or 0/null
function global_init(d: Node) -> pointer {
if (d.a == null) or is_fp(d.ty) { return zero_of(llty(d.ty)) } # a float's value is set by L_init_globals
let e = d.a
if e.kind == E_INT { return int_lit_code(e) }
if 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 (("-") + int_lit_code(e.a)) }
if e.kind == E_MEMBER and e.a.kind == E_ID { # `Enum.Variant` is a compile-time int
let ord = enum_ordinal(e.a.s, e.s)
if ord >= 0 { return itoa(ord) }
}
if (llty(d.ty) == "ptr") { return "null" }
return "0"
}
# does a global's initializer need code at startup (anything global_init cannot fold)?
function global_needs_init_code(d: Node) -> bool {
if (d.a == null) { return false }
if is_fp(d.ty) { return true }
let e = d.a
if e.kind == E_INT or e.kind == E_FLOAT or e.kind == E_BOOL or e.kind == E_NULL { return false }
if e.kind == E_UN and (e.s == ("-")) and e.a.kind == E_INT { return false }
if e.kind == E_MEMBER and e.a.kind == E_ID { if enum_ordinal(e.a.s, e.s) >= 0 { return false } }
return true
}
# @L_init_globals(): evaluate every global initializer global_init could not fold
# (`var run: Progress = new Progress`, `var speed: int = BASE * 2`, a call), in
# declaration order, once at startup — after the runtime boots, before Start.
function emit_global_init_fn() -> void {
emit_init_fn("L_init_runtime", true)
emit_init_fn("L_init_globals", false)
}
# 0.S: the runtime's states are made first, before it boots (rt_init reads them); the rest after
function is_runtime_state_var(d: Node) -> bool { return d.uns == 1 and is_state_ty(d.ty) and is_runtime_file(d.file) }
function emit_init_fn(name: pointer, runtime: bool) -> void {
fence_enter(name)
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
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_VAR and global_needs_init_code(d) and is_runtime_state_var(d) == runtime {
# an initializer is the global's own file's code: its errors, and what its module
# may see (L3), are that file's - not whichever statement was lowered last
g_err_file = d.file
g_err_line = d.line
# a port's bind is written in the app and checked there (frontend/ports.ludic)
if is_port_var(d) {
g_err_file = d.a.file
g_err_line = d.a.line
g_vis_off = true
}
let v = emit_expr(d.a)
g_vis_off = false
let cv = coerce_code(v, d.ty) # any conversion is its own line, before the store
emit(" store "); emit(llty(d.ty)); emit(" "); emit(cv); emit(", ptr @g_"); emit(d.s); emit("\n")
}
i += 1
}
emit(" ret void\n")
code = saved
emit(`define void @{name}() {{\nentry:\n`)
emit(buf_str(falloc))
emit(buf_str(fbody))
emit("}\n\n")
}
function 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 @calloc(i64, i64)\n")
emith("declare ptr @realloc(ptr, i64)\n")
emith("declare void @free(ptr)\n")
emith("declare i64 @fread(ptr, i64, i64, ptr)\n")
emith("declare i64 @fwrite(ptr, i64, i64, ptr)\n")
emith("declare i32 @fclose(ptr)\n")
# on Windows these four are defined by emit_win_prelude, below
if not g_target_win {
emith("declare ptr @fopen(ptr, ptr)\n")
emith("declare i32 @fseek(ptr, i64, i32)\n")
emith("declare i64 @ftell(ptr)\n")
emith("declare i64 @time(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 ptr @memmove(ptr, ptr, i64)\n")
emith("declare i64 @strlen(ptr)\n")
emith("declare i32 @strcmp(ptr, ptr)\n")
emith("declare i32 @strncmp(ptr, ptr, i64)\n")
emith("declare ptr @strstr(ptr, ptr)\n")
emith("declare i32 @atoi(ptr)\n")
emith("declare i32 @getchar()\n")
emith("declare i32 @putchar(i32)\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("declare void @win_held(ptr)\n")
emith("declare i32 @win_key_char(i32)\n")
emith("declare i32 @win_text(ptr, i32)\n")
emith("declare void @win_mouse(ptr)\n")
emith("declare void @win_pad(ptr)\n")
emith("declare void @win_touch(ptr)\n")
emith("declare void @win_cursor_mode(i32)\n")
emith("declare ptr @snd_load(ptr)\n") # #22 audio backend (audio.ll)
emith("declare void @snd_play(ptr, i32, i32, i32)\n")
emith("declare void @snd_stop(ptr)\n")
emith("declare i32 @snd_playing(ptr)\n")
emith("declare void @snd_set_volume(ptr, i32)\n")
emith("declare void @snd_set_rate(ptr, i32)\n")
emith("declare void @snd_set_pan(ptr, i32)\n")
emith("declare ptr @hs_req_new(ptr, ptr)\n") # #6 HTTP transport (http.ll)
emith("declare void @hs_req_header(ptr, ptr, ptr)\n")
emith("declare void @hs_req_body(ptr, ptr, i32)\n")
emith("declare void @hs_send(i32, ptr)\n")
emith("declare i32 @hs_done(i32)\n")
emith("declare i32 @hs_status(i32)\n")
emith("declare ptr @hs_body(i32)\n")
emith("declare i32 @hs_blen(i32)\n")
emith("declare ptr @hs_header(i32, ptr)\n")
emith("declare void @hs_free(i32)\n")
if g_target_win { emit_win_prelude() } # the UCRT/Win32 definitions of the POSIX names above
else {
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_long = private unnamed_addr constant [6 x i8] c\"%lld\\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("@L_clock = internal global i32 0\n") # Clock.* — the game-controlled simulated clock
emith("@.gametitle = private unnamed_addr constant [")
emith(itoa(len(g_game_name) + 1)); emith(" x i8] c\""); emit_escaped(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 {
var j = 0 # the same record twice would silently use the first
while j < i {
if prog[j].kind == N_COMP and (prog[j].s == d.s) {
if (j < g_prog_user_end) and (i >= g_prog_user_end) { perr(`property {d.s} is also a property of the engine runtime; choose another name`) }
perr(`property {d.s} is declared twice`)
}
j += 1
}
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 += 1
}
emith(" }\n")
}
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 {
var j = 0 # the same name twice would be one LLVM global
while j < i {
if prog[j].kind == N_VAR and (prog[j].s == d.s) {
if (j < g_prog_user_end) and (i >= g_prog_user_end) { perr(`variable {d.s} is also a variable of the engine runtime; choose another name`) }
perr(`variable {d.s} is declared twice`)
}
j += 1
}
emith("@g_"); emith(d.s); emith(" = internal global ")
emith(llty(d.ty)); emith(" ")
emith(global_init(d))
emith("\n")
}
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.
function 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 += 1
}
emith(")\n")
}
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.
function emit_str_prelude() -> void {
emith("define i32 @lp_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 @lp_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 @lp_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 string(int)
# (string interpolation of a number). Writes digits from the end of a 24-byte
# buffer, prepends '-' for negatives, and moves them to the buffer's start.
function emit_int_str() -> void {
emith("define ptr @lp_int_str(i32 %n0) {\n")
emith("entry:\n %buf = call ptr @lp_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 store i8 48, ptr %buf\n %z1 = getelementptr inbounds i8, ptr %buf, i32 1\n store i8 0, ptr %z1\n ret ptr %buf\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")
# the digits to the buffer's start, so what is returned is what was malloc'd and can be freed
emith(" %cnt = sub i32 24, %rpos\n %cnt64 = sext i32 %cnt to i64\n call ptr @memmove(ptr %buf, ptr %res, i64 %cnt64)\n ret ptr %buf\n}\n")
}
# the i64 twin of fn_int_str: a signed 64-bit integer -> decimal text. Emitted
# once per program that stringifies a `long` (g_uses_longstr). A 64-bit value is
# at most 20 digits plus sign and NUL, so the 24-byte scratch buffer still fits.
function emit_long_str() -> void {
emith("define ptr @lp_long_str(i64 %n0) {\n")
emith("entry:\n %buf = call ptr @lp_malloc(i64 24)\n")
emith(" %isneg = icmp slt i64 %n0, 0\n %neg = sub i64 0, %n0\n")
emith(" %n = select i1 %isneg, i64 %neg, i64 %n0\n")
emith(" %term = getelementptr inbounds i8, ptr %buf, i32 23\n store i8 0, ptr %term\n")
emith(" %iszero = icmp eq i64 %n0, 0\n br i1 %iszero, label %zc, label %dl\n")
emith("zc:\n store i8 48, ptr %buf\n %z1 = getelementptr inbounds i8, ptr %buf, i32 1\n store i8 0, ptr %z1\n ret ptr %buf\n")
emith("dl:\n br label %dloop\n")
emith("dloop:\n %pos = phi i32 [ 22, %dl ], [ %pos2, %dbody ]\n %cur = phi i64 [ %n, %dl ], [ %cur2, %dbody ]\n")
emith(" %done = icmp eq i64 %cur, 0\n br i1 %done, label %sign, label %dbody\n")
emith("dbody:\n %d = urem i64 %cur, 10\n %d32 = trunc i64 %d to i32\n %ch = add i32 %d32, 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 i64 %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")
# the digits to the buffer's start, so what is returned is what was malloc'd and can be freed
emith(" %cnt = sub i32 24, %rpos\n %cnt64 = sext i32 %cnt to i64\n call ptr @memmove(ptr %buf, ptr %res, i64 %cnt64)\n ret ptr %buf\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.
function emit_str_slice() -> void {
emith("define ptr @lp_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 @lp_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")
}