frame allocs: a click is not a frame, a reducer is its dispatch's, a capped push is bounded; ludic.photo at 0

The analysis made every component function a frame root, event handlers (cmp_x_on_delete) too, and
every reducer, whether its action is dispatched every frame or once a trip: a component's 'on'
handlers are no longer roots, and a dispatch is an edge to its action's reducers, so a reducer
counts only when frame code dispatches it. A push into a field declared @max(n) is bounded by the
fence's own check and no longer counted. Maroon Lake: frame_allocs 395 -> 337, frame_keeps 188 -> 169.

ludic.photo: the roll's order and a page of it are kept lists refilled in place (the pack's page
asked for both every frame), its kept lists say @max(256), and a shot's tags, a photograph's fact
and a new roll are declared (once per shot, sale or trip). 18 allocs and 9 keeps -> 0 and 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 20:02:47 +03:00
parent 5be2422c84
commit 3b9b4a589b
7 changed files with 58578 additions and 57763 deletions

View file

@ -304,6 +304,14 @@ function dr_walk(n: Node, refs: []int) -> void {
return
}
if dr_reg_field(n, refs) { return }
if n.kind == E_NEW and n.b != null and n.s != null { # 25.2: a dispatch reaches its action's reducers
let pre = `ludic_reduce__{n.s}__`
var ri = 0
while ri < len(g_dr_name) {
if str_starts(g_dr_name[ri], pre) { push(refs, ri) }
ri += 1
}
}
if n.kind == S_EMIT and n.s != null { # 25.2: an emit reaches its event's listeners
var li = 0
while li < len(g_onlisten) {

View file

@ -27,6 +27,27 @@ function fr_has_aok(n: Node) -> bool {
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_capped(t: Node) -> bool {
if t == null or t.kind != E_MEMBER or t.s == null { return false }
let tail = `.{t.s}`
var i = 0
while i < len(g_cap_keys) {
let k = g_cap_keys[i]
if len(k) > len(tail) and (k[len(k) - len(tail) .. len(k)] == tail) { return true }
i += 1
}
return false
}
function fr_is_event_handler(name: pointer) -> bool {
var i = 0
while i + 4 <= len(name) {
if name[i] == '_' and name[i + 1] == 'o' and name[i + 2] == 'n' and name[i + 3] == '_' { return true }
i += 1
}
return false
}
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")
}
@ -96,7 +117,11 @@ function fr_alloc_kind(n: Node, text_above: bool) -> pointer {
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 == "push") {
# a push into a field declared @max(n) grows to n at most, and the fence fails the run past it
if len(n.kids) > 0 and fr_capped(n.kids[0]) { return null }
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
@ -150,8 +175,9 @@ function deps_frame(f: pointer) -> void {
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) }
if d.kind == N_FN and d.s != null and d.cm >= 0 { fr_mark_root(d.s) } # a component's own: it runs while its page is open
# a component's own functions run while its page is open - but not its event handlers (`on click`),
# which run when the player does something; a reducer is reached from its action's dispatch
if d.kind == N_FN and d.s != null and d.cm >= 0 and not fr_is_event_handler(d.s) { fr_mark_root(d.s) }
fr_find_roots(d)
i += 1
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff