frame allocs (25.2): what a frame can come to allocate, counted and ratcheted
deps_reach's graph gains the handlers and each @On body (an emit reaches its event's listeners).
Roots: a handler in a frame phase, every reducer, an @On body, and a function stored as a System's
tick. Every allocating construct in what they reach - new, a list literal, push (grow), text built
by + or a template, words/floats/buffer/bytes - is a falloc line with the shortest chain from a
root (root>..>last six), and the program's count is frame_allocs. @alloc_ok("why") on a function or
a handler takes it and what only it reaches out; the reason is required. ludic deps --allocs lists
them, and frame_allocs is a number --check ratchets. Maroon Lake starts at 2661.
Not yet: @frame on a step list's field (only 'tick' is a root field so far), statement-level
@alloc_ok, the three lints.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
691964877b
commit
76b1bd20ae
7 changed files with 82112 additions and 76450 deletions
|
|
@ -304,6 +304,16 @@ function dr_walk(n: Node, refs: []int) -> void {
|
|||
return
|
||||
}
|
||||
if dr_reg_field(n, refs) { return }
|
||||
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) {
|
||||
if (g_onlisten[li].s == n.s) {
|
||||
let lk = dr_index(`@On {n.s}#{itoa(li)}`)
|
||||
if lk >= 0 { push(refs, lk) }
|
||||
}
|
||||
li += 1
|
||||
}
|
||||
}
|
||||
if (n.kind == E_ID or n.kind == E_FNREF) and n.s != null {
|
||||
let k = dr_index(n.s)
|
||||
if k >= 0 { push(refs, k) }
|
||||
|
|
@ -430,7 +440,7 @@ function deps_reach(f: pointer) -> void {
|
|||
var i = 0
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
if d.s != null and (d.kind == N_FN or d.kind == N_VAR or d.kind == N_CONST) and ck_tab_get(g_dr_find_k, g_dr_find_v, d.s) == null {
|
||||
if d.s != null and (d.kind == N_FN or d.kind == N_VAR or d.kind == N_CONST or d.kind == N_SYS) and ck_tab_get(g_dr_find_k, g_dr_find_v, d.s) == null {
|
||||
let h = new Node
|
||||
h.ival = len(g_dr_node)
|
||||
ck_tab_put(g_dr_find_k, g_dr_find_v, d.s, h)
|
||||
|
|
@ -439,6 +449,17 @@ function deps_reach(f: pointer) -> void {
|
|||
}
|
||||
i += 1
|
||||
}
|
||||
# 25.2: each @On body is a node, reached from every `emit` of its event (dr_walk)
|
||||
var li = 0
|
||||
while li < len(g_onlisten) {
|
||||
let nm = `@On {g_onlisten[li].s}#{itoa(li)}`
|
||||
let h = new Node
|
||||
h.ival = len(g_dr_node)
|
||||
ck_tab_put(g_dr_find_k, g_dr_find_v, nm, h)
|
||||
push(g_dr_name, nm)
|
||||
push(g_dr_node, g_onlisten[li])
|
||||
li += 1
|
||||
}
|
||||
let nw = dr_words()
|
||||
var j = 0
|
||||
while j < len(g_dr_node) {
|
||||
|
|
@ -493,4 +514,5 @@ function deps_reach(f: pointer) -> void {
|
|||
}
|
||||
j += 1
|
||||
}
|
||||
deps_frame(f) # 25.2: what a frame can come to allocate
|
||||
}
|
||||
|
|
|
|||
164
selfhost/backend/emit_frame.ludic
Normal file
164
selfhost/backend/emit_frame.ludic
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
# 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_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")
|
||||
}
|
||||
|
||||
function fr_is_frame_field(name: pointer) -> bool {
|
||||
if (name == "tick") { return true }
|
||||
var i = 0
|
||||
while i < len(g_frame_fields) { if (g_frame_fields[i] == name) { return true }; i += 1 }
|
||||
return false
|
||||
}
|
||||
|
||||
function fr_alloc_ok(name: pointer) -> bool {
|
||||
var i = 0
|
||||
while i < len(g_alloc_ok_names) { if (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_REC and n.kids != null {
|
||||
var i = 0
|
||||
while i < len(n.kids) {
|
||||
let f = n.kids[i]
|
||||
if f.s != null and fr_is_frame_field(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 { return `new:{n.s}` }
|
||||
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 }
|
||||
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)}`)
|
||||
}
|
||||
|
|
@ -925,6 +925,7 @@ function parse_one_decl() -> void {
|
|||
var esys_phase: pointer = null
|
||||
var ns_name: pointer = null # @Namespace(Name) — a package-provided Foo.* namespace (#62)
|
||||
var is_det = false # @deterministic — no floating point inside (emit_float.ludic)
|
||||
var is_aok = false # @alloc_ok("why") — not counted by 25.2's frame allocations
|
||||
while is_op("@") {
|
||||
pi += 1; let a = eat_id() # collect a leading @annotation
|
||||
if a == "export" { is_export = true }
|
||||
|
|
@ -953,6 +954,13 @@ function parse_one_decl() -> void {
|
|||
else if a == "EngineSystem" { eat_op("("); esys_comp = eat_id(); eat_op(","); esys_phase = eat_id(); eat_op(")") } # @EngineSystem(Comp, Phase) package engine system (#62)
|
||||
else if a == "Namespace" { eat_op("("); ns_name = eat_id(); eat_op(")") } # @Namespace(Name) package Foo.* namespace (#62)
|
||||
else if a == "deterministic" { is_det = true }
|
||||
else if a == "alloc_ok" { # 25.2: @alloc_ok("why") - this allocates in a frame on purpose
|
||||
eat_op("(")
|
||||
let why = expr()
|
||||
if why.kind != E_STR or len(why.s) == 0 { perr("@alloc_ok needs its reason: @alloc_ok(\"a memo miss, bounded by MM_CAP\")") }
|
||||
eat_op(")")
|
||||
is_aok = true
|
||||
}
|
||||
else if a == "ClearColor" { # @ClearColor(colour) — Render auto-clear + auto-present (#86)
|
||||
eat_op("(")
|
||||
g_clear_color = expr() # a literal, a `const`, or a Color.Name — resolved when emitted
|
||||
|
|
@ -1024,6 +1032,7 @@ function parse_one_decl() -> void {
|
|||
if is_id("handler") {
|
||||
let h = parse_system()
|
||||
if is_det { push(g_det_names, h.s) }
|
||||
if is_aok { push(g_alloc_ok_names, h.s) }
|
||||
if (role != null) { if (role == "server") { h.ival = 1 } else { h.ival = 2 } } # N5: @Server=1 / @Predicted=2
|
||||
if (on_event != null) { register_onlisten(on_event, h.a); return } # @On(Event) listener
|
||||
if (onspawn_model != null) {
|
||||
|
|
@ -1094,6 +1103,7 @@ function parse_one_decl() -> void {
|
|||
let f = parse_fn()
|
||||
if is_unsafe { f.uns = 1 }
|
||||
if is_det { push(g_det_names, f.s) }
|
||||
if is_aok { push(g_alloc_ok_names, f.s) }
|
||||
if is_export { f.ival = 1 }
|
||||
if (sys_phase != null) { push(g_mod_sys_fn, f.s); push(g_mod_sys_phase, sys_phase) } # #64: register at load
|
||||
if (esys_comp != null) { push(g_esys_comp, esys_comp); push(g_esys_fn, f.s); push(g_esys_phase, esys_phase) } # #62: package engine system
|
||||
|
|
|
|||
79183
selfhost/ludicc.seed.ll
79183
selfhost/ludicc.seed.ll
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
|
@ -16,6 +16,7 @@
|
|||
# ludic deps --widest N the N functions that take the most states, and what each reaches
|
||||
# ludic deps --reach N the same, the N that reach the most
|
||||
# ludic deps --wreach N the N that can come to CHANGE the most states
|
||||
# ludic deps --allocs every allocation a frame can come to (25.2), with its chain from a root
|
||||
#
|
||||
# dependencies: module pairs (a, b) where a uses something of b, not counting b when b uses no
|
||||
# other module (a leaf); largest_cycle: the largest set of modules that all reach each other, not
|
||||
|
|
@ -48,6 +49,8 @@ var dp_fn_at: []pointer = null # each of the program's own functions: whe
|
|||
var dp_fn_w: []int = null
|
||||
var dp_fn_r: []int = null
|
||||
var dp_fn_x: []int = null # ... and the states it can come to change
|
||||
var dp_fallocs: []pointer = new []pointer # 25.2: `falloc <kind> <file:line> <chain>`, what a frame can allocate
|
||||
var dp_frame_allocs: int = 0
|
||||
var dp_wreach_n: int = 0
|
||||
var dp_wreach_at: pointer = ""
|
||||
|
||||
|
|
@ -86,6 +89,7 @@ function dp_load(path: pointer) -> bool {
|
|||
dp_reach_n = 0; dp_reach_at = ""; dp_reach_own = 0
|
||||
dp_fn_at = new []pointer; dp_fn_w = new []int; dp_fn_r = new []int; dp_fn_x = new []int
|
||||
dp_wreach_n = 0; dp_wreach_at = ""
|
||||
dp_fallocs = new []pointer; dp_frame_allocs = 0
|
||||
dp_writes = new []pointer; dp_aliases = new []pointer; dp_wowner = new []pointer; dp_wname = new []pointer; dp_wfrom = new []pointer
|
||||
let lines = split_lines(text)
|
||||
for i in 0 .. len(lines) {
|
||||
|
|
@ -107,6 +111,8 @@ function dp_load(path: pointer) -> bool {
|
|||
push(dp_en, w[4])
|
||||
}
|
||||
if len(w) >= 6 and w[0] == "alias" { push(dp_aliases, lines[i]) }
|
||||
if len(w) >= 4 and w[0] == "falloc" { push(dp_fallocs, lines[i]) }
|
||||
if len(w) == 2 and w[0] == "frame_allocs" { dp_frame_allocs = s_to_int(w[1]) }
|
||||
if len(w) >= 5 and w[0] == "width" and w[4] == "0" and s_to_int(w[1]) > dp_wide_n {
|
||||
dp_wide_n = s_to_int(w[1])
|
||||
dp_wide_at = `{dp_fn_name(w[2])} ({w[3]})`
|
||||
|
|
@ -306,7 +312,7 @@ var dp_vals: []int = null
|
|||
var dp_cycle: []int = null
|
||||
var dp_cycle_all: []int = null # the same with the layers' own edges counted
|
||||
function dp_numbers() -> void {
|
||||
dp_names = ["modules", "dependencies", "largest_cycle", "cross_writes", "globals_written_from_outside", "widest_function", "widest_reach", "widest_write_reach"]
|
||||
dp_names = ["modules", "dependencies", "largest_cycle", "cross_writes", "globals_written_from_outside", "widest_function", "widest_reach", "widest_write_reach", "frame_allocs"]
|
||||
var mods = 0
|
||||
for k in 0 .. len(dp_mods) { if dp_own(k) { mods += 1 } }
|
||||
var deps = 0
|
||||
|
|
@ -328,7 +334,7 @@ function dp_numbers() -> void {
|
|||
}
|
||||
}
|
||||
dp_reach_best()
|
||||
dp_vals = [mods, deps, len(dp_cycle), writes, len(seen), dp_wide_n, dp_reach_n, dp_wreach_n]
|
||||
dp_vals = [mods, deps, len(dp_cycle), writes, len(seen), dp_wide_n, dp_reach_n, dp_wreach_n, dp_frame_allocs]
|
||||
}
|
||||
|
||||
# ---- the command --------------------------------------------------------------
|
||||
|
|
@ -482,6 +488,7 @@ function cmd_deps() -> int {
|
|||
if a == "--dot" { mode = "dot" }
|
||||
else if a == "--graph" { mode = "graph" }
|
||||
else if a == "--writes" { mode = "writes" }
|
||||
else if a == "--allocs" { mode = "allocs" }
|
||||
else if a == "--uses" or a == "--check" or a == "--baseline" or a == "--widest" or a == "--reach" or a == "--wreach" {
|
||||
if ai + 1 >= arg_count() { err(`ludic deps: {a} needs an argument\n`); return 2 }
|
||||
mode = sslice(a, 2, slen(a))
|
||||
|
|
@ -523,6 +530,14 @@ function cmd_deps() -> int {
|
|||
return 0
|
||||
}
|
||||
if mode == "uses" { dp_uses_of(arg2); return 0 }
|
||||
if mode == "allocs" { # 25.2: every allocation a frame can come to, with its chain
|
||||
for a in 0 .. len(dp_fallocs) {
|
||||
let w = dp_words(dp_fallocs[a])
|
||||
print(`frame alloc: {w[2]} {w[1]} {w[3]}`)
|
||||
}
|
||||
print(`frame_allocs: {string(dp_frame_allocs)}`)
|
||||
return 0
|
||||
}
|
||||
if mode == "widest" { dp_top(s_to_int(arg2), 0); return 0 }
|
||||
if mode == "reach" { dp_top(s_to_int(arg2), 1); return 0 }
|
||||
if mode == "wreach" { dp_top(s_to_int(arg2), 2); return 0 }
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ function selfhost_frags() -> []pointer {
|
|||
push(f, "selfhost/backend/emit_core.ludic")
|
||||
push(f, "selfhost/backend/emit_fence.ludic")
|
||||
push(f, "selfhost/backend/emit_fence_ir.ludic")
|
||||
push(f, "selfhost/backend/emit_frame.ludic")
|
||||
push(f, "selfhost/backend/emit_head.ludic")
|
||||
push(f, "selfhost/backend/emit_deps.ludic")
|
||||
push(f, "selfhost/backend/emit_addr.ludic")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue