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

@ -18,6 +18,13 @@ const ES_ESC: int = 1
const ES_HEAP: int = 2
var g_es_flag: []int = new []int
var g_es_why: []Node = new []Node # per class: the statement that made it escape
var g_es_swhy: []Node = new []Node # per store edge: its statement
var g_es_stmt: Node = null # the statement being walked
var g_es_aok: int = 0 # inside a statement under @alloc_ok
var g_es_site_aok: []bool = new []bool # the site is (not reported by the region rule)
var g_es_site_grow: []bool = new []bool # the site is a push's growth (25.5's capacities, not the rule)
var g_es_done: bool = false
var g_es_ft: []int = new []int # flow edges
var g_es_ff: []int = new []int
var g_es_st: []int = new []int # store edges: value, target
@ -38,15 +45,20 @@ var g_es_lc: []int = new []int
var g_es_cur: int = -1
var g_es_heap: int = -1 # the one class for globals, states and the unknown
function es_new(flags: int) -> int { push(g_es_flag, flags); return len(g_es_flag) - 1 }
function es_new(flags: int) -> int { push(g_es_flag, flags); push(g_es_why, null); return len(g_es_flag) - 1 }
function es_flow(a: int, b: int) -> void { if a >= 0 and b >= 0 { push(g_es_ff, a); push(g_es_ft, b) } }
function es_store(v: int, t: int) -> void {
if v < 0 { return }
if t < 0 { g_es_flag[v] = g_es_flag[v] | ES_ESC; return }
if t < 0 { es_escape(v); return }
push(g_es_sv, v)
push(g_es_st, t)
push(g_es_swhy, g_es_stmt)
}
function es_escape(v: int) -> void {
if v < 0 { return }
g_es_flag[v] = g_es_flag[v] | ES_ESC
if g_es_why[v] == null { g_es_why[v] = g_es_stmt }
}
function es_escape(v: int) -> void { if v >= 0 { g_es_flag[v] = g_es_flag[v] | ES_ESC } }
function es_local(name: pointer) -> int {
var i = len(g_es_ln) - 1
@ -71,6 +83,8 @@ function es_site(n: Node) -> int {
push(g_es_site, n)
push(g_es_site_cls, c)
push(g_es_site_fn, g_es_cur)
push(g_es_site_aok, g_es_aok > 0)
push(g_es_site_grow, false)
return c
}
@ -156,11 +170,15 @@ function es_call(e: Node) -> int {
es_args_walk(e)
return es_site(e)
}
if (nm == "keep") or (nm == "intern") {
es_args_walk(e)
return g_es_heap
}
if (nm == "push") and len(e.kids) >= 2 {
let t = es_val(e.kids[0])
es_store(es_val(e.kids[1]), t)
# the grown buffer is the list's own
push(g_es_site, e); push(g_es_site_cls, t); push(g_es_site_fn, g_es_cur)
push(g_es_site, e); push(g_es_site_cls, t); push(g_es_site_fn, g_es_cur); push(g_es_site_aok, g_es_aok > 0); push(g_es_site_grow, true)
return -1
}
if (nm == "words") or (nm == "floats") or (nm == "doubles") or (nm == "buffer") or (nm == "bytes") or (nm == "fixeds") or (nm == "pointers") {
@ -228,7 +246,18 @@ function es_call(e: Node) -> int {
function es_walk(n: Node) -> void {
if n == null { return }
if n.uns == FR_AOK_STMT and n.kind != E_ID {
g_es_aok += 1
es_walk_in(n)
g_es_aok -= 1
return
}
es_walk_in(n)
}
function es_walk_in(n: Node) -> void {
let k = n.kind
if k == S_LET or k == S_ASSIGN or k == S_RETURN or k == S_EXPR or k == S_EMIT { g_es_stmt = n }
if k == S_LET {
let c = es_bind_local(n.s)
es_flow(es_val(n.a), c)
@ -293,14 +322,22 @@ function es_solve() -> void {
let a = g_es_ff[i]
let b = g_es_ft[i]
if (g_es_flag[a] & ES_HEAP) != 0 and (g_es_flag[b] & ES_HEAP) == 0 { g_es_flag[b] = g_es_flag[b] | ES_HEAP; changed = true }
if (g_es_flag[b] & ES_ESC) != 0 and (g_es_flag[a] & ES_ESC) == 0 { g_es_flag[a] = g_es_flag[a] | ES_ESC; changed = true }
if (g_es_flag[b] & ES_ESC) != 0 and (g_es_flag[a] & ES_ESC) == 0 {
g_es_flag[a] = g_es_flag[a] | ES_ESC
g_es_why[a] = g_es_why[b]
changed = true
}
i += 1
}
i = 0
while i < len(g_es_sv) {
let v = g_es_sv[i]
let t = g_es_st[i]
if (g_es_flag[t] & (ES_ESC | ES_HEAP)) != 0 and (g_es_flag[v] & ES_ESC) == 0 { g_es_flag[v] = g_es_flag[v] | ES_ESC; changed = true }
if (g_es_flag[t] & (ES_ESC | ES_HEAP)) != 0 and (g_es_flag[v] & ES_ESC) == 0 {
g_es_flag[v] = g_es_flag[v] | ES_ESC
g_es_why[v] = g_es_swhy[i]
changed = true
}
i += 1
}
}
@ -322,6 +359,8 @@ function es_solve() -> void {
var g_es_local_n: int = 0
var g_es_esc_n: int = 0
function escape_analyse() -> void {
if g_es_done { return }
g_es_done = true
g_es_heap = es_new(ES_ESC | ES_HEAP)
ck_tab_init(g_es_fk, g_es_fv)
var i = 0
@ -400,3 +439,38 @@ function escape_report() -> void {
let tot = `escape: {itoa(g_es_local_n)} local, {itoa(g_es_esc_n)} kept\n`
file_write(file_stderr(), tot, len(tot))
}
# keep(x): x copied onto the heap - a string's bytes, a slice's header and elements, a record's
# fields (shallow) - so a frame's value can become a state's on purpose (25.3's region rule)
function emit_keep(a: Val) -> Val {
if (a.ty == "string") {
let n = emit_bind(`call i64 @strlen(ptr {a.code})`)
let n1 = emit_bind(`add i64 {n}, 1`)
let p = emit_bind(`call ptr @lp_malloc(i64 {n1})`)
emit(` call ptr @memcpy(ptr {p}, ptr {a.code}, i64 {n1})\n`)
return val(p, "string")
}
if is_slice_ty(a.ty) {
let esz = emit_sizeof(llty(slice_elem(a.ty)))
let hsz = emit_sizeof("%LSlice")
let h = emit_bind(`call ptr @lp_malloc(i64 {hsz})`)
let od = emit_bind(`load ptr, ptr {slice_field(a.code, 0)}`)
let ln = emit_bind(`load i32, ptr {slice_field(a.code, 1)}`)
let ln64 = emit_bind(`zext i32 {ln} to i64`)
let bytes = emit_bind(`mul i64 {ln64}, {esz}`)
let b1 = emit_bind(`add i64 {bytes}, 1`)
let d = emit_bind(`call ptr @lp_malloc(i64 {b1})`)
emit(` call ptr @memcpy(ptr {d}, ptr {od}, i64 {bytes})\n`)
emit(` store ptr {d}, ptr {slice_field(h, 0)}\n`)
emit(` store i32 {ln}, ptr {slice_field(h, 1)}\n`)
emit(` store i32 {ln}, ptr {slice_field(h, 2)}\n`)
return val(h, a.ty)
}
if layout_node(a.ty) != null {
let sz = emit_sizeof(layout_ty(a.ty))
let p = emit_bind(`call ptr @lp_malloc(i64 {sz})`)
emit(` call ptr @memcpy(ptr {p}, ptr {a.code}, i64 {sz})\n`)
return val(p, a.ty)
}
return a
}