|
16 | 16 |
|
17 | 17 | package org.jacodb.ets.utils |
18 | 18 |
|
| 19 | +import org.jacodb.ets.model.BasicBlock |
| 20 | +import org.jacodb.ets.model.EtsAssignStmt |
19 | 21 | 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 |
20 | 28 |
|
21 | 29 | private fun String.htmlEncode(): String = this |
22 | 30 | .replace("&", "&") |
23 | 31 | .replace("<", "<") |
24 | 32 | .replace(">", ">") |
25 | 33 | .replace("\"", """) |
26 | 34 |
|
27 | | -fun EtsBlockCfg.toDot(useHtml: Boolean = true): String { |
| 35 | +fun EtsBlockCfg.toDot( |
| 36 | + useHtml: Boolean = true |
| 37 | +): String { |
28 | 38 | val lines = mutableListOf<String>() |
29 | 39 | lines += "digraph cfg {" |
30 | 40 | lines += " node [shape=${if (useHtml) "none" else "rect"} fontname=\"monospace\"]" |
@@ -65,3 +75,187 @@ fun EtsBlockCfg.toDot(useHtml: Boolean = true): String { |
65 | 75 | lines += "}" |
66 | 76 | return lines.joinToString("\n") |
67 | 77 | } |
| 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