1# Chapter 2: Emitting Basic MLIR 2 3[TOC] 4 5Now that we're familiar with our language and the AST, let's see how MLIR can 6help to compile Toy. 7 8## Introduction: Multi-Level Intermediate Representation 9 10Other compilers, like LLVM (see the 11[Kaleidoscope tutorial](https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/index.html)), 12offer a fixed set of predefined types and (usually *low-level* / RISC-like) 13instructions. It is up to the frontend for a given language to perform any 14language-specific type-checking, analysis, or transformation before emitting 15LLVM IR. For example, Clang will use its AST to perform not only static analysis 16but also transformations, such as C++ template instantiation through AST cloning 17and rewrite. Finally, languages with construction at a higher-level than C/C++ 18may require non-trivial lowering from their AST to generate LLVM IR. 19 20As a consequence, multiple frontends end up reimplementing significant pieces of 21infrastructure to support the need for these analyses and transformation. MLIR 22addresses this issue by being designed for extensibility. As such, there are few 23pre-defined instructions (*operations* in MLIR terminology) or types. 24 25## Interfacing with MLIR 26 27[Language Reference](../../LangRef.md) 28 29MLIR is designed to be a completely extensible infrastructure; there is no 30closed set of attributes (think: constant metadata), operations, or types. MLIR 31supports this extensibility with the concept of 32[Dialects](../../LangRef.md/#dialects). Dialects provide a grouping mechanism for 33abstraction under a unique `namespace`. 34 35In MLIR, [`Operations`](../../LangRef.md/#operations) are the core unit of 36abstraction and computation, similar in many ways to LLVM instructions. 37Operations can have application-specific semantics and can be used to represent 38all of the core IR structures in LLVM: instructions, globals (like functions), 39modules, etc. 40 41Here is the MLIR assembly for the Toy `transpose` operations: 42 43```mlir 44%t_tensor = "toy.transpose"(%tensor) {inplace = true} : (tensor<2x3xf64>) -> tensor<3x2xf64> loc("example/file/path":12:1) 45``` 46 47Let's break down the anatomy of this MLIR operation: 48 49- `%t_tensor` 50 51 * The name given to the result defined by this operation (which includes 52 [a prefixed sigil to avoid collisions](../../LangRef.md/#identifiers-and-keywords)). 53 An operation may define zero or more results (in the context of Toy, we 54 will limit ourselves to single-result operations), which are SSA values. 55 The name is used during parsing but is not persistent (e.g., it is not 56 tracked in the in-memory representation of the SSA value). 57 58- `"toy.transpose"` 59 60 * The name of the operation. It is expected to be a unique string, with 61 the namespace of the dialect prefixed before the "`.`". This can be read 62 as the `transpose` operation in the `toy` dialect. 63 64- `(%tensor)` 65 66 * A list of zero or more input operands (or arguments), which are SSA 67 values defined by other operations or referring to block arguments. 68 69- `{ inplace = true }` 70 71 * A dictionary of zero or more attributes, which are special operands that 72 are always constant. Here we define a boolean attribute named 'inplace' 73 that has a constant value of true. 74 75- `(tensor<2x3xf64>) -> tensor<3x2xf64>` 76 77 * This refers to the type of the operation in a functional form, spelling 78 the types of the arguments in parentheses and the type of the return 79 values afterward. 80 81- `loc("example/file/path":12:1)` 82 83 * This is the location in the source code from which this operation 84 originated. 85 86Shown here is the general form of an operation. As described above, 87the set of operations in MLIR is extensible. Operations are modeled 88using a small set of concepts, enabling operations to be reasoned 89about and manipulated generically. These concepts are: 90 91- A name for the operation. 92- A list of SSA operand values. 93- A list of [attributes](../../LangRef.md/#attributes). 94- A list of [types](../../LangRef.md/#type-system) for result values. 95- A [source location](../../Diagnostics.md/#source-locations) for debugging 96 purposes. 97- A list of successors [blocks](../../LangRef.md/#blocks) (for branches, 98 mostly). 99- A list of [regions](../../LangRef.md/#regions) (for structural operations 100 like functions). 101 102In MLIR, every operation has a mandatory source location associated with it. 103Contrary to LLVM, where debug info locations are metadata and can be dropped, in 104MLIR, the location is a core requirement, and APIs depend on and manipulate it. 105Dropping a location is thus an explicit choice which cannot happen by mistake. 106 107To provide an illustration: If a transformation replaces an operation by 108another, that new operation must still have a location attached. This makes it 109possible to track where that operation came from. 110 111It's worth noting that the mlir-opt tool - a tool for testing 112compiler passes - does not include locations in the output by default. The 113`-mlir-print-debuginfo` flag specifies to include locations. (Run `mlir-opt 114--help` for more options.) 115 116### Opaque API 117 118MLIR is designed to allow all IR elements, such as attributes, operations, and 119types, to be customized. At the same time, IR elements can always be reduced to 120the above fundamental concepts. This allows MLIR to parse, represent, and 121[round-trip](../../../getting_started/Glossary.md/#round-trip) IR for *any* 122operation. For example, we could place our Toy operation from above into an 123`.mlir` file and round-trip through *mlir-opt* without registering any dialect: 124 125```mlir 126func @toy_func(%tensor: tensor<2x3xf64>) -> tensor<3x2xf64> { 127 %t_tensor = "toy.transpose"(%tensor) { inplace = true } : (tensor<2x3xf64>) -> tensor<3x2xf64> 128 return %t_tensor : tensor<3x2xf64> 129} 130``` 131 132In the cases of unregistered attributes, operations, and types, MLIR will 133enforce some structural constraints (e.g. dominance, etc.), but otherwise they 134are completely opaque. For instance, MLIR has little information about whether 135an unregistered operation can operate on particular data types, how many 136operands it can take, or how many results it produces. This flexibility can be 137useful for bootstrapping purposes, but it is generally advised against in mature 138systems. Unregistered operations must be treated conservatively by 139transformations and analyses, and they are much harder to construct and 140manipulate. 141 142This handling can be observed by crafting what should be an invalid IR for Toy 143and seeing it round-trip without tripping the verifier: 144 145```mlir 146func @main() { 147 %0 = "toy.print"() : () -> tensor<2x3xf64> 148} 149``` 150 151There are multiple problems here: the `toy.print` operation is not a terminator; 152it should take an operand; and it shouldn't return any values. In the next 153section, we will register our dialect and operations with MLIR, plug into the 154verifier, and add nicer APIs to manipulate our operations. 155 156## Defining a Toy Dialect 157 158To effectively interface with MLIR, we will define a new Toy dialect. This 159dialect will model the structure of the Toy language, as well as provide an easy 160avenue for high-level analysis and transformation. 161 162```c++ 163/// This is the definition of the Toy dialect. A dialect inherits from 164/// mlir::Dialect and registers custom attributes, operations, and types. It can 165/// also override virtual methods to change some general behavior, which will be 166/// demonstrated in later chapters of the tutorial. 167class ToyDialect : public mlir::Dialect { 168public: 169 explicit ToyDialect(mlir::MLIRContext *ctx); 170 171 /// Provide a utility accessor to the dialect namespace. 172 static llvm::StringRef getDialectNamespace() { return "toy"; } 173 174 /// An initializer called from the constructor of ToyDialect that is used to 175 /// register attributes, operations, types, and more within the Toy dialect. 176 void initialize(); 177}; 178``` 179 180This is the C++ definition of a dialect, but MLIR also supports defining 181dialects declaratively via 182[tablegen](https://llvm.org/docs/TableGen/ProgRef.html). Using the declarative 183specification is much cleaner as it removes the need for a large portion of the 184boilerplate when defining a new dialect. It also enables easy generation of 185dialect documentation, which can be described directly alongside the dialect. In 186this declarative format, the toy dialect would be specified as: 187 188```tablegen 189// Provide a definition of the 'toy' dialect in the ODS framework so that we 190// can define our operations. 191def Toy_Dialect : Dialect { 192 // The namespace of our dialect, this corresponds 1-1 with the string we 193 // provided in `ToyDialect::getDialectNamespace`. 194 let name = "toy"; 195 196 // A short one-line summary of our dialect. 197 let summary = "A high-level dialect for analyzing and optimizing the " 198 "Toy language"; 199 200 // A much longer description of our dialect. 201 let description = [{ 202 The Toy language is a tensor-based language that allows you to define 203 functions, perform some math computation, and print results. This dialect 204 provides a representation of the language that is amenable to analysis and 205 optimization. 206 }]; 207 208 // The C++ namespace that the dialect class definition resides in. 209 let cppNamespace = "toy"; 210} 211``` 212 213To see what this generates, we can run the `mlir-tblgen` command with the 214`gen-dialect-decls` action like so: 215 216```shell 217${build_root}/bin/mlir-tblgen -gen-dialect-decls ${mlir_src_root}/examples/toy/Ch2/include/toy/Ops.td -I ${mlir_src_root}/include/ 218``` 219 220After the dialect has been defined, it can now be loaded into an MLIRContext: 221 222```c++ 223 context.loadDialect<ToyDialect>(); 224``` 225 226By default, an `MLIRContext` only loads the 227[Builtin Dialect](../../Dialects/Builtin.md), which provides a few core IR 228components, meaning that other dialects, such as our `Toy` dialect, must be 229explicitly loaded. 230 231## Defining Toy Operations 232 233Now that we have a `Toy` dialect, we can start defining the operations. This 234will allow for providing semantic information that the rest of the system can 235hook into. As an example, let's walk through the creation of a `toy.constant` 236operation. This operation will represent a constant value in the Toy language. 237 238```mlir 239 %4 = "toy.constant"() {value = dense<1.0> : tensor<2x3xf64>} : () -> tensor<2x3xf64> 240``` 241 242This operation takes zero operands, a 243[dense elements](../../Dialects/Builtin.md/#denseintorfpelementsattr) attribute named 244`value` to represent the constant value, and returns a single result of 245[RankedTensorType](../../Dialects/Builtin.md/#rankedtensortype). An operation class 246inherits from the [CRTP](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern) 247`mlir::Op` class which also takes some optional [*traits*](../../Traits.md) to 248customize its behavior. `Traits` are a mechanism with which we can inject 249additional behavior into an Operation, such as additional accessors, 250verification, and more. Let's look below at a possible definition for the 251constant operation that we have described above: 252 253```c++ 254class ConstantOp : public mlir::Op< 255 /// `mlir::Op` is a CRTP class, meaning that we provide the 256 /// derived class as a template parameter. 257 ConstantOp, 258 /// The ConstantOp takes zero input operands. 259 mlir::OpTrait::ZeroOperands, 260 /// The ConstantOp returns a single result. 261 mlir::OpTrait::OneResult, 262 /// We also provide a utility `getType` accessor that 263 /// returns the TensorType of the single result. 264 mlir::OpTraits::OneTypedResult<TensorType>::Impl> { 265 266 public: 267 /// Inherit the constructors from the base Op class. 268 using Op::Op; 269 270 /// Provide the unique name for this operation. MLIR will use this to register 271 /// the operation and uniquely identify it throughout the system. The name 272 /// provided here must be prefixed by the parent dialect namespace followed 273 /// by a `.`. 274 static llvm::StringRef getOperationName() { return "toy.constant"; } 275 276 /// Return the value of the constant by fetching it from the attribute. 277 mlir::DenseElementsAttr getValue(); 278 279 /// Operations may provide additional verification beyond what the attached 280 /// traits provide. Here we will ensure that the specific invariants of the 281 /// constant operation are upheld, for example the result type must be 282 /// of TensorType and matches the type of the constant `value`. 283 LogicalResult verify(); 284 285 /// Provide an interface to build this operation from a set of input values. 286 /// This interface is used by the `builder` classes to allow for easily 287 /// generating instances of this operation: 288 /// mlir::OpBuilder::create<ConstantOp>(...) 289 /// This method populates the given `state` that MLIR uses to create 290 /// operations. This state is a collection of all of the discrete elements 291 /// that an operation may contain. 292 /// Build a constant with the given return type and `value` attribute. 293 static void build(mlir::OpBuilder &builder, mlir::OperationState &state, 294 mlir::Type result, mlir::DenseElementsAttr value); 295 /// Build a constant and reuse the type from the given 'value'. 296 static void build(mlir::OpBuilder &builder, mlir::OperationState &state, 297 mlir::DenseElementsAttr value); 298 /// Build a constant by broadcasting the given 'value'. 299 static void build(mlir::OpBuilder &builder, mlir::OperationState &state, 300 double value); 301}; 302``` 303 304and we can register this operation in the `ToyDialect` initializer: 305 306```c++ 307void ToyDialect::initialize() { 308 addOperations<ConstantOp>(); 309} 310``` 311 312### Op vs Operation: Using MLIR Operations 313 314Now that we have defined an operation, we will want to access and transform it. 315In MLIR, there are two main classes related to operations: `Operation` and `Op`. 316The `Operation` class is used to generically model all operations. It is 317'opaque', in the sense that it does not describe the properties of particular 318operations or types of operations. Instead, the `Operation` class provides a 319general API into an operation instance. On the other hand, each specific type of 320operation is represented by an `Op` derived class. For instance `ConstantOp` 321represents a operation with zero inputs, and one output, which is always set to 322the same value. `Op` derived classes act as smart pointer wrapper around a 323`Operation*`, provide operation-specific accessor methods, and type-safe 324properties of operations. This means that when we define our Toy operations, we 325are simply defining a clean, semantically useful interface for building and 326interfacing with the `Operation` class. This is why our `ConstantOp` defines no 327class fields; all of the data for this operation is stored in the referenced 328`Operation`. A side effect of this design is that we always pass around `Op` 329derived classes "by-value", instead of by reference or pointer (*passing by 330value* is a common idiom in MLIR and applies similarly to attributes, types, 331etc). Given a generic `Operation*` instance, we can always get a specific `Op` 332instance using LLVM's casting infrastructure: 333 334```c++ 335void processConstantOp(mlir::Operation *operation) { 336 ConstantOp op = llvm::dyn_cast<ConstantOp>(operation); 337 338 // This operation is not an instance of `ConstantOp`. 339 if (!op) 340 return; 341 342 // Get the internal operation instance wrapped by the smart pointer. 343 mlir::Operation *internalOperation = op.getOperation(); 344 assert(internalOperation == operation && 345 "these operation instances are the same"); 346} 347``` 348 349### Using the Operation Definition Specification (ODS) Framework 350 351In addition to specializing the `mlir::Op` C++ template, MLIR also supports 352defining operations in a declarative manner. This is achieved via the 353[Operation Definition Specification](../../OpDefinitions.md) framework. Facts 354regarding an operation are specified concisely into a TableGen record, which 355will be expanded into an equivalent `mlir::Op` C++ template specialization at 356compile time. Using the ODS framework is the desired way for defining operations 357in MLIR given the simplicity, conciseness, and general stability in the face of 358C++ API changes. 359 360Lets see how to define the ODS equivalent of our ConstantOp: 361 362Operations in ODS are defined by inheriting from the `Op` class. To simplify our 363operation definitions, we will define a base class for operations in the Toy 364dialect. 365 366```tablegen 367// Base class for toy dialect operations. This operation inherits from the base 368// `Op` class in OpBase.td, and provides: 369// * The parent dialect of the operation. 370// * The mnemonic for the operation, or the name without the dialect prefix. 371// * A list of traits for the operation. 372class Toy_Op<string mnemonic, list<OpTrait> traits = []> : 373 Op<Toy_Dialect, mnemonic, traits>; 374``` 375 376With all of the preliminary pieces defined, we can begin to define the constant 377operation. 378 379We define a toy operation by inheriting from our base 'Toy_Op' class above. Here 380we provide the mnemonic and a list of traits for the operation. The 381[mnemonic](../../OpDefinitions.md/#operation-name) here matches the one given in 382`ConstantOp::getOperationName` without the dialect prefix; `toy.`. Missing here 383from our C++ definition are the `ZeroOperands` and `OneResult` traits; these 384will be automatically inferred based upon the `arguments` and `results` fields 385we define later. 386 387```tablegen 388def ConstantOp : Toy_Op<"constant"> { 389} 390``` 391 392At this point you probably might want to know what the C++ code generated by 393TableGen looks like. Simply run the `mlir-tblgen` command with the 394`gen-op-decls` or the `gen-op-defs` action like so: 395 396```shell 397${build_root}/bin/mlir-tblgen -gen-op-defs ${mlir_src_root}/examples/toy/Ch2/include/toy/Ops.td -I ${mlir_src_root}/include/ 398``` 399 400Depending on the selected action, this will print either the `ConstantOp` class 401declaration or its implementation. Comparing this output to the hand-crafted 402implementation is incredibly useful when getting started with TableGen. 403 404#### Defining Arguments and Results 405 406With the shell of the operation defined, we can now provide the 407[inputs](../../OpDefinitions.md/#operation-arguments) and 408[outputs](../../OpDefinitions.md/#operation-results) to our operation. The 409inputs, or arguments, to an operation may be attributes or types for SSA operand 410values. The results correspond to a set of types for the values produced by the 411operation: 412 413```tablegen 414def ConstantOp : Toy_Op<"constant"> { 415 // The constant operation takes an attribute as the only input. 416 // `F64ElementsAttr` corresponds to a 64-bit floating-point ElementsAttr. 417 let arguments = (ins F64ElementsAttr:$value); 418 419 // The constant operation returns a single value of TensorType. 420 // F64Tensor corresponds to a 64-bit floating-point TensorType. 421 let results = (outs F64Tensor); 422} 423``` 424 425By providing a name to the arguments or results, e.g. `$value`, ODS will 426automatically generate a matching accessor: `DenseElementsAttr 427ConstantOp::value()`. 428 429#### Adding Documentation 430 431The next step after defining the operation is to document it. Operations may 432provide 433[`summary` and `description`](../../OpDefinitions.md/#operation-documentation) 434fields to describe the semantics of the operation. This information is useful 435for users of the dialect and can even be used to auto-generate Markdown 436documents. 437 438```tablegen 439def ConstantOp : Toy_Op<"constant"> { 440 // Provide a summary and description for this operation. This can be used to 441 // auto-generate documentation of the operations within our dialect. 442 let summary = "constant operation"; 443 let description = [{ 444 Constant operation turns a literal into an SSA value. The data is attached 445 to the operation as an attribute. For example: 446 447 %0 = "toy.constant"() 448 { value = dense<[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]> : tensor<2x3xf64> } 449 : () -> tensor<2x3xf64> 450 }]; 451 452 // The constant operation takes an attribute as the only input. 453 // `F64ElementsAttr` corresponds to a 64-bit floating-point ElementsAttr. 454 let arguments = (ins F64ElementsAttr:$value); 455 456 // The generic call operation returns a single value of TensorType. 457 // F64Tensor corresponds to a 64-bit floating-point TensorType. 458 let results = (outs F64Tensor); 459} 460``` 461 462#### Verifying Operation Semantics 463 464At this point we've already covered a majority of the original C++ operation 465definition. The next piece to define is the verifier. Luckily, much like the 466named accessor, the ODS framework will automatically generate a lot of the 467necessary verification logic based upon the constraints we have given. This 468means that we don't need to verify the structure of the return type, or even the 469input attribute `value`. In many cases, additional verification is not even 470necessary for ODS operations. To add additional verification logic, an operation 471can override the [`verifier`](../../OpDefinitions.md/#custom-verifier-code) 472field. The `verifier` field allows for defining a C++ code blob that will be run 473as part of `ConstantOp::verify`. This blob can assume that all of the other 474invariants of the operation have already been verified: 475 476```tablegen 477def ConstantOp : Toy_Op<"constant"> { 478 // Provide a summary and description for this operation. This can be used to 479 // auto-generate documentation of the operations within our dialect. 480 let summary = "constant operation"; 481 let description = [{ 482 Constant operation turns a literal into an SSA value. The data is attached 483 to the operation as an attribute. For example: 484 485 %0 = "toy.constant"() 486 { value = dense<[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]> : tensor<2x3xf64> } 487 : () -> tensor<2x3xf64> 488 }]; 489 490 // The constant operation takes an attribute as the only input. 491 // `F64ElementsAttr` corresponds to a 64-bit floating-point ElementsAttr. 492 let arguments = (ins F64ElementsAttr:$value); 493 494 // The generic call operation returns a single value of TensorType. 495 // F64Tensor corresponds to a 64-bit floating-point TensorType. 496 let results = (outs F64Tensor); 497 498 // Add additional verification logic to the constant operation. Here we invoke 499 // a static `verify` method in a C++ source file. This codeblock is executed 500 // inside of ConstantOp::verify, so we can use `this` to refer to the current 501 // operation instance. 502 let verifier = [{ return ::verify(*this); }]; 503} 504``` 505 506#### Attaching `build` Methods 507 508The final missing component here from our original C++ example are the `build` 509methods. ODS can generate some simple build methods automatically, and in this 510case it will generate our first build method for us. For the rest, we define the 511[`builders`](../../OpDefinitions.md/#custom-builder-methods) field. This field 512takes a list of `OpBuilder` objects that take a string corresponding to a list 513of C++ parameters, as well as an optional code block that can be used to specify 514the implementation inline. 515 516```tablegen 517def ConstantOp : Toy_Op<"constant"> { 518 ... 519 520 // Add custom build methods for the constant operation. These methods populate 521 // the `state` that MLIR uses to create operations, i.e. these are used when 522 // using `builder.create<ConstantOp>(...)`. 523 let builders = [ 524 // Build a constant with a given constant tensor value. 525 OpBuilder<(ins "DenseElementsAttr":$value), [{ 526 // Call into an autogenerated `build` method. 527 build(builder, result, value.getType(), value); 528 }]>, 529 530 // Build a constant with a given constant floating-point value. This builder 531 // creates a declaration for `ConstantOp::build` with the given parameters. 532 OpBuilder<(ins "double":$value)> 533 ]; 534} 535``` 536 537#### Specifying a Custom Assembly Format 538 539At this point we can generate our "Toy IR". For example, the following: 540 541```toy 542# User defined generic function that operates on unknown shaped arguments. 543def multiply_transpose(a, b) { 544 return transpose(a) * transpose(b); 545} 546 547def main() { 548 var a<2, 3> = [[1, 2, 3], [4, 5, 6]]; 549 var b<2, 3> = [1, 2, 3, 4, 5, 6]; 550 var c = multiply_transpose(a, b); 551 var d = multiply_transpose(b, a); 552 print(d); 553} 554``` 555 556Results in the following IR: 557 558```mlir 559module { 560 func @multiply_transpose(%arg0: tensor<*xf64>, %arg1: tensor<*xf64>) -> tensor<*xf64> { 561 %0 = "toy.transpose"(%arg0) : (tensor<*xf64>) -> tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":5:10) 562 %1 = "toy.transpose"(%arg1) : (tensor<*xf64>) -> tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":5:25) 563 %2 = "toy.mul"(%0, %1) : (tensor<*xf64>, tensor<*xf64>) -> tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":5:25) 564 "toy.return"(%2) : (tensor<*xf64>) -> () loc("test/Examples/Toy/Ch2/codegen.toy":5:3) 565 } loc("test/Examples/Toy/Ch2/codegen.toy":4:1) 566 func @main() { 567 %0 = "toy.constant"() {value = dense<[[1.000000e+00, 2.000000e+00, 3.000000e+00], [4.000000e+00, 5.000000e+00, 6.000000e+00]]> : tensor<2x3xf64>} : () -> tensor<2x3xf64> loc("test/Examples/Toy/Ch2/codegen.toy":9:17) 568 %1 = "toy.reshape"(%0) : (tensor<2x3xf64>) -> tensor<2x3xf64> loc("test/Examples/Toy/Ch2/codegen.toy":9:3) 569 %2 = "toy.constant"() {value = dense<[1.000000e+00, 2.000000e+00, 3.000000e+00, 4.000000e+00, 5.000000e+00, 6.000000e+00]> : tensor<6xf64>} : () -> tensor<6xf64> loc("test/Examples/Toy/Ch2/codegen.toy":10:17) 570 %3 = "toy.reshape"(%2) : (tensor<6xf64>) -> tensor<2x3xf64> loc("test/Examples/Toy/Ch2/codegen.toy":10:3) 571 %4 = "toy.generic_call"(%1, %3) {callee = @multiply_transpose} : (tensor<2x3xf64>, tensor<2x3xf64>) -> tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":11:11) 572 %5 = "toy.generic_call"(%3, %1) {callee = @multiply_transpose} : (tensor<2x3xf64>, tensor<2x3xf64>) -> tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":12:11) 573 "toy.print"(%5) : (tensor<*xf64>) -> () loc("test/Examples/Toy/Ch2/codegen.toy":13:3) 574 "toy.return"() : () -> () loc("test/Examples/Toy/Ch2/codegen.toy":8:1) 575 } loc("test/Examples/Toy/Ch2/codegen.toy":8:1) 576} loc(unknown) 577``` 578 579One thing to notice here is that all of our Toy operations are printed using the 580generic assembly format. This format is the one shown when breaking down 581`toy.transpose` at the beginning of this chapter. MLIR allows for operations to 582define their own custom assembly format, either 583[declaratively](../../OpDefinitions.md/#declarative-assembly-format) or 584imperatively via C++. Defining a custom assembly format allows for tailoring the 585generated IR into something a bit more readable by removing a lot of the fluff 586that is required by the generic format. Let's walk through an example of an 587operation format that we would like to simplify. 588 589##### `toy.print` 590 591The current form of `toy.print` is a little verbose. There are a lot of 592additional characters that we would like to strip away. Let's begin by thinking 593of what a good format of `toy.print` would be, and see how we can implement it. 594Looking at the basics of `toy.print` we get: 595 596```mlir 597toy.print %5 : tensor<*xf64> loc(...) 598``` 599 600Here we have stripped much of the format down to the bare essentials, and it has 601become much more readable. To provide a custom assembly format, an operation can 602either override the `parser` and `printer` fields for a C++ format, or the 603`assemblyFormat` field for the declarative format. Let's look at the C++ variant 604first, as this is what the declarative format maps to internally. 605 606```tablegen 607/// Consider a stripped definition of `toy.print` here. 608def PrintOp : Toy_Op<"print"> { 609 let arguments = (ins F64Tensor:$input); 610 611 // Divert the printer and parser to static functions in our .cpp 612 // file that correspond to 'print' and 'printPrintOp'. 'printer' and 'parser' 613 // here correspond to an instance of a 'OpAsmParser' and 'OpAsmPrinter'. More 614 // details on these classes is shown below. 615 let printer = [{ return ::print(printer, *this); }]; 616 let parser = [{ return ::parse$cppClass(parser, result); }]; 617} 618``` 619 620A C++ implementation for the printer and parser is shown below: 621 622```c++ 623/// The 'OpAsmPrinter' class is a stream that will allows for formatting 624/// strings, attributes, operands, types, etc. 625static void print(mlir::OpAsmPrinter &printer, PrintOp op) { 626 printer << "toy.print " << op.input(); 627 printer.printOptionalAttrDict(op.getAttrs()); 628 printer << " : " << op.input().getType(); 629} 630 631/// The 'OpAsmParser' class provides a collection of methods for parsing 632/// various punctuation, as well as attributes, operands, types, etc. Each of 633/// these methods returns a `ParseResult`. This class is a wrapper around 634/// `LogicalResult` that can be converted to a boolean `true` value on failure, 635/// or `false` on success. This allows for easily chaining together a set of 636/// parser rules. These rules are used to populate an `mlir::OperationState` 637/// similarly to the `build` methods described above. 638static mlir::ParseResult parsePrintOp(mlir::OpAsmParser &parser, 639 mlir::OperationState &result) { 640 // Parse the input operand, the attribute dictionary, and the type of the 641 // input. 642 mlir::OpAsmParser::OperandType inputOperand; 643 mlir::Type inputType; 644 if (parser.parseOperand(inputOperand) || 645 parser.parseOptionalAttrDict(result.attributes) || parser.parseColon() || 646 parser.parseType(inputType)) 647 return mlir::failure(); 648 649 // Resolve the input operand to the type we parsed in. 650 if (parser.resolveOperand(inputOperand, inputType, result.operands)) 651 return mlir::failure(); 652 653 return mlir::success(); 654} 655``` 656 657With the C++ implementation defined, let's see how this can be mapped to the 658[declarative format](../../OpDefinitions.md/#declarative-assembly-format). The 659declarative format is largely composed of three different components: 660 661* Directives 662 - A type of builtin function, with an optional set of arguments. 663* Literals 664 - A keyword or punctuation surrounded by \`\`. 665* Variables 666 - An entity that has been registered on the operation itself, i.e. an 667 argument(attribute or operand), result, successor, etc. In the `PrintOp` 668 example above, a variable would be `$input`. 669 670A direct mapping of our C++ format looks something like: 671 672```tablegen 673/// Consider a stripped definition of `toy.print` here. 674def PrintOp : Toy_Op<"print"> { 675 let arguments = (ins F64Tensor:$input); 676 677 // In the following format we have two directives, `attr-dict` and `type`. 678 // These correspond to the attribute dictionary and the type of a given 679 // variable represectively. 680 let assemblyFormat = "$input attr-dict `:` type($input)"; 681} 682``` 683 684The [declarative format](../../OpDefinitions.md/#declarative-assembly-format) has 685many more interesting features, so be sure to check it out before implementing a 686custom format in C++. After beautifying the format of a few of our operations we 687now get a much more readable: 688 689```mlir 690module { 691 func @multiply_transpose(%arg0: tensor<*xf64>, %arg1: tensor<*xf64>) -> tensor<*xf64> { 692 %0 = toy.transpose(%arg0 : tensor<*xf64>) to tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":5:10) 693 %1 = toy.transpose(%arg1 : tensor<*xf64>) to tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":5:25) 694 %2 = toy.mul %0, %1 : tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":5:25) 695 toy.return %2 : tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":5:3) 696 } loc("test/Examples/Toy/Ch2/codegen.toy":4:1) 697 func @main() { 698 %0 = toy.constant dense<[[1.000000e+00, 2.000000e+00, 3.000000e+00], [4.000000e+00, 5.000000e+00, 6.000000e+00]]> : tensor<2x3xf64> loc("test/Examples/Toy/Ch2/codegen.toy":9:17) 699 %1 = toy.reshape(%0 : tensor<2x3xf64>) to tensor<2x3xf64> loc("test/Examples/Toy/Ch2/codegen.toy":9:3) 700 %2 = toy.constant dense<[1.000000e+00, 2.000000e+00, 3.000000e+00, 4.000000e+00, 5.000000e+00, 6.000000e+00]> : tensor<6xf64> loc("test/Examples/Toy/Ch2/codegen.toy":10:17) 701 %3 = toy.reshape(%2 : tensor<6xf64>) to tensor<2x3xf64> loc("test/Examples/Toy/Ch2/codegen.toy":10:3) 702 %4 = toy.generic_call @multiply_transpose(%1, %3) : (tensor<2x3xf64>, tensor<2x3xf64>) -> tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":11:11) 703 %5 = toy.generic_call @multiply_transpose(%3, %1) : (tensor<2x3xf64>, tensor<2x3xf64>) -> tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":12:11) 704 toy.print %5 : tensor<*xf64> loc("test/Examples/Toy/Ch2/codegen.toy":13:3) 705 toy.return loc("test/Examples/Toy/Ch2/codegen.toy":8:1) 706 } loc("test/Examples/Toy/Ch2/codegen.toy":8:1) 707} loc(unknown) 708``` 709 710Above we introduce several of the concepts for defining operations in the ODS 711framework, but there are many more that we haven't had a chance to: regions, 712variadic operands, etc. Check out the 713[full specification](../../OpDefinitions.md) for more details. 714 715## Complete Toy Example 716 717We can now generate our "Toy IR". You can build `toyc-ch2` and try yourself on 718the above example: `toyc-ch2 test/Examples/Toy/Ch2/codegen.toy -emit=mlir 719-mlir-print-debuginfo`. We can also check our RoundTrip: `toyc-ch2 720test/Examples/Toy/Ch2/codegen.toy -emit=mlir -mlir-print-debuginfo 2> 721codegen.mlir` followed by `toyc-ch2 codegen.mlir -emit=mlir`. You should also 722use `mlir-tblgen` on the final definition file and study the generated C++ code. 723 724At this point, MLIR knows about our Toy dialect and operations. In the 725[next chapter](Ch-3.md), we will leverage our new dialect to implement some 726high-level language-specific analyses and transformations for the Toy language. 727