ludic/tools/ludic-cli/deps.ludic
Orkuncakilkaya ab0b84b87e feat(lang): 27.3 - a reducer on a table's row: reducer T in S.table on A, @Target, Row<T>, @RowVerb, @Column
An action names one row of a ludic.base Table<T> by its handle, in a field marked @Target, and
`reducer Deer in Herd.deer on Spook(r: mut Row<Deer>, n: Noise, a: Spook)` runs once, for that row
alone (the table may sit down a path, S.w.tab). The drain resolves the handle (tb_row) and hands the
reducer a Row<T> - new in ludic.base: tb, row, h, rec - that the queue keeps, one per row reducer,
filled in place, so a targeted action allocates nothing; a stale handle runs nothing, and
LUDIC_ACTIONS_LOG=1 prints a line for it (@alloc_ok). Row reducers order among an action's by their
state's name, then the table's path.

Checked at compile time (actions_rows.ludic): the row reaches r.rec and r.h only - r.tb / r.row
refused, the view never assigned, stored, copied or handed on except to a @RowVerb (a function of
the record's own module taking Row<T> first; any other function taking a row is refused); a field
marked @Column (a table column mirrors it) is not written through r.rec; only the module owning the
state declares a row reducer; one @Target, an int, per action; the states between the row and the
action are read. `mut` is allowed on a Row<T> parameter.

ludic schema's code section gains row_reducers (record, table, state, action, target, predicted,
net, module, at) and row_verbs (name, record, module, at), and every action its target; row
reducers are left out of `reducers`. ludic deps and ludic-lsp name a row reducer
`reducer Deer in Herd.deer on Spook`. vocab: @Target, @Column, @RowVerb; docs/language pages;
LANGUAGE.md "A reducer on a row"; examples actions/rows and ten rejects; test.ludic feat, reject and
schema cases (not run); changes/row-reducers.md. Reseeded; bootstrap-cfree fixpoint holds (307497
lines); Maroon Lake's `ludic build --check` is clean against this tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:14:59 +03:00

663 lines
26 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
# 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 --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
# 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).
# Both leave out the ROOTS - a function that reads fn values out of a table (a dispatcher), which reaches
# every state by definition - and count every other function without going through one, so the frame
# and the boot count their own work; the roots are listed on a line of their own.
# english_left: text still written in English where a key belongs (phase 26, the compiler's i18n
# checks) - a template's words, a text attribute, a @Text row's English in the data; 0 without a
# `lang` line in package.ludic.
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_fn_root: []bool = null # ... whether it is a ROOT: reads fn values out of a table (a dispatcher)
var dp_fn_r2: []int = null # ... what it reaches and changes NOT going through a dispatcher
var dp_fn_x2: []int = null
var dp_root_n: int = 0 # the roots, left out of widest_reach / widest_write_reach, and the widest of them
var dp_root_reach: int = 0
var dp_root_at: pointer = ""
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_fkeeps: []pointer = new []pointer # 25.3: `fkeep <kind> <file:line> <kept at> <chain>`
var dp_frame_keeps: int = 0
var dp_fbirths: []pointer = new []pointer # 25.2: `fbirth <kind> <file:line> <fn> <where>`
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_oleaks: []pointer = new []pointer # 25.5e: `oleak <Type.field> <file:line> <fn> <released>`
var dp_owned_leaks: int = 0
var dp_english_left: int = 0 # phase 26: a template's words or a @Text row's English, not yet keys
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_fn_root = new []bool; dp_fn_r2 = new []int; dp_fn_x2 = new []int
dp_root_n = 0; dp_root_reach = 0; dp_root_at = ""
dp_wreach_n = 0; dp_wreach_at = ""
dp_fallocs = new []pointer; dp_frame_allocs = 0
dp_fkeeps = new []pointer; dp_frame_keeps = 0
dp_fbirths = new []pointer; dp_birth_leaks = 0
dp_rdrops = new []pointer; dp_resource_drops = 0
dp_oleaks = new []pointer; dp_owned_leaks = 0
dp_english_left = 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) {
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) >= 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] == "fkeep" { push(dp_fkeeps, lines[i]) }
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) == 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] == "oleak" { push(dp_oleaks, lines[i]) }
if len(w) == 2 and w[0] == "owned_leaks" { dp_owned_leaks = s_to_int(w[1]) }
if len(w) == 2 and w[0] == "english_left" { dp_english_left = 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]})`
}
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) >= 6 and w[5] == "1" { dp_fn_root[k] = true }
if len(w) >= 7 and w[0] == "reach" { dp_fn_r2[k] = s_to_int(w[6]) }
if len(w) >= 7 and w[0] == "wreach" { dp_fn_x2[k] = s_to_int(w[6]) }
}
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)
push(dp_fn_root, false)
push(dp_fn_r2, 0)
push(dp_fn_x2, 0)
return len(dp_fn_at) - 1
}
# after the load: the widest reach (a function reaches at least what it takes)
# over the functions that are not roots, each counted without going through a dispatcher: a root (a
# step list's walker, a registry of systems) reaches every state by definition, and what calls one would
# only rise with each new state - no split could lower it. The roots are listed on a line of their own.
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_r2[i] < dp_fn_w[i] { dp_fn_r2[i] = dp_fn_w[i] }
if dp_fn_root[i] {
dp_root_n += 1
if dp_fn_r[i] > dp_root_reach {
dp_root_reach = dp_fn_r[i]
dp_root_at = dp_fn_at[i]
}
continue
}
if dp_fn_r2[i] > dp_reach_n {
dp_reach_n = dp_fn_r2[i]
dp_reach_at = dp_fn_at[i]
dp_reach_own = dp_fn_w[i]
}
if dp_fn_x2[i] > dp_wreach_n {
dp_wreach_n = dp_fn_x2[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) {
var mark = ""
if dp_fn_root[k] { mark = " (root)" }
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]}{mark}`)
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__") and not s_starts(n, "ludic_rowred__") { return n }
let rest = n[14 .. slen(n)]
for i in 0 .. slen(rest) - 1 {
if rest[i] == '_' and rest[i + 1] == '_' { return `reducer {dp_row_name(rest[i + 2 .. slen(rest)])} on {rest[0 .. i]}` }
}
return n
}
# 27.3: a row reducer's symbol says `Deer__in__Herd__deer` for `Deer in Herd.deer`
function dp_row_name(s: pointer) -> pointer {
var out = ""
var seen = false
var i = 0
while i < slen(s) {
if not seen and i + 6 <= slen(s) and s[i .. i + 6] == "__in__" {
out = out + " in "
seen = true
i += 6
} else if seen and i + 2 <= slen(s) and s[i .. i + 2] == "__" {
out = out + "."
i += 2
} else {
out = out + s[i .. i + 1]
i += 1
}
}
return out
}
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", "frame_allocs", "frame_keeps", "birth_leaks", "resource_drops", "owned_leaks", "english_left"]
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, dp_frame_allocs, dp_frame_keeps, dp_birth_leaks, dp_resource_drops, dp_owned_leaks, dp_english_left]
}
# ---- 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`) }
if dp_root_n > 0 { print(`roots, left out of the reaches (they walk a table of fn values; the rest are counted without going through one): {string(dp_root_n)}, the widest {dp_root_at}, {string(dp_root_reach)} states`) }
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 == "--allocs" { mode = "allocs" }
else if a == "--keeps" { mode = "keeps" }
else if a == "--births" { mode = "births" }
else if a == "--resources" { mode = "resources" }
else if a == "--owned" { mode = "resources" }
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 == "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 == "keeps" { # 25.3: what frame code makes and keeps, and where it is kept
for a in 0 .. len(dp_fkeeps) {
let w = dp_words(dp_fkeeps[a])
print(`frame keep: {w[2]} {w[1]} kept at {w[3]} {w[4]}`)
}
print(`frame_keeps: {string(dp_frame_keeps)}`)
return 0
}
if mode == "births" { # 25.2: made, kept by nothing, and on the heap: never given back
for a in 0 .. len(dp_fbirths) {
let w = dp_words(dp_fbirths[a])
var where = ""
if len(w) >= 5 { where = w[4] }
print(`leak at birth: {w[2]} {w[1]} in {w[3]} {where}`)
}
print(`birth_leaks: {string(dp_birth_leaks)}`)
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)}`)
for a in 0 .. len(dp_oleaks) {
let w = dp_words(dp_oleaks[a])
print(`owned field lost: {w[1]} at {w[2]} in {w[3]}, which releases {w[4]}`)
}
print(`owned_leaks: {string(dp_owned_leaks)}`)
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
}