From 238c1ccd1475519f4c90fd742780c4330927b0d8 Mon Sep 17 00:00:00 2001 From: Alexey Menshutin Date: Thu, 26 Jun 2025 15:57:04 +0300 Subject: [PATCH 1/7] New method for highlighted dot --- .../org/jacodb/ets/utils/BlockCfgToDot.kt | 86 ++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt index 49f61afef..d43386d7d 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt @@ -17,6 +17,7 @@ package org.jacodb.ets.utils import org.jacodb.ets.model.EtsBlockCfg +import org.jacodb.ets.model.EtsStmt private fun String.htmlEncode(): String = this .replace("&", "&") @@ -24,7 +25,9 @@ private fun String.htmlEncode(): String = this .replace(">", ">") .replace("\"", """) -fun EtsBlockCfg.toDot(useHtml: Boolean = true): String { +fun EtsBlockCfg.toDot( + useHtml: Boolean = true +): String { val lines = mutableListOf() lines += "digraph cfg {" lines += " node [shape=${if (useHtml) "none" else "rect"} fontname=\"monospace\"]" @@ -65,3 +68,84 @@ fun EtsBlockCfg.toDot(useHtml: Boolean = true): String { lines += "}" return lines.joinToString("\n") } + +fun EtsBlockCfg.toHighlightedDot( + pathStmts: Set, + currentStmt: EtsStmt?, + useHtml: Boolean = true +): String { + val lines = mutableListOf() + + // Start the digraph and set default node attributes + lines += "digraph cfg {" + lines += " node [shape=${if (useHtml) "none" else "rect"} fontname=\"monospace\"]" + + // Generate a node for each basic block + for (block in blocks) { + if (useHtml) { + // Build HTML table rows: one per statement, with background color + val rows = block.statements.joinToString("") { stmt -> + val label = stmt.toDotLabel().htmlEncode() + val bgColor = when { + stmt == currentStmt -> "lightblue" // highlight the current statement + stmt in pathStmts -> "yellow" // highlight the path statements + else -> "white" // default background + } + // Create a table row with left-aligned text + "$label" + } + + // Assemble the complete HTML table for this block + val table = buildString { + append("") + append("") + append(rows) + append("
Block #${block.id}
") + } + + // Emit the node with an HTML label + lines += " ${block.id} [label=<$table>]" + } else { + // Fallback: non-HTML label, prefix symbols for path and current + val body = block.statements.joinToString("") { stmt -> + val raw = stmt.toDotLabel() + val prefix = when { + stmt == currentStmt -> "[▶] " // mark current statement + stmt in pathStmts -> "[·] " // mark path statements + else -> "" + } + "$prefix$raw\\l" + } + lines += " ${block.id} [label=\"Block #${block.id}\\n$body\"]" + } + } + + // Generate edges between blocks based on successors map + for (block in blocks) { + successors[block.id]?.let { succs -> + when (succs.size) { + 0 -> { + // No outgoing edges for this block + } + 1 -> { + // Single successor: unconditional jump + lines += " ${block.id} -> ${succs.single()}" + } + 2 -> { + // Two successors: label them true/false + val (trueBranch, falseBranch) = succs + lines += " ${block.id} -> $trueBranch [label=\"true\"]" + lines += " ${block.id} -> $falseBranch [label=\"false\"]" + } + else -> { + // Should not happen in a well-formed CFG + error("Block ${block.id} has more than two successors") + } + } + } + } + + // Close the digraph + lines += "}" + return lines.joinToString("\n") +} From 116faf9289cf7ddb4981d5b0f38dae100e18f637 Mon Sep 17 00:00:00 2001 From: Alexey Menshutin Date: Fri, 27 Jun 2025 11:08:21 +0300 Subject: [PATCH 2/7] New method for highlighted dot --- .../org/jacodb/ets/utils/BlockCfgToDot.kt | 171 ++++++++++++------ 1 file changed, 115 insertions(+), 56 deletions(-) diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt index d43386d7d..a9cab7205 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt @@ -16,7 +16,10 @@ package org.jacodb.ets.utils +import org.jacodb.ets.model.BasicBlock import org.jacodb.ets.model.EtsBlockCfg +import org.jacodb.ets.model.EtsCallExpr +import org.jacodb.ets.model.EtsCallStmt import org.jacodb.ets.model.EtsStmt private fun String.htmlEncode(): String = this @@ -69,83 +72,139 @@ fun EtsBlockCfg.toDot( return lines.joinToString("\n") } -fun EtsBlockCfg.toHighlightedDot( +/** + * An interprocedural CFG that contains: + * - the main control-flow graph (main) + * - all callee CFGs discovered so far at call sites (keyed by the statement itself) + */ +data class InterproceduralCfg( + val main: EtsBlockCfg, + val callees: Map +) + +/** + * Render the interprocedural CFG (main + callees) as a single Graphviz DOT document, + * highlighting the execution path and current statement, and drawing + * each callee in its own dashed subgraph connected back to the call site. + */ +fun InterproceduralCfg.toHighlightedDotWithCalls( pathStmts: Set, currentStmt: EtsStmt?, useHtml: Boolean = true ): String { val lines = mutableListOf() - // Start the digraph and set default node attributes - lines += "digraph cfg {" + // Start the digraph and allow edges between clusters + lines += "digraph world {" + lines += " compound=true" lines += " node [shape=${if (useHtml) "none" else "rect"} fontname=\"monospace\"]" - // Generate a node for each basic block - for (block in blocks) { + // ----- 1) Render the main CFG ----- + for (block in main.blocks) { + val nodeId = "M_${block.id}" if (useHtml) { - // Build HTML table rows: one per statement, with background color - val rows = block.statements.joinToString("") { stmt -> - val label = stmt.toDotLabel().htmlEncode() - val bgColor = when { - stmt == currentStmt -> "lightblue" // highlight the current statement - stmt in pathStmts -> "yellow" // highlight the path statements - else -> "white" // default background - } - // Create a table row with left-aligned text - "$label" - } - - // Assemble the complete HTML table for this block - val table = buildString { - append("") - append("") - append(rows) - append("
Block #${block.id}
") - } - - // Emit the node with an HTML label - lines += " ${block.id} [label=<$table>]" + val table = buildHtmlTable(block, pathStmts, currentStmt) + lines += " $nodeId [label=<$table>];" } else { - // Fallback: non-HTML label, prefix symbols for path and current - val body = block.statements.joinToString("") { stmt -> - val raw = stmt.toDotLabel() - val prefix = when { - stmt == currentStmt -> "[▶] " // mark current statement - stmt in pathStmts -> "[·] " // mark path statements - else -> "" - } - "$prefix$raw\\l" + val label = buildPlainLabel(block, pathStmts, currentStmt) + lines += " $nodeId [label=\"$label\"];" + } + } + for ((bid, succs) in main.successors) { + val from = "M_$bid" + when (succs.size) { + 1 -> lines += " $from -> M_${succs[0]};" + 2 -> { + val (t, f) = succs + lines += " $from -> M_$t [label=\"true\"];" + lines += " $from -> M_$f [label=\"false\"];" } - lines += " ${block.id} [label=\"Block #${block.id}\\n$body\"]" } } - // Generate edges between blocks based on successors map - for (block in blocks) { - successors[block.id]?.let { succs -> + // ----- 2) Render each discovered callee CFG as a dashed cluster ----- + for ((stmt, cfg) in callees) { + val callId = stmt.hashCode().toString() + val clusterName = "cluster_C_$callId" + + lines += " subgraph $clusterName {" + lines += " label=\"Callee of stmt $callId\";" + lines += " style=dashed;" + + // nodes in callee + for (block in cfg.blocks) { + val nodeId = "C_${callId}_${block.id}" + if (useHtml) { + val table = buildHtmlTable(block, pathStmts, currentStmt) + lines += " $nodeId [label=<$table>];" + } else { + val label = buildPlainLabel(block, pathStmts, currentStmt) + lines += " $nodeId [label=\"$label\"];" + } + } + for ((bid, succs) in cfg.successors) { + val from = "C_${callId}_$bid" when (succs.size) { - 0 -> { - // No outgoing edges for this block - } - 1 -> { - // Single successor: unconditional jump - lines += " ${block.id} -> ${succs.single()}" - } + 1 -> lines += " $from -> C_${callId}_${succs[0]};" 2 -> { - // Two successors: label them true/false - val (trueBranch, falseBranch) = succs - lines += " ${block.id} -> $trueBranch [label=\"true\"]" - lines += " ${block.id} -> $falseBranch [label=\"false\"]" - } - else -> { - // Should not happen in a well-formed CFG - error("Block ${block.id} has more than two successors") + val (t, f) = succs + lines += " $from -> C_${callId}_$t [label=\"true\"];" + lines += " $from -> C_${callId}_$f [label=\"false\"];" } } } + lines += " }" + + // ----- 3) Connect call-site in main to callee entry ----- + // find which main block contains this stmt + val callerBlockId = main.blocks.first { it.statements.contains(stmt) }.id + val callerNode = "M_$callerBlockId" + val entryNode = "C_${callId}_${cfg.blocks.first().id}" + lines += " $callerNode -> $entryNode [ltail=$clusterName lhead=$clusterName style=dotted label=\"call\"];" } - // Close the digraph lines += "}" return lines.joinToString("\n") } + + +/** Build an HTML table label for a block, coloring rows by path/current. */ +private fun buildHtmlTable( + block: BasicBlock, + pathStmts: Set, + currentStmt: EtsStmt? +): String { + val rows = block.statements.joinToString("") { stmt -> + val text = stmt.toDotLabel().htmlEncode() + val bg = when { + stmt == currentStmt -> "lightblue" + stmt in pathStmts -> "yellow" + else -> "white" + } + "$text" + } + return """ + + + $rows +
Block #${block.id}
+ """.trimIndent() +} + +/** Build a plain-text label (non-HTML) for a block, prefixing rows. */ +private fun buildPlainLabel( + block: BasicBlock, + pathStmts: Set, + currentStmt: EtsStmt? +): String { + val body = block.statements.joinToString("") { stmt -> + val raw = stmt.toDotLabel() + val prefix = when { + stmt == currentStmt -> "[▶] " + stmt in pathStmts -> "[·] " + else -> "" + } + "$prefix$raw\\l" + } + return "Block #${block.id}\\n$body" +} From 88d1d5961e60d5ea04a725174ee3e83d89539179 Mon Sep 17 00:00:00 2001 From: Alexey Menshutin Date: Fri, 27 Jun 2025 11:26:38 +0300 Subject: [PATCH 3/7] New method for highlighted dot --- .../org/jacodb/ets/utils/BlockCfgToDot.kt | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt index a9cab7205..ac1068d75 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt @@ -79,7 +79,7 @@ fun EtsBlockCfg.toDot( */ data class InterproceduralCfg( val main: EtsBlockCfg, - val callees: Map + val callees: Map, EtsBlockCfg> ) /** @@ -99,17 +99,20 @@ fun InterproceduralCfg.toHighlightedDotWithCalls( lines += " compound=true" lines += " node [shape=${if (useHtml) "none" else "rect"} fontname=\"monospace\"]" - // ----- 1) Render the main CFG ----- + // --- 1) Render main CFG --- for (block in main.blocks) { val nodeId = "M_${block.id}" if (useHtml) { + // HTML label: wrap table in angle brackets val table = buildHtmlTable(block, pathStmts, currentStmt) lines += " $nodeId [label=<$table>];" } else { + // Plain text fallback: quote the label val label = buildPlainLabel(block, pathStmts, currentStmt) lines += " $nodeId [label=\"$label\"];" } } + // Edges of main CFG for ((bid, succs) in main.successors) { val from = "M_$bid" when (succs.size) { @@ -122,16 +125,17 @@ fun InterproceduralCfg.toHighlightedDotWithCalls( } } - // ----- 2) Render each discovered callee CFG as a dashed cluster ----- - for ((stmt, cfg) in callees) { - val callId = stmt.hashCode().toString() + // --- 2) Render each discovered callee CFG as dashed cluster --- + for ((key, cfg) in callees) { + val (stmt, callerBlockId) = key + val callId = stmt.hashCode().toString() + "_B$callerBlockId" val clusterName = "cluster_C_$callId" lines += " subgraph $clusterName {" - lines += " label=\"Callee of stmt $callId\";" + lines += " label=\"Callee of $callId\";" lines += " style=dashed;" - // nodes in callee + // Nodes in callee CFG for (block in cfg.blocks) { val nodeId = "C_${callId}_${block.id}" if (useHtml) { @@ -142,6 +146,7 @@ fun InterproceduralCfg.toHighlightedDotWithCalls( lines += " $nodeId [label=\"$label\"];" } } + // Edges in callee CFG for ((bid, succs) in cfg.successors) { val from = "C_${callId}_$bid" when (succs.size) { @@ -155,19 +160,17 @@ fun InterproceduralCfg.toHighlightedDotWithCalls( } lines += " }" - // ----- 3) Connect call-site in main to callee entry ----- - // find which main block contains this stmt - val callerBlockId = main.blocks.first { it.statements.contains(stmt) }.id - val callerNode = "M_$callerBlockId" - val entryNode = "C_${callId}_${cfg.blocks.first().id}" - lines += " $callerNode -> $entryNode [ltail=$clusterName lhead=$clusterName style=dotted label=\"call\"];" + // --- 3) Connect call-site in main to callee entry --- + val callerNode = "M_$callerBlockId" + val entryBlock = cfg.blocks.first().id + val calleeEntry = "C_${callId}_$entryBlock" + lines += " $callerNode -> $calleeEntry [ltail=$clusterName lhead=$clusterName style=dotted label=\"call\"];" } lines += "}" return lines.joinToString("\n") } - /** Build an HTML table label for a block, coloring rows by path/current. */ private fun buildHtmlTable( block: BasicBlock, From c6deacd51509f2409fed54ed7490ffbb7dd2aab3 Mon Sep 17 00:00:00 2001 From: Alexey Menshutin Date: Fri, 27 Jun 2025 11:52:20 +0300 Subject: [PATCH 4/7] Overwrite file --- .../org/jacodb/ets/utils/BlockCfgToDot.kt | 112 ++++++++++-------- 1 file changed, 60 insertions(+), 52 deletions(-) diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt index ac1068d75..8cfc557e1 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt @@ -18,9 +18,10 @@ package org.jacodb.ets.utils import org.jacodb.ets.model.BasicBlock import org.jacodb.ets.model.EtsBlockCfg -import org.jacodb.ets.model.EtsCallExpr -import org.jacodb.ets.model.EtsCallStmt import org.jacodb.ets.model.EtsStmt +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths private fun String.htmlEncode(): String = this .replace("&", "&") @@ -75,7 +76,7 @@ fun EtsBlockCfg.toDot( /** * An interprocedural CFG that contains: * - the main control-flow graph (main) - * - all callee CFGs discovered so far at call sites (keyed by the statement itself) + * - all callee CFGs discovered so far at call sites (keyed by statement and its parent block id) */ data class InterproceduralCfg( val main: EtsBlockCfg, @@ -93,26 +94,23 @@ fun InterproceduralCfg.toHighlightedDotWithCalls( useHtml: Boolean = true ): String { val lines = mutableListOf() - - // Start the digraph and allow edges between clusters lines += "digraph world {" lines += " compound=true" lines += " node [shape=${if (useHtml) "none" else "rect"} fontname=\"monospace\"]" - // --- 1) Render main CFG --- + // Main CFG for (block in main.blocks) { - val nodeId = "M_${block.id}" + val id = "M_${block.id}" if (useHtml) { - // HTML label: wrap table in angle brackets val table = buildHtmlTable(block, pathStmts, currentStmt) - lines += " $nodeId [label=<$table>];" + // embed as single-line label + lines += " $id [label=<$table>];" } else { - // Plain text fallback: quote the label - val label = buildPlainLabel(block, pathStmts, currentStmt) - lines += " $nodeId [label=\"$label\"];" + val lbl = buildPlainLabel(block, pathStmts, currentStmt) + lines += " $id [label=\"$lbl\"];" } } - // Edges of main CFG + // Main edges for ((bid, succs) in main.successors) { val from = "M_$bid" when (succs.size) { @@ -125,28 +123,25 @@ fun InterproceduralCfg.toHighlightedDotWithCalls( } } - // --- 2) Render each discovered callee CFG as dashed cluster --- + // Callee clusters for ((key, cfg) in callees) { - val (stmt, callerBlockId) = key - val callId = stmt.hashCode().toString() + "_B$callerBlockId" - val clusterName = "cluster_C_$callId" - - lines += " subgraph $clusterName {" + val (stmt, parentId) = key + val callId = stmt.hashCode().toString() + "_B$parentId" + val cluster = "cluster_$callId" + lines += " subgraph $cluster {" lines += " label=\"Callee of $callId\";" lines += " style=dashed;" - // Nodes in callee CFG - for (block in cfg.blocks) { - val nodeId = "C_${callId}_${block.id}" + for (blk in cfg.blocks) { + val nid = "C_${callId}_${blk.id}" if (useHtml) { - val table = buildHtmlTable(block, pathStmts, currentStmt) - lines += " $nodeId [label=<$table>];" + val table = buildHtmlTable(blk, pathStmts, currentStmt) + lines += " $nid [label=<$table>];" } else { - val label = buildPlainLabel(block, pathStmts, currentStmt) - lines += " $nodeId [label=\"$label\"];" + val lbl = buildPlainLabel(blk, pathStmts, currentStmt) + lines += " $nid [label=\"$lbl\"];" } } - // Edges in callee CFG for ((bid, succs) in cfg.successors) { val from = "C_${callId}_$bid" when (succs.size) { @@ -159,55 +154,68 @@ fun InterproceduralCfg.toHighlightedDotWithCalls( } } lines += " }" - - // --- 3) Connect call-site in main to callee entry --- - val callerNode = "M_$callerBlockId" - val entryBlock = cfg.blocks.first().id - val calleeEntry = "C_${callId}_$entryBlock" - lines += " $callerNode -> $calleeEntry [ltail=$clusterName lhead=$clusterName style=dotted label=\"call\"];" + // call edge + val caller = "M_$parentId" + val entry = cfg.blocks.first().id + lines += " $caller -> C_${callId}_$entry [ltail=$cluster lhead=$cluster style=dotted label=\"call\"];" } lines += "}" return lines.joinToString("\n") } -/** Build an HTML table label for a block, coloring rows by path/current. */ +// ======== Helpers ======== + private fun buildHtmlTable( block: BasicBlock, pathStmts: Set, currentStmt: EtsStmt? ): String { - val rows = block.statements.joinToString("") { stmt -> - val text = stmt.toDotLabel().htmlEncode() + val rows = block.statements.joinToString(separator = "") { stmt -> + val txt = stmt.toDotLabel().htmlEncode() val bg = when { stmt == currentStmt -> "lightblue" - stmt in pathStmts -> "yellow" - else -> "white" + stmt in pathStmts -> "yellow" + else -> "white" } - "$text" + "$txt" } - return """ - - - $rows -
Block #${block.id}
- """.trimIndent() + return "" + + "" + rows + + "
Block #${block.id}
" } -/** Build a plain-text label (non-HTML) for a block, prefixing rows. */ private fun buildPlainLabel( block: BasicBlock, pathStmts: Set, currentStmt: EtsStmt? ): String { - val body = block.statements.joinToString("") { stmt -> + val body = block.statements.joinToString(separator = "") { stmt -> val raw = stmt.toDotLabel() - val prefix = when { + val pfx = when { stmt == currentStmt -> "[▶] " - stmt in pathStmts -> "[·] " - else -> "" + stmt in pathStmts -> "[·] " + else -> "" } - "$prefix$raw\\l" + "$pfx$raw\\l" } - return "Block #${block.id}\\n$body" + return "Block #${block.id}\\n" + body } + +fun renderDotOverwrite( + dot: String, + outputDir: Path = Paths.get("."), + baseName: String = "interproc_cfg", + dotCmd: String = "dot", + viewerCmd: String = when { + System.getProperty("os.name").startsWith("Mac") -> "open" + System.getProperty("os.name").startsWith("Win") -> "cmd /c start" + else -> "xdg-open" + } +) { + val dotFile = outputDir.resolve("$baseName.dot") + val outSvg = outputDir.resolve("$baseName.svg") + Files.write(dotFile, dot.toByteArray()) + Runtime.getRuntime().exec("$dotCmd -Tsvg -o $outSvg $dotFile").waitFor() + Runtime.getRuntime().exec("$viewerCmd $outSvg").waitFor() +} \ No newline at end of file From 52f7bc60d4dfc0fca1425dcf3bdad9a2df35137a Mon Sep 17 00:00:00 2001 From: Alexey Menshutin Date: Fri, 27 Jun 2025 13:03:16 +0300 Subject: [PATCH 5/7] Fix --- .../org/jacodb/ets/utils/BlockCfgToDot.kt | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt index 8cfc557e1..44ac6bb43 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt @@ -103,14 +103,12 @@ fun InterproceduralCfg.toHighlightedDotWithCalls( val id = "M_${block.id}" if (useHtml) { val table = buildHtmlTable(block, pathStmts, currentStmt) - // embed as single-line label lines += " $id [label=<$table>];" } else { val lbl = buildPlainLabel(block, pathStmts, currentStmt) lines += " $id [label=\"$lbl\"];" } } - // Main edges for ((bid, succs) in main.successors) { val from = "M_$bid" when (succs.size) { @@ -123,17 +121,20 @@ fun InterproceduralCfg.toHighlightedDotWithCalls( } } + // helper to sanitize negative hash codes + fun sanitize(id: Int): String = if (id < 0) "N${-id}" else id.toString() + // Callee clusters for ((key, cfg) in callees) { val (stmt, parentId) = key - val callId = stmt.hashCode().toString() + "_B$parentId" - val cluster = "cluster_$callId" - lines += " subgraph $cluster {" - lines += " label=\"Callee of $callId\";" + val h = sanitize(stmt.hashCode()) + val clusterName = "cluster_${h}_B${parentId}" + lines += " subgraph \"$clusterName\" {" + lines += " label=\"Callee of $h\";" lines += " style=dashed;" - + // nodes for (blk in cfg.blocks) { - val nid = "C_${callId}_${blk.id}" + val nid = "C_${h}_${blk.id}" if (useHtml) { val table = buildHtmlTable(blk, pathStmts, currentStmt) lines += " $nid [label=<$table>];" @@ -142,14 +143,15 @@ fun InterproceduralCfg.toHighlightedDotWithCalls( lines += " $nid [label=\"$lbl\"];" } } + // edges for ((bid, succs) in cfg.successors) { - val from = "C_${callId}_$bid" + val from = "C_${h}_$bid" when (succs.size) { - 1 -> lines += " $from -> C_${callId}_${succs[0]};" + 1 -> lines += " $from -> C_${h}_${succs[0]};" 2 -> { val (t, f) = succs - lines += " $from -> C_${callId}_$t [label=\"true\"];" - lines += " $from -> C_${callId}_$f [label=\"false\"];" + lines += " $from -> C_${h}_$t [label=\"true\"];" + lines += " $from -> C_${h}_$f [label=\"false\"];" } } } @@ -157,13 +159,12 @@ fun InterproceduralCfg.toHighlightedDotWithCalls( // call edge val caller = "M_$parentId" val entry = cfg.blocks.first().id - lines += " $caller -> C_${callId}_$entry [ltail=$cluster lhead=$cluster style=dotted label=\"call\"];" + lines += " $caller -> C_${h}_$entry [ltail=\"$clusterName\" lhead=\"$clusterName\" style=dotted label=\"call\"];" } lines += "}" return lines.joinToString("\n") } - // ======== Helpers ======== private fun buildHtmlTable( From b9c028019833daf8b9191b9d46a7a8a47e3d1e43 Mon Sep 17 00:00:00 2001 From: Alexey Menshutin Date: Fri, 27 Jun 2025 15:34:20 +0300 Subject: [PATCH 6/7] Small dot modification --- .../org/jacodb/ets/utils/BlockCfgToDot.kt | 103 +++++++++++++----- 1 file changed, 73 insertions(+), 30 deletions(-) diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt index 44ac6bb43..6499d1e1d 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt @@ -17,7 +17,10 @@ package org.jacodb.ets.utils import org.jacodb.ets.model.BasicBlock +import org.jacodb.ets.model.EtsAssignStmt import org.jacodb.ets.model.EtsBlockCfg +import org.jacodb.ets.model.EtsCallExpr +import org.jacodb.ets.model.EtsCallStmt import org.jacodb.ets.model.EtsStmt import java.nio.file.Files import java.nio.file.Path @@ -98,17 +101,41 @@ fun InterproceduralCfg.toHighlightedDotWithCalls( lines += " compound=true" lines += " node [shape=${if (useHtml) "none" else "rect"} fontname=\"monospace\"]" - // Main CFG + // --- 1) Main CFG with ports on call statements --- for (block in main.blocks) { - val id = "M_${block.id}" + val nodeId = "M_${block.id}" + // compute all call hashes in this block + val callHashes = callees.keys + .filter { it.second == block.id } + .map { sanitize(it.first.hashCode()) } + .toSet() + if (useHtml) { - val table = buildHtmlTable(block, pathStmts, currentStmt) - lines += " $id [label=<$table>];" + // build HTML table rows, adding port attribute on call lines + val rows = block.statements.joinToString(separator = "") { stmt -> + val txt = stmt.toDotLabel().htmlEncode() + val bg = when (stmt) { + currentStmt -> "lightblue" + in pathStmts -> "yellow" + else -> "white" + } + val stmtHash = sanitize(stmt.hashCode()) + val portAttr = if (stmtHash in callHashes) " port=\"p$stmtHash\"" else "" + "$txt" + } + val table = buildString { + append("") + append("") + append(rows) + append("
Block #${block.id}
") + } + lines += " $nodeId [label=<$table>];" } else { val lbl = buildPlainLabel(block, pathStmts, currentStmt) - lines += " $id [label=\"$lbl\"];" + lines += " $nodeId [label=\"$lbl\"];" } } + // Main CFG edges for ((bid, succs) in main.successors) { val from = "M_$bid" when (succs.size) { @@ -121,29 +148,34 @@ fun InterproceduralCfg.toHighlightedDotWithCalls( } } - // helper to sanitize negative hash codes - fun sanitize(id: Int): String = if (id < 0) "N${-id}" else id.toString() - - // Callee clusters + // --- 2) Callee clusters with call-edge from port --- for ((key, cfg) in callees) { - val (stmt, parentId) = key + val (stmt, parentBlock) = key val h = sanitize(stmt.hashCode()) - val clusterName = "cluster_${h}_B${parentId}" + val clusterName = "cluster_${h}_B${parentBlock}" + // method signature label + val methodSig = when (stmt) { + is EtsCallStmt -> stmt.callExpr!!.callee + is EtsAssignStmt -> (stmt.rhv as EtsCallExpr).callee + else -> stmt.toDotLabel() + } + // open subgraph lines += " subgraph \"$clusterName\" {" - lines += " label=\"Callee of $h\";" + lines += " label=\"$methodSig\";" lines += " style=dashed;" - // nodes + + // render callee nodes for (blk in cfg.blocks) { - val nid = "C_${h}_${blk.id}" + val calleeNode = "C_${h}_${blk.id}" if (useHtml) { val table = buildHtmlTable(blk, pathStmts, currentStmt) - lines += " $nid [label=<$table>];" + lines += " $calleeNode [label=<$table>];" } else { val lbl = buildPlainLabel(blk, pathStmts, currentStmt) - lines += " $nid [label=\"$lbl\"];" + lines += " $calleeNode [label=\"$lbl\"];" } } - // edges + // render callee edges for ((bid, succs) in cfg.successors) { val from = "C_${h}_$bid" when (succs.size) { @@ -156,30 +188,38 @@ fun InterproceduralCfg.toHighlightedDotWithCalls( } } lines += " }" - // call edge - val caller = "M_$parentId" - val entry = cfg.blocks.first().id - lines += " $caller -> C_${h}_$entry [ltail=\"$clusterName\" lhead=\"$clusterName\" style=dotted label=\"call\"];" + + // connect from the specific port on the caller block + // connect from the specific port on the caller block using tailport + val caller = "M_${parentBlock}" + val entryId = cfg.blocks.first().id + val calleeEntry = "C_${h}_$entryId" + val stmtHash = sanitize(stmt.hashCode()) + lines += " $caller:p$stmtHash -> $calleeEntry [tailport=\"p$stmtHash\" ltail=\"$clusterName\" lhead=\"$clusterName\" style=dotted label=\"call\"];" } lines += "}" return lines.joinToString("\n") } -// ======== Helpers ======== private fun buildHtmlTable( block: BasicBlock, pathStmts: Set, currentStmt: EtsStmt? ): String { + var i = 0 val rows = block.statements.joinToString(separator = "") { stmt -> val txt = stmt.toDotLabel().htmlEncode() - val bg = when { - stmt == currentStmt -> "lightblue" - stmt in pathStmts -> "yellow" + val stmtHash = sanitize(stmt.hashCode()) + val portAttr = if (stmt.callExpr != null) " port=\"p$stmtHash\"" else "" + + val bg = when (stmt) { + currentStmt -> "lightblue" + in pathStmts -> "yellow" else -> "white" } - "$txt" + i++ + "$txt" } return "" + "" + rows + @@ -193,9 +233,9 @@ private fun buildPlainLabel( ): String { val body = block.statements.joinToString(separator = "") { stmt -> val raw = stmt.toDotLabel() - val pfx = when { - stmt == currentStmt -> "[▶] " - stmt in pathStmts -> "[·] " + val pfx = when (stmt) { + currentStmt -> "[▶] " + in pathStmts -> "[·] " else -> "" } "$pfx$raw\\l" @@ -219,4 +259,7 @@ fun renderDotOverwrite( Files.write(dotFile, dot.toByteArray()) Runtime.getRuntime().exec("$dotCmd -Tsvg -o $outSvg $dotFile").waitFor() Runtime.getRuntime().exec("$viewerCmd $outSvg").waitFor() -} \ No newline at end of file +} + +// helper to sanitize negative hash codes for Graphviz IDs +fun sanitize(id: Int): String = id.toString().let { if (it.startsWith("-")) "N${it.substring(1)}" else it } From cfb5539dcd9687b45cfea577423d1ca094273050 Mon Sep 17 00:00:00 2001 From: Alexey Menshutin Date: Fri, 27 Jun 2025 16:30:26 +0300 Subject: [PATCH 7/7] Fix --- .../src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt index 6499d1e1d..6f75b4b52 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt @@ -154,11 +154,7 @@ fun InterproceduralCfg.toHighlightedDotWithCalls( val h = sanitize(stmt.hashCode()) val clusterName = "cluster_${h}_B${parentBlock}" // method signature label - val methodSig = when (stmt) { - is EtsCallStmt -> stmt.callExpr!!.callee - is EtsAssignStmt -> (stmt.rhv as EtsCallExpr).callee - else -> stmt.toDotLabel() - } + val methodSig = cfg.entries.first().method.signature // open subgraph lines += " subgraph \"$clusterName\" {" lines += " label=\"$methodSig\";"
Block #${block.id}