region rule (25.3c): keep() and intern(), frame_keeps, and --arena-strict

keep(x) copies a string, a slice (header and elements) or a record (shallow) onto the heap; intern(s)
hands back one heap string per distinct text from a fixed table in the runtime (FNV-1a, 65536 slots,
copied the first time; past 49152 only copied). Both are how frame code keeps what it made on purpose:
the escape analysis takes the copy as the heap's and leaves the argument LOCAL.

The analysis now records why a class escapes (the store, the event, the global it reached) and
ludic deps lists every allocation frame code makes and keeps - fkeep lines, 'ludic deps --keeps',
the frame_keeps number --check ratchets - leaving out what is under @alloc_ok and a push's growth
(25.5's capacities). --arena-strict (or 'arena strict') makes each an error naming the store, before
anything is emitted. A test: a template stored into a state is the one error; keep and intern of the
next two, an @alloc_ok push and a scratch temporary are not; 195 frames of arena resets under
R3D_ARENA_CHECK=1 later the kept and interned texts read as made, and intern gives the same string.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 16:33:53 +03:00
parent cd71da9330
commit a0b030290b
10 changed files with 80528 additions and 76677 deletions

View file

@ -190,4 +190,40 @@ function deps_frame(f: pointer) -> void {
i += 1
}
deps_line(f, `frame_allocs {itoa(total)}`)
deps_keeps(f, seen)
}
var g_arena_strict: bool = false # --arena-strict / `arena strict`: a keep in frame code is an error
# 25.3's region rule: an allocation in frame code that is kept past its frame (stored into a state,
# a queue, a global, an event) is a `fkeep` line - and under strict an error naming the store.
# keep(...) / intern(...) is the copy on purpose; @alloc_ok("why") says it is bounded
function deps_keeps(f: pointer, seen: []bool) -> void {
escape_analyse()
var n = 0
var s = 0
while s < len(g_es_site) {
let c = g_es_site_cls[s]
let fi = g_es_site_fn[s]
if c >= 0 and fi >= 0 and (g_es_flag[c] & ES_ESC) != 0 and not g_es_site_aok[s] and not g_es_site_grow[s] {
let d = g_es_fnode[fi]
let k = dr_index(d.s)
if k >= 0 and seen[k] and not fr_alloc_ok(d.s) and d.file != null and not is_runtime_file(d.file) {
let site = g_es_site[s]
let why = g_es_why[c]
var at = "?"
if why != null and why.file != null { at = `{why.file}:{itoa(why.line)}` }
var kind = fr_alloc_kind(site, false)
if kind == null { kind = "text" }
deps_line(f, `fkeep {kind} {d.file}:{itoa(site.line)} {at} {fr_chain(k)}`)
if g_arena_strict {
let m = `{d.file}:{itoa(site.line)}: error: frame code keeps what it makes: this {kind} is kept at {at} ({fr_chain(k)}) - keep(...) or intern(...) to copy it on purpose, or @alloc_ok("why")\n`
file_write(file_stderr(), m, len(m))
}
n += 1
}
}
s += 1
}
deps_line(f, `frame_keeps {itoa(n)}`)
if g_arena_strict and n > 0 { exit(1) }
}