# 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_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") } # `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") { # 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 } # 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 } if g_arena and n.uns == ES_SCRATCH { # the frame's arena takes it: made and gone with the frame return fr_allocs_kids(f, n, k) } 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 } function fr_allocs_kids(f: pointer, n: Node, k: int) -> int { var found = 0 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 { if g_arena { escape_analyse() } # with the arena on, what it takes is not counted 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) } # 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 } 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_fnode[fi].s != null 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 fm = `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")` if g_diag_json { diag_add(d.file, site.line, site.col, "error", fm) } else { let m = `{d.file}:{itoa(site.line)}: error: {fm}\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 { diag_exit(1) } deps_births(f, seen) deps_resources(f) # 25.5e deps_owned(f) } 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 { # what boot reaches: the Start handlers and the entry, before any frame let boot = new []bool let queue = new []int var b = 0 while b < len(g_dr_node) { push(boot, false); b += 1 } b = 0 while b < len(g_dr_node) { let bd = g_dr_node[b] if (bd.kind == N_SYS and (bd.ty == "Start")) or bd.kind == N_MAIN { boot[b] = true push(queue, b) } b += 1 } var qh = 0 while qh < len(queue) { let refs = g_dr_refs[queue[qh]] qh += 1 var r = 0 while r < len(refs) { if not boot[refs[r]] { boot[refs[r]] = true push(queue, refs[r]) } r += 1 } } 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_fnode[fi].s != null 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] # a scratch site is the arena's whenever the game's frames are running - a frame's code, a click # handler, a reducer - and the heap's only before the first frame: boot's code let scratch = g_arena and site.uns == ES_SCRATCH # boot's too: the arena starts at its first use 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)}`) }