diff --git a/mlir/lib/Transforms/ViewOpGraph.cpp b/mlir/lib/Transforms/ViewOpGraph.cpp index fa0af7665ba4c4c..75ee3ed74db5edf 100644 --- a/mlir/lib/Transforms/ViewOpGraph.cpp +++ b/mlir/lib/Transforms/ViewOpGraph.cpp @@ -14,6 +14,7 @@ #include "mlir/IR/Operation.h" #include "mlir/Pass/Pass.h" #include "mlir/Support/IndentedOstream.h" +#include "llvm/ADT/STLExtras.h" #include "llvm/Support/Format.h" #include "llvm/Support/GraphWriter.h" #include @@ -29,7 +30,7 @@ using namespace mlir; static const StringRef kLineStyleControlFlow = "dashed"; static const StringRef kLineStyleDataFlow = "solid"; -static const StringRef kShapeNode = "ellipse"; +static const StringRef kShapeNode = "Mrecord"; static const StringRef kShapeNone = "plain"; /// Return the size limits for eliding large attributes. @@ -49,16 +50,25 @@ static std::string strFromOs(function_ref func) { return buf; } -/// Escape special characters such as '\n' and quotation marks. -static std::string escapeString(std::string str) { - return strFromOs([&](raw_ostream &os) { os.write_escaped(str); }); -} - /// Put quotation marks around a given string. static std::string quoteString(const std::string &str) { return "\"" + str + "\""; } +/// For Graphviz record nodes: +/// " Braces, vertical bars and angle brackets must be escaped with a backslash +/// character if you wish them to appear as a literal character " +std::string escapeLabelString(const std::string &str) { + std::string buf; + llvm::raw_string_ostream os(buf); + for (char c : str) { + if (llvm::is_contained({'{', '|', '<', '}', '>', '\n', '"'}, c)) + os << '\\'; + os << c; + } + return buf; +} + using AttributeMap = std::map; namespace { @@ -79,6 +89,12 @@ struct Node { std::optional clusterId; }; +struct DataFlowEdge { + Value value; + Node node; + std::string port; +}; + /// This pass generates a Graphviz dataflow visualization of an MLIR operation. /// Note: See https://www.graphviz.org/doc/info/lang.html for more information /// about the Graphviz DOT language. @@ -107,7 +123,7 @@ class PrintOpPass : public impl::ViewOpGraphBase { private: /// Generate a color mapping that will color every operation with the same /// name the same way. It'll interpolate the hue in the HSV color-space, - /// attempting to keep the contrast suitable for black text. + /// using muted colors that provide good contrast for black text. template void initColorMapping(T &irEntity) { backgroundColors.clear(); @@ -120,8 +136,10 @@ class PrintOpPass : public impl::ViewOpGraphBase { }); for (auto indexedOps : llvm::enumerate(ops)) { double hue = ((double)indexedOps.index()) / ops.size(); + // Use lower saturation (0.3) and higher value (0.95) for better + // readability backgroundColors[indexedOps.value()->getName()].second = - std::to_string(hue) + " 1.0 1.0"; + std::to_string(hue) + " 0.3 0.95"; } } @@ -129,8 +147,8 @@ class PrintOpPass : public impl::ViewOpGraphBase { /// emitted. void emitAllEdgeStmts() { if (printDataFlowEdges) { - for (const auto &[value, node, label] : dataFlowEdges) { - emitEdgeStmt(valueToNode[value], node, label, kLineStyleDataFlow); + for (const auto &e : dataFlowEdges) { + emitEdgeStmt(valueToNode[e.value], e.node, e.port, kLineStyleDataFlow); } } @@ -147,8 +165,7 @@ class PrintOpPass : public impl::ViewOpGraphBase { os.indent(); // Emit invisible anchor node from/to which arrows can be drawn. Node anchorNode = emitNodeStmt(" ", kShapeNone); - os << attrStmt("label", quoteString(escapeString(std::move(label)))) - << ";\n"; + os << attrStmt("label", quoteString(label)) << ";\n"; builder(); os.unindent(); os << "}\n"; @@ -176,7 +193,8 @@ class PrintOpPass : public impl::ViewOpGraphBase { // Always emit splat attributes. if (isa(attr)) { - attr.print(os); + os << escapeLabelString( + strFromOs([&](raw_ostream &os) { attr.print(os); })); return; } @@ -184,8 +202,8 @@ class PrintOpPass : public impl::ViewOpGraphBase { auto elements = dyn_cast(attr); if (elements && elements.getNumElements() > largeAttrLimit) { os << std::string(elements.getShapedType().getRank(), '[') << "..." - << std::string(elements.getShapedType().getRank(), ']') << " : " - << elements.getType(); + << std::string(elements.getShapedType().getRank(), ']') << " : "; + emitMlirType(os, elements.getType()); return; } @@ -199,19 +217,27 @@ class PrintOpPass : public impl::ViewOpGraphBase { std::string buf; llvm::raw_string_ostream ss(buf); attr.print(ss); - os << truncateString(buf); + os << escapeLabelString(truncateString(buf)); + } + + // Print a truncated and escaped MLIR type to `os`. + void emitMlirType(raw_ostream &os, Type type) { + std::string buf; + llvm::raw_string_ostream ss(buf); + type.print(ss); + os << escapeLabelString(truncateString(buf)); + } + + // Print a truncated and escaped MLIR operand to `os`. + void emitMlirOperand(raw_ostream &os, Value operand) { + operand.printAsOperand(os, OpPrintingFlags()); } /// Append an edge to the list of edges. /// Note: Edges are written to the output stream via `emitAllEdgeStmts`. - void emitEdgeStmt(Node n1, Node n2, std::string label, StringRef style) { + void emitEdgeStmt(Node n1, Node n2, std::string port, StringRef style) { AttributeMap attrs; attrs["style"] = style.str(); - // Do not label edges that start/end at a cluster boundary. Such edges are - // clipped at the boundary, but labels are not. This can lead to labels - // floating around without any edge next to them. - if (!n1.clusterId && !n2.clusterId) - attrs["label"] = quoteString(escapeString(std::move(label))); // Use `ltail` and `lhead` to draw edges between clusters. if (n1.clusterId) attrs["ltail"] = "cluster_" + std::to_string(*n1.clusterId); @@ -219,7 +245,15 @@ class PrintOpPass : public impl::ViewOpGraphBase { attrs["lhead"] = "cluster_" + std::to_string(*n2.clusterId); edges.push_back(strFromOs([&](raw_ostream &os) { - os << llvm::format("v%i -> v%i ", n1.id, n2.id); + os << "v" << n1.id; + if (!port.empty() && !n1.clusterId) + // Attach edge to south compass point of the result + os << ":res" << port << ":s"; + os << " -> "; + os << "v" << n2.id; + if (!port.empty() && !n2.clusterId) + // Attach edge to north compass point of the operand + os << ":arg" << port << ":n"; emitAttrList(os, attrs); })); } @@ -240,11 +274,11 @@ class PrintOpPass : public impl::ViewOpGraphBase { StringRef background = "") { int nodeId = ++counter; AttributeMap attrs; - attrs["label"] = quoteString(escapeString(std::move(label))); + attrs["label"] = quoteString(label); attrs["shape"] = shape.str(); if (!background.empty()) { attrs["style"] = "filled"; - attrs["fillcolor"] = ("\"" + background + "\"").str(); + attrs["fillcolor"] = quoteString(background.str()); } os << llvm::format("v%i ", nodeId); emitAttrList(os, attrs); @@ -252,8 +286,18 @@ class PrintOpPass : public impl::ViewOpGraphBase { return Node(nodeId); } - /// Generate a label for an operation. - std::string getLabel(Operation *op) { + std::string getValuePortName(Value operand) { + // Print value as an operand and omit the leading '%' character. + auto str = strFromOs([&](raw_ostream &os) { + operand.printAsOperand(os, OpPrintingFlags()); + }); + // Replace % and # with _ + std::replace(str.begin(), str.end(), '%', '_'); + std::replace(str.begin(), str.end(), '#', '_'); + return str; + } + + std::string getClusterLabel(Operation *op) { return strFromOs([&](raw_ostream &os) { // Print operation name and type. os << op->getName(); @@ -267,18 +311,73 @@ class PrintOpPass : public impl::ViewOpGraphBase { // Print attributes. if (printAttrs) { - os << "\n"; + os << "\\l"; + for (const NamedAttribute &attr : op->getAttrs()) { + os << escapeLabelString(attr.getName().getValue().str()) << ": "; + emitMlirAttr(os, attr.getValue()); + os << "\\l"; + } + } + }); + } + + /// Generate a label for an operation. + std::string getRecordLabel(Operation *op) { + return strFromOs([&](raw_ostream &os) { + os << "{"; + + // Print operation inputs. + if (op->getNumOperands() > 0) { + os << "{"; + auto operandToPort = [&](Value operand) { + os << " "; + emitMlirOperand(os, operand); + }; + interleave(op->getOperands(), os, operandToPort, "|"); + os << "}|"; + } + // Print operation name and type. + os << op->getName() << "\\l"; + + // Print attributes. + if (printAttrs && !op->getAttrs().empty()) { + // Extra line break to separate attributes from the operation name. + os << "\\l"; for (const NamedAttribute &attr : op->getAttrs()) { - os << '\n' << attr.getName().getValue() << ": "; + os << attr.getName().getValue() << ": "; emitMlirAttr(os, attr.getValue()); + os << "\\l"; } } + + if (op->getNumResults() > 0) { + os << "|{"; + auto resultToPort = [&](Value result) { + os << " "; + emitMlirOperand(os, result); + if (printResultTypes) { + os << " "; + emitMlirType(os, result.getType()); + } + }; + interleave(op->getResults(), os, resultToPort, "|"); + os << "}"; + } + + os << "}"; }); } /// Generate a label for a block argument. std::string getLabel(BlockArgument arg) { - return "arg" + std::to_string(arg.getArgNumber()); + return strFromOs([&](raw_ostream &os) { + os << " "; + arg.printAsOperand(os, OpPrintingFlags()); + if (printResultTypes) { + os << " "; + emitMlirType(os, arg.getType()); + } + }); } /// Process a block. Emit a cluster and one node per block argument and @@ -287,14 +386,12 @@ class PrintOpPass : public impl::ViewOpGraphBase { emitClusterStmt([&]() { for (BlockArgument &blockArg : block.getArguments()) valueToNode[blockArg] = emitNodeStmt(getLabel(blockArg)); - // Emit a node for each operation. std::optional prevNode; for (Operation &op : block) { Node nextNode = processOperation(&op); if (printControlFlowEdges && prevNode) - emitEdgeStmt(*prevNode, nextNode, /*label=*/"", - kLineStyleControlFlow); + emitEdgeStmt(*prevNode, nextNode, /*port=*/"", kLineStyleControlFlow); prevNode = nextNode; } }); @@ -311,18 +408,19 @@ class PrintOpPass : public impl::ViewOpGraphBase { for (Region ®ion : op->getRegions()) processRegion(region); }, - getLabel(op)); + getClusterLabel(op)); } else { - node = emitNodeStmt(getLabel(op), kShapeNode, + node = emitNodeStmt(getRecordLabel(op), kShapeNode, backgroundColors[op->getName()].second); } // Insert data flow edges originating from each operand. if (printDataFlowEdges) { unsigned numOperands = op->getNumOperands(); - for (unsigned i = 0; i < numOperands; i++) - dataFlowEdges.push_back({op->getOperand(i), node, - numOperands == 1 ? "" : std::to_string(i)}); + for (unsigned i = 0; i < numOperands; i++) { + auto operand = op->getOperand(i); + dataFlowEdges.push_back({operand, node, getValuePortName(operand)}); + } } for (Value result : op->getResults()) @@ -352,7 +450,7 @@ class PrintOpPass : public impl::ViewOpGraphBase { /// Mapping of SSA values to Graphviz nodes/clusters. DenseMap valueToNode; /// Output for data flow edges is delayed until the end to handle cycles - std::vector> dataFlowEdges; + std::vector dataFlowEdges; /// Counter for generating unique node/subgraph identifiers. int counter = 0; diff --git a/mlir/test/Transforms/print-op-graph-back-edges.mlir b/mlir/test/Transforms/print-op-graph-back-edges.mlir index ed922dd7cb13bda..7950125e2f73546 100644 --- a/mlir/test/Transforms/print-op-graph-back-edges.mlir +++ b/mlir/test/Transforms/print-op-graph-back-edges.mlir @@ -1,21 +1,21 @@ // RUN: mlir-opt -view-op-graph %s -o %t 2>&1 | FileCheck -check-prefix=DFG %s // DFG-LABEL: digraph G { -// DFG: compound = true; -// DFG: subgraph cluster_1 { -// DFG: v2 [label = " ", shape = plain]; -// DFG: label = "builtin.module : ()\n"; -// DFG: subgraph cluster_3 { -// DFG: v4 [label = " ", shape = plain]; -// DFG: label = ""; -// DFG: v5 [fillcolor = "0.000000 1.0 1.0", label = "arith.addi : (index)\n\noverflowFlags: #arith.overflow v5 [label = "0", style = solid]; -// DFG: v7 -> v5 [label = "1", style = solid]; -// DFG: } +// DFG-NEXT: compound = true; +// DFG-NEXT: subgraph cluster_1 { +// DFG-NEXT: v2 [label = " ", shape = plain]; +// DFG-NEXT: label = "builtin.module : ()\l"; +// DFG-NEXT: subgraph cluster_3 { +// DFG-NEXT: v4 [label = " ", shape = plain]; +// DFG-NEXT: label = ""; +// DFG-NEXT: v5 [fillcolor = "0.000000 0.3 0.95", label = "{{\{\{}} %c0| %c1}|arith.addi\l\loverflowFlags: #arith.overflow\ %0 index}}", shape = Mrecord, style = filled]; +// DFG-NEXT: v6 [fillcolor = "0.333333 0.3 0.95", label = "{arith.constant\l\lvalue: 0 : index\l|{ %c0 index}}", shape = Mrecord, style = filled]; +// DFG-NEXT: v7 [fillcolor = "0.333333 0.3 0.95", label = "{arith.constant\l\lvalue: 1 : index\l|{ %c1 index}}", shape = Mrecord, style = filled]; +// DFG-NEXT: } +// DFG-NEXT: } +// DFG-NEXT: v6:res_c0:s -> v5:arg_c0:n[style = solid]; +// DFG-NEXT: v7:res_c1:s -> v5:arg_c1:n[style = solid]; +// DFG-NEXT: } module { %add = arith.addi %c0, %c1 : index diff --git a/mlir/test/Transforms/print-op-graph-cycles.mlir b/mlir/test/Transforms/print-op-graph-cycles.mlir index 7e4eb5616a28b39..ba989544419f33b 100644 --- a/mlir/test/Transforms/print-op-graph-cycles.mlir +++ b/mlir/test/Transforms/print-op-graph-cycles.mlir @@ -1,45 +1,45 @@ // RUN: mlir-opt -view-op-graph -allow-unregistered-dialect %s -o %t 2>&1 | FileCheck -check-prefix=DFG %s // DFG-LABEL: digraph G { -// DFG: compound = true; -// DFG: subgraph cluster_1 { -// DFG: v2 [label = " ", shape = plain]; -// DFG: label = "builtin.module : ()\n"; -// DFG: subgraph cluster_3 { -// DFG: v4 [label = " ", shape = plain]; -// DFG: label = ""; -// DFG: subgraph cluster_5 { -// DFG: v6 [label = " ", shape = plain]; -// DFG: label = "test.graph_region : ()\n"; -// DFG: subgraph cluster_7 { -// DFG: v8 [label = " ", shape = plain]; -// DFG: label = ""; -// DFG: v9 [fillcolor = "0.000000 1.0 1.0", label = "op1 : (i32)\n", shape = ellipse, style = filled]; -// DFG: subgraph cluster_10 { -// DFG: v11 [label = " ", shape = plain]; -// DFG: label = "test.ssacfg_region : (i32)\n"; -// DFG: subgraph cluster_12 { -// DFG: v13 [label = " ", shape = plain]; -// DFG: label = ""; -// DFG: v14 [fillcolor = "0.166667 1.0 1.0", label = "op2 : (i32)\n", shape = ellipse, style = filled]; -// DFG: } -// DFG: } -// DFG: v15 [fillcolor = "0.166667 1.0 1.0", label = "op2 : (i32)\n", shape = ellipse, style = filled]; -// DFG: v16 [fillcolor = "0.500000 1.0 1.0", label = "op3 : (i32)\n", shape = ellipse, style = filled]; -// DFG: } -// DFG: } -// DFG: } -// DFG: } -// DFG: v9 -> v9 [label = "0", style = solid]; -// DFG: v15 -> v9 [label = "1", style = solid]; -// DFG: v9 -> v14 [label = "0", style = solid]; -// DFG: v11 -> v14 [ltail = cluster_10, style = solid]; -// DFG: v15 -> v14 [label = "2", style = solid]; -// DFG: v16 -> v14 [label = "3", style = solid]; -// DFG: v9 -> v15 [label = "0", style = solid]; -// DFG: v16 -> v15 [label = "1", style = solid]; -// DFG: v9 -> v16 [label = "", style = solid]; -// DFG: } +// DFG-NEXT: compound = true; +// DFG-NEXT: subgraph cluster_1 { +// DFG-NEXT: v2 [label = " ", shape = plain]; +// DFG-NEXT: label = "builtin.module : ()\l"; +// DFG-NEXT: subgraph cluster_3 { +// DFG-NEXT: v4 [label = " ", shape = plain]; +// DFG-NEXT: label = ""; +// DFG-NEXT: subgraph cluster_5 { +// DFG-NEXT: v6 [label = " ", shape = plain]; +// DFG-NEXT: label = "test.graph_region : ()\l"; +// DFG-NEXT: subgraph cluster_7 { +// DFG-NEXT: v8 [label = " ", shape = plain]; +// DFG-NEXT: label = ""; +// DFG-NEXT: v9 [fillcolor = "0.000000 0.3 0.95", label = "{{\{\{}} %0| %2}|op1\l|{ %0 i32}}", shape = Mrecord, style = filled]; +// DFG-NEXT: subgraph cluster_10 { +// DFG-NEXT: v11 [label = " ", shape = plain]; +// DFG-NEXT: label = "test.ssacfg_region : (i32)\l"; +// DFG-NEXT: subgraph cluster_12 { +// DFG-NEXT: v13 [label = " ", shape = plain]; +// DFG-NEXT: label = ""; +// DFG-NEXT: v14 [fillcolor = "0.166667 0.3 0.95", label = "{{\{\{}} %0| %1| %2| %3}|op2\l|{ %4 i32}}", shape = Mrecord, style = filled]; +// DFG-NEXT: } +// DFG-NEXT: } +// DFG-NEXT: v15 [fillcolor = "0.166667 0.3 0.95", label = "{{\{\{}} %0| %3}|op2\l|{ %2 i32}}", shape = Mrecord, style = filled]; +// DFG-NEXT: v16 [fillcolor = "0.500000 0.3 0.95", label = "{{\{\{}} %0}|op3\l|{ %3 i32}}", shape = Mrecord, style = filled]; +// DFG-NEXT: } +// DFG-NEXT: } +// DFG-NEXT: } +// DFG-NEXT: } +// DFG-NEXT: v9:res_0:s -> v9:arg_0:n[style = solid]; +// DFG-NEXT: v15:res_2:s -> v9:arg_2:n[style = solid]; +// DFG-NEXT: v9:res_0:s -> v14:arg_0:n[style = solid]; +// DFG-NEXT: v11 -> v14:arg_1:n[ltail = cluster_10, style = solid]; +// DFG-NEXT: v15:res_2:s -> v14:arg_2:n[style = solid]; +// DFG-NEXT: v16:res_3:s -> v14:arg_3:n[style = solid]; +// DFG-NEXT: v9:res_0:s -> v15:arg_0:n[style = solid]; +// DFG-NEXT: v16:res_3:s -> v15:arg_3:n[style = solid]; +// DFG-NEXT: v9:res_0:s -> v16:arg_0:n[style = solid]; +// DFG-NEXT: } "test.graph_region"() ({ // A Graph region %1 = "op1"(%1, %3) : (i32, i32) -> (i32) // OK: %1, %3 allowed here diff --git a/mlir/test/Transforms/print-op-graph.mlir b/mlir/test/Transforms/print-op-graph.mlir index df03194a663d95f..440b037d780921b 100644 --- a/mlir/test/Transforms/print-op-graph.mlir +++ b/mlir/test/Transforms/print-op-graph.mlir @@ -6,49 +6,49 @@ // DFG: subgraph {{.*}} // DFG: label = "func.func{{.*}}merge_blocks // DFG: subgraph {{.*}} { -// DFG: v[[ARG0:.*]] [label = "arg0" +// DFG: v[[ARG0:.*]] [label = " %arg0 i32" // DFG: v[[CONST10:.*]] [{{.*}}label ={{.*}}10 : i32 // DFG: subgraph [[CLUSTER_MERGE_BLOCKS:.*]] { // DFG: v[[ANCHOR:.*]] [label = " ", shape = plain] // DFG: label = "test.merge_blocks // DFG: subgraph {{.*}} { -// DFG: v[[TEST_BR:.*]] [{{.*}}label = "test.br +// DFG: v[[TEST_BR:.*]] [{{.*}}label = "{{.*}}test.br // DFG: } // DFG: subgraph {{.*}} { // DFG: } // DFG: } -// DFG: v[[TEST_RET:.*]] [{{.*}}label = "test.return -// DFG: v[[ARG0]] -> v[[TEST_BR]] -// DFG: v[[CONST10]] -> v[[TEST_BR]] -// DFG: v[[ANCHOR]] -> v[[TEST_RET]] [ltail = [[CLUSTER_MERGE_BLOCKS]], style = solid]; -// DFG: v[[ANCHOR]] -> v[[TEST_RET]] [ltail = [[CLUSTER_MERGE_BLOCKS]], style = solid]; +// DFG: v[[TEST_RET:.*]] [{{.*}}label = "{{.*}}test.return +// DFG: v[[ARG0]]:res_arg0:s -> v[[TEST_BR]]:arg_arg0:n +// DFG: v[[CONST10]]:res_c10_i32:s -> v[[TEST_BR]] +// DFG: v[[ANCHOR]] -> v[[TEST_RET]]:arg_1_0:n[ltail = [[CLUSTER_MERGE_BLOCKS]], style = solid]; +// DFG: v[[ANCHOR]] -> v[[TEST_RET]]:arg_1_1:n[ltail = [[CLUSTER_MERGE_BLOCKS]], style = solid]; // CFG-LABEL: digraph G { // CFG: subgraph {{.*}} { // CFG: subgraph {{.*}} // CFG: label = "func.func{{.*}}merge_blocks // CFG: subgraph {{.*}} { -// CFG: v[[C1:.*]] [{{.*}}label = "arith.constant -// CFG: v[[C2:.*]] [{{.*}}label = "arith.constant -// CFG: v[[C3:.*]] [{{.*}}label = "arith.constant -// CFG: v[[C4:.*]] [{{.*}}label = "arith.constant -// CFG: v[[TEST_FUNC:.*]] [{{.*}}label = "test.func +// CFG: v[[C1:.*]] [{{.*}}label = "{arith.constant +// CFG: v[[C2:.*]] [{{.*}}label = "{arith.constant +// CFG: v[[C3:.*]] [{{.*}}label = "{arith.constant +// CFG: v[[C4:.*]] [{{.*}}label = "{arith.constant +// CFG: v[[TEST_FUNC:.*]] [{{.*}}label = "{test.func // CFG: subgraph [[CLUSTER_MERGE_BLOCKS:.*]] { // CFG: v[[ANCHOR:.*]] [label = " ", shape = plain] // CFG: label = "test.merge_blocks // CFG: subgraph {{.*}} { -// CFG: v[[TEST_BR:.*]] [{{.*}}label = "test.br +// CFG: v[[TEST_BR:.*]] [{{.*}}label = "{{.*}}test.br // CFG: } // CFG: subgraph {{.*}} { // CFG: } // CFG: } -// CFG: v[[TEST_RET:.*]] [{{.*}}label = "test.return +// CFG: v[[TEST_RET:.*]] [{{.*}}label = "{{.*}}test.return // CFG: v[[C1]] -> v[[C2]] // CFG: v[[C2]] -> v[[C3]] // CFG: v[[C3]] -> v[[C4]] // CFG: v[[C4]] -> v[[TEST_FUNC]] -// CFG: v[[TEST_FUNC]] -> v[[ANCHOR]] [lhead = [[CLUSTER_MERGE_BLOCKS]], style = dashed]; -// CFG: v[[ANCHOR]] -> v[[TEST_RET]] [ltail = [[CLUSTER_MERGE_BLOCKS]], style = dashed]; +// CFG: v[[TEST_FUNC]] -> v[[ANCHOR]][lhead = [[CLUSTER_MERGE_BLOCKS]], style = dashed]; +// CFG: v[[ANCHOR]] -> v[[TEST_RET]][ltail = [[CLUSTER_MERGE_BLOCKS]], style = dashed]; func.func @merge_blocks(%arg0: i32, %arg1 : i32) -> () { %0 = arith.constant dense<[[0, 1], [2, 3]]> : tensor<2x2xi32>