A registry marked `@Machine(Deer.mood)` is the transitions of a machine over that enum field of the records a state's Table<Deer> holds. Its record has from and to (the enum's variants), on: string (an action's name, "" for a transition the tick asks), guard: fn(Row<Deer>, reads...) -> bool and enter: fn(Row<Deer>, reads...) -> void; the states are the enum's variants and the start is the field's default. The rows are data (an .lres or defs), the names the studio already edits. Written by the compiler (machines.ludic, machines_write.ludic): for each action an `on` names, a row reducer in the registry's file (named ..__machine__DeerSteps, so it sits beside the program's own row reducer on the same action, after it): the row's state, the first transition from it on that action whose guard passes, the field set, enter run - guards and enters called by name. When a row leaves a state on a guard alone, `state DeerStepsMachine` (the kept row view) and deer_steps_tick(m: mut DeerStepsMachine, s: mut Herd, reads...), one transition a row a tick. Nothing allocates. The table is the whole machine: the field written anywhere else - an assignment, or a `machine` block's become over it - is a type error (check_stmt.ludic, ck_machine_write). Guards and enters take the row first, are the record's module's, keep a row reducer's rules (and may be handed the row); a guard writes nothing through it. The graph is checked, each error at its row (in the .lres when the rows are there): a state never reached from the start, a state with no way out, an `on` naming no action or an action with no @Target, a self-transition with no guard, two ways out of a state on one trigger behind an unguarded first. Also refused: @Machine off a registry, a field that is not a plain enum with a default, a @Column field, no table (or two) of the record, a transitions record of another shape, a machine outside its table's state's module. ludic schema's code section gains `machines` (registry, record, field, enum, table, start, states, actions, tick, module, at); ludic deps names a machine's reducer `reducer Deer in Herd.deer on Spook (machine DeerSteps)`. vocab @Machine; docs annot-machine, kw-machine; LANGUAGE.md "A machine as data"; examples actions/machine (+ deer_steps.lres) and ten rejects; test.ludic feat, reject and schema cases (not run); changes/machines.md. Reseeded; bootstrap-cfree fixpoint holds (317642 lines); Maroon Lake's `ludic build --check` is clean against this tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
675 lines
26 KiB
Text
675 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`; one a @Machine
|
|
# wrote ends `__machine__DeerSteps` (27.1)
|
|
function dp_row_name(s0: pointer) -> pointer {
|
|
var s = s0
|
|
var tail = ""
|
|
var k = 0
|
|
while k + 11 <= slen(s0) {
|
|
if s0[k .. k + 11] == "__machine__" {
|
|
s = s0[0 .. k]
|
|
tail = ` (machine {s0[k + 11 .. slen(s0)]})`
|
|
break
|
|
}
|
|
k += 1
|
|
}
|
|
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 + tail
|
|
}
|
|
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
|
|
}
|