fence: every runtime call is its line's site, and site 0 comes back when it returns

A block took whatever @lp_site held when it was made. Only malloc, calloc, realloc, concat, the number
texts, the float text, a substring and Text.* set one, so every other runtime helper - intern,
Text.repeat and the string builders, the Fs, Os, unicode, uuid and crypto helpers - was charged to
whichever line had allocated last (walk 9 blamed gvk_tex_storage, m4_new, kept_push$int and
survey_op_reward for intern's 16 B copies).

- Any `call ptr @lp_*` now takes a site of its own, its kind the callee's name when no better one is
  known (intern, str_repeat, fs_list, ...). What the helper makes, in however many blocks and
  through whichever helpers it calls in turn, is that line's.
- When the call returns, @lp_site goes back to site 0, now named "(runtime) (no site) unsited": a
  block made with no site of its own says so instead of borrowing the last one.
- The ECS stores' grows and a mod's registered stores - the only allocations emitted outside
  emit_bind - take a site each.

Golden alloc_fence_sites: two lines take turns keeping memory, Text.repeat and a record, every frame
judged. Each report names its own line (25 x +32 B str_repeat at :11, 25 x +16 B new Box at :12);
the toolchain before this charges 23 of the 32 B texts to the new Box line. The other fence and arena
goldens are unchanged; ludic.base's tests pass; the game's frame ratchets are 0 on main 7cb2b164.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 00:40:28 +03:00
parent 48a8caa292
commit 6ab98292d2
8 changed files with 98453 additions and 79775 deletions

View file

@ -812,6 +812,18 @@ function alloc_fence_rewarm_case() -> void {
if (rc == 86) and (head == "alloc-fence: frame 3000 kept +16 B (1 made, 0 freed)") { ok(lbl) } else { bad2(lbl, `rc={string(rc)} head=[{head}]`) }
}
function alloc_fence_sites_case() -> void {
let lbl = "alloc_fence_sites.ludic (two lines take turns keeping memory, one through a runtime helper; each report charges its own line, never the one that allocated last)"
let b = `{tmp_dir()}/e_alloc_fence_sites`
if not game_build_ok("bin/ludicc", "examples/lang/alloc_fence_sites.ludic", b) { bad2(lbl, "did not build"); return }
let e = `{tmp_dir()}/afs.err`
sh(`R3D_ALLOC_FENCE=warn R3D_ALLOC_WARM_MAX=10 {b} < /dev/null > /dev/null 2>{e}`)
let texts = capture_line(`grep -c '+32 B entry examples/lang/alloc_fence_sites.ludic:11 str_repeat' {e}`)
let boxes = capture_line(`grep -c '+16 B entry examples/lang/alloc_fence_sites.ludic:12 new Box' {e}`)
let wrong = capture_line(`grep -c '+32 B entry examples/lang/alloc_fence_sites.ludic:12' {e}`)
if (texts == "25") and (boxes == "25") and (wrong == "0") { ok(lbl) } else { bad2(lbl, `texts={texts} boxes={boxes} charged-to-the-box-line={wrong}`) }
}
function alloc_fence_leak_case() -> void {
let lbl = "alloc_fence_leak.ludic (a frame that keeps a record fails the run with exit 86 and names its line; count mode runs on)"
let b = `{tmp_dir()}/e_alloc_fence_leak`
@ -1176,6 +1188,7 @@ function cmd_dev_test() -> int {
alloc_fence_leak_case()
alloc_fence_unbounded_case()
alloc_fence_rewarm_case()
alloc_fence_sites_case()
alloc_fence_auto_case()
arena_pool_case()
arena_fnval_case()