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>
32 lines
1.1 KiB
Text
32 lines
1.1 KiB
Text
# alloc_fence.ludic - the allocation fence (docs/plan 25.1) judges frames once play has settled.
|
|
# Everything a frame makes here is gone by the frame's end or goes on being reused: text only
|
|
# compared (freed once compared), records held from before play written in place. No frame
|
|
# keeps anything, so the run - which fails by default when one does - prints
|
|
# `frames 300 kept 0 bad 0`.
|
|
program AllocFence {
|
|
property Box { a: int = 0 }
|
|
|
|
function round(i: int, kept: []Box) -> int {
|
|
var bad = 0
|
|
if `{i}-{i * 2}` != string(i) + "-" + string(i * 2) { bad += 1 }
|
|
if `lake {i}` == "" { bad += 1 }
|
|
for k in 0 .. len(kept) { kept[k].a = i + k } # records reused: List.clear and new ones would keep the old
|
|
return bad
|
|
}
|
|
|
|
entry {
|
|
var kept = new []Box
|
|
for k in 0 .. 8 { push(kept, new Box) }
|
|
Mem.play()
|
|
Mem.settled()
|
|
var bad = 0
|
|
var frames = 0
|
|
for i in 0 .. 300 {
|
|
bad += round(i, kept)
|
|
Mem.frame()
|
|
frames += 1
|
|
}
|
|
let kept_now = Mem.kept() # read before the line below makes its text
|
|
print(`frames {frames} kept {kept_now} bad {Mem.bad_frames()}`)
|
|
}
|
|
}
|