ludic/selfhost/backend/emit_mem.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

48 lines
2.4 KiB
Text

# emit_mem.ludic — the Memory.* namespace: raw buffers and byte pokes. bytes/words
# allocate (as the bare builtins do); copy/fill wrap memcpy/memset; peek/poke
# read and write one byte. The low-level escape hatch, PICO-8's memcpy/memset/
# peek/poke by another name.
function is_mem_ns(meth: pointer) -> bool {
if (meth == "bytes") or (meth == "words") { return true }
if (meth == "copy") or (meth == "fill") or (meth == "peek") or (meth == "poke") { return true }
return false
}
function emit_mem_ns(meth: pointer, e: Node) -> Val {
if (meth == "bytes") { # allocate n bytes -> a byte buffer
let n = emit_expr(e.kids[0])
let w = emit_bind(`zext i32 {n.code} to i64`)
return val(emit_bind(`call ptr @lp_malloc(i64 {w})`), "pointer")
}
if (meth == "words") { # allocate n 32-bit words
let n = emit_expr(e.kids[0])
let by = emit_bind(`mul i32 {n.code}, 4`)
let w = emit_bind(`zext i32 {by} to i64`)
return emit_sized_slice("int", n)
}
if (meth == "copy") { # copy n bytes src -> dst
let dst = raw_expr(e.kids[0]); let src = raw_expr(e.kids[1]); let n = emit_expr(e.kids[2])
let w = emit_bind(`zext i32 {n.code} to i64`)
emit(" call ptr @memcpy(ptr "); emit(dst.code); emit(", ptr "); emit(src.code); emit(", i64 "); emit(w); emit(")\n")
return val("0", "void")
}
if (meth == "fill") { # set n bytes of buf to value v
let buf = raw_expr(e.kids[0]); let v = emit_expr(e.kids[1]); let n = emit_expr(e.kids[2])
let w = emit_bind(`zext i32 {n.code} to i64`)
emit(" call ptr @memset(ptr "); emit(buf.code); emit(", i32 "); emit(v.code); emit(", i64 "); emit(w); emit(")\n")
return val("0", "void")
}
if (meth == "peek") { # read one byte at buf[i], 0..255
let buf = raw_expr(e.kids[0]); let i = emit_expr(e.kids[1])
let p = emit_bind(`getelementptr inbounds i8, ptr {buf.code}, i32 {i.code}`)
let c = emit_bind(`load i8, ptr {p}`)
return val(emit_bind(`zext i8 {c} to i32`), "int")
}
# poke: write the low byte of v at buf[i]
let buf = raw_expr(e.kids[0]); let i = emit_expr(e.kids[1]); let v = emit_expr(e.kids[2])
let p = emit_bind(`getelementptr inbounds i8, ptr {buf.code}, i32 {i.code}`)
let b = emit_bind(`trunc i32 {v.code} to i8`)
emit(" store i8 "); emit(b); emit(", ptr "); emit(p); emit("\n")
return val("0", "void")
}