1# Usage of 'const' in MLIR, for core IR types 2 3aka, where'd `const` go? 4 5The MLIR data structures that represent the IR itself (Instruction, Block, etc) 6form a graph-based data structure, and the compiler analyses and passes 7frequently walk this graph (e.g. traversing from defs to users). The early 8design of MLIR adopted the `const` model of LLVM, which is familiar and well 9understood (even though the LLVM implementation is flawed in many ways). 10 11The design team since decided to change to a different model, which eschews 12`const` entirely for the core IR types: you should never see a `const` method on 13`Operation`, should never see the type `const Value`, and you shouldn't feel bad 14about this. That said, you *should* use `const` for non-IR types, like 15`SmallVector`'s and many other things. 16 17The document below explains this design point from the viewpoint of "why make a 18change", to explain the rationale and the tradeoffs involved that led us to this 19potentially controversial design point. 20 21Bjarke Roune summarized the situation like this: 22 23> In my opinion `const` correctness is highly valuable, catching many bugs and 24> making it clear in a code base where the mutations happen. In my opinion 25> `const` correctness still isn't worth it in particular for IR elements because 26> of the special uses and properties of IRs, in particular that it is common to 27> transfer a pointer/reference to an instruction from an analysis to an 28> optimization which will change the instruction. The analysis should be const, 29> the optimization needs to get a non-`const` pointer. So all analyses either 30> end up being templates (and if they never get instantiated in a const context, 31> then the point of `const` correctness has been defeated), you need to somehow 32> launder the const in a safe way or there will be `const_cast`s. These options 33> are all bad, probably so bad as to out-weigh the benefits of const. 34 35# Reconsidering `const` in MLIR 36 37This document argues this design is introducing significant sub-optimalities 38into the MLIR codebase, argues that the cost/benefit tradeoff of this design is 39a poor tradeoff, and proposes switching to a much simpler approach - eliminating 40the use of const of these IR types entirely. 41 42**Note:** This document is only discussing things like `const Value` and 43`const Operation*`. There is no proposed change for other types, e.g. 44`SmallVector` references, the immutable types like `Attribute`, etc. 45 46## Background: The LLVM Const Model 47 48The LLVM and MLIR data structures provide the IR data structures (like 49`mlir::Operation`s and their users) as a structured cyclic graph data structure. 50Clients of the IR typically walk up and down the graph, perform dynamic down 51casting (of various sorts) to check for patterns, and use some high-abstraction 52pattern matching and binding facilities to do their work. 53 54The basic idea of LLVM's design is that these traversals of the IR should 55preserve the const'ness of a pointer: if you have a const pointer to an 56instruction and ask for its parent (or operand, users, etc), you should get a 57const pointer to the block containing the instruction (or value defining the 58operand, instruction using the instruction, etc). The instruction class looks 59like this: 60 61```c++ 62namespace llvm { 63class Instruction : ... { 64 BasicBlock *Parent; 65public: 66 // A const instruction returns a const parent pointer. 67 inline const BasicBlock *getParent() const { return Parent; } 68 // A non-const instruction returns a non-const parent pointer. 69 inline BasicBlock *getParent() { return Parent; } 70... 71}; 72} 73``` 74 75The rationale for this design is that it would be const-incorrect to return a 76non-const pointer from getParent, because you could then walk the block to find 77the instruction again and get non-const references to the same instruction - all 78without a `const_cast`. 79 80This `const` model is simple and the C++ type system generally supports it through 81code duplication of methods. That said, LLVM is actually inconsistent and buggy 82about this. Even the core classes have bugs: `llvm::Instruction::getOperand()` 83isn't currently const correct! There are other subsystems (e.g. the 84`llvm/IR/PatternMatch.h` APIs) where you can perform a pattern match on a const 85IR object and bind a non-const IR object. 86 87LLVM is a mature technology with hundreds of people working on it. The fact that 88it still isn't correctly following the const model it set out for strongly hints 89that one of: 1) The design is too complicated to be practical, 2) the benefits 90of the model aren't worth the cost of the complexity, or 3) both 1 and 2, 91together in some combination. 92 93## Advantages of Const-correctness in MLIR 94 95Even though this doc argues for eliminating const from MLIR, it is important to 96evaluate that as a tradeoff with the advantages the const model provides, 97allowing us to do a cost/benefit tradeoff. These are the benefits we see: 98 99The major advantage of allowing const on MLIR types is as a marker in APIs that 100indicate that the function will not modify the specified values. For example, 101the dominator APIs have a `dominates(const Block*, const Block*)` method, and 102the consts provide a way of indicating that the call won't modify the blocks 103passed in - similarly predicates like `Instruction::isTerminator() const` do not 104modify the receiver object. 105 106It is also an advantage that MLIR follows the generally prevailing pattern of 107C++ code, which generally uses const. Consistency with the community norm is 108important. 109 110## Costs of Const-correctness in MLIR 111 112As mentioned above, early work on MLIR adopted the same design as LLVM intended, 113allowing const-correct traversals in the APIs. Here we discuss the various costs 114of doing this by looking at some examples, listed in roughly increasing order of 115severity. 116 117### Pervasively duplicated accessors 118 119Just as the getParent() example above shows, achieving this const model requires 120that all of the graph traversal accessors be duplicated into const and non-const 121versions. This causes API bloat and slows compile time, but these are minor 122problems. 123 124The more significant issue is that this duplication can be so significant that 125the signal disappears in the noise, for example `mlir::Operation` ends up with 126things like this, which is twice as much API surface area just to try to satisfy 127const. 128 129```c++ 130 operand_iterator operand_begin(); 131 operand_iterator operand_end(); 132 133 /// Returns an iterator on the underlying Value's (Value ). 134 operand_range getOperands(); 135 136 // Support const operand iteration. 137 using const_operand_iterator = 138 OperandIterator<const Operation, const Value>; 139 using const_operand_range = llvm::iterator_range<const_operand_iterator>; 140 141 const_operand_iterator operand_begin() const; 142 const_operand_iterator operand_end() const; 143 144 /// Returns a const iterator on the underlying Value's (Value ). 145 llvm::iterator_range<const_operand_iterator> getOperands() const; 146 147 ArrayRef<OpOperand> getOpOperands() const { 148 return getOperandStorage().getOperands(); 149 } 150 MutableArrayRef<OpOperand> getOpOperands() { 151 return getOperandStorage().getOperands(); 152 } 153 154 OpOperand &getOpOperand(unsigned idx) { return getOpOperands()[idx]; } 155 const OpOperand &getOpOperand(unsigned idx) const { 156 return getOpOperands()[idx]; 157 } 158 159``` 160 161### Templated accessors 162 163A related issue is that having to provide both const and non-const versions of 164accessors leads to us having to turn more code into templates than would 165otherwise be desirable. Things like `ResultIterator` and `ResultTypeIterator` 166are templates *_only_* because they are generic over const and non-const 167versions of types. This leads to them being defined inline in headers (instead 168of in .cpp files). 169 170Thus, our const model is leading to more code in headers and more complexity in 171the implementation. 172 173### Const incorrect in practice 174 175For some things, const is more trouble than it is worth, so they never get 176updated. 177 178This means that certain API in practice don't provide a const variant, leading 179to pervasive use of `const_cast` to drop the const qualifier. For example the 180logic in `Matchers.h` doesn't support const pointers at all, even 181though matching and binding values themselves makes perfect sense for both const 182and non-const values. Actually fixing this would cause massive code bloat and 183complexity. 184 185Other parts of the code are just outright incorrect. For example, the operation 186cloning methods are defined on `Operation` like this: 187 188```C++ 189Operation *clone(IRMapping &mapper, MLIRContext *context) const; 190 191Operation *clone(MLIRContext *context) const; 192``` 193 194While it makes sense for a clone method to be `const` conceptually (the original 195operation isn't modified) this is a violation of the model, since the returned 196operation must be mutable, and provides access to the full graph of operands as 197the original operation, violating the graph based const model we were shooting 198for. 199 200### The `OpPointer` and `ConstOpPointer` Classes 201 202The "typed operation" classes for registered operations (e.g. like `DimOp` for 203the "memref.dim" operation in memref ops) contain a pointer to an operation and 204provide typed APIs for processing it. 205 206However, this is a problem for our current `const` design - `const DimOp` means 207the pointer itself is immutable, not the pointee. The previous solution for this 208was the `OpPointer<>` and `ConstOpPointer<>` classes, which existed solely to 209provide const correctness when referring to a typed operation. Instead of 210referring to `DimOp` directly, we used `OpPointer<DimOp>` and 211`ConstOpPointer<DimOp>` to preserve this constness. 212 213While `auto` hides many instances of these `OpPointer` classes, their presence 214leads to extremely ugly APIs. It also obscures the fact that the user does not 215have a direct `DimOp` object, creating easy pitfalls with subtly incorrect 216semantics: 217 218```C++ 219// OpPointer encodes unnecessary and superfluous information into the API. 220SmallVector<OpPointer<AffineForOp>, 8> stripmineSink( 221 OpPointer<AffineForOp> forOp, uint64_t factor, 222 ArrayRef<OpPointer<AffineForOp>> targets); 223// Compared to the much cleaner and easier to read... 224SmallVector<AffineForOp, 8> stripmineSink(AffineForOp forOp, uint64_t factor, 225 ArrayRef<AffineForOp> targets); 226 227// OpPointer is easy to misuse. 228if (auto *dimOp = inst->dyn_cast<DimOp>()) { 229 // This is actually undefined behavior because dyn_cast actually returns 230 // OpPointer<DimOp>. OpPointer<DimOp> happily implicitly converts to DimOp * 231 // creating undefined behavior that will execute correctly most of the time. 232} 233``` 234 235It is much better to eliminate them entirely, and just pass around `DimOp` 236directly. For example, instead of: 237 238```c++ 239LogicalResult mlir::getIndexSet(MutableArrayRef<OpPointer<AffineForOp>> forOps, 240 FlatAffineValueConstraints *domain) { 241 242``` 243 244It is a lot nicer to just have: 245 246```c++ 247LogicalResult mlir::getIndexSet(MutableArrayRef<AffineForOp> forOps, 248 FlatAffineValueConstraints *domain) { 249``` 250 251Particularly since all of the `FooOp` classes are already semantically a smart 252pointer to their underlying operation. 253 254## (Accepted) Proposal: Remove `const` from IR objects 255 256As we can see above, there is very little benefit to our const design and 257significant cost, and given that the primary purpose of an IR is to represent 258transformations of code, const is providing very little benefit. 259 260As such, we propose eliminating support for const references to IR objects in 261MLIR. This implies the following changes to the codebase: 262 2631. All of the const-duplicated accessors would be eliminated, e.g. 264 `Operation::getParent() const` would be removed. This is expected to remove 265 approximately ~130 lines of code from just Operation.h alone. 2661. Const-only predicates would be changed to be non-const, e.g. 267 `Operation::isTerminator() const` would have the const removed. 2681. Iterators and other types and functions that are templated to support 269 `const` can have those template arguments removed. 2701. Types like `OpPointer` and `ConstOpPointer` that exist solely to propagate 271 const can be entirely removed from the codebase. 2721. We can close bugs complaining about const incorrectness in the IR. 273