resources (25.5e, first half): @creates(Kind) / @releases(Kind), and resource_drops

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>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 16:54:27 +03:00
parent 1cc507e181
commit e009ea313b
7 changed files with 80045 additions and 77146 deletions

View file

@ -228,6 +228,7 @@ function deps_keeps(f: pointer, seen: []bool) -> void {
deps_line(f, `frame_keeps {itoa(n)}`) deps_line(f, `frame_keeps {itoa(n)}`)
if g_arena_strict and n > 0 { exit(1) } if g_arena_strict and n > 0 { exit(1) }
deps_births(f, seen) deps_births(f, seen)
deps_resources(f) # 25.5e
} }
function fr_consumed(n: Node) -> bool { function fr_consumed(n: Node) -> bool {

View file

@ -0,0 +1,100 @@
# emit_resource.ludic — 25.5e: a resource (a Jolt shape, a body, a GPU buffer) is a handle a function
# @creates(Kind) makes and one @releases(Kind) gives back. A handle made and forgotten - the result of
# a creating call thrown away, or bound to a local that is never released, passed on, stored or
# returned - is never given back: an `rdrop` line, and resource_drops, which ludic deps ratchets.
# (Two owners and a record's owned fields - the rest of 25.5e - wait on a resource type.)
var g_res_make_fn: []pointer = new []pointer
var g_res_make_kind: []pointer = new []pointer
var g_res_free_fn: []pointer = new []pointer
var g_res_free_kind: []pointer = new []pointer
var g_res_pending_make: pointer = null
var g_res_pending_free: pointer = null
function res_kind_made_by(name: pointer) -> pointer {
var i = 0
while i < len(g_res_make_fn) { if (g_res_make_fn[i] == name) { return g_res_make_kind[i] }; i += 1 }
return null
}
# the creating call an expression is, if it is one
function res_made(e: Node) -> pointer {
if e == null or e.kind != E_CALL or e.a == null or e.a.kind != E_ID or e.a.s == null { return null }
return res_kind_made_by(e.a.s)
}
# is the local `name` handed on anywhere in n: an argument, a stored or returned value, an assignment's
# right side (anything but a read into a comparison or an arithmetic)
function res_handed(n: Node, name: pointer) -> bool {
if n == null { return false }
if n.kind == E_CALL and n.kids != null {
var i = 0
while i < len(n.kids) {
let a = n.kids[i]
if a != null and a.kind == E_ID and (a.s == name) { return true }
i += 1
}
}
if (n.kind == S_RETURN or n.kind == S_ASSIGN) {
var v = n.a
if n.kind == S_ASSIGN { v = n.b }
if v != null and v.kind == E_ID and (v.s == name) { return true }
}
if n.kind == E_REC and n.kids != null {
var j = 0
while j < len(n.kids) {
let f = n.kids[j]
if f.a != null and f.a.kind == E_ID and (f.a.s == name) { return true }
j += 1
}
}
if n.kind == E_LIST and n.kids != null {
var l = 0
while l < len(n.kids) { if n.kids[l].kind == E_ID and (n.kids[l].s == name) { return true }; l += 1 }
}
if res_handed(n.a, name) or res_handed(n.b, name) or res_handed(n.c, name) { return true }
if n.kids != null {
var k = 0
while k < len(n.kids) { if res_handed(n.kids[k], name) { return true }; k += 1 }
}
return false
}
var g_res_drops: int = 0
function res_walk(f: pointer, d: Node, n: Node) -> void {
if n == null { return }
if n.kind == S_EXPR and n.a != null {
let kind = res_made(n.a)
if kind != null {
deps_line(f, `rdrop {kind} {d.file}:{itoa(n.line)} {d.s} thrown-away`)
g_res_drops += 1
}
}
if n.kind == S_LET and n.s != null {
let kind = res_made(n.a)
if kind != null and not res_handed(d.a, n.s) {
deps_line(f, `rdrop {kind} {d.file}:{itoa(n.line)} {d.s} {n.s}-never-handed-on`)
g_res_drops += 1
}
}
res_walk(f, d, n.a)
res_walk(f, d, n.b)
res_walk(f, d, n.c)
if n.kids != null {
var i = 0
while i < len(n.kids) { res_walk(f, d, n.kids[i]); i += 1 }
}
}
function deps_resources(f: pointer) -> void {
g_res_drops = 0
if len(g_res_make_fn) > 0 {
var i = 0
while i < len(prog) {
let d = prog[i]
if (d.kind == N_FN or d.kind == N_SYS) and d.file != null and not is_runtime_file(d.file) { res_walk(f, d, d.a) }
i += 1
}
}
deps_line(f, `resource_drops {itoa(g_res_drops)}`)
}

View file

@ -971,6 +971,12 @@ function parse_one_decl() -> void {
eat_op(")") eat_op(")")
g_aok_pending = true # a global: `export` parses the declaration one call down g_aok_pending = true # a global: `export` parses the declaration one call down
} }
else if a == "creates" or a == "releases" { # 25.5e: @creates(PhysShape) / @releases(PhysShape)
eat_op("(")
let kind = eat_id()
eat_op(")")
if a == "creates" { g_res_pending_make = kind } else { g_res_pending_free = kind }
}
else if a == "ClearColor" { # @ClearColor(colour) — Render auto-clear + auto-present (#86) else if a == "ClearColor" { # @ClearColor(colour) — Render auto-clear + auto-present (#86)
eat_op("(") eat_op("(")
g_clear_color = expr() # a literal, a `const`, or a Color.Name — resolved when emitted g_clear_color = expr() # a literal, a `const`, or a Color.Name — resolved when emitted
@ -1114,6 +1120,8 @@ function parse_one_decl() -> void {
if is_unsafe { f.uns = 1 } if is_unsafe { f.uns = 1 }
if is_det { push(g_det_names, f.s) } if is_det { push(g_det_names, f.s) }
if g_aok_pending { push(g_alloc_ok_names, f.s); g_aok_pending = false } if g_aok_pending { push(g_alloc_ok_names, f.s); g_aok_pending = false }
if g_res_pending_make != null { push(g_res_make_fn, f.s); push(g_res_make_kind, g_res_pending_make); g_res_pending_make = null }
if g_res_pending_free != null { push(g_res_free_fn, f.s); push(g_res_free_kind, g_res_pending_free); g_res_pending_free = null }
if is_export { f.ival = 1 } 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 (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 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

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -19,6 +19,7 @@
# ludic deps --allocs every allocation a frame can come to (25.2), with its chain from a root # ludic deps --allocs every allocation a frame can come to (25.2), with its chain from a root
# ludic deps --keeps what frame code makes and keeps past its frame (25.3), and where # ludic deps --keeps what frame code makes and keeps past its frame (25.3), and where
# ludic deps --births what is made, kept by nothing and not the arena's: never given back # ludic deps --births what is made, kept by nothing and not the arena's: never given back
# ludic deps --resources a handle a @creates(Kind) function made, thrown away or never handed on
# #
# dependencies: module pairs (a, b) where a uses something of b, not counting b when b uses no # 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 # other module (a leaf); largest_cycle: the largest set of modules that all reach each other, not
@ -57,6 +58,8 @@ var dp_fkeeps: []pointer = new []pointer # 25.3: `fkeep <kind> <file:li
var dp_frame_keeps: int = 0 var dp_frame_keeps: int = 0
var dp_fbirths: []pointer = new []pointer # 25.2: `fbirth <kind> <file:line> <fn> <where>` var dp_fbirths: []pointer = new []pointer # 25.2: `fbirth <kind> <file:line> <fn> <where>`
var dp_birth_leaks: int = 0 var dp_birth_leaks: int = 0
var dp_rdrops: []pointer = new []pointer # 25.5e: `rdrop <kind> <file:line> <fn> <how>`
var dp_resource_drops: int = 0
var dp_wreach_n: int = 0 var dp_wreach_n: int = 0
var dp_wreach_at: pointer = "" var dp_wreach_at: pointer = ""
@ -98,6 +101,7 @@ function dp_load(path: pointer) -> bool {
dp_fallocs = new []pointer; dp_frame_allocs = 0 dp_fallocs = new []pointer; dp_frame_allocs = 0
dp_fkeeps = new []pointer; dp_frame_keeps = 0 dp_fkeeps = new []pointer; dp_frame_keeps = 0
dp_fbirths = new []pointer; dp_birth_leaks = 0 dp_fbirths = new []pointer; dp_birth_leaks = 0
dp_rdrops = new []pointer; dp_resource_drops = 0
dp_writes = new []pointer; dp_aliases = new []pointer; dp_wowner = new []pointer; dp_wname = new []pointer; dp_wfrom = new []pointer 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) let lines = split_lines(text)
for i in 0 .. len(lines) { for i in 0 .. len(lines) {
@ -125,6 +129,8 @@ function dp_load(path: pointer) -> bool {
if len(w) == 2 and w[0] == "frame_keeps" { dp_frame_keeps = s_to_int(w[1]) } if len(w) == 2 and w[0] == "frame_keeps" { dp_frame_keeps = s_to_int(w[1]) }
if len(w) >= 4 and w[0] == "fbirth" { push(dp_fbirths, lines[i]) } if len(w) >= 4 and w[0] == "fbirth" { push(dp_fbirths, lines[i]) }
if len(w) == 2 and w[0] == "birth_leaks" { dp_birth_leaks = s_to_int(w[1]) } if len(w) == 2 and w[0] == "birth_leaks" { dp_birth_leaks = s_to_int(w[1]) }
if len(w) >= 5 and w[0] == "rdrop" { push(dp_rdrops, lines[i]) }
if len(w) == 2 and w[0] == "resource_drops" { dp_resource_drops = 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 { 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_n = s_to_int(w[1])
dp_wide_at = `{dp_fn_name(w[2])} ({w[3]})` dp_wide_at = `{dp_fn_name(w[2])} ({w[3]})`
@ -324,7 +330,7 @@ var dp_vals: []int = null
var dp_cycle: []int = null var dp_cycle: []int = null
var dp_cycle_all: []int = null # the same with the layers' own edges counted var dp_cycle_all: []int = null # the same with the layers' own edges counted
function dp_numbers() -> void { function dp_numbers() -> void {
dp_names = ["modules", "dependencies", "largest_cycle", "cross_writes", "globals_written_from_outside", "widest_function", "widest_reach", "widest_write_reach", "frame_allocs", "frame_keeps", "birth_leaks"] dp_names = ["modules", "dependencies", "largest_cycle", "cross_writes", "globals_written_from_outside", "widest_function", "widest_reach", "widest_write_reach", "frame_allocs", "frame_keeps", "birth_leaks", "resource_drops"]
var mods = 0 var mods = 0
for k in 0 .. len(dp_mods) { if dp_own(k) { mods += 1 } } for k in 0 .. len(dp_mods) { if dp_own(k) { mods += 1 } }
var deps = 0 var deps = 0
@ -346,7 +352,7 @@ function dp_numbers() -> void {
} }
} }
dp_reach_best() dp_reach_best()
dp_vals = [mods, deps, len(dp_cycle), writes, len(seen), dp_wide_n, dp_reach_n, dp_wreach_n, dp_frame_allocs, dp_frame_keeps, dp_birth_leaks] dp_vals = [mods, deps, len(dp_cycle), writes, len(seen), dp_wide_n, dp_reach_n, dp_wreach_n, dp_frame_allocs, dp_frame_keeps, dp_birth_leaks, dp_resource_drops]
} }
# ---- the command -------------------------------------------------------------- # ---- the command --------------------------------------------------------------
@ -503,6 +509,7 @@ function cmd_deps() -> int {
else if a == "--allocs" { mode = "allocs" } else if a == "--allocs" { mode = "allocs" }
else if a == "--keeps" { mode = "keeps" } else if a == "--keeps" { mode = "keeps" }
else if a == "--births" { mode = "births" } else if a == "--births" { mode = "births" }
else if a == "--resources" { mode = "resources" }
else if a == "--uses" or a == "--check" or a == "--baseline" or a == "--widest" or a == "--reach" or a == "--wreach" { 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 } if ai + 1 >= arg_count() { err(`ludic deps: {a} needs an argument\n`); return 2 }
mode = sslice(a, 2, slen(a)) mode = sslice(a, 2, slen(a))
@ -570,6 +577,14 @@ function cmd_deps() -> int {
print(`birth_leaks: {string(dp_birth_leaks)}`) print(`birth_leaks: {string(dp_birth_leaks)}`)
return 0 return 0
} }
if mode == "resources" { # 25.5e: a handle made (@creates) and never given back
for a in 0 .. len(dp_rdrops) {
let w = dp_words(dp_rdrops[a])
print(`resource dropped: {w[1]} at {w[2]} in {w[3]}: {w[4]}`)
}
print(`resource_drops: {string(dp_resource_drops)}`)
return 0
}
if mode == "widest" { dp_top(s_to_int(arg2), 0); 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 == "reach" { dp_top(s_to_int(arg2), 1); return 0 }
if mode == "wreach" { dp_top(s_to_int(arg2), 2); return 0 } if mode == "wreach" { dp_top(s_to_int(arg2), 2); return 0 }

View file

@ -46,6 +46,7 @@ function selfhost_frags() -> []pointer {
push(f, "selfhost/backend/emit_fence_ir.ludic") push(f, "selfhost/backend/emit_fence_ir.ludic")
push(f, "selfhost/backend/emit_frame.ludic") push(f, "selfhost/backend/emit_frame.ludic")
push(f, "selfhost/backend/emit_escape.ludic") push(f, "selfhost/backend/emit_escape.ludic")
push(f, "selfhost/backend/emit_resource.ludic")
push(f, "selfhost/backend/emit_head.ludic") push(f, "selfhost/backend/emit_head.ludic")
push(f, "selfhost/backend/emit_deps.ludic") push(f, "selfhost/backend/emit_deps.ludic")
push(f, "selfhost/backend/emit_addr.ludic") push(f, "selfhost/backend/emit_addr.ludic")