feat(cli): ludic deps - the module graph as the compiler resolved it
With LUDIC_DEPS=<file> the compiler records every reference the visibility pass resolves (from module, to module) and every assignment to another module's global. ludic deps prints the modules, dependencies, largest cycle, cross writes and globals written from outside, with --graph, --dot, --writes, --uses MOD, --check FILE and --baseline FILE. Reseed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
69352babfd
commit
e738741521
17 changed files with 44510 additions and 41589 deletions
13
LANGUAGE.md
13
LANGUAGE.md
|
|
@ -1828,11 +1828,24 @@ ludic build --headless # headless build (renders out.ppm; read
|
|||
ludic test # compile and run the project's `test` blocks
|
||||
ludic test tests/math.ludic --test adds # just the test named "adds" (-v: every result line)
|
||||
ludic test packages/ludic.base # the test programs under a directory (a package's)
|
||||
ludic deps # how tangled the modules are, as the compiler resolved them
|
||||
ludic deps --check tests/deps-baseline.txt # fail when a number rose (--baseline FILE writes them)
|
||||
|
||||
ludicc app.ludic -o build/app # the compiler directly: a native binary
|
||||
ludicc app.ludic --emit-llvm -o app.ll # stop at LLVM IR
|
||||
```
|
||||
|
||||
`ludic deps` compiles the program (the package's entry, or a file) with the compiler recording every
|
||||
reference its 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. It prints five numbers: `modules` (the
|
||||
program's own; packages are listed but not counted), `dependencies` (pairs of modules where one uses
|
||||
the other, not counting a use of a module that itself uses none), `largest_cycle` (the largest set
|
||||
of modules that all reach each other, named on the last line), `cross_writes` and
|
||||
`globals_written_from_outside`. `--graph` lists each module with its declared `uses` and the edges
|
||||
seen (`!` marks one its `uses` line does not name), `--dot` is the same for Graphviz with the cycle
|
||||
filled, `--writes` lists the writes and `--uses MOD` who uses MOD. `--check FILE` fails when any
|
||||
number is above FILE's `name value` lines; `--baseline FILE` writes them.
|
||||
|
||||
A test program is a file of `test "name" { ... }` blocks with `expect(cond)`, `expect_eq(a, b)` and
|
||||
`expect_near(a, b, tol)` in them (on ints and fixeds, or on floats and doubles, which compare - and
|
||||
print - as floats); a test block is type-checked like `entry`, so a generic function
|
||||
|
|
|
|||
7
changes/ludic-deps.md
Normal file
7
changes/ludic-deps.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**`ludic deps` - the module graph as the compiler resolved it.** It compiles the program with
|
||||
`LUDIC_DEPS` set, and the compiler records every reference its visibility pass sees (from module,
|
||||
to module) and every assignment to another module's global. It prints the modules, dependencies,
|
||||
largest cycle, cross-module writes and globals written from outside; `--graph`, `--dot`, `--writes`,
|
||||
`--uses MOD`, `--check FILE` and `--baseline FILE`.
|
||||
12
examples/modules/tangle.ludic
Normal file
12
examples/modules/tangle.ludic
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# tangle.ludic — the program `ludic deps` is tested on: a and b use each other (the largest cycle is
|
||||
# 2), both use the leaf c (not counted), and a writes b's global b_count (one cross-module write).
|
||||
#
|
||||
# Running it prints: 4
|
||||
import "tangle/c"
|
||||
import "tangle/a"
|
||||
import "tangle/b"
|
||||
program Tangle {
|
||||
entry {
|
||||
print(a_run())
|
||||
}
|
||||
}
|
||||
9
examples/modules/tangle/a/index.ludic
Normal file
9
examples/modules/tangle/a/index.ludic
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# a/index.ludic - uses b, and writes b's global from outside it
|
||||
module a
|
||||
export function a_run() -> int {
|
||||
b_count = b_count + 1
|
||||
return b_run() + c_one()
|
||||
}
|
||||
export function a_two() -> int {
|
||||
return 2
|
||||
}
|
||||
6
examples/modules/tangle/b/index.ludic
Normal file
6
examples/modules/tangle/b/index.ludic
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# b/index.ludic - uses a back: a and b are a cycle
|
||||
module b
|
||||
export var b_count: int = 0
|
||||
export function b_run() -> int {
|
||||
return a_two() + c_one()
|
||||
}
|
||||
5
examples/modules/tangle/c/index.ludic
Normal file
5
examples/modules/tangle/c/index.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# c/index.ludic - uses nothing: a leaf, which the dependencies do not count
|
||||
module c
|
||||
export function c_one() -> int {
|
||||
return 1
|
||||
}
|
||||
112
selfhost/backend/emit_deps.ludic
Normal file
112
selfhost/backend/emit_deps.ludic
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# 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,>
|
||||
# edge <from> <to> <count> <first name>
|
||||
# write <owner> <global> <from> <file>:<line>
|
||||
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] }
|
||||
deps_line(f, `module {m} {itoa(pkg)} {uses}`)
|
||||
}
|
||||
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
|
||||
}
|
||||
file_close(f)
|
||||
}
|
||||
|
|
@ -42,9 +42,14 @@ function emit_assign(st: Node) -> void {
|
|||
let g = find_global(t.s)
|
||||
if (g == null) { perr(`assign to unknown {t.s}`) }
|
||||
vis_check(g, t.s)
|
||||
deps_write(g, t.s)
|
||||
addr = `@g_{t.s}`; ty = g.ty
|
||||
}
|
||||
} else {
|
||||
if deps_on() {
|
||||
let root = deps_target_root(t)
|
||||
if root != null { deps_write(root, root.s) }
|
||||
}
|
||||
if t.kind == E_MEMBER { addr = emit_member_addr(t); ty = g_addr_ty }
|
||||
else { if t.kind == E_INDEX { addr = emit_index_addr(t); ty = g_addr_ty }
|
||||
else { perr("bad assignment target") } }
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ function vis_say(m: pointer) -> void {
|
|||
function vis_check(d: Node, what0: pointer) -> void {
|
||||
if d == null { return }
|
||||
port_check_bound(d)
|
||||
deps_edge(d, what0) # LUDIC_DEPS: the graph ludic deps reads (emit_deps.ludic)
|
||||
if g_vis_off { return }
|
||||
if d.file == null { return }
|
||||
let what = vis_plain(what0)
|
||||
|
|
|
|||
42764
selfhost/ludicc.seed.ll
42764
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -208,6 +208,7 @@ entry {
|
|||
check_duplicate_decls()
|
||||
check_program() # L4: the types agree before anything is emitted
|
||||
emit_program()
|
||||
deps_flush() # LUDIC_DEPS=<file>: the module graph (ludic deps)
|
||||
|
||||
# --emit-llvm, or no binary target: emit IR and stop (stdout when out is null).
|
||||
if emit_ir { ir_flush(out); return }
|
||||
|
|
|
|||
374
tools/ludic-cli/deps.ludic
Normal file
374
tools/ludic-cli/deps.ludic
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
# ---- 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
|
||||
# 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
|
||||
#
|
||||
# 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;
|
||||
# cross_writes: assignments to a global of another module; globals_written_from_outside: those
|
||||
# globals, each counted once.
|
||||
var dp_mods: []pointer = null
|
||||
var dp_pkg: []int = null
|
||||
var dp_uses: []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_wowner: []pointer = null
|
||||
var dp_wname: []pointer = null
|
||||
var dp_wfrom: []pointer = null
|
||||
|
||||
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, "-")
|
||||
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_ef = new []int; dp_et = new []int; dp_ec = new []int; dp_en = new []pointer
|
||||
dp_writes = 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]
|
||||
}
|
||||
}
|
||||
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) >= 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
|
||||
}
|
||||
function s_to_int(s: pointer) -> int {
|
||||
var v = 0
|
||||
var i = 0
|
||||
while s[i] >= 48 and s[i] <= 57 {
|
||||
v = v * 10 + (s[i] - 48)
|
||||
i += 1
|
||||
}
|
||||
return v
|
||||
}
|
||||
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]) }
|
||||
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
|
||||
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) {
|
||||
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
|
||||
function dp_numbers() -> void {
|
||||
dp_names = ["modules", "dependencies", "largest_cycle", "cross_writes", "globals_written_from_outside"]
|
||||
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_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_vals = [mods, deps, len(dp_cycle), writes, len(seen)]
|
||||
}
|
||||
|
||||
# ---- 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)}`)
|
||||
}
|
||||
# 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
|
||||
}
|
||||
function dp_declared(from: int, to: int) -> bool {
|
||||
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])}` }
|
||||
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" {
|
||||
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]\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]))) }
|
||||
return 0
|
||||
}
|
||||
if mode == "uses" { dp_uses_of(arg2); return 0 }
|
||||
if mode == "check" { return dp_check(arg2) }
|
||||
if mode == "baseline" { return dp_baseline(arg2) }
|
||||
dp_print_numbers()
|
||||
return 0
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ program Ludic {
|
|||
import "pack.ludic"
|
||||
import "bundle.ludic"
|
||||
import "scripts.ludic"
|
||||
import "deps.ludic"
|
||||
|
||||
function usage() -> void {
|
||||
print("ludic — the toolchain for the Ludic language")
|
||||
|
|
@ -42,6 +43,8 @@ program Ludic {
|
|||
print(" compile to build/<name> (a native binary, nothing to ship beside it)")
|
||||
print(" test [file...] [-v] [--test NAME]")
|
||||
print(" compile and run the project's tests (-v: every test's line)")
|
||||
print(" deps [file] [--graph|--dot|--writes|--uses MOD|--check F|--baseline F]")
|
||||
print(" the module graph as the compiler sees it, and how tangled it is")
|
||||
print(" clean remove build/")
|
||||
print("")
|
||||
print("packages:")
|
||||
|
|
@ -93,6 +96,7 @@ program Ludic {
|
|||
if (cmd == "build") { return cmd_build() }
|
||||
if (cmd == "run") { return cmd_run() }
|
||||
if (cmd == "test") { return cmd_test() }
|
||||
if (cmd == "deps") { return cmd_deps() }
|
||||
if (cmd == "clean") { return cmd_clean() }
|
||||
if (cmd == "fmt") { return cmd_fmt() }
|
||||
if (cmd == "lsp") { return cmd_lsp() }
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
# the built-in commands a hook can wrap (every user command that does work)
|
||||
function is_hookable(cmd: pointer) -> bool {
|
||||
return (cmd == "build") or (cmd == "run") or (cmd == "test") or (cmd == "bundle") or (cmd == "pack") or (cmd == "clean") or (cmd == "fmt") or (cmd == "get") or (cmd == "add") or (cmd == "update") or (cmd == "verify") or (cmd == "vendor") or (cmd == "assets") or (cmd == "build-lib")
|
||||
return (cmd == "build") or (cmd == "run") or (cmd == "test") or (cmd == "deps") or (cmd == "bundle") or (cmd == "pack") or (cmd == "clean") or (cmd == "fmt") or (cmd == "get") or (cmd == "add") or (cmd == "update") or (cmd == "verify") or (cmd == "vendor") or (cmd == "assets") or (cmd == "build-lib")
|
||||
}
|
||||
|
||||
# the directory this `ludic` binary lives in, with no trailing slash
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ function selfhost_frags() -> []pointer {
|
|||
push(f, "selfhost/frontend/component_class.ludic")
|
||||
push(f, "selfhost/backend/emit_core.ludic")
|
||||
push(f, "selfhost/backend/emit_head.ludic")
|
||||
push(f, "selfhost/backend/emit_deps.ludic")
|
||||
push(f, "selfhost/backend/emit_addr.ludic")
|
||||
push(f, "selfhost/backend/emit_intrin.ludic")
|
||||
push(f, "selfhost/backend/emit_intrin2.ludic")
|
||||
|
|
|
|||
|
|
@ -471,6 +471,23 @@ function lab_plate_case() -> void {
|
|||
ok(`{lbl} (peak {peak} bytes)`)
|
||||
}
|
||||
|
||||
# ludic deps: the module graph as the compiler resolved it, its numbers, a baseline check and --dot
|
||||
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\nin the largest cycle: a b\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 }
|
||||
if not shq(`bin/ludic deps {p} --check {base} > /dev/null`) { bad2(lbl, "--check failed against its own baseline"); return }
|
||||
write_file(base, "dependencies 1\nlargest_cycle 2\ncross_writes 1\nglobals_written_from_outside 1\n")
|
||||
if shq(`bin/ludic deps {p} --check {base} > /dev/null`) { bad2(lbl, "--check passed with more dependencies than the baseline"); return }
|
||||
if not shq(`bin/ludic deps {p} --dot | grep -q '"a" -> "b"'`) { bad2(lbl, "--dot has no a -> b edge"); return }
|
||||
if not shq(`bin/ludic deps {p} --writes | grep -q 'b b_count a examples/modules/tangle/a/index.ludic:4'`) { bad2(lbl, "--writes misses a's write to b_count"); return }
|
||||
ok(lbl)
|
||||
}
|
||||
|
||||
# every package's own tests, the way a package author runs them: `ludic test packages`
|
||||
function packages_test_case() -> void {
|
||||
let lbl = "ludic test packages: every package's tests pass"
|
||||
|
|
@ -824,6 +841,7 @@ function cmd_dev_test() -> int {
|
|||
feat_case("modules/private_kinds", "", "7 hare", "private_kinds.ludic (L3: two modules' private record Catch and event Landed do not collide)")
|
||||
reject_case("rejected/exported_kinds", "'Catch' is defined twice", "two modules that export one record still clash")
|
||||
reject_case("rejected/exported_events", "event 'Landed' is defined twice", "two modules that export one event clash")
|
||||
feat_case("modules/tangle", "", "4", "tangle.ludic (the program ludic deps is tested on)")
|
||||
reject_case("rejected/exported_twins", "function 'seed' is defined twice", "two modules that export one name still clash")
|
||||
vis_report_case("rejected/uses_missing", "uses: examples/rejected/uses_valley/items/index.ludic:3: items.inv_add used from", "LUDIC_VIS_REPORT=1 lists a uses violation and builds")
|
||||
feat_case("lang/checked", "", "7.5 3 12 hi 2 ok", "checked.ludic (L4: literals take their slot's kind, string(p), []string as []pointer, null, named args)")
|
||||
|
|
@ -1044,6 +1062,7 @@ function cmd_dev_test() -> int {
|
|||
package_scripts_case()
|
||||
test_dir_case()
|
||||
packages_test_case()
|
||||
deps_case()
|
||||
lab_plate_case()
|
||||
pack_roundtrip_case()
|
||||
packignore_case()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue