flowgraph: control-flow graphs from the compiler IR
The same control-flow-graph recovery as the flowgraph CLI tool (flowgraph <output.html> [inputs...], source below), running live: paste a program, press Render, and see basic blocks and jump edges recovered from the self-hosted compiler's own IR โ one SVG per function, no rustc. Red curves are conditional (JumpIfFalse), blue are unconditional. With no arguments the CLI tool instead renders a curated set spanning the benchmark, the contracts demo, the point-of-sale bundle, and the all-paradigms demo: open that reference page.
PatLang source: flowgraph_main.patlang
# =============================================================================
# flowgraph: control-flow graph visualizer (concatenate after the compiler
# libs + lib/html.patlang). Lowers a PatLang program with the self-hosted
# compiler, splits each function's IR into basic blocks, and renders the
# blocks and jump edges as SVG in an HTML page.
# =============================================================================
make a function called contains_num takes xs, v returns r
let i = 0
while i < xs.length do
if xs[i] == v then
return true
end
let i = i + 1
end
return false
end
make a function called instr_text takes ins returns s
let s = ins[0]
if ins.length > 1 then
let s = s + " " + ins[1]
end
if ins.length > 2 then
let s = s + " " + ins[2]
end
return s
end
make a function called is_jump takes tag returns r
return (tag == "Jump") or (tag == "JumpIfFalse") or (tag == "Return")
end
# Render one function's CFG as SVG appended to builder b; returns block count
make a function called render_func takes b, f returns blocks
let instrs = f[3]
let n = instrs.length
# block starts: 0, every jump target, and every instr after a jump
let starts = [0]
let i = 0
while i < n do
let ins = instrs[i]
let tag = ins[0]
if (tag == "Jump") or (tag == "JumpIfFalse") then
if not contains_num(starts, ins[1]) then
let starts = list_push(starts, ins[1])
end
if not contains_num(starts, i + 1) then
let starts = list_push(starts, i + 1)
end
end
let i = i + 1
end
# block index for an instruction = count of starts <= idx that we pass
# build blocks in order by scanning
let rowh = 26
let y = 10
sb_push(b, "<h3>" + html_escape(f[1]) + "</h3>" + nl())
let blockTops = []
let blockIds = []
let i = 0
let bod = sb_new()
let edges = sb_new()
let blocks = 0
while i < n do
if contains_num(starts, i) then
let blocks = blocks + 1
let blockTops = list_push(blockTops, y)
let blockIds = list_push(blockIds, i)
sb_push(bod, "<text x='16' y='" + (y + 16) + "' class='bh'>B@" + i + "</text>" + nl())
let y = y + rowh
end
let ins = instrs[i]
sb_push(bod, "<text x='28' y='" + (y + 14) + "'>" + html_escape(instr_text(ins)) + "</text>" + nl())
let tag = ins[0]
if (tag == "Jump") or (tag == "JumpIfFalse") then
# edge from this row to the target block header (drawn after layout)
let edges = edges
sb_push(edges, "" + (y + 8) + " " + ins[1] + " " + tag + "\n")
end
let y = y + rowh
let i = i + 1
end
let height = y + 20
sb_push(b, "<svg viewBox='0 0 560 " + height + "' width='560' height='" + height + "' xmlns='http://www.w3.org/2000/svg'>" + nl())
# draw jump edges as curves on the right margin
let elist = sb_str(edges)
let eh = str_intern(elist)
let elen = sc_len(eh)
let k = 0
let word = sb_new()
let fields = []
while k <= elen do
let c = sc_code(eh, k)
if (c == 32) or (c == 10) or (c == -1) then
let w = sb_str(word)
let word = sb_new()
if w != "" then
let fields = list_push(fields, w)
end
if (c == 10) and (fields.length == 3) then
# fields: fromY targetInstr tag
let fromY = to_num(fields[0])
let tIdx = to_num(fields[1])
# find target block top
let ti = 0
let tY = 10
while ti < blockIds.length do
if blockIds[ti] == tIdx then
let tY = blockTops[ti]
end
let ti = ti + 1
end
let color = "#d64"
if fields[2] == "Jump" then
let color = "#48c"
end
sb_push(b, "<path d='M 330 " + fromY + " C 520 " + fromY + ", 520 " + (tY + 10) + ", 335 " + (tY + 10) + "' fill='none' stroke='" + color + "' marker-end='url(#arr)'/>" + nl())
let fields = []
end
else
sb_push(word, sc_char(eh, k))
end
let k = k + 1
end
sb_push(b, "<defs><marker id='arr' viewBox='0 0 8 8' refX='7' refY='4' markerWidth='6' markerHeight='6' orient='auto'><path d='M 0 0 L 8 4 L 0 8 z' fill='#888'/></marker></defs>" + nl())
sb_push(b, sb_str(bod))
sb_push(b, "</svg>" + nl())
return blocks
end
# Reads a source spec, supporting "lib.patlang+demo.patlang" bundling for
# programs that need a library concatenated ahead of their driver (mirrors
# patbuild's manifest convention, kept independent to avoid file coupling).
make a function called read_maybe_bundle takes spec returns source
let b = sb_new()
let cur = sb_new()
let i = 0
let first = true
while i <= spec.length do
let c = char_code(spec, i)
if (c == 43) or (c == -1) then
let piece = sb_str(cur)
let cur = sb_new()
if piece != "" then
if not first then
sb_push(b, chr(10))
end
sb_push(b, read_file(piece))
let first = false
end
else
sb_push(cur, spec[i])
end
let i = i + 1
end
return sb_str(b)
end
# Renders one program's functions as a labelled section; returns block count.
make a function called render_program_section takes b, label, spec returns blocks
let source = read_maybe_bundle(spec)
let toks = tokenize(source)
let ast = parse_program(toks)
let ir = lower_program(ast)
let funcs = ir[2]
sb_push(b, "<h2 id='" + label + "'>" + html_escape(label) + "</h2>" + nl())
sb_push(b, "<p class='label'><code>" + html_escape(spec) + "</code> — " + funcs.length + " function(s)</p>" + nl())
let i = 0
let total = 0
while i < funcs.length do
let total = total + render_func(b, funcs[i])
let i = i + 1
end
return total
end
# ---- driver ----
# Usage: flowgraph <output.html> [input1] [input2] ...
# With no inputs, renders a curated set of the portfolio's example programs
# into one navigable page (this is how the portfolio card invokes it).
let args = argv()
let output = "portfolio/flowgraph.html"
let specs = []
if args.length >= 1 then
let output = args[0]
end
if args.length >= 2 then
let k = 1
while k < args.length do
let specs = list_push(specs, args[k])
let k = k + 1
end
end
if specs.length == 0 then
let specs = [
"self_hosting/examples/fib_bench.patlang",
"self_hosting/examples/contracts_demo.patlang",
"self_hosting/lib/pos.patlang+self_hosting/examples/pos_demo.patlang",
"self_hosting/examples/feature_demo.patlang"
]
end
let nav = sb_new()
sb_push(nav, "<p>")
let i = 0
while i < specs.length do
if i > 0 then
sb_push(nav, " · ")
end
sb_push(nav, "<a href='#" + specs[i] + "'>" + html_escape(specs[i]) + "</a>")
let i = i + 1
end
sb_push(nav, "</p>" + nl())
let body = sb_new()
sb_push(body, "<h1>PatLang control-flow graphs</h1>" + nl())
sb_push(body, "<p>Basic blocks and jump edges recovered from the self-hosted compiler's own IR, one section per program. Red curves are conditional (JumpIfFalse), blue are unconditional.</p>" + nl())
sb_push(body, "<style>svg text { font: 12px monospace; fill: currentColor; } svg .bh { font-weight: bold; }</style>" + nl())
sb_push(body, sb_str(nav))
let total_funcs = 0
let total_blocks = 0
let i = 0
while i < specs.length do
let total_blocks = total_blocks + render_program_section(body, specs[i], specs[i])
let i = i + 1
end
let page = html_page("PatLang flow graphs", sb_str(body), "")
write_file(output, page)
print("[flowgraph] " + specs.length + " program(s), " + total_blocks + " basic blocks -> " + output)
Native run of the CLI tool on the build machine (curated multi-program set):
[flowgraph] 4 program(s), 63 basic blocks -> portfolio/flowgraph.html