2025-11-08 15:49:25 +09:00
|
|
|
// tools/hako_check/render/graphviz.hako - GraphvizRenderBox (MVP)
|
2025-11-07 19:32:44 +09:00
|
|
|
// Render minimal DOT graph from one or more Analysis IRs.
|
|
|
|
|
|
|
|
|
|
using selfhost.shared.common.string_helpers as Str
|
|
|
|
|
|
|
|
|
|
static box GraphvizRenderBox {
|
|
|
|
|
render_multi(irs) {
|
|
|
|
|
// irs: ArrayBox of IR Map
|
|
|
|
|
print("digraph Hako {")
|
2025-11-08 23:45:29 +09:00
|
|
|
// Internal key names for node/edge key lists (avoid collisions with user names)
|
|
|
|
|
local NK_KEY = "__graphviz_nodes__"
|
|
|
|
|
local EK_KEY = "__graphviz_edges__"
|
2025-11-07 19:32:44 +09:00
|
|
|
// optional graph attributes (kept minimal)
|
|
|
|
|
// print(" rankdir=LR;")
|
|
|
|
|
// Node and edge sets to avoid duplicates
|
|
|
|
|
local nodes = new MapBox()
|
|
|
|
|
local edges = new MapBox()
|
|
|
|
|
if irs != null {
|
|
|
|
|
local gi = 0
|
|
|
|
|
while gi < irs.size() {
|
|
|
|
|
local ir = irs.get(gi)
|
|
|
|
|
me._render_ir(ir, nodes, edges)
|
|
|
|
|
gi = gi + 1
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-08 15:49:25 +09:00
|
|
|
// Build clusters by box: group nodes whose name looks like Box.method/arity
|
|
|
|
|
local groups = new MapBox()
|
|
|
|
|
local group_keys = new ArrayBox()
|
2025-11-08 23:45:29 +09:00
|
|
|
local nk = nodes.get(NK_KEY)
|
|
|
|
|
if nk == null { nk = new ArrayBox() }
|
2025-11-08 15:49:25 +09:00
|
|
|
if nk != null {
|
|
|
|
|
local i = 0
|
|
|
|
|
while i < nk.size() {
|
|
|
|
|
local name = nk.get(i)
|
|
|
|
|
local dot = name.indexOf(".")
|
|
|
|
|
if dot > 0 {
|
|
|
|
|
local box_name = name.substring(0, dot)
|
|
|
|
|
local gkey = "cluster_" + box_name
|
|
|
|
|
local arr = groups.get(gkey)
|
|
|
|
|
if arr == null { arr = new ArrayBox(); groups.set(gkey, arr); group_keys.push(gkey) }
|
|
|
|
|
// dedup in group
|
|
|
|
|
local seen = 0; local j=0; while j < arr.size() { if arr.get(j) == name { seen = 1; break } j = j + 1 }
|
|
|
|
|
if seen == 0 { arr.push(name) }
|
|
|
|
|
}
|
|
|
|
|
i = i + 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Emit clusters
|
|
|
|
|
local gi = 0
|
|
|
|
|
while gi < group_keys.size() {
|
|
|
|
|
local gk = group_keys.get(gi)
|
|
|
|
|
print(" subgraph \"" + gk + "\" {")
|
|
|
|
|
// label = box name (strip "cluster_")
|
|
|
|
|
local label = gk.substring("cluster_".length())
|
|
|
|
|
print(" label=\"" + label + "\";")
|
|
|
|
|
local arr = groups.get(gk)
|
|
|
|
|
local j = 0; while j < arr.size() { print(" \"" + arr.get(j) + "\";"); j = j + 1 }
|
|
|
|
|
print(" }")
|
|
|
|
|
gi = gi + 1
|
|
|
|
|
}
|
|
|
|
|
// Emit edges
|
2025-11-07 19:32:44 +09:00
|
|
|
// Emit edges
|
|
|
|
|
if edges != null {
|
|
|
|
|
// edges map key = from + "\t" + to
|
|
|
|
|
// naive iteration by trying to get keys from a stored list
|
|
|
|
|
// We kept an ArrayBox under edges.get("__keys__") for listing
|
2025-11-08 23:45:29 +09:00
|
|
|
local ks = edges.get(EK_KEY)
|
|
|
|
|
if ks == null { ks = new ArrayBox() }
|
2025-11-07 19:32:44 +09:00
|
|
|
if ks != null {
|
|
|
|
|
local ei = 0
|
|
|
|
|
while ei < ks.size() {
|
|
|
|
|
local key = ks.get(ei)
|
|
|
|
|
local tab = key.indexOf("\t")
|
|
|
|
|
if tab > 0 {
|
|
|
|
|
local src = key.substring(0, tab)
|
|
|
|
|
local dst = key.substring(tab+1)
|
|
|
|
|
print(" \"" + src + "\" -> \"" + dst + "\";")
|
2025-11-08 15:49:25 +09:00
|
|
|
// also register nodes (in case they weren't explicitly collected)
|
2025-11-07 19:32:44 +09:00
|
|
|
nodes.set(src, 1)
|
|
|
|
|
nodes.set(dst, 1)
|
|
|
|
|
}
|
|
|
|
|
ei = ei + 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-08 15:49:25 +09:00
|
|
|
// Emit standalone nodes not covered by clusters
|
2025-11-07 19:32:44 +09:00
|
|
|
if nk != null {
|
|
|
|
|
local ni = 0
|
|
|
|
|
while ni < nk.size() {
|
|
|
|
|
local name = nk.get(ni)
|
2025-11-08 15:49:25 +09:00
|
|
|
local dot = name.indexOf(".")
|
|
|
|
|
if dot < 0 { print(" \"" + name + "\";") }
|
2025-11-07 19:32:44 +09:00
|
|
|
ni = ni + 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
print("}")
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
_render_ir(ir, nodes, edges) {
|
|
|
|
|
if ir == null { return }
|
|
|
|
|
// methods
|
|
|
|
|
local ms = ir.get("methods")
|
|
|
|
|
if ms != null {
|
|
|
|
|
local mi = 0
|
|
|
|
|
while mi < ms.size() {
|
|
|
|
|
me._add_node(nodes, ms.get(mi))
|
|
|
|
|
mi = mi + 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// calls
|
|
|
|
|
local cs = ir.get("calls")
|
|
|
|
|
if cs != null {
|
|
|
|
|
local ci = 0
|
|
|
|
|
while ci < cs.size() {
|
|
|
|
|
local c = cs.get(ci)
|
|
|
|
|
local f = c.get("from")
|
|
|
|
|
local t = c.get("to")
|
|
|
|
|
me._add_edge(edges, f, t)
|
|
|
|
|
ci = ci + 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_add_node(nodes, name) {
|
|
|
|
|
if name == null { return }
|
|
|
|
|
nodes.set(name, 1)
|
|
|
|
|
// also store a list of keys for emitting (since Map has no key iterator)
|
2025-11-08 23:45:29 +09:00
|
|
|
local arr = nodes.get("__graphviz_nodes__"); if arr == null { arr = new ArrayBox(); nodes.set("__graphviz_nodes__", arr) }
|
2025-11-07 19:32:44 +09:00
|
|
|
// avoid duplicates
|
|
|
|
|
local seen = 0
|
|
|
|
|
local i = 0; while i < arr.size() { if arr.get(i) == name { seen = 1; break } i = i + 1 }
|
|
|
|
|
if seen == 0 { arr.push(name) }
|
|
|
|
|
}
|
|
|
|
|
_add_edge(edges, src, dst) {
|
|
|
|
|
if src == null || dst == null { return }
|
|
|
|
|
local key = src + "\t" + dst
|
|
|
|
|
if edges.get(key) == null { edges.set(key, 1) }
|
2025-11-08 23:45:29 +09:00
|
|
|
local arr = edges.get("__graphviz_edges__"); if arr == null { arr = new ArrayBox(); edges.set("__graphviz_edges__", arr) }
|
2025-11-07 19:32:44 +09:00
|
|
|
// avoid duplicates
|
|
|
|
|
local seen = 0
|
|
|
|
|
local i = 0; while i < arr.size() { if arr.get(i) == key { seen = 1; break } i = i + 1 }
|
|
|
|
|
if seen == 0 { arr.push(key) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static box GraphvizRenderMain { method main(args) { return 0 } }
|