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>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-26 23:19:54 +03:00
parent c64f8750e2
commit 5df0747642
7 changed files with 46858 additions and 43583 deletions

View file

@ -15,6 +15,7 @@
# 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
@ -23,7 +24,8 @@
# 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).
# 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
@ -45,6 +47,9 @@ 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
@ -79,7 +84,8 @@ function dp_load(path: pointer) -> bool {
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_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) {
@ -105,9 +111,9 @@ function dp_load(path: pointer) -> bool {
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") and w[4] == "0" {
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 { dp_fn_r[k] = s_to_int(w[1]) }
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])
@ -124,6 +130,7 @@ function dp_fn(at: pointer) -> int {
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)
@ -135,42 +142,51 @@ function dp_reach_best() -> void {
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_reach: bool) -> void {
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_reach) {
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 function")
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_fn_at[k]}`)
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
}
}
}
function dp_top_before(x: int, y: int, by_reach: bool) -> bool {
# 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_reach {
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
}
@ -290,7 +306,7 @@ 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"]
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
@ -312,7 +328,7 @@ function dp_numbers() -> void {
}
}
dp_reach_best()
dp_vals = [mods, deps, len(dp_cycle), writes, len(seen), dp_wide_n, dp_reach_n]
dp_vals = [mods, deps, len(dp_cycle), writes, len(seen), dp_wide_n, dp_reach_n, dp_wreach_n]
}
# ---- the command --------------------------------------------------------------
@ -322,6 +338,8 @@ function dp_print_numbers() -> void {
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}`)
@ -464,7 +482,7 @@ function cmd_deps() -> int {
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" {
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
@ -472,7 +490,7 @@ function cmd_deps() -> int {
}
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]\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 }
@ -505,8 +523,9 @@ function cmd_deps() -> int {
return 0
}
if mode == "uses" { dp_uses_of(arg2); return 0 }
if mode == "widest" { dp_top(s_to_int(arg2), false); return 0 }
if mode == "reach" { dp_top(s_to_int(arg2), true); 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()

View file

@ -476,7 +476,7 @@ function deps_case() -> void {
let lbl = "ludic deps: the numbers, --check against a baseline, --dot, --writes"
let p = "examples/modules/tangle.ludic"
let got = capture(`bin/ludic deps {p} 2>&1`)
let want = "modules: 3\ndependencies: 2\nlargest_cycle: 2\ncross_writes: 1\nglobals_written_from_outside: 1\nwidest_function: 1\nwidest_reach: 1\nin the largest cycle: a b\nthe widest function: a_run (examples/modules/tangle/a/index.ludic:3), 1 state\n"
let want = "modules: 3\ndependencies: 2\nlargest_cycle: 2\ncross_writes: 1\nglobals_written_from_outside: 1\nwidest_function: 1\nwidest_reach: 1\nwidest_write_reach: 1\nin the largest cycle: a b\nthe widest function: a_run (examples/modules/tangle/a/index.ludic:3), 1 state\nthe widest write reach: a_run (examples/modules/tangle/a/index.ludic:3), 1 state it can come to change\n"
if not (s_trim(got) == s_trim(want)) { bad2(lbl, `said [{s_trim(got)}]`); return }
let base = `{tmp_dir()}/deps-base.txt`
if not shq(`bin/ludic deps {p} --baseline {base} > /dev/null`) { bad2(lbl, "--baseline failed"); return }
@ -590,9 +590,16 @@ function deps_reach_case() -> void {
let got = capture(`bin/ludic deps examples/state/reach.ludic 2>&1`)
if not s_contains(got, "widest_reach: 3") or not s_contains(got, "the widest reach: one (examples/state/reach.ludic:13), 3 states (2 through calls and fn values it does not take)") { bad2(lbl, s_trim(got)); return }
let top = capture(`bin/ludic deps examples/state/reach.ludic --reach 2 2>&1`)
if not s_contains(top, " 1 3 one (examples/state/reach.ludic:13)\n 0 2 run_steps (examples/state/reach.ludic:10)") { bad2(lbl, s_trim(top)); return }
if not s_contains(top, " 1 3 3 one (examples/state/reach.ludic:13)\n 0 2 2 run_steps (examples/state/reach.ludic:10)") { bad2(lbl, s_trim(top)); return }
let wide = capture(`bin/ludic deps examples/state/reach.ludic --widest 1 2>&1`)
if s_contains(wide, "takes reaches function") and s_contains(wide, "one (examples/state/reach.ludic:13)") and not s_contains(wide, "step_a") { ok(lbl) } else { bad2(lbl, s_trim(wide)) }
if s_contains(wide, "takes reaches changes function") and s_contains(wide, "one (examples/state/reach.ludic:13)") and not s_contains(wide, "step_a") { ok(lbl) } else { bad2(lbl, s_trim(wide)) }
}
# 0.R5: what a function can come to CHANGE; a port member and a registry field reach only themselves
function deps_wreach_case() -> void {
let lbl = "ludic deps: --wreach, a question through a port or a registry field changes nothing"
let top = capture(`bin/ludic deps examples/state/write_reach.ludic --wreach 10 2>&1`)
if not s_contains(top, " 0 2 2 changes (examples/state/write_reach.ludic:27)") or not s_contains(top, " 0 1 0 asks (examples/state/write_reach.ludic:22)") or s_contains(top, "asks_local") { bad2(lbl, s_trim(top)); return }
ok(lbl)
}
# a program built headless with the toolchain and run: its first line
function headless_case(path: pointer, exp: pointer, label: pointer) -> void {
@ -1017,6 +1024,7 @@ function cmd_dev_test() -> int {
feat_case("actions/modules", "", "2", "modules.ludic (0.R: an action in one module, its reducer in another)")
actions_deps_case()
deps_reach_case()
deps_wreach_case()
feat_case("state/reach", "", "3", "reach.ludic (0.R4: a step list of fn values supplies each step its states)")
feat_case("actions/phases", "dd a q", "1 0 2 0 2 1 1 1 1 2 1 2", "phases.ludic (0.R: the frame loop drains the queue after every phase - Input's actions are reduced before Update)")
feat_case("actions/runaway", "", "actions: Ping is still being dispatched after 64 rounds of reducers - a reducer dispatches what dispatches it", "runaway.ludic (0.R: a reducer that dispatches what dispatches it is stopped by name)")