* Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "TritonToGraph/ControlFlowGraph.h"
#include "triton/Dialect/Triton/IR/Dialect.h"
#include "triton/Dialect/Triton/IR/Types.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/raw_ostream.h"
#include <deque>
#include <fstream>
#include <sstream>
#include <algorithm>
using namespace mlir;
using namespace triton;
using namespace cfg;
std::string Instruction::getAsString() const {
if (!operation) return "<null operation>";
std::string str;
llvm::raw_string_ostream os(str);
operation->print(os);
return str;
}
void Instruction::print(raw_ostream &os, unsigned indent) const {
std::string instStr = getAsString();
os << "Inst[" << id << "]: ";
if (operation) {
BlockType parentType = parentBlock->getType();
if (parentType == BlockType::IF_COND || parentType == BlockType::FOR_COND ||
parentType == BlockType::WHILE_COND || parentType == BlockType::COND_BR ||
parentType == BlockType::BR) {
size_t newlinePos = instStr.find('\n');
if (newlinePos != std::string::npos) {
instStr = instStr.substr(0, newlinePos) + " ...";
}
}
os.indent(indent) << instStr;
} else {
os << "<null>\n";
}
}
void Instruction::dump() const {
print(llvm::errs());
}
void BasicBlock::addInstruction(std::unique_ptr<Instruction> inst) {
instructions.push_back(std::move(inst));
}
Instruction *BasicBlock::getInstruction(size_t idx) const {
if (idx < instructions.size()) {
return instructions[idx].get();
}
return nullptr;
}
void BasicBlock::addSuccessor(BasicBlock *succ) {
if (!succ) return;
for (auto *s : successors) {
if (s == succ) return;
}
successors.push_back(succ);
succ->addPredecessor(this);
}
void BasicBlock::addPredecessor(BasicBlock *pred) {
if (!pred) return;
for (auto *p : predecessors) {
if (p == pred) return;
}
predecessors.push_back(pred);
}
std::string BasicBlock::getName() const {
std::string name = "BB";
name += std::to_string(id);
return name;
}
bool BasicBlock::endsWithReturnOp() const {
if (instructions.empty()) {
return false;
}
const Instruction *lastInst = instructions.back().get();
if (!lastInst) {
return false;
}
Operation *op = lastInst->getOperation();
if (!op) {
return false;
}
return isa<triton::ReturnOp>(op);
}
StringRef BasicBlock::getTypeString() const {
switch (type) {
case BlockType::NORMAL: return "NORMAL";
case BlockType::ENTRY: return "ENTRY";
case BlockType::EXIT: return "EXIT";
case BlockType::IF_COND: return "IF_COND";
case BlockType::FOR_COND: return "FOR_COND";
case BlockType::WHILE_COND: return "WHILE_COND";
case BlockType::COND_BR: return "COND_BR";
case BlockType::BR: return "BR";
case BlockType::LOOP_BODY: return "LOOP_BODY";
case BlockType::LOOP_EXIT: return "LOOP_EXIT";
}
return "UNKNOWN";
}
void BasicBlock::print(raw_ostream &os) const {
os << "============================================================\n";
os << "BasicBlock " << getName() << " [" << getTypeString() << "]\n";
if (parentStructure) {
os << " Parent Structure: " << parentStructure->getName() << "\n";
}
if (!predecessors.empty()) {
os << " Predecessors: [";
for (size_t i = 0; i < predecessors.size(); ++i) {
if (i > 0) os << ", ";
os << predecessors[i]->getName();
}
os << "]\n";
}
if (!successors.empty()) {
os << " Successors: [";
for (size_t i = 0; i < successors.size(); ++i) {
if (i > 0) os << ", ";
os << successors[i]->getName();
}
os << "]\n";
}
os << " Instructions (" << instructions.size() << "):\n";
for (const auto &inst : instructions) {
std::string instStr = inst->getAsString();
if (type == BlockType::IF_COND || type == BlockType::FOR_COND ||
type == BlockType::WHILE_COND) {
size_t newlinePos = instStr.find('\n');
if (newlinePos != std::string::npos) {
instStr = instStr.substr(0, newlinePos) + " ...";
}
}
os.indent(4) << "Inst[" << inst->getId() << "]: " << instStr << "\n";
}
os << "\n";
}
void BasicBlock::dump() const {
print(llvm::errs());
}
void BasicBlock::exportToJSON(raw_ostream &os, unsigned indent) const {
std::string ind(indent, ' ');
os << ind << "{\n";
os << ind << " \"id\": " << id << ",\n";
os << ind << " \"name\": \"" << getName() << "\",\n";
os << ind << " \"type\": \"" << getTypeString() << "\",\n";
os << ind << " \"parentStructure\": " << (parentStructure ? std::to_string(parentStructure->getId()) : "null") << ",\n";
os << ind << " \"predecessors\": [";
for (size_t i = 0; i < predecessors.size(); ++i) {
if (i > 0) os << ", ";
os << predecessors[i]->getId();
}
os << "],\n";
os << ind << " \"successors\": [";
for (size_t i = 0; i < successors.size(); ++i) {
if (i > 0) os << ", ";
os << successors[i]->getId();
}
os << "],\n";
os << ind << " \"instructions\": [\n";
for (size_t i = 0; i < instructions.size(); ++i) {
os << ind << " {\n";
os << ind << " \"id\": " << instructions[i]->getId() << ",\n";
std::string instStr = instructions[i]->getAsString();
if (type == BlockType::IF_COND || type == BlockType::FOR_COND ||
type == BlockType::WHILE_COND) {
size_t newlinePos = instStr.find('\n');
if (newlinePos != std::string::npos) {
instStr = instStr.substr(0, newlinePos) + " ...";
}
}
std::string escaped;
for (char c : instStr) {
if (c == '"') escaped += "\\\"";
else if (c == '\\') escaped += "\\\\";
else if (c == '\n') escaped += "\\n";
else if (c == '\r') escaped += "\\r";
else if (c == '\t') escaped += "\\t";
else if ((unsigned char)c < 0x20) {
char buf[8];
snprintf(buf, sizeof(buf), "\\u%04x", c);
escaped += buf;
} else {
escaped += c;
}
}
os << ind << " \"operation\": \"" << escaped << "\"\n";
os << ind << " }";
if (i < instructions.size() - 1) os << ",";
os << "\n";
}
os << ind << " ]\n";
os << ind << "}";
}
ControlFlowGraph::ControlFlowGraph(triton::FuncOp func) : function(func) {}
ControlFlowGraph::~ControlFlowGraph() = default;
BasicBlock *ControlFlowGraph::createBasicBlock(BlockType type, BasicBlock *parentStructure) {
auto bb = std::make_unique<BasicBlock>(nextBlockId++, type, parentStructure);
BasicBlock *bbPtr = bb.get();
basicBlocks.push_back(std::move(bb));
if (type == BlockType::ENTRY) entryBlock = bbPtr;
if (type == BlockType::EXIT) exitBlock = bbPtr;
return bbPtr;
}
void ControlFlowGraph::addEdge(BasicBlock *from, BasicBlock *to) {
if (!from || !to) return;
from->addSuccessor(to);
}
bool ControlFlowGraph::isBackEdge(BasicBlock *from, BasicBlock *to) const {
if (to->getType() != BlockType::FOR_COND &&
to->getType() != BlockType::WHILE_COND)
return false;
BasicBlock *current = from;
while (current) {
if (current == to) return true;
current = current->getParentStructure();
}
return false;
}
void ControlFlowGraph::searchNormalBlock(BasicBlock* block, OperationVisitor callback) const
{
for (const auto &inst : block->getInstructions()) {
if (Operation *op = inst->getOperation()) {
callback(op);
}
}
}
void ControlFlowGraph::searchCondBlock(BasicBlock *block, OperationVisitor callback) const
{
if (!block)
return;
BasicBlock *exitBlock = block->getExitBlock();
for (BasicBlock *succ : block->getSuccessors()) {
if (!succ)
continue;
std::deque<BasicBlock *> workList;
workList.push_back(succ);
while (!workList.empty()) {
BasicBlock *current = workList.front();
workList.pop_front();
if (current == exitBlock)
break;
searchBlock(current, callback);
if(current->getType() == BlockType::NORMAL)
{
for (BasicBlock *next : current->getSuccessors()) {
if (isBackEdge(current, next))
continue;
workList.push_back(next);
}
}
else if (block->getType() == BlockType::IF_COND ||
block->getType() == BlockType::FOR_COND ||
block->getType() == BlockType::WHILE_COND)
{
BasicBlock *next = block->getExitBlock();
if (isBackEdge(current, next))
continue;
workList.push_back(next);
}
}
}
}
void ControlFlowGraph::searchBlock(BasicBlock *block, OperationVisitor callback) const
{
if (block->getType() == BlockType::NORMAL)
{
searchNormalBlock(block, callback);
}
else if (block->getType() == BlockType::IF_COND ||
block->getType() == BlockType::FOR_COND ||
block->getType() == BlockType::WHILE_COND)
{
searchCondBlock(block, callback);
}
}
void ControlFlowGraph::traverse(BlockVisitor visitor) {
std::vector<BasicBlock *> topoOrder;
DenseSet<BasicBlock *> visited;
std::function<void(BasicBlock *)> dfs = [&](BasicBlock *bb) {
if (!bb || visited.contains(bb))
return;
visited.insert(bb);
for (BasicBlock *succ : bb->getSuccessors()) {
dfs(succ);
}
topoOrder.push_back(bb);
};
dfs(entryBlock);
std::reverse(topoOrder.begin(), topoOrder.end());
for (BasicBlock *bb : topoOrder) {
visitor(*bb);
}
}
void ControlFlowGraph::print(raw_ostream &os) const {
auto funcName = const_cast<triton::FuncOp&>(function).getName();
os << "=================================================================\n";
os << "Control Flow Graph for function '" << (funcName.empty() ? "unnamed" : funcName) << "'\n";
os << "Number of blocks: " << basicBlocks.size() << "\n";
os << "=================================================================\n\n";
for (const auto &bb : basicBlocks) {
bb->print(os);
}
}
void ControlFlowGraph::dump() const {
print(llvm::errs());
}
void ControlFlowGraph::exportDOT(raw_ostream &os) const {
auto funcName = const_cast<triton::FuncOp&>(function).getName();
std::string funcNameStr = funcName.empty() ? "unnamed" : funcName.str();
std::string cleanFuncName;
for (char c : funcNameStr) {
if (isalnum(c) || c == '_') cleanFuncName += c;
else cleanFuncName += '_';
}
os << "digraph CFG_" << cleanFuncName << " {\n";
os << " label=\"CFG for " << funcNameStr << "\";\n";
os << " labelloc=t;\n";
os << " rankdir=TB;\n";
os << " splines=true;\n";
os << " overlap=false;\n";
os << " nodesep=0.6;\n";
os << " ranksep=1.2;\n";
os << " fontsize=12;\n\n";
for (const auto &bb : basicBlocks) {
os << " \"" << bb->getName() << "\" [";
os << "label=\"" << bb->getName() << "\\n(" << bb->getTypeString() << ")";
size_t numInsts = bb->getNumInstructions();
if (numInsts > 0) {
os << "\\n";
if (bb->getType() == BlockType::IF_COND ||
bb->getType() == BlockType::FOR_COND ||
bb->getType() == BlockType::WHILE_COND) {
std::string instStr = bb->getInstruction(0)->getAsString();
size_t newlinePos = instStr.find('\n');
if (newlinePos != std::string::npos) {
instStr = instStr.substr(0, newlinePos);
}
if (instStr.length() > 40) instStr = instStr.substr(0, 37) + "...";
std::string escaped;
for (char c : instStr) {
if (c == '"') escaped += "\\\"";
else if (c == '\\') escaped += "\\\\";
else if (c == '\n') escaped += "\\n";
else escaped += c;
}
os << escaped;
if (numInsts > 1) os << "\\n... (" << (numInsts - 1) << " more)";
} else {
for (size_t i = 0; i < std::min(numInsts, (size_t)3); ++i) {
std::string instStr = bb->getInstruction(i)->getAsString();
size_t newlinePos = instStr.find('\n');
if (newlinePos != std::string::npos) {
instStr = instStr.substr(0, newlinePos) + "...";
}
if (instStr.length() > 40) instStr = instStr.substr(0, 37) + "...";
std::string escaped;
for (char c : instStr) {
if (c == '"') escaped += "\\\"";
else if (c == '\\') escaped += "\\\\";
else if (c == '\n') escaped += "\\n";
else escaped += c;
}
os << escaped << "\\n";
}
if (numInsts > 3) os << "... (" << (numInsts - 3) << " more)\\n";
}
}
os << "\", ";
switch (bb->getType()) {
case BlockType::ENTRY:
os << "style=filled, fillcolor=lightgreen, shape=ellipse";
break;
case BlockType::EXIT:
os << "style=filled, fillcolor=lightcoral, shape=ellipse";
break;
case BlockType::IF_COND:
os << "style=filled, fillcolor=lightyellow, shape=diamond";
break;
case BlockType::FOR_COND:
case BlockType::WHILE_COND:
os << "style=filled, fillcolor=lightblue, shape=diamond";
break;
case BlockType::LOOP_BODY:
os << "style=filled, fillcolor=lightcyan, shape=box";
break;
case BlockType::LOOP_EXIT:
os << "style=filled, fillcolor=lightpink, shape=box";
break;
default:
os << "shape=box";
break;
}
os << ", fontsize=10];\n";
}
os << "\n";
for (const auto &bb : basicBlocks) {
for (auto *succ : bb->getSuccessors()) {
bool isBack = isBackEdge(bb.get(), succ);
os << " \"" << bb->getName() << "\" -> \"" << succ->getName() << "\"";
os << " [color=" << (isBack ? "red" : "green");
os << ", penwidth=" << (isBack ? "2.5" : "1.5") << "";
if (isBack) {
os << ", style=dashed";
}
os << "];\n";
}
}
os << "}\n";
}
llvm::Error ControlFlowGraph::exportToFile(StringRef filename) const {
std::error_code ec;
llvm::raw_fd_ostream os(filename, ec, llvm::sys::fs::OF_Text);
if (ec) {
return llvm::createStringError(llvm::inconvertibleErrorCode(),
"Failed to open file: " + filename);
}
if (filename.ends_with(".dot")) {
exportDOT(os);
} else if (filename.ends_with(".json")) {
exportToJSON(os);
} else {
print(os);
}
os.close();
return llvm::Error::success();
}
void ControlFlowGraph::exportToJSON(raw_ostream &os) const {
auto funcName = const_cast<triton::FuncOp&>(function).getName();
std::string funcNameStr = funcName.empty() ? "unnamed" : funcName.str();
os << "{\n";
os << " \"functionName\": \"" << funcNameStr << "\",\n";
os << " \"numBlocks\": " << basicBlocks.size() << ",\n";
os << " \"blocks\": [\n";
for (size_t i = 0; i < basicBlocks.size(); ++i) {
basicBlocks[i]->exportToJSON(os, 4);
if (i < basicBlocks.size() - 1) os << ",";
os << "\n";
}
os << " ],\n";
os << " \"edges\": [\n";
bool first = true;
for (const auto &bb : basicBlocks) {
for (auto *succ : bb->getSuccessors()) {
if (!first) os << ",\n";
first = false;
bool isBack = isBackEdge(bb.get(), succ);
os << " {\"from\": " << bb->getId()
<< ", \"to\": " << succ->getId()
<< ", \"fromName\": \"" << bb->getName() << "\""
<< ", \"toName\": \"" << succ->getName() << "\""
<< ", \"type\": \"" << (isBack ? "back" : "normal") << "\"}";
}
}
os << "\n ]\n";
os << "}\n";
}
llvm::Error ControlFlowGraph::exportToHTML(StringRef filename) const {
std::error_code ec;
llvm::raw_fd_ostream os(filename, ec, llvm::sys::fs::OF_Text);
if (ec) {
return llvm::createStringError(llvm::inconvertibleErrorCode(),
"Failed to open file: " + filename);
}
auto funcName = const_cast<triton::FuncOp&>(function).getName();
std::string funcNameStr = funcName.empty() ? "unnamed" : funcName.str();
os << R"html(<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>CFG for )html" << funcNameStr << R"html(</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://unpkg.com/dagre@0.8.5/dist/dagre.min.js"></script>
<script src="https://unpkg.com/dagre-d3@0.6.4/dist/dagre-d3.min.js"></script>
<style type="text/css">
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
display: flex;
height: 100vh;
background: #f0f2f5; /* 浅色背景 */
overflow: hidden;
}
#svg-container {
flex: 1;
overflow: auto;
background: #ffffff; /* 纯白画布 */
cursor: grab;
border-right: 1px solid #ddd;
}
#svg-container:active {
cursor: grabbing;
}
#sidebar {
width: 520px;
background: #ffffff;
padding: 24px;
overflow-y: auto;
box-shadow: -2px 0 8px rgba(0,0,0,0.08);
z-index: 10;
}
h1 {
margin-top: 0;
color: #1a1a1a;
font-size: 24px;
border-bottom: 3px solid #1976d2;
padding-bottom: 12px;
font-weight: 600;
}
h2 {
color: #444;
font-size: 16px;
border-bottom: 1px solid #e0e0e0;
padding-bottom: 8px;
margin-top: 28px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.block-info {
background: #f5f7fa;
padding: 14px;
border-radius: 8px;
margin-bottom: 16px;
border-left: 4px solid #1976d2;
font-size: 14px;
line-height: 1.6;
color: #333;
}
.instruction {
background: #ffffff;
border: 1px solid #e0e0e0;
border-left: 3px solid #ff9800;
border-radius: 6px;
padding: 14px;
margin: 10px 0;
font-family: 'Consolas', 'Monaco', 'SF Mono', monospace;
font-size: 15px; /* 大字体 */
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.instruction-id {
color: #555;
font-size: 13px;
background: #eceff1;
padding: 3px 8px;
border-radius: 4px;
margin-right: 8px;
font-weight: 600;
}
#default-msg {
color: #888;
text-align: center;
margin-top: 120px;
font-style: italic;
font-size: 16px;
}
.legend {
position: absolute;
top: 16px;
left: 16px;
background: rgba(255,255,255,0.96);
padding: 14px;
border-radius: 8px;
border: 1px solid #ccc;
font-size: 13px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
z-index: 100;
color: #333;
}
.legend-item {
margin: 8px 0;
display: flex;
align-items: center;
}
.legend-line {
width: 30px;
height: 3px;
margin-right: 12px;
border-radius: 2px;
}
/* SVG 样式 - 适配浅色背景 */
.node rect {
stroke-width: 2px;
rx: 6;
ry: 6;
}
.node text {
font-family: 'Consolas', 'Monaco', 'SF Mono', monospace;
font-size: 13px; /* 节点内字体 */
font-weight: 500;
pointer-events: none;
fill: #212121;
}
.edgePath path {
stroke-width: 2.5px;
stroke-linecap: square;
fill: none;
}
.arrowhead {
fill-opacity: 1;
}
</style>
</head>
<body>
<div id="svg-container">
<svg id="svg-canvas" width="2400" height="1800"></svg>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-line" style="background: #2e7d32;"></div>正常控制流 (向下)</div>
<div class="legend-item"><div class="legend-line" style="background: #d32f2f; border-top: 2px dashed #d32f2f; height: 0;"></div>循环回边 (向上)</div>
</div>
<div id="sidebar">
<h1>CFG: )html" << funcNameStr << R"html(</h1>
<div id="default-msg">
<p>Click on a node to view block details</p>
</div>
<div id="block-details" style="display:none;">
<div class="block-info">
<strong>Block:</strong> <span id="block-name"></span><br>
<strong>Type:</strong> <span id="block-type"></span><br>
<strong>ID:</strong> <span id="block-id"></span>
</div>
<h2>Predecessors</h2>
<div id="preds"></div>
<h2>Successors</h2>
<div id="succs"></div>
<h2>Instructions (<span id="inst-count"></span>)</h2>
<div id="instructions"></div>
</div>
</div>
<script type="text/javascript">
var cfgData = )html";
exportToJSON(os);
os << R"html(;
// 使用 dagre 计算布局
const g = new dagre.graphlib.Graph({ compound: true });
g.setGraph({
rankdir: 'TB', // 自上而下
ranksep: 120, // 层间距
nodesep: 100, // 节点水平间距
edgesep: 25,
marginx: 60,
marginy: 60,
acyclicer: 'greedy',
ranker: 'longest-path'
});
g.setDefaultEdgeLabel(function() { return {}; });
// 颜色配置 - 适配浅色背景
const colorMap = {
'ENTRY': '#c8e6c9', // 柔和绿
'EXIT': '#ffcdd2', // 柔和红
'IF_COND': '#fff9c4', // 柔和黄
'FOR_COND': '#b3e5fc', // 柔和蓝
'WHILE_COND': '#b3e5fc', // 柔和蓝
'LOOP_BODY': '#e1f5fe', // 浅蓝
'LOOP_EXIT': '#f8bbd0', // 浅粉
'NORMAL': '#f5f5f5' // 浅灰
};
const borderMap = {
'ENTRY': '#2e7d32',
'EXIT': '#c62828',
'IF_COND': '#f57c00',
'FOR_COND': '#0288d1',
'WHILE_COND': '#0288d1',
'LOOP_BODY': '#0288d1',
'LOOP_EXIT': '#c2185b',
'NORMAL': '#616161'
};
const nodeWidth = 320; // 节点加宽以容纳更多文本
const lineHeight = 18; // 每行高度
// 辅助函数:截断指令文本
function truncateLine(text, maxLen) {
if (text.length <= maxLen) return text;
return text.substring(0, maxLen-3) + '...';
}
// 添加节点
cfgData.blocks.forEach(block => {
const isCond = ['IF_COND', 'FOR_COND', 'WHILE_COND'].includes(block.type);
const isEntry = block.type === 'ENTRY';
const isExit = block.type === 'EXIT';
// 构建多行标签
let lines = [];
lines.push(block.name + ' (' + block.type + ')');
if (isCond && block.instructions.length > 0) {
// COND节点:只显示第一行指令
let firstLine = truncateLine(block.instructions[0].operation.split('\n')[0], 58);
lines.push(firstLine);
if (block.instructions.length > 1) {
lines.push(' +' + (block.instructions.length - 1) + ' instructions');
}
} else if (block.instructions.length > 0) {
// 非COND节点:最多显示3行指令
const showCount = Math.min(block.instructions.length, 3);
for (let i = 0; i < showCount; i++) {
let line = truncateLine(block.instructions[i].operation.split('\n')[0], 58);
lines.push(line);
}
if (block.instructions.length > 3) {
lines.push(' +' + (block.instructions.length - 3) + ' more...');
}
}
const label = lines.join('\n');
const height = 50 + lines.length * lineHeight; // 基础高度+行高
const nodeConfig = {
label: label,
width: nodeWidth,
height: height,
customData: block,
fillColor: colorMap[block.type] || '#ffffff',
strokeColor: borderMap[block.type] || '#424242',
isCond: isCond
};
// ENTRY固定最上层(rank: source),EXIT固定最下层(rank: sink)
if (isEntry) {
nodeConfig.rank = 'source';
} else if (isExit) {
nodeConfig.rank = 'sink';
}
g.setNode(block.id.toString(), nodeConfig);
});
// 添加边
if (cfgData.edges) {
cfgData.edges.forEach(edge => {
const isBack = edge.type === 'back';
g.setEdge(edge.from.toString(), edge.to.toString(), {
lineInterpolate: 'orthogonal', // Manhattan routing
isBack: isBack,
strokeColor: isBack ? '#d32f2f' : '#2e7d32',
strokeDasharray: isBack ? '6,4' : '0',
strokeWidth: isBack ? 2.5 : 2
});
});
}
// 计算布局
dagre.layout(g);
// 使用 D3 绘制
const svg = d3.select('#svg-canvas');
const svgGroup = svg.append('g');
// 缩放和平移
const zoom = d3.zoom()
.scaleExtent([0.3, 3])
.on('zoom', function(event) {
svgGroup.attr('transform', event.transform);
});
svg.call(zoom);
// 绘制边 - 正交路径
const edgeSelection = svgGroup.selectAll('.edgePath')
.data(g.edges())
.enter()
.append('g')
.attr('class', 'edgePath');
edgeSelection.append('path')
.attr('d', function(e) {
const edge = g.edge(e);
const points = edge.points;
// Manhattan routing: 直角线段
let d = d3.path();
d.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
d.lineTo(points[i].x, points[i].y);
}
return d.toString();
})
.attr('stroke', function(e) { return g.edge(e).strokeColor; })
.attr('stroke-width', function(e) { return g.edge(e).strokeWidth; })
.attr('stroke-dasharray', function(e) { return g.edge(e).strokeDasharray; })
.attr('fill', 'none');
// 箭头
edgeSelection.append('polygon')
.attr('class', 'arrowhead')
.attr('points', function(e) {
const edge = g.edge(e);
const points = edge.points;
const last = points[points.length - 1];
const prev = points[points.length - 2] || points[0];
const dx = last.x - prev.x;
const dy = last.y - prev.y;
const angle = Math.atan2(dy, dx);
const size = 10;
const angle1 = angle + Math.PI * 0.85;
const angle2 = angle - Math.PI * 0.85;
return [
[last.x, last.y],
[last.x + Math.cos(angle1) * size, last.y + Math.sin(angle1) * size],
[last.x + Math.cos(angle2) * size, last.y + Math.sin(angle2) * size]
].join(' ');
})
.attr('fill', function(e) { return g.edge(e).strokeColor; });
// 绘制节点
const nodeSelection = svgGroup.selectAll('.node')
.data(g.nodes())
.enter()
.append('g')
.attr('class', 'node')
.attr('transform', function(v) {
const node = g.node(v);
return 'translate(' + (node.x - node.width/2) + ',' + (node.y - node.height/2) + ')';
})
.style('cursor', 'pointer')
.on('click', function(event, v) {
const node = g.node(v);
showBlockDetails(node.customData);
});
// 节点矩形
nodeSelection.append('rect')
.attr('width', function(v) { return g.node(v).width; })
.attr('height', function(v) { return g.node(v).height; })
.attr('fill', function(v) { return g.node(v).fillColor; })
.attr('stroke', function(v) { return g.node(v).strokeColor; })
.attr('stroke-width', 2)
.style('filter', 'drop-shadow(0 2px 4px rgba(0,0,0,0.1))');
// 节点文本 - 多行
nodeSelection.each(function(v) {
const node = g.node(v);
const lines = node.label.split('\n');
const selection = d3.select(this);
lines.forEach((line, i) => {
selection.append('text')
.attr('x', 14)
.attr('y', 22 + i * lineHeight)
.text(line)
.style('font-weight', i === 0 ? 'bold' : (node.isCond && i === 1 ? '600' : 'normal'))
.style('fill', i === 0 ? '#1565c0' : '#424242') // 第一行标题蓝色,其余深灰
.style('font-size', i === 0 ? '14px' : '13px');
});
});
// 调整画布尺寸并居中
const graphWidth = g.graph().width + 120;
const graphHeight = g.graph().height + 120;
svg.attr('width', graphWidth).attr('height', graphHeight);
const container = document.getElementById('svg-container');
const scale = Math.min(
container.clientWidth / graphWidth,
container.clientHeight / graphHeight,
1.0
) * 0.9;
const transform = d3.zoomIdentity
.translate(container.clientWidth/2 - graphWidth*scale/2, 40)
.scale(scale);
svg.call(zoom.transform, transform);
// 详情面板
function showBlockDetails(block) {
document.getElementById('default-msg').style.display = 'none';
document.getElementById('block-details').style.display = 'block';
document.getElementById('block-name').textContent = block.name;
document.getElementById('block-type').textContent = block.type;
document.getElementById('block-id').textContent = block.id;
let predsHtml = '';
if (block.predecessors.length === 0) {
predsHtml = '<em style="color:#888;">None</em>';
} else {
block.predecessors.forEach(predId => {
const pred = cfgData.blocks.find(b => b.id === predId);
const isBack = cfgData.edges.find(e => e.from === predId && e.to === block.id && e.type === 'back');
const style = isBack ? 'color: #d32f2f; font-weight: bold;' : 'color: #333;';
predsHtml += `<div style="${style} margin: 5px 0; font-size: 14px;">← ${pred ? pred.name : 'BB'+predId}${isBack ? ' [回边]' : ''}</div>`;
});
}
document.getElementById('preds').innerHTML = predsHtml;
let succsHtml = '';
if (block.successors.length === 0) {
succsHtml = '<em style="color:#888;">None</em>';
} else {
block.successors.forEach(succId => {
const succ = cfgData.blocks.find(b => b.id === succId);
const isBack = cfgData.edges.find(e => e.from === block.id && e.to === succId && e.type === 'back');
const style = isBack ? 'color: #d32f2f; font-weight: bold;' : 'color: #333;';
succsHtml += `<div style="${style} margin: 5px 0; font-size: 14px;">→ ${succ ? succ.name : 'BB'+succId}${isBack ? ' [回边]' : ''}</div>`;
});
}
document.getElementById('succs').innerHTML = succsHtml;
document.getElementById('inst-count').textContent = block.instructions.length;
let instHtml = '';
block.instructions.forEach(inst => {
instHtml += `<div class="instruction"><span class="instruction-id">[${inst.id}]</span>${escapeHtml(inst.operation)}</div>`;
});
document.getElementById('instructions').innerHTML = instHtml;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>)html";
os.close();
return llvm::Error::success();
}