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