1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
|
# Understanding the IR Structure
The MLIR Language Reference describes the
[High Level Structure](../LangRef.md/#high-level-structure), this document
illustrates this structure through examples, and introduces at the same time the
C++ APIs involved in manipulating it.
We will implement a [pass](../PassManagement.md/#operation-pass) that traverses any
MLIR input and prints the entity inside the IR. A pass (or in general almost any
piece of IR) is always rooted with an operation. Most of the time the top-level
operation is a `ModuleOp`, the MLIR `PassManager` is actually limited to
operation on a top-level `ModuleOp`. As such a pass starts with an operation,
and so will our traversal:
```
void runOnOperation() override {
Operation *op = getOperation();
resetIndent();
printOperation(op);
}
```
## Traversing the IR Nesting
The IR is recursively nested, an `Operation` can have one or multiple nested
`Region`s, each of which is actually a list of `Blocks`, each of which itself
wraps a list of `Operation`s. Our traversal will follow this structure with
three methods: `printOperation()`, `printRegion()`, and `printBlock()`.
The first method inspects the properties of an operation, before iterating on
the nested regions and print them individually:
```c++
void printOperation(Operation *op) {
// Print the operation itself and some of its properties
printIndent() << "visiting op: '" << op->getName() << "' with "
<< op->getNumOperands() << " operands and "
<< op->getNumResults() << " results\n";
// Print the operation attributes
if (!op->getAttrs().empty()) {
printIndent() << op->getAttrs().size() << " attributes:\n";
for (NamedAttribute attr : op->getAttrs())
printIndent() << " - '" << attr.first << "' : '" << attr.second
<< "'\n";
}
// Recurse into each of the regions attached to the operation.
printIndent() << " " << op->getNumRegions() << " nested regions:\n";
auto indent = pushIndent();
for (Region ®ion : op->getRegions())
printRegion(region);
}
```
A `Region` does not hold anything other than a list of `Block`s:
```c++
void printRegion(Region ®ion) {
// A region does not hold anything by itself other than a list of blocks.
printIndent() << "Region with " << region.getBlocks().size()
<< " blocks:\n";
auto indent = pushIndent();
for (Block &block : region.getBlocks())
printBlock(block);
}
```
Finally, a `Block` has a list of arguments, and holds a list of `Operation`s:
```c++
void printBlock(Block &block) {
// Print the block intrinsics properties (basically: argument list)
printIndent()
<< "Block with " << block.getNumArguments() << " arguments, "
<< block.getNumSuccessors()
<< " successors, and "
// Note, this `.size()` is traversing a linked-list and is O(n).
<< block.getOperations().size() << " operations\n";
// A block main role is to hold a list of Operations: let's recurse into
// printing each operation.
auto indent = pushIndent();
for (Operation &op : block.getOperations())
printOperation(&op);
}
```
The code for the pass is available
[here in the repo](https://github.com/llvm/llvm-project/blob/main/mlir/test/lib/IR/TestPrintNesting.cpp)
and can be exercised with `mlir-opt -test-print-nesting`.
### Example
The Pass introduced in the previous section can be applied on the following IR
with `mlir-opt -test-print-nesting -allow-unregistered-dialect
llvm-project/mlir/test/IR/print-ir-nesting.mlir`:
```mlir
"builtin.module"() ( {
%0:4 = "dialect.op1"() {"attribute name" = 42 : i32} : () -> (i1, i16, i32, i64)
"dialect.op2"() ( {
"dialect.innerop1"(%0#0, %0#1) : (i1, i16) -> ()
}, {
"dialect.innerop2"() : () -> ()
"dialect.innerop3"(%0#0, %0#2, %0#3)[^bb1, ^bb2] : (i1, i32, i64) -> ()
^bb1(%1: i32): // pred: ^bb0
"dialect.innerop4"() : () -> ()
"dialect.innerop5"() : () -> ()
^bb2(%2: i64): // pred: ^bb0
"dialect.innerop6"() : () -> ()
"dialect.innerop7"() : () -> ()
}) {"other attribute" = 42 : i64} : () -> ()
}) : () -> ()
```
And will yield the following output:
```
visiting op: 'builtin.module' with 0 operands and 0 results
1 nested regions:
Region with 1 blocks:
Block with 0 arguments, 0 successors, and 3 operations
visiting op: 'dialect.op1' with 0 operands and 4 results
1 attributes:
- 'attribute name' : '42 : i32'
0 nested regions:
visiting op: 'dialect.op2' with 0 operands and 0 results
2 nested regions:
Region with 1 blocks:
Block with 0 arguments, 0 successors, and 1 operations
visiting op: 'dialect.innerop1' with 2 operands and 0 results
0 nested regions:
Region with 3 blocks:
Block with 0 arguments, 2 successors, and 2 operations
visiting op: 'dialect.innerop2' with 0 operands and 0 results
0 nested regions:
visiting op: 'dialect.innerop3' with 3 operands and 0 results
0 nested regions:
Block with 1 arguments, 0 successors, and 2 operations
visiting op: 'dialect.innerop4' with 0 operands and 0 results
0 nested regions:
visiting op: 'dialect.innerop5' with 0 operands and 0 results
0 nested regions:
Block with 1 arguments, 0 successors, and 2 operations
visiting op: 'dialect.innerop6' with 0 operands and 0 results
0 nested regions:
visiting op: 'dialect.innerop7' with 0 operands and 0 results
0 nested regions:
0 nested regions:
```
## Other IR Traversal Methods.
In many cases, unwrapping the recursive structure of the IR is cumbersome and
you may be interested in using other helpers.
### Filtered iterator: `getOps<OpTy>()`
For example the `Block` class exposes a convenient templated method
`getOps<OpTy>()` that provided a filtered iterator. Here is an example:
```c++
auto varOps = entryBlock.getOps<spirv::GlobalVariableOp>();
for (spirv::GlobalVariableOp gvOp : varOps) {
// process each GlobalVariable Operation in the block.
...
}
```
Similarly, the `Region` class exposes the same `getOps` method that will iterate
on all the blocks in the region.
### Walkers
The `getOps<OpTy>()` is useful to iterate on some Operations immediately listed
inside a single block (or a single region), however it is frequently interesting
to traverse the IR in a nested fashion. To this end MLIR exposes the `walk()`
helper on `Operation`, `Block`, and `Region`. This helper takes a single
argument: a callback method that will be invoked for every operation recursively
nested under the provided entity.
```c++
// Recursively traverse all the regions and blocks nested inside the function
// and apply the callback on every single operation in post-order.
getFunction().walk([&](mlir::Operation *op) {
// process Operation `op`.
});
```
The provided callback can be specialized to filter on a particular type of
Operation, for example the following will apply the callback only on `LinalgOp`
operations nested inside the function:
```c++
getFunction().walk([](LinalgOp linalgOp) {
// process LinalgOp `linalgOp`.
});
```
Finally, the callback can optionally stop the walk by returning a
`WalkResult::interrupt()` value. For example the following walk will find all
`AllocOp` nested inside the function and interrupt the traversal if one of them
does not satisfy a criteria:
```c++
WalkResult result = getFunction().walk([&](AllocOp allocOp) {
if (!isValid(allocOp))
return WalkResult::interrupt();
return WalkResult::advance();
});
if (result.wasInterrupted())
// One alloc wasn't matching.
...
```
## Traversing the def-use chains
Another relationship in the IR is the one that links a `Value` with its users.
As defined in the
[language reference](../LangRef.md/#high-level-structure),
each Value is either a `BlockArgument` or the result of exactly one `Operation`
(an `Operation` can have multiple results, each of them is a separate `Value`).
The users of a `Value` are `Operation`s, through their arguments: each
`Operation` argument references a single `Value`.
Here is a code sample that inspects the operands of an `Operation` and prints
some information about them:
```c++
// Print information about the producer of each of the operands.
for (Value operand : op->getOperands()) {
if (Operation *producer = operand.getDefiningOp()) {
llvm::outs() << " - Operand produced by operation '"
<< producer->getName() << "'\n";
} else {
// If there is no defining op, the Value is necessarily a Block
// argument.
auto blockArg = operand.cast<BlockArgument>();
llvm::outs() << " - Operand produced by Block argument, number "
<< blockArg.getArgNumber() << "\n";
}
}
```
Similarly, the following code sample iterates through the result `Value`s
produced by an `Operation` and for each result will iterate the users of these
results and print informations about them:
```c++
// Print information about the user of each of the result.
llvm::outs() << "Has " << op->getNumResults() << " results:\n";
for (auto indexedResult : llvm::enumerate(op->getResults())) {
Value result = indexedResult.value();
llvm::outs() << " - Result " << indexedResult.index();
if (result.use_empty()) {
llvm::outs() << " has no uses\n";
continue;
}
if (result.hasOneUse()) {
llvm::outs() << " has a single use: ";
} else {
llvm::outs() << " has "
<< std::distance(result.getUses().begin(),
result.getUses().end())
<< " uses:\n";
}
for (Operation *userOp : result.getUsers()) {
llvm::outs() << " - " << userOp->getName() << "\n";
}
}
```
The illustrating code for this pass is available
[here in the repo](https://github.com/llvm/llvm-project/blob/main/mlir/test/lib/IR/TestPrintDefUse.cpp)
and can be exercised with `mlir-opt -test-print-defuse`.
The chaining of `Value`s and their uses can be viewed as following:

The uses of a `Value` (`OpOperand` or `BlockOperand`) are also chained in a
doubly linked-list, which is particularly useful when replacing all uses of a
`Value` with a new one ("RAUW"):

|