ludic/selfhost/backend/emit_deps.ludic
Orkuncakilkaya 63a1fa1378 feat(cli): ludic deps --writes warns about writes through a local alias
A local bound straight from another module's global (let t =
thing_cur), or from such a local, is followed within its function, and
a write through its field or element is listed as a warning after the
counted writes. A reference from a function's result is not followed.
Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:22:37 +03:00

189 lines
6.4 KiB
Text

# emit_deps.ludic — the module graph as the compiler saw it, for `ludic deps`. With LUDIC_DEPS=<file>
# every reference vis_check is asked about is an edge from the module it is written in to the module
# of what it names, and every assignment to a global of another module is a write; the file lists
# the modules (their declared `uses`, and whether they are a package's), the edges with a count, and
# the writes, one a line:
# module <name> <package 0|1> <uses: -|,a,b,> <layer: -|name>
# edge <from> <to> <count> <first name>
# write <owner> <global> <from> <file>:<line>
# alias <owner> <global> <from> <file>:<line> <local> (a write through a local, below)
var g_dp_on: int = -1
var g_dp_key: []pointer = new []pointer # "from to"
var g_dp_cnt: []int = new []int
var g_dp_name: []pointer = new []pointer
var g_dp_writes: []pointer = new []pointer
function deps_on() -> bool {
if g_dp_on < 0 {
g_dp_on = 0
if getenv("LUDIC_DEPS") != null { g_dp_on = 1 }
}
return g_dp_on == 1
}
function deps_edge(d: Node, what: pointer) -> void {
if not deps_on() or d.file == null or g_err_file == null { return }
let from = module_of(g_err_file)
let to = module_for_uses(d.file)
if (from == "") or (to == "") or (from == to) { return }
let key = `{from} {to}`
var i = 0
while i < len(g_dp_key) {
if (g_dp_key[i] == key) {
g_dp_cnt[i] = g_dp_cnt[i] + 1
return
}
i += 1
}
push(g_dp_key, key)
push(g_dp_cnt, 1)
push(g_dp_name, vis_plain(what))
}
# an assignment into global `g` (directly, or through an index or a field of it)
function deps_write(g: Node, name: pointer) -> void {
if not deps_on() or g == null or g.file == null or g_err_file == null { return }
let from = module_of(g_err_file)
let owner = module_for_uses(g.file)
if (from == "") or (owner == "") or (from == owner) { return }
push(g_dp_writes, `write {owner} {name} {from} {g_err_file}:{itoa(g_err_line)}`)
}
# the global an assignment target is rooted in (`a[i].f = ...` writes a), or null for a local
function deps_target_root(t: Node) -> Node {
var n = t
while n != null and (n.kind == E_INDEX or n.kind == E_MEMBER) { n = n.a }
if n == null or n.kind != E_ID { return null }
if loc_find(n.s) >= 0 { return null }
return find_global(n.s)
}
function deps_line(f: pointer, s: pointer) -> void {
file_write(f, s, len(s))
file_write(f, "\n", 1)
}
function deps_flush() -> void {
if not deps_on() { return }
let f = file_open(getenv("LUDIC_DEPS"), "wb")
if f == null { return }
let seen = new []pointer
var i = 0
while i < len(g_mod_name) {
let m = g_mod_name[i]
var dup = false
var j = 0
while j < len(seen) {
if (seen[j] == m) { dup = true }
j += 1
}
if not dup and not (m == "") {
push(seen, m)
var pkg = 0
if not (pkg_of_file(g_mod_file[i]) == "") { pkg = 1 }
var uses: pointer = "-"
let k = mod_find_uses(m)
if k >= 0 { uses = g_mu_list[k] }
var layer = mod_layer(m)
if (layer == "") { layer = "-" }
deps_line(f, `module {m} {itoa(pkg)} {uses} {layer}`)
}
i += 1
}
# a package with no module line of its own is named for its directory (module_for_uses)
i = 0
while i < len(g_pk_name) {
let m = g_pk_name[i]
var dup = false
var j = 0
while j < len(seen) {
if (seen[j] == m) { dup = true }
j += 1
}
if not dup and not (m == "") {
push(seen, m)
deps_line(f, `module {m} 1 - -`)
}
i += 1
}
i = 0
while i < len(g_dp_key) {
deps_line(f, `edge {g_dp_key[i]} {itoa(g_dp_cnt[i])} {g_dp_name[i]}`)
i += 1
}
i = 0
while i < len(g_dp_writes) {
deps_line(f, g_dp_writes[i])
i += 1
}
i = 0
while i < len(g_dp_aliases) {
deps_line(f, g_dp_aliases[i])
i += 1
}
file_close(f)
}
# A local that holds another module's global record (or a piece of one) - `let t = thing_cur`,
# `let s = slots[i]` - writes into that module's state when it is written through: `t.used = 1`.
# That is caught here for a local bound straight from the global (or from such a local) in the
# function being lowered, and listed as `alias <owner> <global> <from> <file>:<line> <local>`. A
# reference that arrives any other way - returned by a function, read out of a field of another
# record - is not followed: that needs knowing where every reference can point, which this is not.
var g_dp_al_local: []pointer = new []pointer
var g_dp_al_owner: []pointer = new []pointer
var g_dp_al_global: []pointer = new []pointer
var g_dp_aliases: []pointer = new []pointer
function deps_alias_reset() -> void {
if not deps_on() { return }
g_dp_al_local = new []pointer
g_dp_al_owner = new []pointer
g_dp_al_global = new []pointer
}
function deps_alias_find(local: pointer) -> int {
var i = len(g_dp_al_local) - 1
while i >= 0 {
if (g_dp_al_local[i] == local) { return i }
i -= 1
}
return -1
}
function deps_alias_drop(local: pointer) -> void {
let k = deps_alias_find(local)
if k >= 0 { g_dp_al_local[k] = "" }
}
# the E_ID an index / field chain starts at
function deps_chain_root(t: Node) -> Node {
var n = t
while n != null and (n.kind == E_INDEX or n.kind == E_MEMBER) { n = n.a }
if n == null or n.kind != E_ID { return null }
return n
}
# `let local = <expr>`: an alias when the expression is another module's global, or an alias
function deps_alias_let(local: pointer, e: Node) -> void {
if not deps_on() or g_err_file == null { return }
deps_alias_drop(local)
if e == null { return }
let r = deps_chain_root(e)
if r == null { return }
if loc_find(r.s) >= 0 {
let k = deps_alias_find(r.s)
if k < 0 { return }
push(g_dp_al_local, local)
push(g_dp_al_owner, g_dp_al_owner[k])
push(g_dp_al_global, g_dp_al_global[k])
return
}
let g = find_global(r.s)
if g == null or g.file == null { return }
let owner = module_for_uses(g.file)
let from = module_of(g_err_file)
if (from == "") or (owner == "") or (from == owner) { return }
push(g_dp_al_local, local)
push(g_dp_al_owner, owner)
push(g_dp_al_global, r.s)
}
# an assignment through a local's field or element: a write into the state the local aliases
function deps_alias_write(t: Node) -> void {
if not deps_on() or g_err_file == null { return }
let r = deps_chain_root(t)
if r == null or loc_find(r.s) < 0 { return }
let k = deps_alias_find(r.s)
if k < 0 { return }
push(g_dp_aliases, `alias {g_dp_al_owner[k]} {g_dp_al_global[k]} {module_of(g_err_file)} {g_err_file}:{itoa(g_err_line)} {r.s}`)
}