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>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 15:35:29 +03:00
parent fc0f3d3790
commit a8d54e9878
34 changed files with 106868 additions and 82677 deletions

View file

@ -164,13 +164,13 @@ function emit_os_prelude() -> void {
# concatenate two NUL-terminated strings into a fresh malloc'd buffer
emith("define ptr @lp_os_join2(ptr %a, ptr %b) {\n")
emith("entry:\n %la = call i64 @strlen(ptr %a)\n %lb = call i64 @strlen(ptr %b)\n")
emith(" %sum = add i64 %la, %lb\n %tot = add i64 %sum, 1\n %m = call ptr @malloc(i64 %tot)\n")
emith(" %sum = add i64 %la, %lb\n %tot = add i64 %sum, 1\n %m = call ptr @lp_malloc(i64 %tot)\n")
emith(" call ptr @memcpy(ptr %m, ptr %a, i64 %la)\n")
emith(" %m2 = getelementptr i8, ptr %m, i64 %la\n call ptr @memcpy(ptr %m2, ptr %b, i64 %lb)\n")
emith(" %end = getelementptr i8, ptr %m, i64 %sum\n store i8 0, ptr %end\n ret ptr %m\n}\n")
emith("define ptr @lp_os_join3(ptr %a, ptr %b, ptr %c) {\n")
emith("entry:\n %ab = call ptr @lp_os_join2(ptr %a, ptr %b)\n %r = call ptr @lp_os_join2(ptr %ab, ptr %c)\n call void @free(ptr %ab)\n ret ptr %r\n}\n")
emith("entry:\n %ab = call ptr @lp_os_join2(ptr %a, ptr %b)\n %r = call ptr @lp_os_join2(ptr %ab, ptr %c)\n call void @lp_free(ptr %ab)\n ret ptr %r\n}\n")
# Windows has its own known folders and no uname; emit_win.ludic supplies them
if g_target_win { emit_os_win_known(k_dot, k_slash); return }
@ -204,9 +204,9 @@ function emit_os_prelude() -> void {
emith("define i32 @lp_os_is_darwin() {\n")
emith("entry:\n %c = load i32, ptr @L_os_darwin\n %known = icmp sge i32 %c, 0\n br i1 %known, label %done, label %probe\n")
emith("done:\n ret i32 %c\n")
emith("probe:\n %buf = call ptr @malloc(i64 8192)\n call i32 @uname(ptr %buf)\n")
emith("probe:\n %buf = call ptr @lp_malloc(i64 8192)\n call i32 @uname(ptr %buf)\n")
emith(` %cd = call i32 @strncmp(ptr %buf, ptr {k_darw}, i64 6)\n %isd = icmp eq i32 %cd, 0\n`)
emith(" %r = select i1 %isd, i32 1, i32 0\n call void @free(ptr %buf)\n store i32 %r, ptr @L_os_darwin\n ret i32 %r\n}\n")
emith(" %r = select i1 %isd, i32 1, i32 0\n call void @lp_free(ptr %buf)\n store i32 %r, ptr @L_os_darwin\n ret i32 %r\n}\n")
# $<var> when it is set and non-empty, else $HOME<rel>; the result always ends in '/'
emith("define ptr @lp_os_xdg(ptr %var, ptr %rel) {\n")
@ -219,7 +219,7 @@ function emit_os_prelude() -> void {
emith("define ptr @lp_os_known(ptr %app, ptr %mac, ptr %var, ptr %rel) {\n")
emith("entry:\n %d = call i32 @lp_os_is_darwin()\n %isd = icmp ne i32 %d, 0\n br i1 %isd, label %m, label %x\n")
emith("m:\n %h = call ptr @lp_os_home()\n %r1 = call ptr @lp_os_join3(ptr %h, ptr %mac, ptr %app)\n ret ptr %r1\n")
emith("x:\n %b = call ptr @lp_os_xdg(ptr %var, ptr %rel)\n %r2 = call ptr @lp_os_join2(ptr %b, ptr %app)\n call void @free(ptr %b)\n ret ptr %r2\n}\n")
emith("x:\n %b = call ptr @lp_os_xdg(ptr %var, ptr %rel)\n %r2 = call ptr @lp_os_join2(ptr %b, ptr %app)\n call void @lp_free(ptr %b)\n ret ptr %r2\n}\n")
emith(`define ptr @lp_os_save_dir(ptr %app) {{\n %r = call ptr @lp_os_known(ptr %app, ptr {k_appsp}, ptr {k_xdg_data}, ptr {k_dot_share})\n ret ptr %r\n}}\n`)
emith(`define ptr @lp_os_config_dir(ptr %app) {{\n %r = call ptr @lp_os_known(ptr %app, ptr {k_appsp}, ptr {k_xdg_cfg}, ptr {k_dot_config})\n ret ptr %r\n}}\n`)
@ -230,7 +230,7 @@ function emit_os_prelude() -> void {
# snapshot the caller may iterate or index like any other []string.
emith("define ptr @lp_os_args() {\n")
emith("entry:\n %c = load i32, ptr @L_argc\n %v = load ptr, ptr @L_argv\n")
emith(" %h = call ptr @malloc(i64 16)\n")
emith(" %h = call ptr @lp_malloc(i64 16)\n")
emith(" %d0 = getelementptr inbounds %LSlice, ptr %h, i32 0, i32 0\n store ptr %v, ptr %d0\n")
emith(" %d1 = getelementptr inbounds %LSlice, ptr %h, i32 0, i32 1\n store i32 %c, ptr %d1\n")
emith(" %d2 = getelementptr inbounds %LSlice, ptr %h, i32 0, i32 2\n store i32 %c, ptr %d2\n")
@ -242,7 +242,7 @@ function emit_os_prelude() -> void {
emith("define ptr @lp_os_uts() {\n")
emith("entry:\n %c = load ptr, ptr @L_os_uts\n %have = icmp ne ptr %c, null\n br i1 %have, label %done, label %make\n")
emith("done:\n ret ptr %c\n")
emith("make:\n %buf = call ptr @malloc(i64 8192)\n call i32 @uname(ptr %buf)\n store ptr %buf, ptr @L_os_uts\n ret ptr %buf\n}\n")
emith("make:\n %buf = call ptr @lp_malloc(i64 8192)\n call i32 @uname(ptr %buf)\n store ptr %buf, ptr @L_os_uts\n ret ptr %buf\n}\n")
# platform(): uname sysname (field 0, portable) mapped to a short id
emith("define ptr @lp_os_platform() {\n")