A function marked @creates(PhysShape) makes a handle one marked @releases(PhysShape) gives back. ludic deps --resources lists every creating call whose handle is thrown away, or bound to a local that is never released, passed on, stored or returned, and resource_drops is a number --check ratchets. A test: a thrown-away create and one bound and never handed on are the two found; one stored in a state and one released are not. A record's owned fields and a borrow form (a shape used by several scaled ones) are the second half, with a resource type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
267 lines
10 KiB
Text
267 lines
10 KiB
Text
# emit_frame.ludic — 25.2: what a frame can come to allocate. The roots are the code that runs every
|
|
# frame - a handler in a frame phase, every reducer (the drain runs after each phase), an `@On` body,
|
|
# a function stored as a System's `tick`, and one stored in a field whose type marks it `@frame`
|
|
# (a step list) - and every allocating construct in what they reach through deps_reach's graph is a
|
|
# `falloc` line with the shortest chain from a root. A function under `@alloc_ok("why")` counts
|
|
# nothing and is not followed. ludic deps counts the lines (frame_allocs) and ratchets them.
|
|
|
|
var g_alloc_ok_names: []pointer = new []pointer # functions and handlers under @alloc_ok("why")
|
|
var g_frame_fields: []pointer = new []pointer # record fields declared `@frame`
|
|
var g_aok_pending: bool = false # @alloc_ok read, its function or handler not yet
|
|
const FR_AOK_STMT: int = 78 # Node.uns on a statement under @alloc_ok("why")
|
|
var g_aok_seen: bool = false # any statement is: bodies are searched only then
|
|
|
|
function fr_aok_stmt(n: Node) -> bool { return n.uns == FR_AOK_STMT and n.kind != E_ID }
|
|
|
|
# does this body hold a statement under @alloc_ok? (its function then keeps the scope's depth)
|
|
function fr_has_aok(n: Node) -> bool {
|
|
if n == null or not g_aok_seen { return false }
|
|
if fr_aok_stmt(n) { return true }
|
|
if fr_has_aok(n.a) or fr_has_aok(n.b) or fr_has_aok(n.c) { return true }
|
|
if n.kids != null {
|
|
var i = 0
|
|
while i < len(n.kids) { if fr_has_aok(n.kids[i]) { return true }; i += 1 }
|
|
}
|
|
return false
|
|
}
|
|
var g_fr_root: []bool = new []bool
|
|
var g_fr_from: []int = new []int # the node a reached one was first reached from
|
|
|
|
function fr_phase_frame(ph: pointer) -> bool {
|
|
return (ph == "Input") or (ph == "FixedUpdate") or (ph == "Update") or (ph == "LateUpdate") or (ph == "Render") or (ph == "Overlay")
|
|
}
|
|
|
|
# `tick` is a System's, by any name's record; any other field is a frame root only where its own
|
|
# property says `@frame` (Prop.field), so a step list of loads with a `run` field is not one
|
|
function fr_is_frame_field(name: pointer) -> bool { return (name == "tick") }
|
|
function fr_is_frame_prop_field(prop: pointer, name: pointer) -> bool {
|
|
if (name == "tick") { return true }
|
|
let key = `{prop}.{name}`
|
|
var i = 0
|
|
while i < len(g_frame_fields) { if (g_frame_fields[i] == key) { return true }; i += 1 }
|
|
return false
|
|
}
|
|
|
|
# a generic's instance (kept_push$NetFact) is under its declaration's @alloc_ok
|
|
function fr_alloc_ok(name: pointer) -> bool {
|
|
if str_starts(name, "ludic_act_") { return true } # the action queue's own kept records, grown to its high-water mark
|
|
var base = name
|
|
var d = 0
|
|
while d < len(name) {
|
|
if name[d] == '$' { base = name[0..d]; d = len(name) }
|
|
d += 1
|
|
}
|
|
var i = 0
|
|
while i < len(g_alloc_ok_names) { if (g_alloc_ok_names[i] == base) or (g_alloc_ok_names[i] == name) { return true }; i += 1 }
|
|
return false
|
|
}
|
|
|
|
function fr_mark_root(name: pointer) -> void {
|
|
let k = dr_index(name)
|
|
if k >= 0 { g_fr_root[k] = true }
|
|
}
|
|
|
|
# `x.tick = fn f`, `tick: fn f` in a record, and the same for an @frame field: f is a root
|
|
function fr_find_roots(n: Node) -> void {
|
|
if n == null { return }
|
|
if n.kind == S_ASSIGN and n.a != null and n.a.kind == E_MEMBER and n.a.s != null and fr_is_frame_field(n.a.s) and n.b != null and n.b.kind == E_FNREF { fr_mark_root(n.b.s) }
|
|
if n.kind == E_NEW and n.s != null and n.a != null and n.a.kind == E_REC and n.a.kids != null {
|
|
var i = 0
|
|
while i < len(n.a.kids) {
|
|
let f = n.a.kids[i]
|
|
if f.s != null and fr_is_frame_prop_field(n.s, f.s) and f.a != null and f.a.kind == E_FNREF { fr_mark_root(f.a.s) }
|
|
i += 1
|
|
}
|
|
}
|
|
fr_find_roots(n.a)
|
|
fr_find_roots(n.b)
|
|
fr_find_roots(n.c)
|
|
if n.kids != null {
|
|
var j = 0
|
|
while j < len(n.kids) { fr_find_roots(n.kids[j]); j += 1 }
|
|
}
|
|
}
|
|
|
|
function fr_is_textish(n: Node) -> bool {
|
|
if n == null { return false }
|
|
if n.kind == E_STR { return true }
|
|
if n.kind == E_CALL and n.a != null and n.a.kind == E_ID and (n.a.s == "string") { return true }
|
|
if n.kind == E_BIN and (n.s == "+") { return fr_is_textish(n.a) or fr_is_textish(n.b) }
|
|
return false
|
|
}
|
|
|
|
function fr_alloc_kind(n: Node, text_above: bool) -> pointer {
|
|
if n.kind == E_NEW and n.b == null { return `new:{n.s}` } # a dispatch's `new` fills the queue's kept record (n.b)
|
|
if n.kind == E_LIST { return "list" }
|
|
if n.kind == E_BIN and (n.s == "+") and not text_above and fr_is_textish(n) { return "text" }
|
|
if n.kind == E_CALL and n.a != null and n.a.kind == E_ID and n.a.s != null {
|
|
let c = n.a.s
|
|
if (c == "push") { return "grow" }
|
|
if (c == "words") or (c == "floats") or (c == "doubles") or (c == "buffer") or (c == "bytes") or (c == "fixeds") or (c == "pointers") { return `{c}()` }
|
|
}
|
|
return null
|
|
}
|
|
|
|
# the allocating constructs in one declaration, each a falloc line with its chain
|
|
function fr_allocs(f: pointer, n: Node, k: int, text_above: bool) -> int {
|
|
if n == null { return 0 }
|
|
if fr_aok_stmt(n) { return 0 }
|
|
var found = 0
|
|
let kind = fr_alloc_kind(n, text_above)
|
|
if kind != null {
|
|
deps_line(f, `falloc {kind} {g_dr_node[k].file}:{itoa(n.line)} {fr_chain(k)}`)
|
|
found += 1
|
|
}
|
|
let text = (n.kind == E_BIN) and (n.s == "+") and fr_is_textish(n)
|
|
found += fr_allocs(f, n.a, k, text)
|
|
found += fr_allocs(f, n.b, k, text)
|
|
found += fr_allocs(f, n.c, k, false)
|
|
if n.kids != null {
|
|
var i = 0
|
|
while i < len(n.kids) { found += fr_allocs(f, n.kids[i], k, false); i += 1 }
|
|
}
|
|
return found
|
|
}
|
|
|
|
# root > ... > this declaration, by the first way it was reached: the root, then the last six
|
|
function fr_chain(k: int) -> pointer {
|
|
var s = g_dr_name[k]
|
|
var at = g_fr_from[k]
|
|
var steps = 0
|
|
var root = k
|
|
while at >= 0 {
|
|
if steps < 6 { s = `{g_dr_name[at]}>{s}` }
|
|
root = at
|
|
at = g_fr_from[at]
|
|
steps += 1
|
|
}
|
|
if steps > 6 { s = `{g_dr_name[root]}>..>{s}` }
|
|
return s
|
|
}
|
|
|
|
function deps_frame(f: pointer) -> void {
|
|
let n = len(g_dr_node)
|
|
g_fr_root = new []bool
|
|
g_fr_from = new []int
|
|
let seen = new []bool
|
|
var i = 0
|
|
while i < n { push(g_fr_root, false); push(g_fr_from, -1); push(seen, false); i += 1 }
|
|
i = 0
|
|
while i < len(prog) {
|
|
let d = prog[i]
|
|
if d.kind == N_SYS and d.s != null and fr_phase_frame(d.ty) { fr_mark_root(d.s) }
|
|
if d.kind == N_FN and d.s != null and str_starts(d.s, "ludic_reduce__") { fr_mark_root(d.s) }
|
|
fr_find_roots(d)
|
|
i += 1
|
|
}
|
|
var j = 0
|
|
while j < len(g_onlisten) {
|
|
fr_mark_root(`@On {g_onlisten[j].s}#{itoa(j)}`)
|
|
j += 1
|
|
}
|
|
# breadth first from every root, so a chain is the shortest
|
|
let queue = new []int
|
|
i = 0
|
|
while i < n {
|
|
if g_fr_root[i] and not fr_alloc_ok(g_dr_name[i]) { seen[i] = true; push(queue, i) }
|
|
i += 1
|
|
}
|
|
var qh = 0
|
|
while qh < len(queue) {
|
|
let k = queue[qh]
|
|
qh += 1
|
|
let refs = g_dr_refs[k]
|
|
var r = 0
|
|
while r < len(refs) {
|
|
let t = refs[r]
|
|
if not seen[t] and not fr_alloc_ok(g_dr_name[t]) {
|
|
seen[t] = true
|
|
g_fr_from[t] = k
|
|
push(queue, t)
|
|
}
|
|
r += 1
|
|
}
|
|
}
|
|
var total = 0
|
|
i = 0
|
|
while i < len(queue) {
|
|
let k = queue[i]
|
|
let d = g_dr_node[k]
|
|
if (d.kind == N_FN or d.kind == N_SYS or d.kind == N_BLOCK) and d.file != null and not is_runtime_file(d.file) { total += fr_allocs(f, d.a, k, false) }
|
|
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) }
|
|
deps_births(f, seen)
|
|
deps_resources(f) # 25.5e
|
|
}
|
|
|
|
function fr_consumed(n: Node) -> bool {
|
|
var i = 0
|
|
while i < len(g_es_consumed) { if g_es_consumed[i] == n { return true }; i += 1 }
|
|
return false
|
|
}
|
|
|
|
# 25.2's leak-at-birth: an allocation nothing keeps, made where the arena does not take it - outside
|
|
# frame code (a boot, a load), in a function spanning frames, or anywhere with the arena off - is
|
|
# made and dropped, never given back. `fbirth` lines, and the birth_leaks number
|
|
function deps_births(f: pointer, seen: []bool) -> void {
|
|
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]
|
|
let site = g_es_site[s]
|
|
if c >= 0 and fi >= 0 and (g_es_flag[c] & (ES_ESC | ES_FREED)) == 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)
|
|
let inframe = k >= 0 and seen[k]
|
|
let scratch = g_arena and site.uns == ES_SCRATCH and inframe # before the first frame the arena is not running
|
|
if not scratch and d.file != null and not is_runtime_file(d.file) and not fr_alloc_ok(d.s) and not fr_consumed(site) {
|
|
var kind = fr_alloc_kind(site, false)
|
|
if kind == null { kind = "text" }
|
|
var where = "outside-a-frame"
|
|
if inframe { where = fr_chain(k) }
|
|
deps_line(f, `fbirth {kind} {d.file}:{itoa(site.line)} {d.s} {where}`)
|
|
n += 1
|
|
}
|
|
}
|
|
s += 1
|
|
}
|
|
deps_line(f, `birth_leaks {itoa(n)}`)
|
|
}
|