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>
111 lines
5.2 KiB
Text
111 lines
5.2 KiB
Text
# 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).
|
|
function is_intrinsic(name: pointer) -> bool {
|
|
if (name == "resize") { return true }
|
|
if (name == "file_open") or (name == "file_read") or (name == "file_write") { return true }
|
|
if (name == "file_seek") or (name == "file_tell") or (name == "file_close") { return true }
|
|
if (name == "arg_count") or (name == "arg") or (name == "exit") { return true }
|
|
if (name == "file_stderr") or (name == "file_stdout") { return true }
|
|
if (name == "run") or (name == "getenv") { return true }
|
|
if (name == "world_despawn") { return true } # #84 — despawn an entity by id from a system
|
|
return false
|
|
}
|
|
# emit " <r> = <rest>\n" and return r
|
|
function emit_bind(rest: pointer) -> pointer { fence_bind(rest); let r = nreg(); emit(" "); emit(r); emit(" = "); emit(rest); emit("\n"); return r }
|
|
|
|
function arg_code(e: Node, i: int) -> pointer { let v = emit_expr(e.kids[i]); return v.code }
|
|
# a buffer handed to the platform layer: a slice's elements, never its header (as an extern's are)
|
|
function arg_buf(e: Node, i: int) -> pointer {
|
|
let v = emit_expr(e.kids[i])
|
|
if is_slice_ty(v.ty) { return emit_bind(`load ptr, ptr {slice_field(v.code, 0)}`) }
|
|
return v.code
|
|
}
|
|
|
|
function emit_intrinsic(name: pointer, e: Node) -> Val {
|
|
g_intrin_ok = true
|
|
# ptr_null / ptr_is_null are the `null` literal and `x == null` now.
|
|
# mem_alloc is bytes(n) / words(n) now (see emit_call).
|
|
if (name == "resize") {
|
|
let p = arg_code(e, 0); let n = arg_code(e, 1)
|
|
let w = emit_bind(`zext i32 {n} to i64`)
|
|
return val(emit_bind(`call ptr @lp_realloc(ptr {p}, i64 {w})`), "pointer")
|
|
}
|
|
# Every asset a Ludic program loads arrives through here — gltf_load, tex_load,
|
|
# Audio.load, Fs.read_text and the renderer's own shader loads all bottom out at
|
|
# file_open — so this one call is where a shipped game's asset pack is spliced
|
|
# in. @lp_pak_open serves the bytes from a mounted pack when the path is in one
|
|
# and falls through to @fopen when it is not, which is every open during
|
|
# development. See selfhost/backend/stdlib/emit_pak.ludic.
|
|
if (name == "file_open") {
|
|
use_pak()
|
|
let p = arg_code(e, 0); let m = arg_code(e, 1)
|
|
return val(emit_bind(`call ptr @lp_pak_open(ptr {p}, ptr {m})`), "pointer")
|
|
}
|
|
if (name == "file_read") or (name == "file_write") {
|
|
let f = arg_code(e, 0); let b = raw_expr(e.kids[1]).code; let n = arg_code(e, 2)
|
|
let w = emit_bind(`zext i32 {n} to i64`)
|
|
var fn2 = "@fread"
|
|
if (name == "file_write") { fn2 = "@fwrite" }
|
|
let r = emit_bind(`call i64 {fn2}(ptr {b}, i64 1, i64 {w}, ptr {f})`)
|
|
return val(emit_bind(`trunc i64 {r} to i32`), "int")
|
|
}
|
|
if (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(`sext i32 {off} to i64`)
|
|
return val(emit_bind(`call i32 @fseek(ptr {f}, i64 {o}, i32 {wh})`), "int")
|
|
}
|
|
if (name == "file_tell") {
|
|
let f = arg_code(e, 0)
|
|
let r = emit_bind(`call i64 @ftell(ptr {f})`)
|
|
return val(emit_bind(`trunc i64 {r} to i32`), "int")
|
|
}
|
|
if (name == "file_close") {
|
|
let f = arg_code(e, 0)
|
|
emit(" call i32 @fclose(ptr "); emit(f); emit(")\n")
|
|
return val("0", "void")
|
|
}
|
|
# #84 — despawn an entity by id (runs its @OnDespawn hooks + frees the slot),
|
|
# callable from a system (esys_bounds kill) or as World.despawn(e). The helper
|
|
# @fn_world_despawn is emitted with the ECS defs when g_uses_world_despawn.
|
|
if (name == "world_despawn") {
|
|
g_uses_world_despawn = true # emit @fn_world_despawn in the tail (after all uses seen)
|
|
let ent = arg_code(e, 0)
|
|
emit(" call void @fn_world_despawn(i32 "); emit(ent); emit(")\n")
|
|
return val("0", "void")
|
|
}
|
|
# print_str / print_int are the polymorphic `print(x)` builtin now (emit_call).
|
|
if (name == "arg_count") { return val(emit_bind("load i32, ptr @L_argc"), "int") }
|
|
if (name == "arg") {
|
|
let i = arg_code(e, 0)
|
|
let v = emit_bind("load ptr, ptr @L_argv")
|
|
let q = emit_bind(`getelementptr ptr, ptr {v}, i32 {i}`)
|
|
return val(emit_bind(`load ptr, ptr {q}`), "string")
|
|
}
|
|
if (name == "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 (name == "file_stderr") { return val(emit_bind(stdstream_rhs(2)), "pointer") }
|
|
if (name == "file_stdout") { return val(emit_bind(stdstream_rhs(1)), "pointer") }
|
|
if (name == "run") {
|
|
let c = arg_code(e, 0)
|
|
return val(emit_bind(`call i32 @system(ptr {c})`), "int")
|
|
}
|
|
if (name == "getenv") {
|
|
let n = arg_code(e, 0)
|
|
return val(emit_bind(`call ptr @getenv(ptr {n})`), "string")
|
|
}
|
|
# bitwise ops are the operators & | ^ << >> ~ now (see emit_bin / p_mul).
|
|
# peek32 / poke32 are `words` indexing now: w[i] and w[i] = v (see emit_index_addr).
|
|
g_intrin_ok = false
|
|
return val("0", "void")
|
|
}
|