escape (fix): kept memory is HEAP, so what is pushed through an alias of it is kept

render3d's stream_new holds its pool through a local (`let live = s.chunks; push(live, new
Chunk)`): the flow edge from s.chunks to live carried ESC to nothing, the Chunk records were
LOCAL, and the arena reset them under the stream - the row and horse scenarios' crash at
0xdddd... in fn_stream_update. An ESC class is now HEAP too, so every alias of kept memory is,
and a value stored through it is kept. Bidirectional alias edges were tried first and over-kept
through returns (el_place, rim).

- examples/lang/arena_alias.ludic: the stream_new shape; poisoned it read 3 3000, now 3 1518
- examples/modules/alloc_ok_private.ludic: @alloc_ok on a module's private function and on a
  statement in its private generic, declared at run time (it already passes: a guard)
- the game: frame_allocs, frame_keeps, owned_leaks 0; the lab builds under `arena strict`; the row
  scenario poisoned (R3D_ARENA_CHECK=1, 2400 frames) runs clean, bad 0 kept 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 21:15:32 +03:00
parent 4d3ecfb72c
commit de6d78bef5
7 changed files with 45874 additions and 45571 deletions

View file

@ -0,0 +1,28 @@
# arena_alias.ludic - a record whose list is filled through a local alias (`let live = s.cells`, as
# render3d's stream_new fills its chunk pool) and which a pool then keeps. After `let x = y`, x and y
# are one piece of memory: what is stored through x is kept when y is. Built with --arena and
# poisoned on every reset it prints `3 1518`, as the heap does.
program ArenaAlias {
property Cell { v: int = 0 }
property Holder { cells: []Cell = new []Cell }
property Pool { all: []Holder = new []Holder }
function make(p: Pool, f: int) -> void {
let s = new Holder
let live = s.cells
for i in 0 .. 3 { push(live, new Cell { v: f + i }) }
push(p.all, s)
}
function step(p: Pool, f: int) -> void { if f == 500 - 1 or f == 5 { make(p, f) } }
entry {
let p = new Pool
for f in 0 .. 500 {
step(p, f)
Mem.frame()
}
var sum = 0
for i in 0 .. len(p.all[0].cells) { sum += p.all[0].cells[i].v }
let h = p.all[len(p.all) - 1]
for i in 0 .. len(h.cells) { sum += h.cells[i].v }
print(`{len(p.all[0].cells)} {sum}`)
}
}

View file

@ -0,0 +1,16 @@
# alloc_ok_private.ludic - 25.1: @alloc_ok on a module's PRIVATE function, and on a statement in its
# private generic, is declared at run time like an exported one's (the module rules rename a private
# name, and the fence once lost the declaration with it). The failing fence lets it through:
# bad 0 kept 0
import "memo"
program AllocOkPrivate {
entry {
let m = new Memo
for f in 0 .. 900 {
if f > 650 and f % 25 == 0 { memo_miss(m, f) }
Mem.frame()
}
let kept_now = Mem.kept()
print(`bad {Mem.bad_frames()} kept {kept_now}`)
}
}

View file

@ -0,0 +1,22 @@
# memo/index.ludic - a module whose @alloc_ok helpers are private: renamed by the module rules, they
# must still be declared at run time (alloc_ok_private.ludic)
module memo
export property Rule { parts: []string = null }
export property Memo { rules: []Rule = new []Rule, ints: []int = new []int }
function kept<T>(xs: []T, v: T) -> void {
@alloc_ok("a kept list: grows to the most it held")
push(xs, v)
}
@alloc_ok("a memo miss")
function split(s: string) -> []string {
let out = new []string
push(out, s[0 .. 1])
push(out, s[1 .. 2])
return out
}
export function memo_miss(m: Memo, f: int) -> void {
@alloc_ok("the memo keeps a rule per text")
let r = new Rule { parts: split(`ab{f}`) }
kept(m.rules, r)
kept(m.ints, f)
}

View file

@ -527,6 +527,13 @@ function es_solve() -> void {
} }
i += 1 i += 1
} }
# kept memory outlives the frame: an ESC class is HEAP too, so every alias of it (let x = y,
# a load, a call's result) is, and what is stored through the alias is kept
i = 0
while i < len(g_es_flag) {
if (g_es_flag[i] & ES_ESC) != 0 and (g_es_flag[i] & ES_HEAP) == 0 { g_es_flag[i] = g_es_flag[i] | ES_HEAP; changed = true }
i += 1
}
i = 0 i = 0
while i < len(g_es_sv) { while i < len(g_es_sv) {
let v = g_es_sv[i] let v = g_es_sv[i]

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -835,6 +835,14 @@ function arena_fnval_case() -> void {
if (poisoned == "5 1053") { ok(lbl) } else { bad2(lbl, `poisoned [{poisoned}]`) } if (poisoned == "5 1053") { ok(lbl) } else { bad2(lbl, `poisoned [{poisoned}]`) }
} }
function arena_alias_case() -> void {
let lbl = "arena_alias.ludic (--arena: what is pushed through an alias of kept memory (let live = s.cells) is kept; 0xDD poison changes nothing)"
let b = `{tmp_dir()}/e_arena_alias`
if not shq(`bin/ludicc --arena examples/lang/arena_alias.ludic -o {b} 2>/dev/null`) { bad2(lbl, "did not build"); return }
let poisoned = capture_line(`R3D_ARENA_CHECK=1 R3D_ALLOC_FENCE=off {b} < /dev/null`)
if (poisoned == "3 1518") { ok(lbl) } else { bad2(lbl, `poisoned [{poisoned}]`) }
}
function arena_pool_case() -> void { function arena_pool_case() -> void {
let lbl = "arena_pool.ludic (--arena: a generic push into a kept pool keeps its records and its buffer on the heap; 0xDD poison changes nothing)" let lbl = "arena_pool.ludic (--arena: a generic push into a kept pool keeps its records and its buffer on the heap; 0xDD poison changes nothing)"
let b = `{tmp_dir()}/e_arena_pool` let b = `{tmp_dir()}/e_arena_pool`
@ -1146,6 +1154,8 @@ function cmd_dev_test() -> int {
alloc_fence_auto_case() alloc_fence_auto_case()
arena_pool_case() arena_pool_case()
arena_fnval_case() arena_fnval_case()
arena_alias_case()
feat_case("modules/alloc_ok_private", "", "bad 0 kept 0", "alloc_ok_private.ludic (25.1: @alloc_ok on a module's private function and its private generic's statement is declared at run time)")
arena_defaults_case() arena_defaults_case()
feat_case("lang/nested_templates", "", "outer [inner 3 {x}] \"`}\" end", "nested_templates.ludic (a template literal inside another's hole)") feat_case("lang/nested_templates", "", "outer [inner 3 {x}] \"`}\" end", "nested_templates.ludic (a template literal inside another's hole)")
feat_case("lang/aliases", "", "10 5 3", "aliases.ludic (L6: a namespace method declared as an alias of a function, labelled or by its parameters)") feat_case("lang/aliases", "", "10 5 3", "aliases.ludic (L6: a namespace method declared as an alias of a function, labelled or by its parameters)")