Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 195 additions & 1 deletion jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,25 @@

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
import java.nio.file.Paths

private fun String.htmlEncode(): String = this
.replace("&", "&")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")

fun EtsBlockCfg.toDot(useHtml: Boolean = true): String {
fun EtsBlockCfg.toDot(
useHtml: Boolean = true
): String {
val lines = mutableListOf<String>()
lines += "digraph cfg {"
lines += " node [shape=${if (useHtml) "none" else "rect"} fontname=\"monospace\"]"
Expand Down Expand Up @@ -65,3 +75,187 @@
lines += "}"
return lines.joinToString("\n")
}

/**
* An interprocedural CFG that contains:
* - the main control-flow graph (main)
* - all callee CFGs discovered so far at call sites (keyed by statement and its parent block id)
*/
data class InterproceduralCfg(
val main: EtsBlockCfg,
val callees: Map<Pair<EtsStmt, Int>, EtsBlockCfg>

Check warning on line 86 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L84 - L86 were not covered by tests
)

/**
* 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(

Check warning on line 94 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L94 was not covered by tests
pathStmts: Set<EtsStmt>,
currentStmt: EtsStmt?,
useHtml: Boolean = true

Check warning on line 97 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L97 was not covered by tests
): String {
val lines = mutableListOf<String>()
lines += "digraph world {"
lines += " compound=true"

Check warning on line 101 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L99 - L101 were not covered by tests
lines += " node [shape=${if (useHtml) "none" else "rect"} fontname=\"monospace\"]"

// --- 1) Main CFG with ports on call statements ---
for (block in main.blocks) {
val nodeId = "M_${block.id}"

Check warning on line 106 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L106 was not covered by tests
// compute all call hashes in this block
val callHashes = callees.keys

Check warning on line 108 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L108 was not covered by tests
.filter { it.second == block.id }
.map { sanitize(it.first.hashCode()) }
.toSet()

Check warning on line 111 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L110 - L111 were not covered by tests

if (useHtml) {
// 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) {

Check warning on line 117 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L115 - L117 were not covered by tests
currentStmt -> "lightblue"
in pathStmts -> "yellow"
else -> "white"

Check warning on line 120 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L120 was not covered by tests
}
val stmtHash = sanitize(stmt.hashCode())

Check warning on line 122 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L122 was not covered by tests
val portAttr = if (stmtHash in callHashes) " port=\"p$stmtHash\"" else ""
"<tr><td balign=\"left\" bgcolor=\"$bg\"$portAttr>$txt</td></tr>"

Check warning on line 124 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L124 was not covered by tests
}
val table = buildString {
append("<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">")
append("<tr><td><b>Block #${block.id}</b></td></tr>")
append(rows)
append("</table>")
}
lines += " $nodeId [label=<$table>];"

Check warning on line 132 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L126 - L132 were not covered by tests
} else {
val lbl = buildPlainLabel(block, pathStmts, currentStmt)
lines += " $nodeId [label=\"$lbl\"];"

Check warning on line 135 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L134 - L135 were not covered by tests
}
}
// Main CFG edges
for ((bid, succs) in main.successors) {
val from = "M_$bid"

Check warning on line 140 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L140 was not covered by tests
when (succs.size) {
1 -> lines += " $from -> M_${succs[0]};"

Check warning on line 142 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L142 was not covered by tests
2 -> {
val (t, f) = succs
lines += " $from -> M_$t [label=\"true\"];"
lines += " $from -> M_$f [label=\"false\"];"

Check warning on line 146 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L144 - L146 were not covered by tests
}
}
}

// --- 2) Callee clusters with call-edge from port ---
for ((key, cfg) in callees) {
val (stmt, parentBlock) = key
val h = sanitize(stmt.hashCode())
val clusterName = "cluster_${h}_B${parentBlock}"

Check warning on line 155 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L153 - L155 were not covered by tests
// method signature label
val methodSig = cfg.entries.first().method.signature

Check warning on line 157 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L157 was not covered by tests
// open subgraph
lines += " subgraph \"$clusterName\" {"
lines += " label=\"$methodSig\";"
lines += " style=dashed;"

Check warning on line 161 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L159 - L161 were not covered by tests

// render callee nodes
for (blk in cfg.blocks) {
val calleeNode = "C_${h}_${blk.id}"

Check warning on line 165 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L165 was not covered by tests
if (useHtml) {
val table = buildHtmlTable(blk, pathStmts, currentStmt)
lines += " $calleeNode [label=<$table>];"

Check warning on line 168 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L167 - L168 were not covered by tests
} else {
val lbl = buildPlainLabel(blk, pathStmts, currentStmt)
lines += " $calleeNode [label=\"$lbl\"];"

Check warning on line 171 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L170 - L171 were not covered by tests
}
}
// render callee edges
for ((bid, succs) in cfg.successors) {
val from = "C_${h}_$bid"

Check warning on line 176 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L176 was not covered by tests
when (succs.size) {
1 -> lines += " $from -> C_${h}_${succs[0]};"

Check warning on line 178 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L178 was not covered by tests
2 -> {
val (t, f) = succs
lines += " $from -> C_${h}_$t [label=\"true\"];"
lines += " $from -> C_${h}_$f [label=\"false\"];"

Check warning on line 182 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L180 - L182 were not covered by tests
}
}
}
lines += " }"

Check warning on line 186 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L186 was not covered by tests

// 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\"];"

Check warning on line 194 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L190 - L194 were not covered by tests
}

lines += "}"
return lines.joinToString("\n")

Check warning on line 198 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L197 - L198 were not covered by tests
}

private fun buildHtmlTable(
block: BasicBlock,
pathStmts: Set<EtsStmt>,
currentStmt: EtsStmt?
): String {
var i = 0
val rows = block.statements.joinToString(separator = "") { stmt ->
val txt = stmt.toDotLabel().htmlEncode()
val stmtHash = sanitize(stmt.hashCode())

Check warning on line 209 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L206 - L209 were not covered by tests
val portAttr = if (stmt.callExpr != null) " port=\"p$stmtHash\"" else ""

val bg = when (stmt) {

Check warning on line 212 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L212 was not covered by tests
currentStmt -> "lightblue"
in pathStmts -> "yellow"
else -> "white"

Check warning on line 215 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L215 was not covered by tests
}
i++
"<tr><td balign=\"left\" bgcolor=\"$bg\"$portAttr>$txt</td></tr>"

Check warning on line 218 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L217 - L218 were not covered by tests
}
return "<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">" +
"<tr><td><b>Block #${block.id}</b></td></tr>" + rows +
"</table>"

Check warning on line 222 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L220 - L222 were not covered by tests
}

private fun buildPlainLabel(
block: BasicBlock,
pathStmts: Set<EtsStmt>,
currentStmt: EtsStmt?
): String {
val body = block.statements.joinToString(separator = "") { stmt ->
val raw = stmt.toDotLabel()
val pfx = when (stmt) {

Check warning on line 232 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L230 - L232 were not covered by tests
currentStmt -> "[▶] "
in pathStmts -> "[·] "
else -> ""

Check warning on line 235 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L235 was not covered by tests
}
"$pfx$raw\\l"

Check warning on line 237 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L237 was not covered by tests
}
return "Block #${block.id}\\n" + body

Check warning on line 239 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L239 was not covered by tests
}

fun renderDotOverwrite(

Check warning on line 242 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L242 was not covered by tests
dot: String,
outputDir: Path = Paths.get("."),
baseName: String = "interproc_cfg",
dotCmd: String = "dot",
viewerCmd: String = when {

Check warning on line 247 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L244 - L247 were not covered by tests
System.getProperty("os.name").startsWith("Mac") -> "open"
System.getProperty("os.name").startsWith("Win") -> "cmd /c start"
else -> "xdg-open"

Check warning on line 250 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L250 was not covered by tests
}
) {
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()
}

Check warning on line 258 in jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L253 - L258 were not covered by tests

// 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 }