ludic/tools/ludic-cli/deps.ludic
Orkuncakilkaya 5df0747642 feat(cli): 0.R5 - ludic deps says what a function can come to change (widest_write_reach, --wreach N); a port member and a registry field reach only themselves
A reach through Port.member() follows that member's binding (or its default), and Registry[i].field -
or a local holding Registry[i] - follows that field in each entry, so a question asked of a port or a
table that also holds verbs no longer reaches the verbs. In Maroon Lake that took the valley's
'what is this Thing called' from 47 states it could change to 1. examples/state/write_reach.ludic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:19:54 +03:00

533 lines
19 KiB
Text

# ---- ludic deps ---------------------------------------------------------------
# How tangled a program's modules are, from the compiler's own view: it compiles the program
# with LUDIC_DEPS set (selfhost/backend/emit_deps.ludic), which records every reference the
# visibility pass resolves - from the module it is written in to the module of what it names -
# and every assignment to another module's global. A package's modules are listed but not
# counted; the program's own modules are.
#
# ludic deps [file] the numbers (the same five tools/deps.py printed)
# ludic deps --graph every module: its declared uses and the edges seen
# ludic deps --dot the graph for Graphviz (dot -Tsvg)
# ludic deps --writes every write to another module's global, then (as warnings)
# every write through a local bound to one (let t = g; t.f = 1)
# ludic deps --uses MOD what the other modules use of MOD
# ludic deps --check FILE fail if any number is above FILE's
# ludic deps --baseline FILE write the numbers to FILE
# 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
#
# 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
# counting the edges inside a declared layer (which may go round by design - reported beside it);
# cross_writes: assignments to a global of another module; globals_written_from_outside: those
# globals, each counted once; widest_function: the most states any function or entry point of the
# program's own takes (0.R2 - a reducer takes one, and the dispatchers should come down to a few);
# widest_reach: the most states any of them can come to - what it takes, and through calls, `fn f`
# and globals holding fn values (a step list, a registry of systems) what those take (0.R4);
# widest_write_reach: of those, the most it can come to change - taken `mut` somewhere down (0.R5).
var dp_mods: []pointer = null
var dp_pkg: []int = null
var dp_uses: []pointer = null
var dp_layer: []pointer = null
var dp_ef: []int = null
var dp_et: []int = null
var dp_ec: []int = null
var dp_en: []pointer = null
var dp_writes: []pointer = null
var dp_aliases: []pointer = null # writes through a local holding another module's global
var dp_wowner: []pointer = null
var dp_wname: []pointer = null
var dp_wfrom: []pointer = null
var dp_wide_n: int = 0 # 0.R2: the widest function's state parameters, and which
var dp_wide_at: pointer = ""
var dp_reach_n: int = 0 # 0.R4: the most states a function reaches, fn values included
var dp_reach_at: pointer = ""
var dp_reach_own: int = 0 # ... of which it takes itself
var dp_fn_at: []pointer = null # each of the program's own functions: where, what it takes, what it reaches
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_wreach_n: int = 0
var dp_wreach_at: pointer = ""
function dp_words(line: pointer) -> []pointer {
let out = new []pointer
let n = slen(line)
var a = 0
var i = 0
while i <= n {
if i == n or line[i] == 32 {
if i > a { push(out, sslice(line, a, i)) }
a = i + 1
}
i += 1
}
return out
}
function dp_mod(name: pointer) -> int {
var i = 0
while i < len(dp_mods) {
if dp_mods[i] == name { return i }
i += 1
}
push(dp_mods, name)
push(dp_pkg, 1)
push(dp_uses, "-")
push(dp_layer, "-")
return len(dp_mods) - 1
}
function dp_load(path: pointer) -> bool {
let text = read_file(path)
if text == null { return false }
dp_mods = new []pointer; dp_pkg = new []int; dp_uses = new []pointer; dp_layer = new []pointer
dp_ef = new []int; dp_et = new []int; dp_ec = new []int; dp_en = new []pointer
dp_wide_n = 0; dp_wide_at = ""
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_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) {
let w = dp_words(lines[i])
if len(w) >= 4 and w[0] == "module" {
let k = dp_mod(w[1])
dp_pkg[k] = 0
if w[2] == "1" { dp_pkg[k] = 1 }
dp_uses[k] = w[3]
if len(w) >= 5 { dp_layer[k] = w[4] }
}
}
for i in 0 .. len(lines) {
let w = dp_words(lines[i])
if len(w) >= 5 and w[0] == "edge" {
push(dp_ef, dp_mod(w[1]))
push(dp_et, dp_mod(w[2]))
push(dp_ec, s_to_int(w[3]))
push(dp_en, w[4])
}
if len(w) >= 6 and w[0] == "alias" { push(dp_aliases, lines[i]) }
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]})`
}
if len(w) >= 5 and (w[0] == "width" or w[0] == "reach" or w[0] == "wreach") and w[4] == "0" {
let k = dp_fn(`{dp_fn_name(w[2])} ({w[3]})`)
if w[0] == "width" { dp_fn_w[k] = s_to_int(w[1]) } else if w[0] == "reach" { dp_fn_r[k] = s_to_int(w[1]) } else { dp_fn_x[k] = s_to_int(w[1]) }
}
if len(w) >= 5 and w[0] == "write" {
push(dp_wowner, w[1])
push(dp_wname, w[2])
push(dp_wfrom, w[3])
push(dp_writes, lines[i])
}
}
return true
}
# a function of the program's own, by `name (file:line)`
function dp_fn(at: pointer) -> int {
for i in 0 .. len(dp_fn_at) { if dp_fn_at[i] == at { return i } }
push(dp_fn_at, at)
push(dp_fn_w, 0)
push(dp_fn_r, 0)
push(dp_fn_x, 0)
return len(dp_fn_at) - 1
}
# after the load: the widest reach (a function reaches at least what it takes)
function dp_reach_best() -> void {
for i in 0 .. len(dp_fn_at) {
if dp_fn_r[i] < dp_fn_w[i] { dp_fn_r[i] = dp_fn_w[i] }
if dp_fn_r[i] > dp_reach_n {
dp_reach_n = dp_fn_r[i]
dp_reach_at = dp_fn_at[i]
dp_reach_own = dp_fn_w[i]
}
if dp_fn_x[i] > dp_wreach_n {
dp_wreach_n = dp_fn_x[i]
dp_wreach_at = dp_fn_at[i]
}
}
}
# --widest N / --reach N: the top N functions by what they take (or by what they reach), both shown
function dp_top(n: int, by: int) -> void {
let order = new []int
for i in 0 .. len(dp_fn_at) { push(order, i) }
for a in 1 .. len(order) {
let x = order[a]
var b = a - 1
while b >= 0 and dp_top_before(x, order[b], by) {
order[b + 1] = order[b]
b -= 1
}
order[b + 1] = x
}
print("takes reaches changes function")
var shown = 0
for i in 0 .. len(order) {
let k = order[i]
if shown < n and (dp_fn_w[k] > 0 or dp_fn_r[k] > 0) {
print(`{dp_pad(string(dp_fn_w[k]), 5)} {dp_pad(string(dp_fn_r[k]), 7)} {dp_pad(string(dp_fn_x[k]), 7)} {dp_fn_at[k]}`)
shown += 1
}
}
}
# by: 0 what it takes, 1 what it reaches, 2 what it can come to change
function dp_top_before(x: int, y: int, by: int) -> bool {
var kx = dp_fn_w[x]
var ky = dp_fn_w[y]
var tx = dp_fn_r[x]
var ty = dp_fn_r[y]
if by == 1 {
kx = dp_fn_r[x]
ky = dp_fn_r[y]
tx = dp_fn_w[x]
ty = dp_fn_w[y]
}
if by == 2 {
kx = dp_fn_x[x]
ky = dp_fn_x[y]
}
if kx != ky { return kx > ky }
return tx > ty
}
function dp_pad(s: pointer, n: int) -> pointer {
var out = s
while slen(out) < n { out = " " + out }
return out
}
# a function as its source names it: a reducer is `reducer Bag on PickUp`, not its symbol
function dp_fn_name(n: pointer) -> pointer {
if not s_starts(n, "ludic_reduce__") { return n }
let rest = n[14 .. slen(n)]
for i in 0 .. slen(rest) - 1 {
if rest[i] == '_' and rest[i + 1] == '_' { return `reducer {rest[i + 2 .. slen(rest)]} on {rest[0 .. i]}` }
}
return n
}
function dp_own(k: int) -> bool { return dp_pkg[k] == 0 }
# an edge the numbers count: between two of the program's own modules
function dp_counted(e: int) -> bool { return dp_own(dp_ef[e]) and dp_own(dp_et[e]) }
# an edge inside a declared layer: allowed to go round, so not part of the cycle the numbers count
function dp_in_layer(e: int) -> bool {
let a = dp_layer[dp_ef[e]]
return a != "-" and a == dp_layer[dp_et[e]]
}
function dp_leaf(k: int) -> bool {
for e in 0 .. len(dp_ef) { if dp_ef[e] == k and dp_counted(e) { return false } }
return true
}
# ---- the largest strongly connected set (Tarjan) ----------------------------
var tj_index: []int = null
var tj_low: []int = null
var tj_on: []int = null
var tj_stack: []int = null
var tj_sp: int = 0
var tj_n: int = 0
var tj_best: []int = null
var dp_skip_layers: bool = true # the largest cycle leaves out the edges inside a declared layer
function tj_visit(v: int) -> void {
tj_index[v] = tj_n
tj_low[v] = tj_n
tj_n += 1
tj_stack[tj_sp] = v
tj_sp += 1
tj_on[v] = 1
for e in 0 .. len(dp_ef) {
if dp_ef[e] == v and dp_counted(e) and not (dp_skip_layers and dp_in_layer(e)) {
let w = dp_et[e]
if tj_index[w] < 0 {
tj_visit(w)
if tj_low[w] < tj_low[v] { tj_low[v] = tj_low[w] }
} else if tj_on[w] == 1 and tj_index[w] < tj_low[v] { tj_low[v] = tj_index[w] }
}
}
if tj_low[v] == tj_index[v] {
let comp = new []int
while true {
tj_sp -= 1
let w = tj_stack[tj_sp]
tj_on[w] = 0
push(comp, w)
if w == v { break }
}
if len(comp) > len(tj_best) { tj_best = comp }
}
}
function dp_largest_cycle() -> []int {
let n = len(dp_mods)
tj_index = new []int; tj_low = new []int; tj_on = new []int; tj_stack = new []int
for i in 0 .. n {
push(tj_index, -1)
push(tj_low, 0)
push(tj_on, 0)
push(tj_stack, 0)
}
tj_sp = 0
tj_n = 0
tj_best = new []int
for v in 0 .. n { if dp_own(v) and tj_index[v] < 0 { tj_visit(v) } }
return tj_best
}
function dp_sorted_names(ks: []int) -> pointer {
let names = new []pointer
for i in 0 .. len(ks) { push(names, dp_mods[ks[i]]) }
# a handful to a few hundred: an insertion sort
var i = 1
while i < len(names) {
let x = names[i]
var j = i - 1
while j >= 0 and s_less(x, names[j]) {
names[j + 1] = names[j]
j -= 1
}
names[j + 1] = x
i += 1
}
var out = ""
for k in 0 .. len(names) {
if k > 0 { out = out + " " }
out = out + names[k]
}
return out
}
function s_less(a: pointer, b: pointer) -> bool {
var i = 0
while a[i] != 0 and b[i] != 0 {
if a[i] != b[i] { return a[i] < b[i] }
i += 1
}
return a[i] == 0 and b[i] != 0
}
# ---- the numbers ------------------------------------------------------------
var dp_names: []pointer = null
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"]
var mods = 0
for k in 0 .. len(dp_mods) { if dp_own(k) { mods += 1 } }
var deps = 0
for e in 0 .. len(dp_ef) { if dp_counted(e) and not dp_leaf(dp_et[e]) { deps += 1 } }
dp_skip_layers = false
dp_cycle_all = dp_largest_cycle()
dp_skip_layers = true
dp_cycle = dp_largest_cycle()
var writes = 0
let seen = new []pointer
for w in 0 .. len(dp_writes) {
let ok = dp_own(dp_mod(dp_wowner[w]))
if ok {
writes += 1
let key = `{dp_wowner[w]}.{dp_wname[w]}`
var dup = false
for s in 0 .. len(seen) { if seen[s] == key { dup = true } }
if not dup { push(seen, key) }
}
}
dp_reach_best()
dp_vals = [mods, deps, len(dp_cycle), writes, len(seen), dp_wide_n, dp_reach_n, dp_wreach_n]
}
# ---- the command --------------------------------------------------------------
function dp_print_numbers() -> void {
for i in 0 .. len(dp_names) { print(`{dp_names[i]}: {string(dp_vals[i])}`) }
print(`in the largest cycle: {dp_sorted_names(dp_cycle)}`)
if dp_wide_n == 1 { print(`the widest function: {dp_wide_at}, 1 state`) }
if dp_wide_n > 1 { print(`the widest function: {dp_wide_at}, {string(dp_wide_n)} states`) }
if dp_reach_n > dp_reach_own { print(`the widest reach: {dp_reach_at}, {string(dp_reach_n)} states ({string(dp_reach_n - dp_reach_own)} through calls and fn values it does not take)`) }
if dp_wreach_n == 1 { print(`the widest write reach: {dp_wreach_at}, 1 state it can come to change`) }
if dp_wreach_n > 1 { print(`the widest write reach: {dp_wreach_at}, {string(dp_wreach_n)} states it can come to change`) }
let layers = dp_layers()
if layers != "" {
print(`layers: {layers}`)
print(`largest cycle counting the layers' own edges: {string(len(dp_cycle_all))}`)
}
}
# FILE: `name value` lines, `#` comments; every number but modules may only go down
function dp_check(path: pointer) -> int {
let text = read_file(path)
if text == null { err(`ludic deps: cannot read {path}\n`); return 2 }
let lines = split_lines(text)
var worse = 0
var better = 0
for i in 0 .. len(dp_names) {
var base = -1
for l in 0 .. len(lines) {
let w = dp_words(lines[l])
if len(w) == 2 and w[0] == dp_names[i] { base = s_to_int(w[1]) }
}
var shown = "-"
if base >= 0 { shown = string(base) }
var mark = ""
if i > 0 and base >= 0 and dp_vals[i] > base {
mark = " WORSE"
worse += 1
}
if i > 0 and base >= 0 and dp_vals[i] < base { better += 1 }
print(` deps: {dp_names[i]} {string(dp_vals[i])} (baseline {shown}){mark}`)
}
if worse > 0 {
print("deps: the code is more tangled than the baseline - see ludic deps --writes and --graph")
return 1
}
if better > 0 { print(`deps: better than the baseline - lower it: ludic deps --baseline {path}`) }
print(" deps: OK")
return 0
}
function dp_baseline(path: pointer) -> int {
var s = "# ludic deps --check: each number may only go down\n"
for i in 1 .. len(dp_names) { s = s + `{dp_names[i]} {string(dp_vals[i])}\n` }
if not write_file(path, s) { err(`ludic deps: cannot write {path}\n`); return 2 }
print(`deps: wrote {path}`)
return 0
}
# "app (11: core flow ...), ..." for the declared layers, "" when there are none
function dp_layers() -> pointer {
var out = ""
let seen = new []pointer
for k in 0 .. len(dp_mods) {
let l = dp_layer[k]
if l != "-" {
var dup = false
for s in 0 .. len(seen) { if seen[s] == l { dup = true } }
if not dup {
push(seen, l)
let members = new []int
for m in 0 .. len(dp_mods) { if dp_layer[m] == l { push(members, m) } }
if out != "" { out = out + "; " }
out = out + `{l} ({string(len(members))}: {dp_sorted_names(members)})`
}
}
}
return out
}
function dp_declared(from: int, to: int) -> bool {
if dp_layer[from] != "-" and dp_layer[from] == dp_layer[to] { return true }
let u = dp_uses[from]
if u == "-" { return false }
return s_contains(u, `,{dp_mods[to]},`)
}
# ",a,b," as "a, b" ("nothing" for ",")
function dp_list(u: pointer) -> pointer {
let n = slen(u)
if n <= 1 { return "nothing" }
var out = ""
var i = 1
while i < n - 1 {
if u[i] == 44 { out = out + ", " } else { out = out + sslice(u, i, i + 1) }
i += 1
}
return out
}
function dp_graph() -> void {
for k in 0 .. len(dp_mods) {
if dp_own(k) {
var uses = "(no uses line)"
if dp_uses[k] != "-" { uses = `uses {dp_list(dp_uses[k])}` }
if dp_layer[k] != "-" { uses = `layer {dp_layer[k]}, {uses}` }
var to = ""
for e in 0 .. len(dp_ef) {
if dp_ef[e] == k {
var tag = ""
if dp_uses[k] != "-" and not dp_declared(k, dp_et[e]) { tag = "!" }
if dp_pkg[dp_et[e]] == 1 { tag = tag + "(pkg)" }
to = to + ` {dp_mods[dp_et[e]]}{tag}:{string(dp_ec[e])}`
}
}
print(`{dp_mods[k]} {uses} ->{to}`)
}
}
}
function dp_in(xs: []int, k: int) -> bool {
for i in 0 .. len(xs) { if xs[i] == k { return true } }
return false
}
function dp_dot() -> void {
print("digraph modules {")
print(" rankdir=LR; node [shape=box, fontname=Helvetica];")
for k in 0 .. len(dp_mods) {
if dp_own(k) {
var style = ""
if dp_in(dp_cycle, k) { style = ", style=filled, fillcolor=\"#f6d6d6\"" }
print(` "{dp_mods[k]}" [label="{dp_mods[k]}"{style}];`)
}
}
for e in 0 .. len(dp_ef) {
if dp_counted(e) {
let f = dp_ef[e]
let t = dp_et[e]
var style = ""
if dp_uses[f] != "-" and not dp_declared(f, t) { style = ", color=red" }
if dp_in(dp_cycle, f) and dp_in(dp_cycle, t) { style = style + ", penwidth=2" }
print(` "{dp_mods[f]}" -> "{dp_mods[t]}" [label="{string(dp_ec[e])}"{style}];`)
}
}
print("}")
}
function dp_uses_of(target: pointer) -> void {
for e in 0 .. len(dp_ef) {
if dp_mods[dp_et[e]] == target { print(`{dp_mods[dp_ef[e]]} {string(dp_ec[e])} uses, e.g. {dp_en[e]}`) }
}
}
function cmd_deps() -> int {
var src = ""
var mode = "numbers"
var arg2 = ""
var ai = 2
while ai < arg_count() {
let a = arg(ai)
if a == "--dot" { mode = "dot" }
else if a == "--graph" { mode = "graph" }
else if a == "--writes" { mode = "writes" }
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))
ai += 1
arg2 = arg(ai)
}
else if a[0] == '-' {
err(`ludic deps: unknown option {a}\n`)
err(" usage: ludic deps [file] [--graph | --dot | --writes | --uses MOD | --check FILE | --baseline FILE | --widest N | --reach N | --wreach N]\n")
return 2
}
else { src = a }
ai += 1
}
let entry = find_entry(src)
if entry == "" { return no_entry() }
ensure_ludicc()
let graph = tmp_path("deps.txt")
let ll = tmp_path("deps.ll")
# the compiler records the graph as it resolves names; a module rule broken on the way is listed
# (LUDIC_VIS_REPORT) rather than stopping the count
if not shq(`LUDIC_DEPS={graph} LUDIC_VIS_REPORT=1 {ludicc()} --headless{unsafe_flag()} {entry} --emit-llvm -o {ll} 2> {tmp_path("deps.err")}`) {
err(`ludic deps: {entry} did not compile:\n`)
err(capture(`grep -i error {tmp_path("deps.err")} | head -5`))
return 1
}
shell(`rm -f {ll}`)
if not dp_load(graph) { err("ludic deps: the compiler wrote no graph (is it older than ludic deps?)\n"); return 1 }
dp_numbers()
if mode == "dot" { dp_dot(); return 0 }
if mode == "graph" { dp_graph(); return 0 }
if mode == "writes" {
for w in 0 .. len(dp_writes) { print(sslice(dp_writes[w], 6, slen(dp_writes[w]))) }
# a warning, not a number: a write through a local bound to the global (not counted above)
for a in 0 .. len(dp_aliases) {
let w = dp_words(dp_aliases[a])
print(`warning: {w[1]} {w[2]} {w[3]} {w[4]} through the local {w[5]}`)
}
return 0
}
if mode == "uses" { dp_uses_of(arg2); 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 }
if mode == "check" { return dp_check(arg2) }
if mode == "baseline" { return dp_baseline(arg2) }
dp_print_numbers()
return 0
}