Skip to content

Commit 5889d3c

Browse files
authored
Dot visualisation for interprocedural graphs (#327)
1 parent 4ff7243 commit 5889d3c

File tree

1 file changed

+195
-1
lines changed

1 file changed

+195
-1
lines changed

jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,25 @@
1616

1717
package org.jacodb.ets.utils
1818

19+
import org.jacodb.ets.model.BasicBlock
20+
import org.jacodb.ets.model.EtsAssignStmt
1921
import org.jacodb.ets.model.EtsBlockCfg
22+
import org.jacodb.ets.model.EtsCallExpr
23+
import org.jacodb.ets.model.EtsCallStmt
24+
import org.jacodb.ets.model.EtsStmt
25+
import java.nio.file.Files
26+
import java.nio.file.Path
27+
import java.nio.file.Paths
2028

2129
private fun String.htmlEncode(): String = this
2230
.replace("&", "&")
2331
.replace("<", "&lt;")
2432
.replace(">", "&gt;")
2533
.replace("\"", "&quot;")
2634

27-
fun EtsBlockCfg.toDot(useHtml: Boolean = true): String {
35+
fun EtsBlockCfg.toDot(
36+
useHtml: Boolean = true
37+
): String {
2838
val lines = mutableListOf<String>()
2939
lines += "digraph cfg {"
3040
lines += " node [shape=${if (useHtml) "none" else "rect"} fontname=\"monospace\"]"
@@ -65,3 +75,187 @@ fun EtsBlockCfg.toDot(useHtml: Boolean = true): String {
6575
lines += "}"
6676
return lines.joinToString("\n")
6777
}
78+
79+
/**
80+
* An interprocedural CFG that contains:
81+
* - the main control-flow graph (main)
82+
* - all callee CFGs discovered so far at call sites (keyed by statement and its parent block id)
83+
*/
84+
data class InterproceduralCfg(
85+
val main: EtsBlockCfg,
86+
val callees: Map<Pair<EtsStmt, Int>, EtsBlockCfg>
87+
)
88+
89+
/**
90+
* Render the interprocedural CFG (main + callees) as a single Graphviz DOT document,
91+
* highlighting the execution path and current statement, and drawing
92+
* each callee in its own dashed subgraph connected back to the call site.
93+
*/
94+
fun InterproceduralCfg.toHighlightedDotWithCalls(
95+
pathStmts: Set<EtsStmt>,
96+
currentStmt: EtsStmt?,
97+
useHtml: Boolean = true
98+
): String {
99+
val lines = mutableListOf<String>()
100+
lines += "digraph world {"
101+
lines += " compound=true"
102+
lines += " node [shape=${if (useHtml) "none" else "rect"} fontname=\"monospace\"]"
103+
104+
// --- 1) Main CFG with ports on call statements ---
105+
for (block in main.blocks) {
106+
val nodeId = "M_${block.id}"
107+
// compute all call hashes in this block
108+
val callHashes = callees.keys
109+
.filter { it.second == block.id }
110+
.map { sanitize(it.first.hashCode()) }
111+
.toSet()
112+
113+
if (useHtml) {
114+
// build HTML table rows, adding port attribute on call lines
115+
val rows = block.statements.joinToString(separator = "") { stmt ->
116+
val txt = stmt.toDotLabel().htmlEncode()
117+
val bg = when (stmt) {
118+
currentStmt -> "lightblue"
119+
in pathStmts -> "yellow"
120+
else -> "white"
121+
}
122+
val stmtHash = sanitize(stmt.hashCode())
123+
val portAttr = if (stmtHash in callHashes) " port=\"p$stmtHash\"" else ""
124+
"<tr><td balign=\"left\" bgcolor=\"$bg\"$portAttr>$txt</td></tr>"
125+
}
126+
val table = buildString {
127+
append("<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">")
128+
append("<tr><td><b>Block #${block.id}</b></td></tr>")
129+
append(rows)
130+
append("</table>")
131+
}
132+
lines += " $nodeId [label=<$table>];"
133+
} else {
134+
val lbl = buildPlainLabel(block, pathStmts, currentStmt)
135+
lines += " $nodeId [label=\"$lbl\"];"
136+
}
137+
}
138+
// Main CFG edges
139+
for ((bid, succs) in main.successors) {
140+
val from = "M_$bid"
141+
when (succs.size) {
142+
1 -> lines += " $from -> M_${succs[0]};"
143+
2 -> {
144+
val (t, f) = succs
145+
lines += " $from -> M_$t [label=\"true\"];"
146+
lines += " $from -> M_$f [label=\"false\"];"
147+
}
148+
}
149+
}
150+
151+
// --- 2) Callee clusters with call-edge from port ---
152+
for ((key, cfg) in callees) {
153+
val (stmt, parentBlock) = key
154+
val h = sanitize(stmt.hashCode())
155+
val clusterName = "cluster_${h}_B${parentBlock}"
156+
// method signature label
157+
val methodSig = cfg.entries.first().method.signature
158+
// open subgraph
159+
lines += " subgraph \"$clusterName\" {"
160+
lines += " label=\"$methodSig\";"
161+
lines += " style=dashed;"
162+
163+
// render callee nodes
164+
for (blk in cfg.blocks) {
165+
val calleeNode = "C_${h}_${blk.id}"
166+
if (useHtml) {
167+
val table = buildHtmlTable(blk, pathStmts, currentStmt)
168+
lines += " $calleeNode [label=<$table>];"
169+
} else {
170+
val lbl = buildPlainLabel(blk, pathStmts, currentStmt)
171+
lines += " $calleeNode [label=\"$lbl\"];"
172+
}
173+
}
174+
// render callee edges
175+
for ((bid, succs) in cfg.successors) {
176+
val from = "C_${h}_$bid"
177+
when (succs.size) {
178+
1 -> lines += " $from -> C_${h}_${succs[0]};"
179+
2 -> {
180+
val (t, f) = succs
181+
lines += " $from -> C_${h}_$t [label=\"true\"];"
182+
lines += " $from -> C_${h}_$f [label=\"false\"];"
183+
}
184+
}
185+
}
186+
lines += " }"
187+
188+
// connect from the specific port on the caller block
189+
// connect from the specific port on the caller block using tailport
190+
val caller = "M_${parentBlock}"
191+
val entryId = cfg.blocks.first().id
192+
val calleeEntry = "C_${h}_$entryId"
193+
val stmtHash = sanitize(stmt.hashCode())
194+
lines += " $caller:p$stmtHash -> $calleeEntry [tailport=\"p$stmtHash\" ltail=\"$clusterName\" lhead=\"$clusterName\" style=dotted label=\"call\"];"
195+
}
196+
197+
lines += "}"
198+
return lines.joinToString("\n")
199+
}
200+
201+
private fun buildHtmlTable(
202+
block: BasicBlock,
203+
pathStmts: Set<EtsStmt>,
204+
currentStmt: EtsStmt?
205+
): String {
206+
var i = 0
207+
val rows = block.statements.joinToString(separator = "") { stmt ->
208+
val txt = stmt.toDotLabel().htmlEncode()
209+
val stmtHash = sanitize(stmt.hashCode())
210+
val portAttr = if (stmt.callExpr != null) " port=\"p$stmtHash\"" else ""
211+
212+
val bg = when (stmt) {
213+
currentStmt -> "lightblue"
214+
in pathStmts -> "yellow"
215+
else -> "white"
216+
}
217+
i++
218+
"<tr><td balign=\"left\" bgcolor=\"$bg\"$portAttr>$txt</td></tr>"
219+
}
220+
return "<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">" +
221+
"<tr><td><b>Block #${block.id}</b></td></tr>" + rows +
222+
"</table>"
223+
}
224+
225+
private fun buildPlainLabel(
226+
block: BasicBlock,
227+
pathStmts: Set<EtsStmt>,
228+
currentStmt: EtsStmt?
229+
): String {
230+
val body = block.statements.joinToString(separator = "") { stmt ->
231+
val raw = stmt.toDotLabel()
232+
val pfx = when (stmt) {
233+
currentStmt -> "[▶] "
234+
in pathStmts -> "[·] "
235+
else -> ""
236+
}
237+
"$pfx$raw\\l"
238+
}
239+
return "Block #${block.id}\\n" + body
240+
}
241+
242+
fun renderDotOverwrite(
243+
dot: String,
244+
outputDir: Path = Paths.get("."),
245+
baseName: String = "interproc_cfg",
246+
dotCmd: String = "dot",
247+
viewerCmd: String = when {
248+
System.getProperty("os.name").startsWith("Mac") -> "open"
249+
System.getProperty("os.name").startsWith("Win") -> "cmd /c start"
250+
else -> "xdg-open"
251+
}
252+
) {
253+
val dotFile = outputDir.resolve("$baseName.dot")
254+
val outSvg = outputDir.resolve("$baseName.svg")
255+
Files.write(dotFile, dot.toByteArray())
256+
Runtime.getRuntime().exec("$dotCmd -Tsvg -o $outSvg $dotFile").waitFor()
257+
Runtime.getRuntime().exec("$viewerCmd $outSvg").waitFor()
258+
}
259+
260+
// helper to sanitize negative hash codes for Graphviz IDs
261+
fun sanitize(id: Int): String = id.toString().let { if (it.startsWith("-")) "N${it.substring(1)}" else it }

0 commit comments

Comments
 (0)